diff --git a/diffusers/CITATION.cff b/diffusers/CITATION.cff new file mode 100644 index 0000000000000000000000000000000000000000..18c0151d10a2a4c86cbc0d35841dc328cb7298b3 --- /dev/null +++ b/diffusers/CITATION.cff @@ -0,0 +1,40 @@ +cff-version: 1.2.0 +title: 'Diffusers: State-of-the-art diffusion models' +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Patrick + family-names: von Platen + - given-names: Suraj + family-names: Patil + - given-names: Anton + family-names: Lozhkov + - given-names: Pedro + family-names: Cuenca + - given-names: Nathan + family-names: Lambert + - given-names: Kashif + family-names: Rasul + - given-names: Mishig + family-names: Davaadorj + - given-names: Thomas + family-names: Wolf +repository-code: 'https://github.com/huggingface/diffusers' +abstract: >- + Diffusers provides pretrained diffusion models across + multiple modalities, such as vision and audio, and serves + as a modular toolbox for inference and training of + diffusion models. +keywords: + - deep-learning + - pytorch + - image-generation + - diffusion + - text2image + - image2image + - score-based-generative-modeling + - stable-diffusion +license: Apache-2.0 +version: 0.12.1 diff --git a/diffusers/CODE_OF_CONDUCT.md b/diffusers/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..c8ad966288a9faeeb71b2fad3ba12f6048e1a03f --- /dev/null +++ b/diffusers/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +feedback@huggingface.co. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/diffusers/CONTRIBUTING.md b/diffusers/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..9780dae7f7dff0005e0624cdf688335551ea16fa --- /dev/null +++ b/diffusers/CONTRIBUTING.md @@ -0,0 +1,294 @@ + + +# How to contribute to diffusers? + +Everyone is welcome to contribute, and we value everybody's contribution. Code +is thus not the only way to help the community. Answering questions, helping +others, reaching out and improving the documentations are immensely valuable to +the community. + +It also helps us if you spread the word: reference the library from blog posts +on the awesome projects it made possible, shout out on Twitter every time it has +helped you, or simply star the repo to say "thank you". + +Whichever way you choose to contribute, please be mindful to respect our +[code of conduct](https://github.com/huggingface/diffusers/blob/main/CODE_OF_CONDUCT.md). + +## You can contribute in so many ways! + +There are 4 ways you can contribute to diffusers: +* Fixing outstanding issues with the existing code; +* Implementing [new diffusion pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines#contribution), [new schedulers](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers) or [new models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models) +* [Contributing to the examples](https://github.com/huggingface/diffusers/tree/main/examples) or to the documentation; +* Submitting issues related to bugs or desired new features. + +In particular there is a special [Good First Issue](https://github.com/huggingface/diffusers/contribute) listing. +It will give you a list of open Issues that are open to anybody to work on. Just comment in the issue that you'd like to work on it. +In that same listing you will also find some Issues with `Good Second Issue` label. These are +typically slightly more complicated than the Issues with just `Good First Issue` label. But if you +feel you know what you're doing, go for it. + +*All are equally valuable to the community.* + +## Submitting a new issue or feature request + +Do your best to follow these guidelines when submitting an issue or a feature +request. It will make it easier for us to come back to you quickly and with good +feedback. + +### Did you find a bug? + +The 🧨 Diffusers library is robust and reliable thanks to the users who notify us of +the problems they encounter. So thank you for reporting an issue. + +First, we would really appreciate it if you could **make sure the bug was not +already reported** (use the search bar on Github under Issues). + +### Do you want to implement a new diffusion pipeline / diffusion model? + +Awesome! Please provide the following information: + +* Short description of the diffusion pipeline and link to the paper; +* Link to the implementation if it is open-source; +* Link to the model weights if they are available. + +If you are willing to contribute the model yourself, let us know so we can best +guide you. + +### Do you want a new feature (that is not a model)? + +A world-class feature request addresses the following points: + +1. Motivation first: + * Is it related to a problem/frustration with the library? If so, please explain + why. Providing a code snippet that demonstrates the problem is best. + * Is it related to something you would need for a project? We'd love to hear + about it! + * Is it something you worked on and think could benefit the community? + Awesome! Tell us what problem it solved for you. +2. Write a *full paragraph* describing the feature; +3. Provide a **code snippet** that demonstrates its future use; +4. In case this is related to a paper, please attach a link; +5. Attach any additional information (drawings, screenshots, etc.) you think may help. + +If your issue is well written we're already 80% of the way there by the time you +post it. + +## Start contributing! (Pull Requests) + +Before writing code, we strongly advise you to search through the existing PRs or +issues to make sure that nobody is already working on the same thing. If you are +unsure, it is always a good idea to open an issue to get some feedback. + +You will need basic `git` proficiency to be able to contribute to +🧨 Diffusers. `git` is not the easiest tool to use but it has the greatest +manual. Type `git --help` in a shell and enjoy. If you prefer books, [Pro +Git](https://git-scm.com/book/en/v2) is a very good reference. + +Follow these steps to start contributing ([supported Python versions](https://github.com/huggingface/diffusers/blob/main/setup.py#L426)): + +1. Fork the [repository](https://github.com/huggingface/diffusers) by + clicking on the 'Fork' button on the repository's page. This creates a copy of the code + under your GitHub user account. + +2. Clone your fork to your local disk, and add the base repository as a remote: + + ```bash + $ git clone git@github.com:/diffusers.git + $ cd diffusers + $ git remote add upstream https://github.com/huggingface/diffusers.git + ``` + +3. Create a new branch to hold your development changes: + + ```bash + $ git checkout -b a-descriptive-name-for-my-changes + ``` + + **Do not** work on the `main` branch. + +4. Set up a development environment by running the following command in a virtual environment: + + ```bash + $ pip install -e ".[dev]" + ``` + + (If diffusers was already installed in the virtual environment, remove + it with `pip uninstall diffusers` before reinstalling it in editable + mode with the `-e` flag.) + + To run the full test suite, you might need the additional dependency on `transformers` and `datasets` which requires a separate source + install: + + ```bash + $ git clone https://github.com/huggingface/transformers + $ cd transformers + $ pip install -e . + ``` + + ```bash + $ git clone https://github.com/huggingface/datasets + $ cd datasets + $ pip install -e . + ``` + + If you have already cloned that repo, you might need to `git pull` to get the most recent changes in the `datasets` + library. + +5. Develop the features on your branch. + + As you work on the features, you should make sure that the test suite + passes. You should run the tests impacted by your changes like this: + + ```bash + $ pytest tests/.py + ``` + + You can also run the full suite with the following command, but it takes + a beefy machine to produce a result in a decent amount of time now that + Diffusers has grown a lot. Here is the command for it: + + ```bash + $ make test + ``` + + For more information about tests, check out the + [dedicated documentation](https://huggingface.co/docs/diffusers/testing) + + 🧨 Diffusers relies on `black` and `isort` to format its source code + consistently. After you make changes, apply automatic style corrections and code verifications + that can't be automated in one go with: + + ```bash + $ make style + ``` + + 🧨 Diffusers also uses `ruff` and a few custom scripts to check for coding mistakes. Quality + control runs in CI, however you can also run the same checks with: + + ```bash + $ make quality + ``` + + Once you're happy with your changes, add changed files using `git add` and + make a commit with `git commit` to record your changes locally: + + ```bash + $ git add modified_file.py + $ git commit + ``` + + It is a good idea to sync your copy of the code with the original + repository regularly. This way you can quickly account for changes: + + ```bash + $ git fetch upstream + $ git rebase upstream/main + ``` + + Push the changes to your account using: + + ```bash + $ git push -u origin a-descriptive-name-for-my-changes + ``` + +6. Once you are satisfied (**and the checklist below is happy too**), go to the + webpage of your fork on GitHub. Click on 'Pull request' to send your changes + to the project maintainers for review. + +7. It's ok if maintainers ask you for changes. It happens to core contributors + too! So everyone can see the changes in the Pull request, work in your local + branch and push the changes to your fork. They will automatically appear in + the pull request. + + +### Checklist + +1. The title of your pull request should be a summary of its contribution; +2. If your pull request addresses an issue, please mention the issue number in + the pull request description to make sure they are linked (and people + consulting the issue know you are working on it); +3. To indicate a work in progress please prefix the title with `[WIP]`. These + are useful to avoid duplicated work, and to differentiate it from PRs ready + to be merged; +4. Make sure existing tests pass; +5. Add high-coverage tests. No quality testing = no merge. + - If you are adding new `@slow` tests, make sure they pass using + `RUN_SLOW=1 python -m pytest tests/test_my_new_model.py`. + - If you are adding a new tokenizer, write tests, and make sure + `RUN_SLOW=1 python -m pytest tests/test_tokenization_{your_model_name}.py` passes. + CircleCI does not run the slow tests, but github actions does every night! +6. All public methods must have informative docstrings that work nicely with sphinx. See `modeling_bert.py` for an + example. +7. Due to the rapidly growing repository, it is important to make sure that no files that would significantly weigh down the repository are added. This includes images, videos and other non-text files. We prefer to leverage a hf.co hosted `dataset` like + the ones hosted on [`hf-internal-testing`](https://huggingface.co/hf-internal-testing) in which to place these files and reference + them by URL. We recommend putting them in the following dataset: [huggingface/documentation-images](https://huggingface.co/datasets/huggingface/documentation-images). + If an external contribution, feel free to add the images to your PR and ask a Hugging Face member to migrate your images + to this dataset. + +### Tests + +An extensive test suite is included to test the library behavior and several examples. Library tests can be found in +the [tests folder](https://github.com/huggingface/diffusers/tree/main/tests). + +We like `pytest` and `pytest-xdist` because it's faster. From the root of the +repository, here's how to run tests with `pytest` for the library: + +```bash +$ python -m pytest -n auto --dist=loadfile -s -v ./tests/ +``` + +In fact, that's how `make test` is implemented (sans the `pip install` line)! + +You can specify a smaller set of tests in order to test only the feature +you're working on. + +By default, slow tests are skipped. Set the `RUN_SLOW` environment variable to +`yes` to run them. This will download many gigabytes of models — make sure you +have enough disk space and a good Internet connection, or a lot of patience! + +```bash +$ RUN_SLOW=yes python -m pytest -n auto --dist=loadfile -s -v ./tests/ +``` + +This means `unittest` is fully supported. Here's how to run tests with +`unittest`: + +```bash +$ python -m unittest discover -s tests -t . -v +$ python -m unittest discover -s examples -t examples -v +``` + + +### Style guide + +For documentation strings, 🧨 Diffusers follows the [google style](https://google.github.io/styleguide/pyguide.html). + +**This guide was heavily inspired by the awesome [scikit-learn guide to contributing](https://github.com/scikit-learn/scikit-learn/blob/main/CONTRIBUTING.md).** + +### Syncing forked main with upstream (HuggingFace) main + +To avoid pinging the upstream repository which adds reference notes to each upstream PR and sends unnecessary notifications to the developers involved in these PRs, +when syncing the main branch of a forked repository, please, follow these steps: +1. When possible, avoid syncing with the upstream using a branch and PR on the forked repository. Instead merge directly into the forked main. +2. If a PR is absolutely necessary, use the following steps after checking out your branch: +``` +$ git checkout -b your-branch-for-syncing +$ git pull --squash --no-commit upstream main +$ git commit -m '' +$ git push --set-upstream origin your-branch-for-syncing +``` diff --git a/diffusers/LICENSE b/diffusers/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/diffusers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/diffusers/MANIFEST.in b/diffusers/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..b22fe1a28a1ef881fdb36af3c30b14c0a5d10aa5 --- /dev/null +++ b/diffusers/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include src/diffusers/utils/model_card_template.md diff --git a/diffusers/Makefile b/diffusers/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..94af6d2f12724c9e22a09143be9277aaace3cd85 --- /dev/null +++ b/diffusers/Makefile @@ -0,0 +1,96 @@ +.PHONY: deps_table_update modified_only_fixup extra_style_checks quality style fixup fix-copies test test-examples + +# make sure to test the local checkout in scripts and not the pre-installed one (don't use quotes!) +export PYTHONPATH = src + +check_dirs := examples scripts src tests utils + +modified_only_fixup: + $(eval modified_py_files := $(shell python utils/get_modified_files.py $(check_dirs))) + @if test -n "$(modified_py_files)"; then \ + echo "Checking/fixing $(modified_py_files)"; \ + black $(modified_py_files); \ + ruff $(modified_py_files); \ + else \ + echo "No library .py files were modified"; \ + fi + +# Update src/diffusers/dependency_versions_table.py + +deps_table_update: + @python setup.py deps_table_update + +deps_table_check_updated: + @md5sum src/diffusers/dependency_versions_table.py > md5sum.saved + @python setup.py deps_table_update + @md5sum -c --quiet md5sum.saved || (printf "\nError: the version dependency table is outdated.\nPlease run 'make fixup' or 'make style' and commit the changes.\n\n" && exit 1) + @rm md5sum.saved + +# autogenerating code + +autogenerate_code: deps_table_update + +# Check that the repo is in a good state + +repo-consistency: + python utils/check_dummies.py + python utils/check_repo.py + python utils/check_inits.py + +# this target runs checks on all files + +quality: + black --check $(check_dirs) + ruff $(check_dirs) + doc-builder style src/diffusers docs/source --max_len 119 --check_only --path_to_docs docs/source + python utils/check_doc_toc.py + +# Format source code automatically and check is there are any problems left that need manual fixing + +extra_style_checks: + python utils/custom_init_isort.py + doc-builder style src/diffusers docs/source --max_len 119 --path_to_docs docs/source + python utils/check_doc_toc.py --fix_and_overwrite + +# this target runs checks on all files and potentially modifies some of them + +style: + black $(check_dirs) + ruff $(check_dirs) --fix + ${MAKE} autogenerate_code + ${MAKE} extra_style_checks + +# Super fast fix and check target that only works on relevant modified files since the branch was made + +fixup: modified_only_fixup extra_style_checks autogenerate_code repo-consistency + +# Make marked copies of snippets of codes conform to the original + +fix-copies: + python utils/check_copies.py --fix_and_overwrite + python utils/check_dummies.py --fix_and_overwrite + +# Run tests for the library + +test: + python -m pytest -n auto --dist=loadfile -s -v ./tests/ + +# Run tests for examples + +test-examples: + python -m pytest -n auto --dist=loadfile -s -v ./examples/pytorch/ + + +# Release stuff + +pre-release: + python utils/release.py + +pre-patch: + python utils/release.py --patch + +post-release: + python utils/release.py --post_release + +post-patch: + python utils/release.py --post_release --patch diff --git a/diffusers/README.md b/diffusers/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fc384c9f8fb29fa26f1c63c336b102aa1875c5e8 --- /dev/null +++ b/diffusers/README.md @@ -0,0 +1,563 @@ +

+
+ +
+

+

+ + GitHub + + + GitHub release + + + Contributor Covenant + +

+ +🤗 Diffusers provides pretrained diffusion models across multiple modalities, such as vision and audio, and serves +as a modular toolbox for inference and training of diffusion models. + +More precisely, 🤗 Diffusers offers: + +- State-of-the-art diffusion pipelines that can be run in inference with just a couple of lines of code (see [src/diffusers/pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines)). Check [this overview](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/README.md#pipelines-summary) to see all supported pipelines and their corresponding official papers. +- Various noise schedulers that can be used interchangeably for the preferred speed vs. quality trade-off in inference (see [src/diffusers/schedulers](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers)). +- Multiple types of models, such as UNet, can be used as building blocks in an end-to-end diffusion system (see [src/diffusers/models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models)). +- Training examples to show how to train the most popular diffusion model tasks (see [examples](https://github.com/huggingface/diffusers/tree/main/examples), *e.g.* [unconditional-image-generation](https://github.com/huggingface/diffusers/tree/main/examples/unconditional_image_generation)). + +## Installation + +### For PyTorch + +**With `pip`** (official package) + +```bash +pip install --upgrade diffusers[torch] +``` + +**With `conda`** (maintained by the community) + +```sh +conda install -c conda-forge diffusers +``` + +### For Flax + +**With `pip`** + +```bash +pip install --upgrade diffusers[flax] +``` + +**Apple Silicon (M1/M2) support** + +Please, refer to [the documentation](https://huggingface.co/docs/diffusers/optimization/mps). + +## Contributing + +We ❤️ contributions from the open-source community! +If you want to contribute to this library, please check out our [Contribution guide](https://github.com/huggingface/diffusers/blob/main/CONTRIBUTING.md). +You can look out for [issues](https://github.com/huggingface/diffusers/issues) you'd like to tackle to contribute to the library. +- See [Good first issues](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) for general opportunities to contribute +- See [New model/pipeline](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22New+pipeline%2Fmodel%22) to contribute exciting new diffusion models / diffusion pipelines +- See [New scheduler](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22New+scheduler%22) + +Also, say 👋 in our public Discord channel Join us on Discord. We discuss the hottest trends about diffusion models, help each other with contributions, personal projects or +just hang out ☕. + +## Quickstart + +In order to get started, we recommend taking a look at two notebooks: + +- The [Getting started with Diffusers](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/diffusers_intro.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/diffusers_intro.ipynb) notebook, which showcases an end-to-end example of usage for diffusion models, schedulers and pipelines. + Take a look at this notebook to learn how to use the pipeline abstraction, which takes care of everything (model, scheduler, noise handling) for you, and also to understand each independent building block in the library. +- The [Training a diffusers model](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) notebook summarizes diffusion models training methods. This notebook takes a step-by-step approach to training your + diffusion models on an image dataset, with explanatory graphics. + +## Stable Diffusion is fully compatible with `diffusers`! + +Stable Diffusion is a text-to-image latent diffusion model created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), [LAION](https://laion.ai/) and [RunwayML](https://runwayml.com/). It's trained on 512x512 images from a subset of the [LAION-5B](https://laion.ai/blog/laion-5b/) database. This model uses a frozen CLIP ViT-L/14 text encoder to condition the model on text prompts. With its 860M UNet and 123M text encoder, the model is relatively lightweight and runs on a GPU with at least 4GB VRAM. +See the [model card](https://huggingface.co/CompVis/stable-diffusion) for more information. + + +### Text-to-Image generation with Stable Diffusion + +First let's install + +```bash +pip install --upgrade diffusers transformers accelerate +``` + +We recommend using the model in [half-precision (`fp16`)](https://pytorch.org/blog/accelerating-training-on-nvidia-gpus-with-pytorch-automatic-mixed-precision/) as it gives almost always the same results as full +precision while being roughly twice as fast and requiring half the amount of GPU RAM. + +```python +import torch +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] +``` + +#### Running the model locally + +You can also simply download the model folder and pass the path to the local folder to the `StableDiffusionPipeline`. + +``` +git lfs install +git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +``` + +Assuming the folder is stored locally under `./stable-diffusion-v1-5`, you can run stable diffusion +as follows: + +```python +pipe = StableDiffusionPipeline.from_pretrained("./stable-diffusion-v1-5") +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] +``` + +If you are limited by GPU memory, you might want to consider chunking the attention computation in addition +to using `fp16`. +The following snippet should result in less than 4GB VRAM. + +```python +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +pipe.enable_attention_slicing() +image = pipe(prompt).images[0] +``` + +If you wish to use a different scheduler (e.g.: DDIM, LMS, PNDM/PLMS), you can instantiate +it before the pipeline and pass it to `from_pretrained`. + +```python +from diffusers import LMSDiscreteScheduler + +pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] + +image.save("astronaut_rides_horse.png") +``` + +If you want to run Stable Diffusion on CPU or you want to have maximum precision on GPU, +please run the model in the default *full-precision* setting: + +```python +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + +# disable the following line if you run on CPU +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] + +image.save("astronaut_rides_horse.png") +``` + +### JAX/Flax + +Diffusers offers a JAX / Flax implementation of Stable Diffusion for very fast inference. JAX shines specially on TPU hardware because each TPU server has 8 accelerators working in parallel, but it runs great on GPUs too. + +Running the pipeline with the default PNDMScheduler: + +```python +import jax +import numpy as np +from flax.jax_utils import replicate +from flax.training.common_utils import shard + +from diffusers import FlaxStableDiffusionPipeline + +pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", revision="flax", dtype=jax.numpy.bfloat16 +) + +prompt = "a photo of an astronaut riding a horse on mars" + +prng_seed = jax.random.PRNGKey(0) +num_inference_steps = 50 + +num_samples = jax.device_count() +prompt = num_samples * [prompt] +prompt_ids = pipeline.prepare_inputs(prompt) + +# shard inputs and rng +params = replicate(params) +prng_seed = jax.random.split(prng_seed, jax.device_count()) +prompt_ids = shard(prompt_ids) + +images = pipeline(prompt_ids, params, prng_seed, num_inference_steps, jit=True).images +images = pipeline.numpy_to_pil(np.asarray(images.reshape((num_samples,) + images.shape[-3:]))) +``` + +**Note**: +If you are limited by TPU memory, please make sure to load the `FlaxStableDiffusionPipeline` in `bfloat16` precision instead of the default `float32` precision as done above. You can do so by telling diffusers to load the weights from "bf16" branch. + +```python +import jax +import numpy as np +from flax.jax_utils import replicate +from flax.training.common_utils import shard + +from diffusers import FlaxStableDiffusionPipeline + +pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", revision="bf16", dtype=jax.numpy.bfloat16 +) + +prompt = "a photo of an astronaut riding a horse on mars" + +prng_seed = jax.random.PRNGKey(0) +num_inference_steps = 50 + +num_samples = jax.device_count() +prompt = num_samples * [prompt] +prompt_ids = pipeline.prepare_inputs(prompt) + +# shard inputs and rng +params = replicate(params) +prng_seed = jax.random.split(prng_seed, jax.device_count()) +prompt_ids = shard(prompt_ids) + +images = pipeline(prompt_ids, params, prng_seed, num_inference_steps, jit=True).images +images = pipeline.numpy_to_pil(np.asarray(images.reshape((num_samples,) + images.shape[-3:]))) +``` + +Diffusers also has a Image-to-Image generation pipeline with Flax/Jax +```python +import jax +import numpy as np +import jax.numpy as jnp +from flax.jax_utils import replicate +from flax.training.common_utils import shard +import requests +from io import BytesIO +from PIL import Image +from diffusers import FlaxStableDiffusionImg2ImgPipeline + +def create_key(seed=0): + return jax.random.PRNGKey(seed) +rng = create_key(0) + +url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" +response = requests.get(url) +init_img = Image.open(BytesIO(response.content)).convert("RGB") +init_img = init_img.resize((768, 512)) + +prompts = "A fantasy landscape, trending on artstation" + +pipeline, params = FlaxStableDiffusionImg2ImgPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", revision="flax", + dtype=jnp.bfloat16, +) + +num_samples = jax.device_count() +rng = jax.random.split(rng, jax.device_count()) +prompt_ids, processed_image = pipeline.prepare_inputs(prompt=[prompts]*num_samples, image = [init_img]*num_samples) +p_params = replicate(params) +prompt_ids = shard(prompt_ids) +processed_image = shard(processed_image) + +output = pipeline( + prompt_ids=prompt_ids, + image=processed_image, + params=p_params, + prng_seed=rng, + strength=0.75, + num_inference_steps=50, + jit=True, + height=512, + width=768).images + +output_images = pipeline.numpy_to_pil(np.asarray(output.reshape((num_samples,) + output.shape[-3:]))) +``` + +Diffusers also has a Text-guided inpainting pipeline with Flax/Jax + +```python +import jax +import numpy as np +from flax.jax_utils import replicate +from flax.training.common_utils import shard +import PIL +import requests +from io import BytesIO + + +from diffusers import FlaxStableDiffusionInpaintPipeline + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +pipeline, params = FlaxStableDiffusionInpaintPipeline.from_pretrained("xvjiarui/stable-diffusion-2-inpainting") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +prng_seed = jax.random.PRNGKey(0) +num_inference_steps = 50 + +num_samples = jax.device_count() +prompt = num_samples * [prompt] +init_image = num_samples * [init_image] +mask_image = num_samples * [mask_image] +prompt_ids, processed_masked_images, processed_masks = pipeline.prepare_inputs(prompt, init_image, mask_image) + + +# shard inputs and rng +params = replicate(params) +prng_seed = jax.random.split(prng_seed, jax.device_count()) +prompt_ids = shard(prompt_ids) +processed_masked_images = shard(processed_masked_images) +processed_masks = shard(processed_masks) + +images = pipeline(prompt_ids, processed_masks, processed_masked_images, params, prng_seed, num_inference_steps, jit=True).images +images = pipeline.numpy_to_pil(np.asarray(images.reshape((num_samples,) + images.shape[-3:]))) +``` + +### Image-to-Image text-guided generation with Stable Diffusion + +The `StableDiffusionImg2ImgPipeline` lets you pass a text prompt and an initial image to condition the generation of new images. + +```python +import requests +import torch +from PIL import Image +from io import BytesIO + +from diffusers import StableDiffusionImg2ImgPipeline + +# load the pipeline +device = "cuda" +model_id_or_path = "runwayml/stable-diffusion-v1-5" +pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) + +# or download via git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +# and pass `model_id_or_path="./stable-diffusion-v1-5"`. +pipe = pipe.to(device) + +# let's download an initial image +url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((768, 512)) + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +images[0].save("fantasy_landscape.png") +``` +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) + +### In-painting using Stable Diffusion + +The `StableDiffusionInpaintPipeline` lets you edit specific parts of an image by providing a mask and a text prompt. + +```python +import PIL +import requests +import torch +from io import BytesIO + +from diffusers import StableDiffusionInpaintPipeline + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16) +pipe = pipe.to("cuda") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0] +``` + +### Tweak prompts reusing seeds and latents + +You can generate your own latents to reproduce results, or tweak your prompt on a specific result you liked. +Please have a look at [Reusing seeds for deterministic generation](https://huggingface.co/docs/diffusers/main/en/using-diffusers/reusing_seeds). + +## Fine-Tuning Stable Diffusion + +Fine-tuning techniques make it possible to adapt Stable Diffusion to your own dataset, or add new subjects to it. These are some of the techniques supported in `diffusers`: + +Textual Inversion is a technique for capturing novel concepts from a small number of example images in a way that can later be used to control text-to-image pipelines. It does so by learning new 'words' in the embedding space of the pipeline's text encoder. These special words can then be used within text prompts to achieve very fine-grained control of the resulting images. + +- Textual Inversion. Capture novel concepts from a small set of sample images, and associate them with new "words" in the embedding space of the text encoder. Please, refer to [our training examples](https://github.com/huggingface/diffusers/tree/main/examples/textual_inversion) or [documentation](https://huggingface.co/docs/diffusers/training/text_inversion) to try for yourself. + +- Dreambooth. Another technique to capture new concepts in Stable Diffusion. This method fine-tunes the UNet (and, optionally, also the text encoder) of the pipeline to achieve impressive results. Please, refer to [our training example](https://github.com/huggingface/diffusers/tree/main/examples/dreambooth) and [training report](https://huggingface.co/blog/dreambooth) for additional details and training recommendations. + +- Full Stable Diffusion fine-tuning. If you have a more sizable dataset with a specific look or style, you can fine-tune Stable Diffusion so that it outputs images following those examples. This was the approach taken to create [a Pokémon Stable Diffusion model](https://huggingface.co/justinpinkney/pokemon-stable-diffusion) (by Justing Pinkney / Lambda Labs), [a Japanese specific version of Stable Diffusion](https://huggingface.co/spaces/rinna/japanese-stable-diffusion) (by [Rinna Co.](https://github.com/rinnakk/japanese-stable-diffusion/) and others. You can start at [our text-to-image fine-tuning example](https://github.com/huggingface/diffusers/tree/main/examples/text_to_image) and go from there. + + +## Stable Diffusion Community Pipelines + +The release of Stable Diffusion as an open source model has fostered a lot of interesting ideas and experimentation. +Our [Community Examples folder](https://github.com/huggingface/diffusers/tree/main/examples/community) contains many ideas worth exploring, like interpolating to create animated videos, using CLIP Guidance for additional prompt fidelity, term weighting, and much more! [Take a look](https://huggingface.co/docs/diffusers/using-diffusers/custom_pipeline_overview) and [contribute your own](https://huggingface.co/docs/diffusers/using-diffusers/contribute_pipeline). + +## Other Examples + +There are many ways to try running Diffusers! Here we outline code-focused tools (primarily using `DiffusionPipeline`s and Google Colab) and interactive web-tools. + +### Running Code + +If you want to run the code yourself 💻, you can try out: +- [Text-to-Image Latent Diffusion](https://huggingface.co/CompVis/ldm-text2im-large-256) +```python +# !pip install diffusers["torch"] transformers +from diffusers import DiffusionPipeline + +device = "cuda" +model_id = "CompVis/ldm-text2im-large-256" + +# load model and scheduler +ldm = DiffusionPipeline.from_pretrained(model_id) +ldm = ldm.to(device) + +# run pipeline in inference (sample random noise and denoise) +prompt = "A painting of a squirrel eating a burger" +image = ldm([prompt], num_inference_steps=50, eta=0.3, guidance_scale=6).images[0] + +# save image +image.save("squirrel.png") +``` +- [Unconditional Diffusion with discrete scheduler](https://huggingface.co/google/ddpm-celebahq-256) +```python +# !pip install diffusers["torch"] +from diffusers import DDPMPipeline, DDIMPipeline, PNDMPipeline + +model_id = "google/ddpm-celebahq-256" +device = "cuda" + +# load model and scheduler +ddpm = DDPMPipeline.from_pretrained(model_id) # you can replace DDPMPipeline with DDIMPipeline or PNDMPipeline for faster inference +ddpm.to(device) + +# run pipeline in inference (sample random noise and denoise) +image = ddpm().images[0] + +# save image +image.save("ddpm_generated_image.png") +``` +- [Unconditional Latent Diffusion](https://huggingface.co/CompVis/ldm-celebahq-256) +- [Unconditional Diffusion with continuous scheduler](https://huggingface.co/google/ncsnpp-ffhq-1024) + +**Other Image Notebooks**: +* [image-to-image generation with Stable Diffusion](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) ![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg), +* [tweak images via repeated Stable Diffusion seeds](https://colab.research.google.com/github/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb) ![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg), + +**Diffusers for Other Modalities**: +* [Molecule conformation generation](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/geodiff_molecule_conformation.ipynb) ![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg), +* [Model-based reinforcement learning](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/reinforcement_learning_with_diffusers.ipynb) ![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg), + +### Web Demos +If you just want to play around with some web demos, you can try out the following 🚀 Spaces: +| Model | Hugging Face Spaces | +|-------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Text-to-Image Latent Diffusion | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/CompVis/text2img-latent-diffusion) | +| Faces generator | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/CompVis/celeba-latent-diffusion) | +| DDPM with different schedulers | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/fusing/celeba-diffusion) | +| Conditional generation from sketch | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/huggingface/diffuse-the-rest) | +| Composable diffusion | [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/Shuang59/Composable-Diffusion) | + +## Definitions + +**Models**: Neural network that models $p_\theta(\mathbf{x}_{t-1}|\mathbf{x}_t)$ (see image below) and is trained end-to-end to *denoise* a noisy input to an image. +*Examples*: UNet, Conditioned UNet, 3D UNet, Transformer UNet + +

+ +
+ Figure from DDPM paper (https://arxiv.org/abs/2006.11239). +

+ +**Schedulers**: Algorithm class for both **inference** and **training**. +The class provides functionality to compute previous image according to alpha, beta schedule as well as predict noise for training. Also known as **Samplers**. +*Examples*: [DDPM](https://arxiv.org/abs/2006.11239), [DDIM](https://arxiv.org/abs/2010.02502), [PNDM](https://arxiv.org/abs/2202.09778), [DEIS](https://arxiv.org/abs/2204.13902) + +

+ +
+ Sampling and training algorithms. Figure from DDPM paper (https://arxiv.org/abs/2006.11239). +

+ + +**Diffusion Pipeline**: End-to-end pipeline that includes multiple diffusion models, possible text encoders, ... +*Examples*: Glide, Latent-Diffusion, Imagen, DALL-E 2 + +

+ +
+ Figure from ImageGen (https://imagen.research.google/). +

+ +## Philosophy + +- Readability and clarity is preferred over highly optimized code. A strong importance is put on providing readable, intuitive and elementary code design. *E.g.*, the provided [schedulers](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers) are separated from the provided [models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models) and provide well-commented code that can be read alongside the original paper. +- Diffusers is **modality independent** and focuses on providing pretrained models and tools to build systems that generate **continuous outputs**, *e.g.* vision and audio. +- Diffusion models and schedulers are provided as concise, elementary building blocks. In contrast, diffusion pipelines are a collection of end-to-end diffusion systems that can be used out-of-the-box, should stay as close as possible to their original implementation and can include components of another library, such as text-encoders. Examples for diffusion pipelines are [Glide](https://github.com/openai/glide-text2im) and [Latent Diffusion](https://github.com/CompVis/latent-diffusion). + +## In the works + +For the first release, 🤗 Diffusers focuses on text-to-image diffusion techniques. However, diffusers can be used for much more than that! Over the upcoming releases, we'll be focusing on: + +- Diffusers for audio +- Diffusers for reinforcement learning (initial work happening in https://github.com/huggingface/diffusers/pull/105). +- Diffusers for video generation +- Diffusers for molecule generation (initial work happening in https://github.com/huggingface/diffusers/pull/54) + +A few pipeline components are already being worked on, namely: + +- BDDMPipeline for spectrogram-to-sound vocoding +- GLIDEPipeline to support OpenAI's GLIDE model +- Grad-TTS for text to audio generation / conditional audio generation + +We want diffusers to be a toolbox useful for diffusers models in general; if you find yourself limited in any way by the current API, or would like to see additional models, schedulers, or techniques, please open a [GitHub issue](https://github.com/huggingface/diffusers/issues) mentioning what you would like to see. + +## Credits + +This library concretizes previous work by many different authors and would not have been possible without their great research and implementations. We'd like to thank, in particular, the following implementations which have helped us in our development and without which the API could not have been as polished today: + +- @CompVis' latent diffusion models library, available [here](https://github.com/CompVis/latent-diffusion) +- @hojonathanho original DDPM implementation, available [here](https://github.com/hojonathanho/diffusion) as well as the extremely useful translation into PyTorch by @pesser, available [here](https://github.com/pesser/pytorch_diffusion) +- @ermongroup's DDIM implementation, available [here](https://github.com/ermongroup/ddim). +- @yang-song's Score-VE and Score-VP implementations, available [here](https://github.com/yang-song/score_sde_pytorch) + +We also want to thank @heejkoo for the very helpful overview of papers, code and resources on diffusion models, available [here](https://github.com/heejkoo/Awesome-Diffusion-Models) as well as @crowsonkb and @rromb for useful discussions and insights. + +## Citation + +```bibtex +@misc{von-platen-etal-2022-diffusers, + author = {Patrick von Platen and Suraj Patil and Anton Lozhkov and Pedro Cuenca and Nathan Lambert and Kashif Rasul and Mishig Davaadorj and Thomas Wolf}, + title = {Diffusers: State-of-the-art diffusion models}, + year = {2022}, + publisher = {GitHub}, + journal = {GitHub repository}, + howpublished = {\url{https://github.com/huggingface/diffusers}} +} +``` diff --git a/diffusers/_typos.toml b/diffusers/_typos.toml new file mode 100644 index 0000000000000000000000000000000000000000..551099f981e7885fbda9ed28e297bace0e13407b --- /dev/null +++ b/diffusers/_typos.toml @@ -0,0 +1,13 @@ +# Files for typos +# Instruction: https://github.com/marketplace/actions/typos-action#getting-started + +[default.extend-identifiers] + +[default.extend-words] +NIN="NIN" # NIN is used in scripts/convert_ncsnpp_original_checkpoint_to_diffusers.py +nd="np" # nd may be np (numpy) +parms="parms" # parms is used in scripts/convert_original_stable_diffusion_to_diffusers.py + + +[files] +extend-exclude = ["_typos.toml"] diff --git a/diffusers/docker/diffusers-flax-cpu/Dockerfile b/diffusers/docker/diffusers-flax-cpu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..57a9c1ec742200b48f8c2f906d1152e85e60584a --- /dev/null +++ b/diffusers/docker/diffusers-flax-cpu/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +# follow the instructions here: https://cloud.google.com/tpu/docs/run-in-container#train_a_jax_model_in_a_docker_container +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --upgrade --no-cache-dir \ + clu \ + "jax[cpu]>=0.2.16,!=0.3.2" \ + "flax>=0.4.1" \ + "jaxlib>=0.1.65" && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docker/diffusers-flax-tpu/Dockerfile b/diffusers/docker/diffusers-flax-tpu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2517da586d74b43c4c94a0eca4651f047345ec4d --- /dev/null +++ b/diffusers/docker/diffusers-flax-tpu/Dockerfile @@ -0,0 +1,46 @@ +FROM ubuntu:20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +# follow the instructions here: https://cloud.google.com/tpu/docs/run-in-container#train_a_jax_model_in_a_docker_container +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir \ + "jax[tpu]>=0.2.16,!=0.3.2" \ + -f https://storage.googleapis.com/jax-releases/libtpu_releases.html && \ + python3 -m pip install --upgrade --no-cache-dir \ + clu \ + "flax>=0.4.1" \ + "jaxlib>=0.1.65" && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docker/diffusers-onnxruntime-cpu/Dockerfile b/diffusers/docker/diffusers-onnxruntime-cpu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..75f45be87a033e9476c4038218c9c2fd2f1255a5 --- /dev/null +++ b/diffusers/docker/diffusers-onnxruntime-cpu/Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir \ + torch \ + torchvision \ + torchaudio \ + onnxruntime \ + --extra-index-url https://download.pytorch.org/whl/cpu && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docker/diffusers-onnxruntime-cuda/Dockerfile b/diffusers/docker/diffusers-onnxruntime-cuda/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2129dbcaf68c57755485e1e54e867af05b937336 --- /dev/null +++ b/diffusers/docker/diffusers-onnxruntime-cuda/Dockerfile @@ -0,0 +1,44 @@ +FROM nvidia/cuda:11.6.2-cudnn8-devel-ubuntu20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir \ + torch \ + torchvision \ + torchaudio \ + "onnxruntime-gpu>=1.13.1" \ + --extra-index-url https://download.pytorch.org/whl/cu117 && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docker/diffusers-pytorch-cpu/Dockerfile b/diffusers/docker/diffusers-pytorch-cpu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a70eff4c852b21e51c576e1e43172dd8dc25e1a0 --- /dev/null +++ b/diffusers/docker/diffusers-pytorch-cpu/Dockerfile @@ -0,0 +1,43 @@ +FROM ubuntu:20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir \ + torch \ + torchvision \ + torchaudio \ + --extra-index-url https://download.pytorch.org/whl/cpu && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docker/diffusers-pytorch-cuda/Dockerfile b/diffusers/docker/diffusers-pytorch-cuda/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1c5ac3998faa4a04fa67f2a640dd5ab28a838963 --- /dev/null +++ b/diffusers/docker/diffusers-pytorch-cuda/Dockerfile @@ -0,0 +1,43 @@ +FROM nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu20.04 +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && \ + apt install -y bash \ + build-essential \ + git \ + git-lfs \ + curl \ + ca-certificates \ + libsndfile1-dev \ + python3.8 \ + python3-pip \ + python3.8-venv && \ + rm -rf /var/lib/apt/lists + +# make sure to use venv +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# pre-install the heavy dependencies (these can later be overridden by the deps from setup.py) +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir \ + torch \ + torchvision \ + torchaudio \ + --extra-index-url https://download.pytorch.org/whl/cu117 && \ + python3 -m pip install --no-cache-dir \ + accelerate \ + datasets \ + hf-doc-builder \ + huggingface-hub \ + Jinja2 \ + librosa \ + numpy \ + scipy \ + tensorboard \ + transformers + +CMD ["/bin/bash"] \ No newline at end of file diff --git a/diffusers/docs/README.md b/diffusers/docs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..77d5c89326f503e3b7f0a5e6a8ff21423e4496f4 --- /dev/null +++ b/diffusers/docs/README.md @@ -0,0 +1,271 @@ + + +# Generating the documentation + +To generate the documentation, you first have to build it. Several packages are necessary to build the doc, +you can install them with the following command, at the root of the code repository: + +```bash +pip install -e ".[docs]" +``` + +Then you need to install our open source documentation builder tool: + +```bash +pip install git+https://github.com/huggingface/doc-builder +``` + +--- +**NOTE** + +You only need to generate the documentation to inspect it locally (if you're planning changes and want to +check how they look before committing for instance). You don't have to commit the built documentation. + +--- + +## Previewing the documentation + +To preview the docs, first install the `watchdog` module with: + +```bash +pip install watchdog +``` + +Then run the following command: + +```bash +doc-builder preview {package_name} {path_to_docs} +``` + +For example: + +```bash +doc-builder preview diffusers docs/source/en +``` + +The docs will be viewable at [http://localhost:3000](http://localhost:3000). You can also preview the docs once you have opened a PR. You will see a bot add a comment to a link where the documentation with your changes lives. + +--- +**NOTE** + +The `preview` command only works with existing doc files. When you add a completely new file, you need to update `_toctree.yml` & restart `preview` command (`ctrl-c` to stop it & call `doc-builder preview ...` again). + +--- + +## Adding a new element to the navigation bar + +Accepted files are Markdown (.md or .mdx). + +Create a file with its extension and put it in the source directory. You can then link it to the toc-tree by putting +the filename without the extension in the [`_toctree.yml`](https://github.com/huggingface/diffusers/blob/main/docs/source/_toctree.yml) file. + +## Renaming section headers and moving sections + +It helps to keep the old links working when renaming the section header and/or moving sections from one document to another. This is because the old links are likely to be used in Issues, Forums, and Social media and it'd make for a much more superior user experience if users reading those months later could still easily navigate to the originally intended information. + +Therefore, we simply keep a little map of moved sections at the end of the document where the original section was. The key is to preserve the original anchor. + +So if you renamed a section from: "Section A" to "Section B", then you can add at the end of the file: + +``` +Sections that were moved: + +[ Section A ] +``` +and of course, if you moved it to another file, then: + +``` +Sections that were moved: + +[ Section A ] +``` + +Use the relative style to link to the new file so that the versioned docs continue to work. + +For an example of a rich moved section set please see the very end of [the transformers Trainer doc](https://github.com/huggingface/transformers/blob/main/docs/source/en/main_classes/trainer.mdx). + + +## Writing Documentation - Specification + +The `huggingface/diffusers` documentation follows the +[Google documentation](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) style for docstrings, +although we can write them directly in Markdown. + +### Adding a new tutorial + +Adding a new tutorial or section is done in two steps: + +- Add a new file under `docs/source`. This file can either be ReStructuredText (.rst) or Markdown (.md). +- Link that file in `docs/source/_toctree.yml` on the correct toc-tree. + +Make sure to put your new file under the proper section. It's unlikely to go in the first section (*Get Started*), so +depending on the intended targets (beginners, more advanced users, or researchers) it should go in sections two, three, or four. + +### Adding a new pipeline/scheduler + +When adding a new pipeline: + +- create a file `xxx.mdx` under `docs/source/api/pipelines` (don't hesitate to copy an existing file as template). +- Link that file in (*Diffusers Summary*) section in `docs/source/api/pipelines/overview.mdx`, along with the link to the paper, and a colab notebook (if available). +- Write a short overview of the diffusion model: + - Overview with paper & authors + - Paper abstract + - Tips and tricks and how to use it best + - Possible an end-to-end example of how to use it +- Add all the pipeline classes that should be linked in the diffusion model. These classes should be added using our Markdown syntax. By default as follows: + +``` +## XXXPipeline + +[[autodoc]] XXXPipeline + - all + - __call__ +``` + +This will include every public method of the pipeline that is documented, as well as the `__call__` method that is not documented by default. If you just want to add additional methods that are not documented, you can put the list of all methods to add in a list that contains `all`. + +``` +[[autodoc]] XXXPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention +``` + +You can follow the same process to create a new scheduler under the `docs/source/api/schedulers` folder + +### Writing source documentation + +Values that should be put in `code` should either be surrounded by backticks: \`like so\`. Note that argument names +and objects like True, None, or any strings should usually be put in `code`. + +When mentioning a class, function, or method, it is recommended to use our syntax for internal links so that our tool +adds a link to its documentation with this syntax: \[\`XXXClass\`\] or \[\`function\`\]. This requires the class or +function to be in the main package. + +If you want to create a link to some internal class or function, you need to +provide its path. For instance: \[\`pipelines.ImagePipelineOutput\`\]. This will be converted into a link with +`pipelines.ImagePipelineOutput` in the description. To get rid of the path and only keep the name of the object you are +linking to in the description, add a ~: \[\`~pipelines.ImagePipelineOutput\`\] will generate a link with `ImagePipelineOutput` in the description. + +The same works for methods so you can either use \[\`XXXClass.method\`\] or \[~\`XXXClass.method\`\]. + +#### Defining arguments in a method + +Arguments should be defined with the `Args:` (or `Arguments:` or `Parameters:`) prefix, followed by a line return and +an indentation. The argument should be followed by its type, with its shape if it is a tensor, a colon, and its +description: + +``` + Args: + n_layers (`int`): The number of layers of the model. +``` + +If the description is too long to fit in one line, another indentation is necessary before writing the description +after the argument. + +Here's an example showcasing everything so far: + +``` + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. + + Indices can be obtained using [`AlbertTokenizer`]. See [`~PreTrainedTokenizer.encode`] and + [`~PreTrainedTokenizer.__call__`] for details. + + [What are input IDs?](../glossary#input-ids) +``` + +For optional arguments or arguments with defaults we follow the following syntax: imagine we have a function with the +following signature: + +``` +def my_function(x: str = None, a: float = 1): +``` + +then its documentation should look like this: + +``` + Args: + x (`str`, *optional*): + This argument controls ... + a (`float`, *optional*, defaults to 1): + This argument is used to ... +``` + +Note that we always omit the "defaults to \`None\`" when None is the default for any argument. Also note that even +if the first line describing your argument type and its default gets long, you can't break it on several lines. You can +however write as many lines as you want in the indented description (see the example above with `input_ids`). + +#### Writing a multi-line code block + +Multi-line code blocks can be useful for displaying examples. They are done between two lines of three backticks as usual in Markdown: + + +```` +``` +# first line of code +# second line +# etc +``` +```` + +#### Writing a return block + +The return block should be introduced with the `Returns:` prefix, followed by a line return and an indentation. +The first line should be the type of the return, followed by a line return. No need to indent further for the elements +building the return. + +Here's an example of a single value return: + +``` + Returns: + `List[int]`: A list of integers in the range [0, 1] --- 1 for a special token, 0 for a sequence token. +``` + +Here's an example of a tuple return, comprising several objects: + +``` + Returns: + `tuple(torch.FloatTensor)` comprising various elements depending on the configuration ([`BertConfig`]) and inputs: + - ** loss** (*optional*, returned when `masked_lm_labels` is provided) `torch.FloatTensor` of shape `(1,)` -- + Total loss is the sum of the masked language modeling loss and the next sequence prediction (classification) loss. + - **prediction_scores** (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.vocab_size)`) -- + Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax). +``` + +#### Adding an image + +Due to the rapidly growing repository, it is important to make sure that no files that would significantly weigh down the repository are added. This includes images, videos, and other non-text files. We prefer to leverage a hf.co hosted `dataset` like +the ones hosted on [`hf-internal-testing`](https://huggingface.co/hf-internal-testing) in which to place these files and reference +them by URL. We recommend putting them in the following dataset: [huggingface/documentation-images](https://huggingface.co/datasets/huggingface/documentation-images). +If an external contribution, feel free to add the images to your PR and ask a Hugging Face member to migrate your images +to this dataset. + +## Styling the docstring + +We have an automatic script running with the `make style` command that will make sure that: +- the docstrings fully take advantage of the line width +- all code examples are formatted using black, like the code of the Transformers library + +This script may have some weird failures if you made a syntax mistake or if you uncover a bug. Therefore, it's +recommended to commit your changes before running `make style`, so you can revert the changes done by that script +easily. + diff --git a/diffusers/docs/TRANSLATING.md b/diffusers/docs/TRANSLATING.md new file mode 100644 index 0000000000000000000000000000000000000000..32cd95f2ade9ba90ed6a10b1c54169b26a79d01d --- /dev/null +++ b/diffusers/docs/TRANSLATING.md @@ -0,0 +1,57 @@ +### Translating the Diffusers documentation into your language + +As part of our mission to democratize machine learning, we'd love to make the Diffusers library available in many more languages! Follow the steps below if you want to help translate the documentation into your language 🙏. + +**🗞️ Open an issue** + +To get started, navigate to the [Issues](https://github.com/huggingface/diffusers/issues) page of this repo and check if anyone else has opened an issue for your language. If not, open a new issue by selecting the "Translation template" from the "New issue" button. + +Once an issue exists, post a comment to indicate which chapters you'd like to work on, and we'll add your name to the list. + + +**🍴 Fork the repository** + +First, you'll need to [fork the Diffusers repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo). You can do this by clicking on the **Fork** button on the top-right corner of this repo's page. + +Once you've forked the repo, you'll want to get the files on your local machine for editing. You can do that by cloning the fork with Git as follows: + +```bash +git clone https://github.com/YOUR-USERNAME/diffusers.git +``` + +**📋 Copy-paste the English version with a new language code** + +The documentation files are in one leading directory: + +- [`docs/source`](https://github.com/huggingface/diffusers/tree/main/docs/source): All the documentation materials are organized here by language. + +You'll only need to copy the files in the [`docs/source/en`](https://github.com/huggingface/diffusers/tree/main/docs/source/en) directory, so first navigate to your fork of the repo and run the following: + +```bash +cd ~/path/to/diffusers/docs +cp -r source/en source/LANG-ID +``` + +Here, `LANG-ID` should be one of the ISO 639-1 or ISO 639-2 language codes -- see [here](https://www.loc.gov/standards/iso639-2/php/code_list.php) for a handy table. + +**✍️ Start translating** + +The fun part comes - translating the text! + +The first thing we recommend is translating the part of the `_toctree.yml` file that corresponds to your doc chapter. This file is used to render the table of contents on the website. + +> 🙋 If the `_toctree.yml` file doesn't yet exist for your language, you can create one by copy-pasting from the English version and deleting the sections unrelated to your chapter. Just make sure it exists in the `docs/source/LANG-ID/` directory! + +The fields you should add are `local` (with the name of the file containing the translation; e.g. `autoclass_tutorial`), and `title` (with the title of the doc in your language; e.g. `Load pretrained instances with an AutoClass`) -- as a reference, here is the `_toctree.yml` for [English](https://github.com/huggingface/diffusers/blob/main/docs/source/en/_toctree.yml): + +```yaml +- sections: + - local: pipeline_tutorial # Do not change this! Use the same name for your .md file + title: Pipelines for inference # Translate this! + ... + title: Tutorials # Translate this! +``` + +Once you have translated the `_toctree.yml` file, you can start translating the [MDX](https://mdxjs.com/) files associated with your docs chapter. + +> 🙋 If you'd like others to help you with the translation, you should [open an issue](https://github.com/huggingface/diffusers/issues) and tag @patrickvonplaten. diff --git a/diffusers/docs/source/en/_toctree.yml b/diffusers/docs/source/en/_toctree.yml new file mode 100644 index 0000000000000000000000000000000000000000..f3175e9b7f8ac92e0069558c04fe5f50d399508f --- /dev/null +++ b/diffusers/docs/source/en/_toctree.yml @@ -0,0 +1,212 @@ +- sections: + - local: index + title: 🧨 Diffusers + - local: quicktour + title: Quicktour + - local: stable_diffusion + title: Stable Diffusion + - local: installation + title: Installation + title: Get started +- sections: + - sections: + - local: using-diffusers/loading + title: Loading Pipelines, Models, and Schedulers + - local: using-diffusers/schedulers + title: Using different Schedulers + - local: using-diffusers/configuration + title: Configuring Pipelines, Models, and Schedulers + - local: using-diffusers/custom_pipeline_overview + title: Loading and Adding Custom Pipelines + - local: using-diffusers/kerascv + title: Using KerasCV Stable Diffusion Checkpoints in Diffusers + title: Loading & Hub + - sections: + - local: using-diffusers/unconditional_image_generation + title: Unconditional Image Generation + - local: using-diffusers/conditional_image_generation + title: Text-to-Image Generation + - local: using-diffusers/img2img + title: Text-Guided Image-to-Image + - local: using-diffusers/inpaint + title: Text-Guided Image-Inpainting + - local: using-diffusers/depth2img + title: Text-Guided Depth-to-Image + - local: using-diffusers/reusing_seeds + title: Reusing seeds for deterministic generation + - local: using-diffusers/reproducibility + title: Reproducibility + - local: using-diffusers/custom_pipeline_examples + title: Community Pipelines + - local: using-diffusers/contribute_pipeline + title: How to contribute a Pipeline + - local: using-diffusers/using_safetensors + title: Using safetensors + title: Pipelines for Inference + - sections: + - local: using-diffusers/rl + title: Reinforcement Learning + - local: using-diffusers/audio + title: Audio + - local: using-diffusers/other-modalities + title: Other Modalities + title: Taking Diffusers Beyond Images + title: Using Diffusers +- sections: + - local: optimization/fp16 + title: Memory and Speed + - local: optimization/xformers + title: xFormers + - local: optimization/onnx + title: ONNX + - local: optimization/open_vino + title: OpenVINO + - local: optimization/mps + title: MPS + - local: optimization/habana + title: Habana Gaudi + title: Optimization/Special Hardware +- sections: + - local: training/overview + title: Overview + - local: training/unconditional_training + title: Unconditional Image Generation + - local: training/text_inversion + title: Textual Inversion + - local: training/dreambooth + title: Dreambooth + - local: training/text2image + title: Text-to-image fine-tuning + - local: training/lora + title: LoRA Support in Diffusers + title: Training +- sections: + - local: conceptual/philosophy + title: Philosophy + - local: conceptual/contribution + title: How to contribute? + - local: conceptual/ethical_guidelines + title: Diffusers' Ethical Guidelines + title: Conceptual Guides +- sections: + - sections: + - local: api/models + title: Models + - local: api/diffusion_pipeline + title: Diffusion Pipeline + - local: api/logging + title: Logging + - local: api/configuration + title: Configuration + - local: api/outputs + title: Outputs + - local: api/loaders + title: Loaders + title: Main Classes + - sections: + - local: api/pipelines/overview + title: Overview + - local: api/pipelines/alt_diffusion + title: AltDiffusion + - local: api/pipelines/audio_diffusion + title: Audio Diffusion + - local: api/pipelines/cycle_diffusion + title: Cycle Diffusion + - local: api/pipelines/dance_diffusion + title: Dance Diffusion + - local: api/pipelines/ddim + title: DDIM + - local: api/pipelines/ddpm + title: DDPM + - local: api/pipelines/dit + title: DiT + - local: api/pipelines/latent_diffusion + title: Latent Diffusion + - local: api/pipelines/paint_by_example + title: PaintByExample + - local: api/pipelines/pndm + title: PNDM + - local: api/pipelines/repaint + title: RePaint + - local: api/pipelines/stable_diffusion_safe + title: Safe Stable Diffusion + - local: api/pipelines/score_sde_ve + title: Score SDE VE + - sections: + - local: api/pipelines/stable_diffusion/overview + title: Overview + - local: api/pipelines/stable_diffusion/text2img + title: Text-to-Image + - local: api/pipelines/stable_diffusion/img2img + title: Image-to-Image + - local: api/pipelines/stable_diffusion/inpaint + title: Inpaint + - local: api/pipelines/stable_diffusion/depth2img + title: Depth-to-Image + - local: api/pipelines/stable_diffusion/image_variation + title: Image-Variation + - local: api/pipelines/stable_diffusion/upscale + title: Super-Resolution + - local: api/pipelines/stable_diffusion/latent_upscale + title: Stable-Diffusion-Latent-Upscaler + - local: api/pipelines/stable_diffusion/pix2pix + title: InstructPix2Pix + title: Stable Diffusion + - local: api/pipelines/stable_diffusion_2 + title: Stable Diffusion 2 + - local: api/pipelines/stochastic_karras_ve + title: Stochastic Karras VE + - local: api/pipelines/unclip + title: UnCLIP + - local: api/pipelines/latent_diffusion_uncond + title: Unconditional Latent Diffusion + - local: api/pipelines/versatile_diffusion + title: Versatile Diffusion + - local: api/pipelines/vq_diffusion + title: VQ Diffusion + title: Pipelines + - sections: + - local: api/schedulers/overview + title: Overview + - local: api/schedulers/ddim + title: DDIM + - local: api/schedulers/ddpm + title: DDPM + - local: api/schedulers/deis + title: DEIS + - local: api/schedulers/dpm_discrete + title: DPM Discrete Scheduler + - local: api/schedulers/dpm_discrete_ancestral + title: DPM Discrete Scheduler with ancestral sampling + - local: api/schedulers/euler_ancestral + title: Euler Ancestral Scheduler + - local: api/schedulers/euler + title: Euler scheduler + - local: api/schedulers/heun + title: Heun Scheduler + - local: api/schedulers/ipndm + title: IPNDM + - local: api/schedulers/lms_discrete + title: Linear Multistep + - local: api/schedulers/multistep_dpm_solver + title: Multistep DPM-Solver + - local: api/schedulers/pndm + title: PNDM + - local: api/schedulers/repaint + title: RePaint Scheduler + - local: api/schedulers/singlestep_dpm_solver + title: Singlestep DPM-Solver + - local: api/schedulers/stochastic_karras_ve + title: Stochastic Kerras VE + - local: api/schedulers/score_sde_ve + title: VE-SDE + - local: api/schedulers/score_sde_vp + title: VP-SDE + - local: api/schedulers/vq_diffusion + title: VQDiffusionScheduler + title: Schedulers + - sections: + - local: api/experimental/rl + title: RL Planning + title: Experimental Features + title: API diff --git a/diffusers/docs/source/en/api/configuration.mdx b/diffusers/docs/source/en/api/configuration.mdx new file mode 100644 index 0000000000000000000000000000000000000000..423c31f462b6deb087cc70b9b56232e347dad58b --- /dev/null +++ b/diffusers/docs/source/en/api/configuration.mdx @@ -0,0 +1,23 @@ + + +# Configuration + +In Diffusers, schedulers of type [`schedulers.scheduling_utils.SchedulerMixin`], and models of type [`ModelMixin`] inherit from [`ConfigMixin`] which conveniently takes care of storing all parameters that are +passed to the respective `__init__` methods in a JSON-configuration file. + +## ConfigMixin + +[[autodoc]] ConfigMixin + - load_config + - from_config + - save_config diff --git a/diffusers/docs/source/en/api/diffusion_pipeline.mdx b/diffusers/docs/source/en/api/diffusion_pipeline.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9b779564ae69955888c35e9c485b1ab230ced248 --- /dev/null +++ b/diffusers/docs/source/en/api/diffusion_pipeline.mdx @@ -0,0 +1,47 @@ + + +# Pipelines + +The [`DiffusionPipeline`] is the easiest way to load any pretrained diffusion pipeline from the [Hub](https://huggingface.co/models?library=diffusers) and to use it in inference. + + + + One should not use the Diffusion Pipeline class for training or fine-tuning a diffusion model. Individual + components of diffusion pipelines are usually trained individually, so we suggest to directly work + with [`UNetModel`] and [`UNetConditionModel`]. + + + +Any diffusion pipeline that is loaded with [`~DiffusionPipeline.from_pretrained`] will automatically +detect the pipeline type, *e.g.* [`StableDiffusionPipeline`] and consequently load each component of the +pipeline and pass them into the `__init__` function of the pipeline, *e.g.* [`~StableDiffusionPipeline.__init__`]. + +Any pipeline object can be saved locally with [`~DiffusionPipeline.save_pretrained`]. + +## DiffusionPipeline +[[autodoc]] DiffusionPipeline + - all + - __call__ + - device + - to + - components + +## ImagePipelineOutput +By default diffusion pipelines return an object of class + +[[autodoc]] pipelines.ImagePipelineOutput + +## AudioPipelineOutput +By default diffusion pipelines return an object of class + +[[autodoc]] pipelines.AudioPipelineOutput diff --git a/diffusers/docs/source/en/api/experimental/rl.mdx b/diffusers/docs/source/en/api/experimental/rl.mdx new file mode 100644 index 0000000000000000000000000000000000000000..65abb06e7523e0bebfdf6299afbeba2223994b32 --- /dev/null +++ b/diffusers/docs/source/en/api/experimental/rl.mdx @@ -0,0 +1,15 @@ + + +# TODO + +Coming soon! \ No newline at end of file diff --git a/diffusers/docs/source/en/api/loaders.mdx b/diffusers/docs/source/en/api/loaders.mdx new file mode 100644 index 0000000000000000000000000000000000000000..a93d9db1df88ac9e54f4fd32ad7b305ac57806e2 --- /dev/null +++ b/diffusers/docs/source/en/api/loaders.mdx @@ -0,0 +1,30 @@ + + +# Loaders + +There are many ways to train adapter neural networks for diffusion models, such as +- [Textual Inversion](./training/text_inversion.mdx) +- [LoRA](https://github.com/cloneofsimo/lora) +- [Hypernetworks](https://arxiv.org/abs/1609.09106) + +Such adapter neural networks often only consist of a fraction of the number of weights compared +to the pretrained model and as such are very portable. The Diffusers library offers an easy-to-use +API to load such adapter neural networks via the [`loaders.py` module](https://github.com/huggingface/diffusers/blob/main/src/diffusers/loaders.py). + +**Note**: This module is still highly experimental and prone to future changes. + +## LoaderMixins + +### UNet2DConditionLoadersMixin + +[[autodoc]] loaders.UNet2DConditionLoadersMixin diff --git a/diffusers/docs/source/en/api/logging.mdx b/diffusers/docs/source/en/api/logging.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b726d46d92a1cf71a5c5a55155129103ca437dad --- /dev/null +++ b/diffusers/docs/source/en/api/logging.mdx @@ -0,0 +1,98 @@ + + +# Logging + +🧨 Diffusers has a centralized logging system, so that you can setup the verbosity of the library easily. + +Currently the default verbosity of the library is `WARNING`. + +To change the level of verbosity, just use one of the direct setters. For instance, here is how to change the verbosity +to the INFO level. + +```python +import diffusers + +diffusers.logging.set_verbosity_info() +``` + +You can also use the environment variable `DIFFUSERS_VERBOSITY` to override the default verbosity. You can set it +to one of the following: `debug`, `info`, `warning`, `error`, `critical`. For example: + +```bash +DIFFUSERS_VERBOSITY=error ./myprogram.py +``` + +Additionally, some `warnings` can be disabled by setting the environment variable +`DIFFUSERS_NO_ADVISORY_WARNINGS` to a true value, like *1*. This will disable any warning that is logged using +[`logger.warning_advice`]. For example: + +```bash +DIFFUSERS_NO_ADVISORY_WARNINGS=1 ./myprogram.py +``` + +Here is an example of how to use the same logger as the library in your own module or script: + +```python +from diffusers.utils import logging + +logging.set_verbosity_info() +logger = logging.get_logger("diffusers") +logger.info("INFO") +logger.warning("WARN") +``` + + +All the methods of this logging module are documented below, the main ones are +[`logging.get_verbosity`] to get the current level of verbosity in the logger and +[`logging.set_verbosity`] to set the verbosity to the level of your choice. In order (from the least +verbose to the most verbose), those levels (with their corresponding int values in parenthesis) are: + +- `diffusers.logging.CRITICAL` or `diffusers.logging.FATAL` (int value, 50): only report the most + critical errors. +- `diffusers.logging.ERROR` (int value, 40): only report errors. +- `diffusers.logging.WARNING` or `diffusers.logging.WARN` (int value, 30): only reports error and + warnings. This the default level used by the library. +- `diffusers.logging.INFO` (int value, 20): reports error, warnings and basic information. +- `diffusers.logging.DEBUG` (int value, 10): report all information. + +By default, `tqdm` progress bars will be displayed during model download. [`logging.disable_progress_bar`] and [`logging.enable_progress_bar`] can be used to suppress or unsuppress this behavior. + +## Base setters + +[[autodoc]] logging.set_verbosity_error + +[[autodoc]] logging.set_verbosity_warning + +[[autodoc]] logging.set_verbosity_info + +[[autodoc]] logging.set_verbosity_debug + +## Other functions + +[[autodoc]] logging.get_verbosity + +[[autodoc]] logging.set_verbosity + +[[autodoc]] logging.get_logger + +[[autodoc]] logging.enable_default_handler + +[[autodoc]] logging.disable_default_handler + +[[autodoc]] logging.enable_explicit_format + +[[autodoc]] logging.reset_format + +[[autodoc]] logging.enable_progress_bar + +[[autodoc]] logging.disable_progress_bar diff --git a/diffusers/docs/source/en/api/models.mdx b/diffusers/docs/source/en/api/models.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c709c2a93802e346b14b6b07701929a2d2500b4c --- /dev/null +++ b/diffusers/docs/source/en/api/models.mdx @@ -0,0 +1,83 @@ + + +# Models + +Diffusers contains pretrained models for popular algorithms and modules for creating the next set of diffusion models. +The primary function of these models is to denoise an input sample, by modeling the distribution $p_\theta(\mathbf{x}_{t-1}|\mathbf{x}_t)$. +The models are built on the base class ['ModelMixin'] that is a `torch.nn.module` with basic functionality for saving and loading models both locally and from the HuggingFace hub. + +## ModelMixin +[[autodoc]] ModelMixin + +## UNet2DOutput +[[autodoc]] models.unet_2d.UNet2DOutput + +## UNet2DModel +[[autodoc]] UNet2DModel + +## UNet1DOutput +[[autodoc]] models.unet_1d.UNet1DOutput + +## UNet1DModel +[[autodoc]] UNet1DModel + +## UNet2DConditionOutput +[[autodoc]] models.unet_2d_condition.UNet2DConditionOutput + +## UNet2DConditionModel +[[autodoc]] UNet2DConditionModel + +## DecoderOutput +[[autodoc]] models.vae.DecoderOutput + +## VQEncoderOutput +[[autodoc]] models.vq_model.VQEncoderOutput + +## VQModel +[[autodoc]] VQModel + +## AutoencoderKLOutput +[[autodoc]] models.autoencoder_kl.AutoencoderKLOutput + +## AutoencoderKL +[[autodoc]] AutoencoderKL + +## Transformer2DModel +[[autodoc]] Transformer2DModel + +## Transformer2DModelOutput +[[autodoc]] models.transformer_2d.Transformer2DModelOutput + +## PriorTransformer +[[autodoc]] models.prior_transformer.PriorTransformer + +## PriorTransformerOutput +[[autodoc]] models.prior_transformer.PriorTransformerOutput + +## FlaxModelMixin +[[autodoc]] FlaxModelMixin + +## FlaxUNet2DConditionOutput +[[autodoc]] models.unet_2d_condition_flax.FlaxUNet2DConditionOutput + +## FlaxUNet2DConditionModel +[[autodoc]] FlaxUNet2DConditionModel + +## FlaxDecoderOutput +[[autodoc]] models.vae_flax.FlaxDecoderOutput + +## FlaxAutoencoderKLOutput +[[autodoc]] models.vae_flax.FlaxAutoencoderKLOutput + +## FlaxAutoencoderKL +[[autodoc]] FlaxAutoencoderKL diff --git a/diffusers/docs/source/en/api/outputs.mdx b/diffusers/docs/source/en/api/outputs.mdx new file mode 100644 index 0000000000000000000000000000000000000000..291a79756a169c4385a4ff0c76c399046f9ee5bc --- /dev/null +++ b/diffusers/docs/source/en/api/outputs.mdx @@ -0,0 +1,55 @@ + + +# BaseOutputs + +All models have outputs that are instances of subclasses of [`~utils.BaseOutput`]. Those are +data structures containing all the information returned by the model, but that can also be used as tuples or +dictionaries. + +Let's see how this looks in an example: + +```python +from diffusers import DDIMPipeline + +pipeline = DDIMPipeline.from_pretrained("google/ddpm-cifar10-32") +outputs = pipeline() +``` + +The `outputs` object is a [`~pipelines.ImagePipelineOutput`], as we can see in the +documentation of that class below, it means it has an image attribute. + +You can access each attribute as you would usually do, and if that attribute has not been returned by the model, you will get `None`: + +```python +outputs.images +``` + +or via keyword lookup + +```python +outputs["images"] +``` + +When considering our `outputs` object as tuple, it only considers the attributes that don't have `None` values. +Here for instance, we could retrieve images via indexing: + +```python +outputs[:1] +``` + +which will return the tuple `(outputs.images)` for instance. + +## BaseOutput + +[[autodoc]] utils.BaseOutput + - to_tuple diff --git a/diffusers/docs/source/en/api/pipelines/alt_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/alt_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..95bc80b1365c540695c5ebc0fecc7cdee918ca1e --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/alt_diffusion.mdx @@ -0,0 +1,83 @@ + + +# AltDiffusion + +AltDiffusion was proposed in [AltCLIP: Altering the Language Encoder in CLIP for Extended Language Capabilities](https://arxiv.org/abs/2211.06679) by Zhongzhi Chen, Guang Liu, Bo-Wen Zhang, Fulong Ye, Qinghong Yang, Ledell Wu + +The abstract of the paper is the following: + +*In this work, we present a conceptually simple and effective method to train a strong bilingual multimodal representation model. Starting from the pretrained multimodal representation model CLIP released by OpenAI, we switched its text encoder with a pretrained multilingual text encoder XLM-R, and aligned both languages and image representations by a two-stage training schema consisting of teacher learning and contrastive learning. We validate our method through evaluations of a wide range of tasks. We set new state-of-the-art performances on a bunch of tasks including ImageNet-CN, Flicker30k- CN, and COCO-CN. Further, we obtain very close performances with CLIP on almost all tasks, suggesting that one can simply alter the text encoder in CLIP for extended capabilities such as multilingual understanding.* + + +*Overview*: + +| Pipeline | Tasks | Colab | Demo +|---|---|:---:|:---:| +| [pipeline_alt_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion.py) | *Text-to-Image Generation* | - | - +| [pipeline_alt_diffusion_img2img.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion_img2img.py) | *Image-to-Image Text-Guided Generation* | - |- + +## Tips + +- AltDiffusion is conceptually exaclty the same as [Stable Diffusion](./api/pipelines/stable_diffusion/overview). + +- *Run AltDiffusion* + +AltDiffusion can be tested very easily with the [`AltDiffusionPipeline`], [`AltDiffusionImg2ImgPipeline`] and the `"BAAI/AltDiffusion-m9"` checkpoint exactly in the same way it is shown in the [Conditional Image Generation Guide](./using-diffusers/conditional_image_generation) and the [Image-to-Image Generation Guide](./using-diffusers/img2img). + +- *How to load and use different schedulers.* + +The alt diffusion pipeline uses [`DDIMScheduler`] scheduler by default. But `diffusers` provides many other schedulers that can be used with the alt diffusion pipeline such as [`PNDMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], [`EulerAncestralDiscreteScheduler`] etc. +To use a different scheduler, you can either change it via the [`ConfigMixin.from_config`] method or pass the `scheduler` argument to the `from_pretrained` method of the pipeline. For example, to use the [`EulerDiscreteScheduler`], you can do the following: + +```python +>>> from diffusers import AltDiffusionPipeline, EulerDiscreteScheduler + +>>> pipeline = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion-m9") +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +>>> # or +>>> euler_scheduler = EulerDiscreteScheduler.from_pretrained("BAAI/AltDiffusion-m9", subfolder="scheduler") +>>> pipeline = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion-m9", scheduler=euler_scheduler) +``` + + +- *How to convert all use cases with multiple or single pipeline* + +If you want to use all possible use cases in a single `DiffusionPipeline` we recommend using the `components` functionality to instantiate all components in the most memory-efficient way: + +```python +>>> from diffusers import ( +... AltDiffusionPipeline, +... AltDiffusionImg2ImgPipeline, +... ) + +>>> text2img = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion-m9") +>>> img2img = AltDiffusionImg2ImgPipeline(**text2img.components) + +>>> # now you can use text2img(...) and img2img(...) just like the call methods of each respective pipeline +``` + +## AltDiffusionPipelineOutput +[[autodoc]] pipelines.alt_diffusion.AltDiffusionPipelineOutput + - all + - __call__ + +## AltDiffusionPipeline +[[autodoc]] AltDiffusionPipeline + - all + - __call__ + +## AltDiffusionImg2ImgPipeline +[[autodoc]] AltDiffusionImg2ImgPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/audio_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/audio_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ec9b1fb2d3043a6363920aa6cd1c556fcc762b32 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/audio_diffusion.mdx @@ -0,0 +1,98 @@ + + +# Audio Diffusion + +## Overview + +[Audio Diffusion](https://github.com/teticio/audio-diffusion) by Robert Dargavel Smith. + +Audio Diffusion leverages the recent advances in image generation using diffusion models by converting audio samples to +and from mel spectrogram images. + +The original codebase of this implementation can be found [here](https://github.com/teticio/audio-diffusion), including +training scripts and example notebooks. + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_audio_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/audio_diffusion/pipeline_audio_diffusion.py) | *Unconditional Audio Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/teticio/audio-diffusion/blob/master/notebooks/audio_diffusion_pipeline.ipynb) | + + +## Examples: + +### Audio Diffusion + +```python +import torch +from IPython.display import Audio +from diffusers import DiffusionPipeline + +device = "cuda" if torch.cuda.is_available() else "cpu" +pipe = DiffusionPipeline.from_pretrained("teticio/audio-diffusion-256").to(device) + +output = pipe() +display(output.images[0]) +display(Audio(output.audios[0], rate=mel.get_sample_rate())) +``` + +### Latent Audio Diffusion + +```python +import torch +from IPython.display import Audio +from diffusers import DiffusionPipeline + +device = "cuda" if torch.cuda.is_available() else "cpu" +pipe = DiffusionPipeline.from_pretrained("teticio/latent-audio-diffusion-256").to(device) + +output = pipe() +display(output.images[0]) +display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate())) +``` + +### Audio Diffusion with DDIM (faster) + +```python +import torch +from IPython.display import Audio +from diffusers import DiffusionPipeline + +device = "cuda" if torch.cuda.is_available() else "cpu" +pipe = DiffusionPipeline.from_pretrained("teticio/audio-diffusion-ddim-256").to(device) + +output = pipe() +display(output.images[0]) +display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate())) +``` + +### Variations, in-painting, out-painting etc. + +```python +output = pipe( + raw_audio=output.audios[0, 0], + start_step=int(pipe.get_default_steps() / 2), + mask_start_secs=1, + mask_end_secs=1, +) +display(output.images[0]) +display(Audio(output.audios[0], rate=pipe.mel.get_sample_rate())) +``` + +## AudioDiffusionPipeline +[[autodoc]] AudioDiffusionPipeline + - all + - __call__ + +## Mel +[[autodoc]] Mel diff --git a/diffusers/docs/source/en/api/pipelines/cycle_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/cycle_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..70986bd39a3d890122b0fd4ebd4a989f59f13b48 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/cycle_diffusion.mdx @@ -0,0 +1,100 @@ + + +# Cycle Diffusion + +## Overview + +Cycle Diffusion is a Text-Guided Image-to-Image Generation model proposed in [Unifying Diffusion Models' Latent Space, with Applications to CycleDiffusion and Guidance](https://arxiv.org/abs/2210.05559) by Chen Henry Wu, Fernando De la Torre. + +The abstract of the paper is the following: + +*Diffusion models have achieved unprecedented performance in generative modeling. The commonly-adopted formulation of the latent code of diffusion models is a sequence of gradually denoised samples, as opposed to the simpler (e.g., Gaussian) latent space of GANs, VAEs, and normalizing flows. This paper provides an alternative, Gaussian formulation of the latent space of various diffusion models, as well as an invertible DPM-Encoder that maps images into the latent space. While our formulation is purely based on the definition of diffusion models, we demonstrate several intriguing consequences. (1) Empirically, we observe that a common latent space emerges from two diffusion models trained independently on related domains. In light of this finding, we propose CycleDiffusion, which uses DPM-Encoder for unpaired image-to-image translation. Furthermore, applying CycleDiffusion to text-to-image diffusion models, we show that large-scale text-to-image diffusion models can be used as zero-shot image-to-image editors. (2) One can guide pre-trained diffusion models and GANs by controlling the latent codes in a unified, plug-and-play formulation based on energy-based models. Using the CLIP model and a face recognition model as guidance, we demonstrate that diffusion models have better coverage of low-density sub-populations and individuals than GANs.* + +*Tips*: +- The Cycle Diffusion pipeline is fully compatible with any [Stable Diffusion](./stable_diffusion) checkpoints +- Currently Cycle Diffusion only works with the [`DDIMScheduler`]. + +*Example*: + +In the following we should how to best use the [`CycleDiffusionPipeline`] + +```python +import requests +import torch +from PIL import Image +from io import BytesIO + +from diffusers import CycleDiffusionPipeline, DDIMScheduler + +# load the pipeline +# make sure you're logged in with `huggingface-cli login` +model_id_or_path = "CompVis/stable-diffusion-v1-4" +scheduler = DDIMScheduler.from_pretrained(model_id_or_path, subfolder="scheduler") +pipe = CycleDiffusionPipeline.from_pretrained(model_id_or_path, scheduler=scheduler).to("cuda") + +# let's download an initial image +url = "https://raw.githubusercontent.com/ChenWu98/cycle-diffusion/main/data/dalle2/An%20astronaut%20riding%20a%20horse.png" +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((512, 512)) +init_image.save("horse.png") + +# let's specify a prompt +source_prompt = "An astronaut riding a horse" +prompt = "An astronaut riding an elephant" + +# call the pipeline +image = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.8, + guidance_scale=2, + source_guidance_scale=1, +).images[0] + +image.save("horse_to_elephant.png") + +# let's try another example +# See more samples at the original repo: https://github.com/ChenWu98/cycle-diffusion +url = "https://raw.githubusercontent.com/ChenWu98/cycle-diffusion/main/data/dalle2/A%20black%20colored%20car.png" +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((512, 512)) +init_image.save("black.png") + +source_prompt = "A black colored car" +prompt = "A blue colored car" + +# call the pipeline +torch.manual_seed(0) +image = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.85, + guidance_scale=3, + source_guidance_scale=1, +).images[0] + +image.save("black_to_blue.png") +``` + +## CycleDiffusionPipeline +[[autodoc]] CycleDiffusionPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/dance_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/dance_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8264de7db6037ff6d7d10ff8fbb2858d80b594da --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/dance_diffusion.mdx @@ -0,0 +1,34 @@ + + +# Dance Diffusion + +## Overview + +[Dance Diffusion](https://github.com/Harmonai-org/sample-generator) by Zach Evans. + +Dance Diffusion is the first in a suite of generative audio tools for producers and musicians to be released by Harmonai. +For more info or to get involved in the development of these tools, please visit https://harmonai.org and fill out the form on the front page. + +The original codebase of this implementation can be found [here](https://github.com/Harmonai-org/sample-generator). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_dance_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/dance_diffusion/pipeline_dance_diffusion.py) | *Unconditional Audio Generation* | - | + + +## DanceDiffusionPipeline +[[autodoc]] DanceDiffusionPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/ddim.mdx b/diffusers/docs/source/en/api/pipelines/ddim.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b1dfa3b056a8096f2343f14ba813c2680d57602d --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/ddim.mdx @@ -0,0 +1,36 @@ + + +# DDIM + +## Overview + +[Denoising Diffusion Implicit Models](https://arxiv.org/abs/2010.02502) (DDIM) by Jiaming Song, Chenlin Meng and Stefano Ermon. + +The abstract of the paper is the following: + +Denoising diffusion probabilistic models (DDPMs) have achieved high quality image generation without adversarial training, yet they require simulating a Markov chain for many steps to produce a sample. To accelerate sampling, we present denoising diffusion implicit models (DDIMs), a more efficient class of iterative implicit probabilistic models with the same training procedure as DDPMs. In DDPMs, the generative process is defined as the reverse of a Markovian diffusion process. We construct a class of non-Markovian diffusion processes that lead to the same training objective, but whose reverse process can be much faster to sample from. We empirically demonstrate that DDIMs can produce high quality samples 10× to 50× faster in terms of wall-clock time compared to DDPMs, allow us to trade off computation for sample quality, and can perform semantically meaningful image interpolation directly in the latent space. + +The original codebase of this paper can be found here: [ermongroup/ddim](https://github.com/ermongroup/ddim). +For questions, feel free to contact the author on [tsong.me](https://tsong.me/). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_ddim.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/ddim/pipeline_ddim.py) | *Unconditional Image Generation* | - | + + +## DDIMPipeline +[[autodoc]] DDIMPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/ddpm.mdx b/diffusers/docs/source/en/api/pipelines/ddpm.mdx new file mode 100644 index 0000000000000000000000000000000000000000..92cee580d15237ebe755de8ba1b8f395dc6a416c --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/ddpm.mdx @@ -0,0 +1,37 @@ + + +# DDPM + +## Overview + +[Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2006.11239) + (DDPM) by Jonathan Ho, Ajay Jain and Pieter Abbeel proposes the diffusion based model of the same name, but in the context of the 🤗 Diffusers library, DDPM refers to the discrete denoising scheduler from the paper as well as the pipeline. + +The abstract of the paper is the following: + +We present high quality image synthesis results using diffusion probabilistic models, a class of latent variable models inspired by considerations from nonequilibrium thermodynamics. Our best results are obtained by training on a weighted variational bound designed according to a novel connection between diffusion probabilistic models and denoising score matching with Langevin dynamics, and our models naturally admit a progressive lossy decompression scheme that can be interpreted as a generalization of autoregressive decoding. On the unconditional CIFAR10 dataset, we obtain an Inception score of 9.46 and a state-of-the-art FID score of 3.17. On 256x256 LSUN, we obtain sample quality similar to ProgressiveGAN. + +The original codebase of this paper can be found [here](https://github.com/hojonathanho/diffusion). + + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_ddpm.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/ddpm/pipeline_ddpm.py) | *Unconditional Image Generation* | - | + + +# DDPMPipeline +[[autodoc]] DDPMPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/dit.mdx b/diffusers/docs/source/en/api/pipelines/dit.mdx new file mode 100644 index 0000000000000000000000000000000000000000..bcc2c993fc20658a2efd662049916ae79de123d3 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/dit.mdx @@ -0,0 +1,59 @@ + + +# Scalable Diffusion Models with Transformers (DiT) + +## Overview + +[Scalable Diffusion Models with Transformers](https://arxiv.org/abs/2212.09748) (DiT) by William Peebles and Saining Xie. + +The abstract of the paper is the following: + +*We explore a new class of diffusion models based on the transformer architecture. We train latent diffusion models of images, replacing the commonly-used U-Net backbone with a transformer that operates on latent patches. We analyze the scalability of our Diffusion Transformers (DiTs) through the lens of forward pass complexity as measured by Gflops. We find that DiTs with higher Gflops -- through increased transformer depth/width or increased number of input tokens -- consistently have lower FID. In addition to possessing good scalability properties, our largest DiT-XL/2 models outperform all prior diffusion models on the class-conditional ImageNet 512x512 and 256x256 benchmarks, achieving a state-of-the-art FID of 2.27 on the latter.* + +The original codebase of this paper can be found here: [facebookresearch/dit](https://github.com/facebookresearch/dit). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_dit.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/dit/pipeline_dit.py) | *Conditional Image Generation* | - | + + +## Usage example + +```python +from diffusers import DiTPipeline, DPMSolverMultistepScheduler +import torch + +pipe = DiTPipeline.from_pretrained("facebook/DiT-XL-2-256", torch_dtype=torch.float16) +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +# pick words from Imagenet class labels +pipe.labels # to print all available words + +# pick words that exist in ImageNet +words = ["white shark", "umbrella"] + +class_ids = pipe.get_label_ids(words) + +generator = torch.manual_seed(33) +output = pipe(class_labels=class_ids, num_inference_steps=25, generator=generator) + +image = output.images[0] # label 'white shark' +``` + +## DiTPipeline +[[autodoc]] DiTPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/latent_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/latent_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..475957d93cd88fd969cf6cee4ac4de97567c63ed --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/latent_diffusion.mdx @@ -0,0 +1,49 @@ + + +# Latent Diffusion + +## Overview + +Latent Diffusion was proposed in [High-Resolution Image Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) by Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, Björn Ommer. + +The abstract of the paper is the following: + +*By decomposing the image formation process into a sequential application of denoising autoencoders, diffusion models (DMs) achieve state-of-the-art synthesis results on image data and beyond. Additionally, their formulation allows for a guiding mechanism to control the image generation process without retraining. However, since these models typically operate directly in pixel space, optimization of powerful DMs often consumes hundreds of GPU days and inference is expensive due to sequential evaluations. To enable DM training on limited computational resources while retaining their quality and flexibility, we apply them in the latent space of powerful pretrained autoencoders. In contrast to previous work, training diffusion models on such a representation allows for the first time to reach a near-optimal point between complexity reduction and detail preservation, greatly boosting visual fidelity. By introducing cross-attention layers into the model architecture, we turn diffusion models into powerful and flexible generators for general conditioning inputs such as text or bounding boxes and high-resolution synthesis becomes possible in a convolutional manner. Our latent diffusion models (LDMs) achieve a new state of the art for image inpainting and highly competitive performance on various tasks, including unconditional image generation, semantic scene synthesis, and super-resolution, while significantly reducing computational requirements compared to pixel-based DMs.* + +The original codebase can be found [here](https://github.com/CompVis/latent-diffusion). + +## Tips: + +- +- +- + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_latent_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py) | *Text-to-Image Generation* | - | +| [pipeline_latent_diffusion_superresolution.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion_superresolution.py) | *Super Resolution* | - | + +## Examples: + + +## LDMTextToImagePipeline +[[autodoc]] LDMTextToImagePipeline + - all + - __call__ + +## LDMSuperResolutionPipeline +[[autodoc]] LDMSuperResolutionPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/latent_diffusion_uncond.mdx b/diffusers/docs/source/en/api/pipelines/latent_diffusion_uncond.mdx new file mode 100644 index 0000000000000000000000000000000000000000..03f1f31cee5dcd2238342a56dcbdba1e84666d44 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/latent_diffusion_uncond.mdx @@ -0,0 +1,42 @@ + + +# Unconditional Latent Diffusion + +## Overview + +Unconditional Latent Diffusion was proposed in [High-Resolution Image Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) by Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, Björn Ommer. + +The abstract of the paper is the following: + +*By decomposing the image formation process into a sequential application of denoising autoencoders, diffusion models (DMs) achieve state-of-the-art synthesis results on image data and beyond. Additionally, their formulation allows for a guiding mechanism to control the image generation process without retraining. However, since these models typically operate directly in pixel space, optimization of powerful DMs often consumes hundreds of GPU days and inference is expensive due to sequential evaluations. To enable DM training on limited computational resources while retaining their quality and flexibility, we apply them in the latent space of powerful pretrained autoencoders. In contrast to previous work, training diffusion models on such a representation allows for the first time to reach a near-optimal point between complexity reduction and detail preservation, greatly boosting visual fidelity. By introducing cross-attention layers into the model architecture, we turn diffusion models into powerful and flexible generators for general conditioning inputs such as text or bounding boxes and high-resolution synthesis becomes possible in a convolutional manner. Our latent diffusion models (LDMs) achieve a new state of the art for image inpainting and highly competitive performance on various tasks, including unconditional image generation, semantic scene synthesis, and super-resolution, while significantly reducing computational requirements compared to pixel-based DMs.* + +The original codebase can be found [here](https://github.com/CompVis/latent-diffusion). + +## Tips: + +- +- +- + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_latent_diffusion_uncond.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion_uncond/pipeline_latent_diffusion_uncond.py) | *Unconditional Image Generation* | - | + +## Examples: + +## LDMPipeline +[[autodoc]] LDMPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/overview.mdx b/diffusers/docs/source/en/api/pipelines/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..fa2968351345b30805eaf5d595dd85737027a8e1 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/overview.mdx @@ -0,0 +1,200 @@ + + +# Pipelines + +Pipelines provide a simple way to run state-of-the-art diffusion models in inference. +Most diffusion systems consist of multiple independently-trained models and highly adaptable scheduler +components - all of which are needed to have a functioning end-to-end diffusion system. + +As an example, [Stable Diffusion](https://huggingface.co/blog/stable_diffusion) has three independently trained models: +- [Autoencoder](./api/models#vae) +- [Conditional Unet](./api/models#UNet2DConditionModel) +- [CLIP text encoder](https://huggingface.co/docs/transformers/v4.21.2/en/model_doc/clip#transformers.CLIPTextModel) +- a scheduler component, [scheduler](./api/scheduler#pndm), +- a [CLIPFeatureExtractor](https://huggingface.co/docs/transformers/v4.21.2/en/model_doc/clip#transformers.CLIPFeatureExtractor), +- as well as a [safety checker](./stable_diffusion#safety_checker). +All of these components are necessary to run stable diffusion in inference even though they were trained +or created independently from each other. + +To that end, we strive to offer all open-sourced, state-of-the-art diffusion system under a unified API. +More specifically, we strive to provide pipelines that +- 1. can load the officially published weights and yield 1-to-1 the same outputs as the original implementation according to the corresponding paper (*e.g.* [LDMTextToImagePipeline](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/latent_diffusion), uses the officially released weights of [High-Resolution Image Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752)), +- 2. have a simple user interface to run the model in inference (see the [Pipelines API](#pipelines-api) section), +- 3. are easy to understand with code that is self-explanatory and can be read along-side the official paper (see [Pipelines summary](#pipelines-summary)), +- 4. can easily be contributed by the community (see the [Contribution](#contribution) section). + +**Note** that pipelines do not (and should not) offer any training functionality. +If you are looking for *official* training examples, please have a look at [examples](https://github.com/huggingface/diffusers/tree/main/examples). + +## 🧨 Diffusers Summary + +The following table summarizes all officially supported pipelines, their corresponding paper, and if +available a colab notebook to directly try them out. + + +| Pipeline | Paper | Tasks | Colab +|---|---|:---:|:---:| +| [alt_diffusion](./alt_diffusion) | [**AltDiffusion**](https://arxiv.org/abs/2211.06679) | Image-to-Image Text-Guided Generation | - +| [audio_diffusion](./audio_diffusion) | [**Audio Diffusion**](https://github.com/teticio/audio_diffusion.git) | Unconditional Audio Generation | +| [cycle_diffusion](./cycle_diffusion) | [**Cycle Diffusion**](https://arxiv.org/abs/2210.05559) | Image-to-Image Text-Guided Generation | +| [dance_diffusion](./dance_diffusion) | [**Dance Diffusion**](https://github.com/williamberman/diffusers.git) | Unconditional Audio Generation | +| [ddpm](./ddpm) | [**Denoising Diffusion Probabilistic Models**](https://arxiv.org/abs/2006.11239) | Unconditional Image Generation | +| [ddim](./ddim) | [**Denoising Diffusion Implicit Models**](https://arxiv.org/abs/2010.02502) | Unconditional Image Generation | +| [latent_diffusion](./latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Text-to-Image Generation | +| [latent_diffusion](./latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Super Resolution Image-to-Image | +| [latent_diffusion_uncond](./latent_diffusion_uncond) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752) | Unconditional Image Generation | +| [paint_by_example](./paint_by_example) | [**Paint by Example: Exemplar-based Image Editing with Diffusion Models**](https://arxiv.org/abs/2211.13227) | Image-Guided Image Inpainting | +| [pndm](./pndm) | [**Pseudo Numerical Methods for Diffusion Models on Manifolds**](https://arxiv.org/abs/2202.09778) | Unconditional Image Generation | +| [score_sde_ve](./score_sde_ve) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [score_sde_vp](./score_sde_vp) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [stable_diffusion](./stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-to-Image Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [stable_diffusion](./stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Image-to-Image Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) +| [stable_diffusion](./stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-Guided Image Inpainting | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) +| [stable_diffusion_2](./stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-to-Image Generation | +| [stable_diffusion_2](./stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Image Inpainting | +| [stable_diffusion_2](./stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Super Resolution Image-to-Image | +| [stable_diffusion_safe](./stable_diffusion_safe) | [**Safe Stable Diffusion**](https://arxiv.org/abs/2211.05105) | Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ml-research/safe-latent-diffusion/blob/main/examples/Safe%20Latent%20Diffusion.ipynb) +| [stochastic_karras_ve](./stochastic_karras_ve) | [**Elucidating the Design Space of Diffusion-Based Generative Models**](https://arxiv.org/abs/2206.00364) | Unconditional Image Generation | +| [unclip](./unclip) | [Hierarchical Text-Conditional Image Generation with CLIP Latents](https://arxiv.org/abs/2204.06125) | Text-to-Image Generation | +| [versatile_diffusion](./versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Text-to-Image Generation | +| [versatile_diffusion](./versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Image Variations Generation | +| [versatile_diffusion](./versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Dual Image and Text Guided Generation | +| [vq_diffusion](./vq_diffusion) | [Vector Quantized Diffusion Model for Text-to-Image Synthesis](https://arxiv.org/abs/2111.14822) | Text-to-Image Generation | + + +**Note**: Pipelines are simple examples of how to play around with the diffusion systems as described in the corresponding papers. + +However, most of them can be adapted to use different scheduler components or even different model components. Some pipeline examples are shown in the [Examples](#examples) below. + +## Pipelines API + +Diffusion models often consist of multiple independently-trained models or other previously existing components. + + +Each model has been trained independently on a different task and the scheduler can easily be swapped out and replaced with a different one. +During inference, we however want to be able to easily load all components and use them in inference - even if one component, *e.g.* CLIP's text encoder, originates from a different library, such as [Transformers](https://github.com/huggingface/transformers). To that end, all pipelines provide the following functionality: + +- [`from_pretrained` method](../diffusion_pipeline) that accepts a Hugging Face Hub repository id, *e.g.* [runwayml/stable-diffusion-v1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5) or a path to a local directory, *e.g.* +"./stable-diffusion". To correctly retrieve which models and components should be loaded, one has to provide a `model_index.json` file, *e.g.* [runwayml/stable-diffusion-v1-5/model_index.json](https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/model_index.json), which defines all components that should be +loaded into the pipelines. More specifically, for each model/component one needs to define the format `: ["", ""]`. `` is the attribute name given to the loaded instance of `` which can be found in the library or pipeline folder called `""`. +- [`save_pretrained`](../diffusion_pipeline) that accepts a local path, *e.g.* `./stable-diffusion` under which all models/components of the pipeline will be saved. For each component/model a folder is created inside the local path that is named after the given attribute name, *e.g.* `./stable_diffusion/unet`. +In addition, a `model_index.json` file is created at the root of the local path, *e.g.* `./stable_diffusion/model_index.json` so that the complete pipeline can again be instantiated +from the local path. +- [`to`](../diffusion_pipeline) which accepts a `string` or `torch.device` to move all models that are of type `torch.nn.Module` to the passed device. The behavior is fully analogous to [PyTorch's `to` method](https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.to). +- [`__call__`] method to use the pipeline in inference. `__call__` defines inference logic of the pipeline and should ideally encompass all aspects of it, from pre-processing to forwarding tensors to the different models and schedulers, as well as post-processing. The API of the `__call__` method can strongly vary from pipeline to pipeline. *E.g.* a text-to-image pipeline, such as [`StableDiffusionPipeline`](./stable_diffusion) should accept among other things the text prompt to generate the image. A pure image generation pipeline, such as [DDPMPipeline](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/ddpm) on the other hand can be run without providing any inputs. To better understand what inputs can be adapted for +each pipeline, one should look directly into the respective pipeline. + +**Note**: All pipelines have PyTorch's autograd disabled by decorating the `__call__` method with a [`torch.no_grad`](https://pytorch.org/docs/stable/generated/torch.no_grad.html) decorator because pipelines should +not be used for training. If you want to store the gradients during the forward pass, we recommend writing your own pipeline, see also our [community-examples](https://github.com/huggingface/diffusers/tree/main/examples/community) + +## Contribution + +We are more than happy about any contribution to the officially supported pipelines 🤗. We aspire +all of our pipelines to be **self-contained**, **easy-to-tweak**, **beginner-friendly** and for **one-purpose-only**. + +- **Self-contained**: A pipeline shall be as self-contained as possible. More specifically, this means that all functionality should be either directly defined in the pipeline file itself, should be inherited from (and only from) the [`DiffusionPipeline` class](.../diffusion_pipeline) or be directly attached to the model and scheduler components of the pipeline. +- **Easy-to-use**: Pipelines should be extremely easy to use - one should be able to load the pipeline and +use it for its designated task, *e.g.* text-to-image generation, in just a couple of lines of code. Most +logic including pre-processing, an unrolled diffusion loop, and post-processing should all happen inside the `__call__` method. +- **Easy-to-tweak**: Certain pipelines will not be able to handle all use cases and tasks that you might like them to. If you want to use a certain pipeline for a specific use case that is not yet supported, you might have to copy the pipeline file and tweak the code to your needs. We try to make the pipeline code as readable as possible so that each part –from pre-processing to diffusing to post-processing– can easily be adapted. If you would like the community to benefit from your customized pipeline, we would love to see a contribution to our [community-examples](https://github.com/huggingface/diffusers/tree/main/examples/community). If you feel that an important pipeline should be part of the official pipelines but isn't, a contribution to the [official pipelines](./overview) would be even better. +- **One-purpose-only**: Pipelines should be used for one task and one task only. Even if two tasks are very similar from a modeling point of view, *e.g.* image2image translation and in-painting, pipelines shall be used for one task only to keep them *easy-to-tweak* and *readable*. + +## Examples + +### Text-to-Image generation with Stable Diffusion + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline, LMSDiscreteScheduler + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] + +image.save("astronaut_rides_horse.png") +``` + +### Image-to-Image text-guided generation with Stable Diffusion + +The `StableDiffusionImg2ImgPipeline` lets you pass a text prompt and an initial image to condition the generation of new images. + +```python +import requests +from PIL import Image +from io import BytesIO + +from diffusers import StableDiffusionImg2ImgPipeline + +# load the pipeline +device = "cuda" +pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16).to( + device +) + +# let's download an initial image +url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((768, 512)) + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +images[0].save("fantasy_landscape.png") +``` +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) + +### Tweak prompts reusing seeds and latents + +You can generate your own latents to reproduce results, or tweak your prompt on a specific result you liked. [This notebook](https://github.com/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb) shows how to do it step by step. You can also run it in Google Colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb). + + +### In-painting using Stable Diffusion + +The `StableDiffusionInpaintPipeline` lets you edit specific parts of an image by providing a mask and text prompt. + +```python +import PIL +import requests +import torch +from io import BytesIO + +from diffusers import StableDiffusionInpaintPipeline + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0] +``` + +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) diff --git a/diffusers/docs/source/en/api/pipelines/paint_by_example.mdx b/diffusers/docs/source/en/api/pipelines/paint_by_example.mdx new file mode 100644 index 0000000000000000000000000000000000000000..91b936d98ac0426a0a4f61bf8afc7c54ff9106ca --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/paint_by_example.mdx @@ -0,0 +1,74 @@ + + +# PaintByExample + +## Overview + +[Paint by Example: Exemplar-based Image Editing with Diffusion Models](https://arxiv.org/abs/2211.13227) by Binxin Yang, Shuyang Gu, Bo Zhang, Ting Zhang, Xuejin Chen, Xiaoyan Sun, Dong Chen, Fang Wen + +The abstract of the paper is the following: + +*Language-guided image editing has achieved great success recently. In this paper, for the first time, we investigate exemplar-guided image editing for more precise control. We achieve this goal by leveraging self-supervised training to disentangle and re-organize the source image and the exemplar. However, the naive approach will cause obvious fusing artifacts. We carefully analyze it and propose an information bottleneck and strong augmentations to avoid the trivial solution of directly copying and pasting the exemplar image. Meanwhile, to ensure the controllability of the editing process, we design an arbitrary shape mask for the exemplar image and leverage the classifier-free guidance to increase the similarity to the exemplar image. The whole framework involves a single forward of the diffusion model without any iterative optimization. We demonstrate that our method achieves an impressive performance and enables controllable editing on in-the-wild images with high fidelity.* + +The original codebase can be found [here](https://github.com/Fantasy-Studio/Paint-by-Example). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_paint_by_example.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/paint_by_example/pipeline_paint_by_example.py) | *Image-Guided Image Painting* | - | + +## Tips + +- PaintByExample is supported by the official [Fantasy-Studio/Paint-by-Example](https://huggingface.co/Fantasy-Studio/Paint-by-Example) checkpoint. The checkpoint has been warm-started from the [CompVis/stable-diffusion-v1-4](https://huggingface.co/CompVis/stable-diffusion-v1-4) and with the objective to inpaint partly masked images conditioned on example / reference images +- To quickly demo *PaintByExample*, please have a look at [this demo](https://huggingface.co/spaces/Fantasy-Studio/Paint-by-Example) +- You can run the following code snippet as an example: + + +```python +# !pip install diffusers transformers + +import PIL +import requests +import torch +from io import BytesIO +from diffusers import DiffusionPipeline + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +img_url = "https://raw.githubusercontent.com/Fantasy-Studio/Paint-by-Example/main/examples/image/example_1.png" +mask_url = "https://raw.githubusercontent.com/Fantasy-Studio/Paint-by-Example/main/examples/mask/example_1.png" +example_url = "https://raw.githubusercontent.com/Fantasy-Studio/Paint-by-Example/main/examples/reference/example_1.jpg" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) +example_image = download_image(example_url).resize((512, 512)) + +pipe = DiffusionPipeline.from_pretrained( + "Fantasy-Studio/Paint-by-Example", + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +image = pipe(image=init_image, mask_image=mask_image, example_image=example_image).images[0] +image +``` + +## PaintByExamplePipeline +[[autodoc]] PaintByExamplePipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/pndm.mdx b/diffusers/docs/source/en/api/pipelines/pndm.mdx new file mode 100644 index 0000000000000000000000000000000000000000..824a927d8bc3cffde7e8aa53bbdfc417ab699dd2 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/pndm.mdx @@ -0,0 +1,35 @@ + + +# PNDM + +## Overview + +[Pseudo Numerical methods for Diffusion Models on manifolds](https://arxiv.org/abs/2202.09778) (PNDM) by Luping Liu, Yi Ren, Zhijie Lin and Zhou Zhao. + +The abstract of the paper is the following: + +Denoising Diffusion Probabilistic Models (DDPMs) can generate high-quality samples such as image and audio samples. However, DDPMs require hundreds to thousands of iterations to produce final samples. Several prior works have successfully accelerated DDPMs through adjusting the variance schedule (e.g., Improved Denoising Diffusion Probabilistic Models) or the denoising equation (e.g., Denoising Diffusion Implicit Models (DDIMs)). However, these acceleration methods cannot maintain the quality of samples and even introduce new noise at a high speedup rate, which limit their practicability. To accelerate the inference process while keeping the sample quality, we provide a fresh perspective that DDPMs should be treated as solving differential equations on manifolds. Under such a perspective, we propose pseudo numerical methods for diffusion models (PNDMs). Specifically, we figure out how to solve differential equations on manifolds and show that DDIMs are simple cases of pseudo numerical methods. We change several classical numerical methods to corresponding pseudo numerical methods and find that the pseudo linear multi-step method is the best in most situations. According to our experiments, by directly using pre-trained models on Cifar10, CelebA and LSUN, PNDMs can generate higher quality synthetic images with only 50 steps compared with 1000-step DDIMs (20x speedup), significantly outperform DDIMs with 250 steps (by around 0.4 in FID) and have good generalization on different variance schedules. + +The original codebase can be found [here](https://github.com/luping-liu/PNDM). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_pndm.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pndm/pipeline_pndm.py) | *Unconditional Image Generation* | - | + + +## PNDMPipeline +[[autodoc]] PNDMPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/repaint.mdx b/diffusers/docs/source/en/api/pipelines/repaint.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d0a3a6875b24ac54ee3efde020c44ffeaa0a60da --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/repaint.mdx @@ -0,0 +1,77 @@ + + +# RePaint + +## Overview + +[RePaint: Inpainting using Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2201.09865) (PNDM) by Andreas Lugmayr, Martin Danelljan, Andres Romero, Fisher Yu, Radu Timofte, Luc Van Gool. + +The abstract of the paper is the following: + +Free-form inpainting is the task of adding new content to an image in the regions specified by an arbitrary binary mask. Most existing approaches train for a certain distribution of masks, which limits their generalization capabilities to unseen mask types. Furthermore, training with pixel-wise and perceptual losses often leads to simple textural extensions towards the missing areas instead of semantically meaningful generation. In this work, we propose RePaint: A Denoising Diffusion Probabilistic Model (DDPM) based inpainting approach that is applicable to even extreme masks. We employ a pretrained unconditional DDPM as the generative prior. To condition the generation process, we only alter the reverse diffusion iterations by sampling the unmasked regions using the given image information. Since this technique does not modify or condition the original DDPM network itself, the model produces high-quality and diverse output images for any inpainting form. We validate our method for both faces and general-purpose image inpainting using standard and extreme masks. +RePaint outperforms state-of-the-art Autoregressive, and GAN approaches for at least five out of six mask distributions. + +The original codebase can be found [here](https://github.com/andreas128/RePaint). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|-------------------------------------------------------------------------------------------------------------------------------|--------------------|:---:| +| [pipeline_repaint.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/repaint/pipeline_repaint.py) | *Image Inpainting* | - | + +## Usage example + +```python +from io import BytesIO + +import torch + +import PIL +import requests +from diffusers import RePaintPipeline, RePaintScheduler + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +img_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/repaint/celeba_hq_256.png" +mask_url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/repaint/mask_256.png" + +# Load the original image and the mask as PIL images +original_image = download_image(img_url).resize((256, 256)) +mask_image = download_image(mask_url).resize((256, 256)) + +# Load the RePaint scheduler and pipeline based on a pretrained DDPM model +scheduler = RePaintScheduler.from_pretrained("google/ddpm-ema-celebahq-256") +pipe = RePaintPipeline.from_pretrained("google/ddpm-ema-celebahq-256", scheduler=scheduler) +pipe = pipe.to("cuda") + +generator = torch.Generator(device="cuda").manual_seed(0) +output = pipe( + original_image=original_image, + mask_image=mask_image, + num_inference_steps=250, + eta=0.0, + jump_length=10, + jump_n_sample=10, + generator=generator, +) +inpainted_image = output.images[0] +``` + +## RePaintPipeline +[[autodoc]] RePaintPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/score_sde_ve.mdx b/diffusers/docs/source/en/api/pipelines/score_sde_ve.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7a5d7ee83aa59345be36fd688f9cf4e773b60fc8 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/score_sde_ve.mdx @@ -0,0 +1,36 @@ + + +# Score SDE VE + +## Overview + +[Score-Based Generative Modeling through Stochastic Differential Equations](https://arxiv.org/abs/2011.13456) (Score SDE) by Yang Song, Jascha Sohl-Dickstein, Diederik P. Kingma, Abhishek Kumar, Stefano Ermon and Ben Poole. + +The abstract of the paper is the following: + +Creating noise from data is easy; creating data from noise is generative modeling. We present a stochastic differential equation (SDE) that smoothly transforms a complex data distribution to a known prior distribution by slowly injecting noise, and a corresponding reverse-time SDE that transforms the prior distribution back into the data distribution by slowly removing the noise. Crucially, the reverse-time SDE depends only on the time-dependent gradient field (\aka, score) of the perturbed data distribution. By leveraging advances in score-based generative modeling, we can accurately estimate these scores with neural networks, and use numerical SDE solvers to generate samples. We show that this framework encapsulates previous approaches in score-based generative modeling and diffusion probabilistic modeling, allowing for new sampling procedures and new modeling capabilities. In particular, we introduce a predictor-corrector framework to correct errors in the evolution of the discretized reverse-time SDE. We also derive an equivalent neural ODE that samples from the same distribution as the SDE, but additionally enables exact likelihood computation, and improved sampling efficiency. In addition, we provide a new way to solve inverse problems with score-based models, as demonstrated with experiments on class-conditional generation, image inpainting, and colorization. Combined with multiple architectural improvements, we achieve record-breaking performance for unconditional image generation on CIFAR-10 with an Inception score of 9.89 and FID of 2.20, a competitive likelihood of 2.99 bits/dim, and demonstrate high fidelity generation of 1024 x 1024 images for the first time from a score-based generative model. + +The original codebase can be found [here](https://github.com/yang-song/score_sde_pytorch). + +This pipeline implements the Variance Expanding (VE) variant of the method. + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_score_sde_ve.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/score_sde_ve/pipeline_score_sde_ve.py) | *Unconditional Image Generation* | - | + +## ScoreSdeVePipeline +[[autodoc]] ScoreSdeVePipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/depth2img.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/depth2img.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7902042cf00ad4463bf717b193925f3a02e30052 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/depth2img.mdx @@ -0,0 +1,33 @@ + + +# Depth-to-Image Generation + +## StableDiffusionDepth2ImgPipeline + +The depth-guided stable diffusion model was created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), and [LAION](https://laion.ai/), as part of Stable Diffusion 2.0. It uses [MiDas](https://github.com/isl-org/MiDaS) to infer depth based on an image. + +[`StableDiffusionDepth2ImgPipeline`] lets you pass a text prompt and an initial image to condition the generation of new images as well as a `depth_map` to preserve the images’ structure. + +The original codebase can be found here: +- *Stable Diffusion v2*: [Stability-AI/stablediffusion](https://github.com/Stability-AI/stablediffusion#depth-conditional-stable-diffusion) + +Available Checkpoints are: +- *stable-diffusion-2-depth*: [stabilityai/stable-diffusion-2-depth](https://huggingface.co/stabilityai/stable-diffusion-2-depth) + +[[autodoc]] StableDiffusionDepth2ImgPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/image_variation.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/image_variation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..cc4dcd43d31055e53a538468211f825b255995ba --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/image_variation.mdx @@ -0,0 +1,31 @@ + + +# Image Variation + +## StableDiffusionImageVariationPipeline + +[`StableDiffusionImageVariationPipeline`] lets you generate variations from an input image using Stable Diffusion. It uses a fine-tuned version of Stable Diffusion model, trained by [Justin Pinkney](https://www.justinpinkney.com/) (@Buntworthy) at [Lambda](https://lambdalabs.com/) + +The original codebase can be found here: +[Stable Diffusion Image Variations](https://github.com/LambdaLabsML/lambda-diffusers#stable-diffusion-image-variations) + +Available Checkpoints are: +- *sd-image-variations-diffusers*: [lambdalabs/sd-image-variations-diffusers](https://huggingface.co/lambdalabs/sd-image-variations-diffusers) + +[[autodoc]] StableDiffusionImageVariationPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/img2img.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/img2img.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5ece114f92674969d8ddd283b0f05a344a29e9ff --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/img2img.mdx @@ -0,0 +1,29 @@ + + +# Image-to-Image Generation + +## StableDiffusionImg2ImgPipeline + +The Stable Diffusion model was created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), [runway](https://github.com/runwayml), and [LAION](https://laion.ai/). The [`StableDiffusionImg2ImgPipeline`] lets you pass a text prompt and an initial image to condition the generation of new images using Stable Diffusion. + +The original codebase can be found here: [CampVis/stable-diffusion](https://github.com/CompVis/stable-diffusion/blob/main/scripts/img2img.py) + +[`StableDiffusionImg2ImgPipeline`] is compatible with all Stable Diffusion checkpoints for [Text-to-Image](./text2img) + +[[autodoc]] StableDiffusionImg2ImgPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/inpaint.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/inpaint.mdx new file mode 100644 index 0000000000000000000000000000000000000000..312bd33d23ba669eac4a3f337b2a546367111371 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/inpaint.mdx @@ -0,0 +1,33 @@ + + +# Text-Guided Image Inpainting + +## StableDiffusionInpaintPipeline + +The Stable Diffusion model was created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), [runway](https://github.com/runwayml), and [LAION](https://laion.ai/). The [`StableDiffusionInpaintPipeline`] lets you edit specific parts of an image by providing a mask and a text prompt using Stable Diffusion. + +The original codebase can be found here: +- *Stable Diffusion V1*: [CampVis/stable-diffusion](https://github.com/runwayml/stable-diffusion#inpainting-with-stable-diffusion) +- *Stable Diffusion V2*: [Stability-AI/stablediffusion](https://github.com/Stability-AI/stablediffusion#image-inpainting-with-stable-diffusion) + +Available checkpoints are: +- *stable-diffusion-inpainting (512x512 resolution)*: [runwayml/stable-diffusion-inpainting](https://huggingface.co/runwayml/stable-diffusion-inpainting) +- *stable-diffusion-2-inpainting (512x512 resolution)*: [stabilityai/stable-diffusion-2-inpainting](https://huggingface.co/stabilityai/stable-diffusion-2-inpainting) + +[[autodoc]] StableDiffusionInpaintPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/latent_upscale.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/latent_upscale.mdx new file mode 100644 index 0000000000000000000000000000000000000000..61fd2f799114de345400a692c115811fbf222871 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/latent_upscale.mdx @@ -0,0 +1,33 @@ + + +# Stable Diffusion Latent Upscaler + +## StableDiffusionLatentUpscalePipeline + +The Stable Diffusion Latent Upscaler model was created by [Katherine Crowson](https://github.com/crowsonkb/k-diffusion) in collaboration with [Stability AI](https://stability.ai/). It can be used on top of any [`StableDiffusionUpscalePipeline`] checkpoint to enhance its output image resolution by a factor of 2. + +A notebook that demonstrates the original implementation can be found here: +- [Stable Diffusion Upscaler Demo](https://colab.research.google.com/drive/1o1qYJcFeywzCIdkfKJy7cTpgZTCM2EI4) + +Available Checkpoints are: +- *stabilityai/latent-upscaler*: [stabilityai/sd-x2-latent-upscaler](https://huggingface.co/stabilityai/sd-x2-latent-upscaler) + + +[[autodoc]] StableDiffusionLatentUpscalePipeline + - all + - __call__ + - enable_sequential_cpu_offload + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/overview.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5d3fb77c7aad1db1ce7b932391ef328d3e7b3d94 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/overview.mdx @@ -0,0 +1,79 @@ + + +# Stable diffusion pipelines + +Stable Diffusion is a text-to-image _latent diffusion_ model created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/) and [LAION](https://laion.ai/). It's trained on 512x512 images from a subset of the [LAION-5B](https://laion.ai/blog/laion-5b/) dataset. This model uses a frozen CLIP ViT-L/14 text encoder to condition the model on text prompts. With its 860M UNet and 123M text encoder, the model is relatively lightweight and can run on consumer GPUs. + +Latent diffusion is the research on top of which Stable Diffusion was built. It was proposed in [High-Resolution Image Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) by Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, Björn Ommer. You can learn more details about it in the [specific pipeline for latent diffusion](pipelines/latent_diffusion) that is part of 🤗 Diffusers. + +For more details about how Stable Diffusion works and how it differs from the base latent diffusion model, please refer to the official [launch announcement post](https://stability.ai/blog/stable-diffusion-announcement) and [this section of our own blog post](https://huggingface.co/blog/stable_diffusion#how-does-stable-diffusion-work). + +*Tips*: +- To tweak your prompts on a specific result you liked, you can generate your own latents, as demonstrated in the following notebook: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb) + +*Overview*: + +| Pipeline | Tasks | Colab | Demo +|---|---|:---:|:---:| +| [StableDiffusionPipeline](./text2img) | *Text-to-Image Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/stable_diffusion.ipynb) | [🤗 Stable Diffusion](https://huggingface.co/spaces/stabilityai/stable-diffusion) +| [StableDiffusionImg2ImgPipeline](./img2img) | *Image-to-Image Text-Guided Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) | [🤗 Diffuse the Rest](https://huggingface.co/spaces/huggingface/diffuse-the-rest) +| [StableDiffusionInpaintPipeline](./inpaint) | **Experimental** – *Text-Guided Image Inpainting* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) | Coming soon +| [StableDiffusionDepth2ImgPipeline](./depth2img) | **Experimental** – *Depth-to-Image Text-Guided Generation * | | Coming soon +| [StableDiffusionImageVariationPipeline](./image_variation) | **Experimental** – *Image Variation Generation * | | [🤗 Stable Diffusion Image Variations](https://huggingface.co/spaces/lambdalabs/stable-diffusion-image-variations) +| [StableDiffusionUpscalePipeline](./upscale) | **Experimental** – *Text-Guided Image Super-Resolution * | | Coming soon +| [StableDiffusionLatentUpscalePipeline](./latent_upscale) | **Experimental** – *Text-Guided Image Super-Resolution * | | Coming soon +| [StableDiffusionInstructPix2PixPipeline](./pix2pix) | **Experimental** – *Text-Based Image Editing * | | [InstructPix2Pix: Learning to Follow Image Editing Instructions](https://huggingface.co/spaces/timbrooks/instruct-pix2pix) + + + +## Tips + +### How to load and use different schedulers. + +The stable diffusion pipeline uses [`PNDMScheduler`] scheduler by default. But `diffusers` provides many other schedulers that can be used with the stable diffusion pipeline such as [`DDIMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], [`EulerAncestralDiscreteScheduler`] etc. +To use a different scheduler, you can either change it via the [`ConfigMixin.from_config`] method or pass the `scheduler` argument to the `from_pretrained` method of the pipeline. For example, to use the [`EulerDiscreteScheduler`], you can do the following: + +```python +>>> from diffusers import StableDiffusionPipeline, EulerDiscreteScheduler + +>>> pipeline = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4") +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +>>> # or +>>> euler_scheduler = EulerDiscreteScheduler.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="scheduler") +>>> pipeline = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", scheduler=euler_scheduler) +``` + + +### How to convert all use cases with multiple or single pipeline + +If you want to use all possible use cases in a single `DiffusionPipeline` you can either: +- Make use of the [Stable Diffusion Mega Pipeline](https://github.com/huggingface/diffusers/tree/main/examples/community#stable-diffusion-mega) or +- Make use of the `components` functionality to instantiate all components in the most memory-efficient way: + +```python +>>> from diffusers import ( +... StableDiffusionPipeline, +... StableDiffusionImg2ImgPipeline, +... StableDiffusionInpaintPipeline, +... ) + +>>> text2img = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4") +>>> img2img = StableDiffusionImg2ImgPipeline(**text2img.components) +>>> inpaint = StableDiffusionInpaintPipeline(**text2img.components) + +>>> # now you can use text2img(...), img2img(...), inpaint(...) just like the call methods of each respective pipeline +``` + +## StableDiffusionPipelineOutput +[[autodoc]] pipelines.stable_diffusion.StableDiffusionPipelineOutput diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/pix2pix.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/pix2pix.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ac5a1a0627a2882b01862ad9b5b38024a84b625b --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/pix2pix.mdx @@ -0,0 +1,70 @@ + + +# InstructPix2Pix: Learning to Follow Image Editing Instructions + +## Overview + +[InstructPix2Pix: Learning to Follow Image Editing Instructions](https://arxiv.org/abs/2211.09800) by Tim Brooks, Aleksander Holynski and Alexei A. Efros. + +The abstract of the paper is the following: + +*We propose a method for editing images from human instructions: given an input image and a written instruction that tells the model what to do, our model follows these instructions to edit the image. To obtain training data for this problem, we combine the knowledge of two large pretrained models -- a language model (GPT-3) and a text-to-image model (Stable Diffusion) -- to generate a large dataset of image editing examples. Our conditional diffusion model, InstructPix2Pix, is trained on our generated data, and generalizes to real images and user-written instructions at inference time. Since it performs edits in the forward pass and does not require per example fine-tuning or inversion, our model edits images quickly, in a matter of seconds. We show compelling editing results for a diverse collection of input images and written instructions.* + +Resources: + +* [Project Page](https://www.timothybrooks.com/instruct-pix2pix). +* [Paper](https://arxiv.org/abs/2211.09800). +* [Original Code](https://github.com/timothybrooks/instruct-pix2pix). +* [Demo](https://huggingface.co/spaces/timbrooks/instruct-pix2pix). + + +## Available Pipelines: + +| Pipeline | Tasks | Demo +|---|---|:---:| +| [StableDiffusionInstructPix2PixPipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py) | *Text-Based Image Editing* | [🤗 Space](https://huggingface.co/spaces/timbrooks/instruct-pix2pix) | + + + +## Usage example + +```python +import PIL +import requests +import torch +from diffusers import StableDiffusionInstructPix2PixPipeline + +model_id = "timbrooks/instruct-pix2pix" +pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +url = "https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/mountain.png" + + +def download_image(url): + image = PIL.Image.open(requests.get(url, stream=True).raw) + image = PIL.ImageOps.exif_transpose(image) + image = image.convert("RGB") + return image + + +image = download_image(url) + +prompt = "make the mountains snowy" +edit = pipe(prompt, image=image, num_inference_steps=20, image_guidance_scale=1.5, guidance_scale=7).images[0] +images[0].save("snowy_mountains.png") +``` + +## StableDiffusionInstructPix2PixPipeline +[[autodoc]] StableDiffusionInstructPix2PixPipeline + - __call__ + - all diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/text2img.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/text2img.mdx new file mode 100644 index 0000000000000000000000000000000000000000..952ad24808b8181b93e91dcc8f6786baf1e422f4 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/text2img.mdx @@ -0,0 +1,39 @@ + + +# Text-to-Image Generation + +## StableDiffusionPipeline + +The Stable Diffusion model was created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), [runway](https://github.com/runwayml), and [LAION](https://laion.ai/). The [`StableDiffusionPipeline`] is capable of generating photo-realistic images given any text input using Stable Diffusion. + +The original codebase can be found here: +- *Stable Diffusion V1*: [CampVis/stable-diffusion](https://github.com/CompVis/stable-diffusion) +- *Stable Diffusion v2*: [Stability-AI/stablediffusion](https://github.com/Stability-AI/stablediffusion) + +Available Checkpoints are: +- *stable-diffusion-v1-4 (512x512 resolution)* [CompVis/stable-diffusion-v1-4](https://huggingface.co/CompVis/stable-diffusion-v1-4) +- *stable-diffusion-v1-5 (512x512 resolution)* [runwayml/stable-diffusion-v1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5) +- *stable-diffusion-2-base (512x512 resolution)*: [stabilityai/stable-diffusion-2-base](https://huggingface.co/stabilityai/stable-diffusion-2-base) +- *stable-diffusion-2 (768x768 resolution)*: [stabilityai/stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) +- *stable-diffusion-2-1-base (512x512 resolution)* [stabilityai/stable-diffusion-2-1-base](https://huggingface.co/stabilityai/stable-diffusion-2-1-base) +- *stable-diffusion-2-1 (768x768 resolution)*: [stabilityai/stable-diffusion-2-1](https://huggingface.co/stabilityai/stable-diffusion-2-1) + +[[autodoc]] StableDiffusionPipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_vae_slicing + - disable_vae_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion/upscale.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion/upscale.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5185903edc0977263746b2a6f82592a1c64a8522 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion/upscale.mdx @@ -0,0 +1,32 @@ + + +# Super-Resolution + +## StableDiffusionUpscalePipeline + +The upscaler diffusion model was created by the researchers and engineers from [CompVis](https://github.com/CompVis), [Stability AI](https://stability.ai/), and [LAION](https://laion.ai/), as part of Stable Diffusion 2.0. [`StableDiffusionUpscalePipeline`] can be used to enhance the resolution of input images by a factor of 4. + +The original codebase can be found here: +- *Stable Diffusion v2*: [Stability-AI/stablediffusion](https://github.com/Stability-AI/stablediffusion#image-upscaling-with-stable-diffusion) + +Available Checkpoints are: +- *stabilityai/stable-diffusion-x4-upscaler (x4 resolution resolution)*: [stable-diffusion-x4-upscaler](https://huggingface.co/stabilityai/stable-diffusion-x4-upscaler) + + +[[autodoc]] StableDiffusionUpscalePipeline + - all + - __call__ + - enable_attention_slicing + - disable_attention_slicing + - enable_xformers_memory_efficient_attention + - disable_xformers_memory_efficient_attention \ No newline at end of file diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion_2.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion_2.mdx new file mode 100644 index 0000000000000000000000000000000000000000..67cb9c792059b8045d55c60aa6d1e1964fc6ef7b --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion_2.mdx @@ -0,0 +1,176 @@ + + +# Stable diffusion 2 + +Stable Diffusion 2 is a text-to-image _latent diffusion_ model built upon the work of [Stable Diffusion 1](https://stability.ai/blog/stable-diffusion-public-release). +The project to train Stable Diffusion 2 was led by Robin Rombach and Katherine Crowson from [Stability AI](https://stability.ai/) and [LAION](https://laion.ai/). + +*The Stable Diffusion 2.0 release includes robust text-to-image models trained using a brand new text encoder (OpenCLIP), developed by LAION with support from Stability AI, which greatly improves the quality of the generated images compared to earlier V1 releases. The text-to-image models in this release can generate images with default resolutions of both 512x512 pixels and 768x768 pixels. +These models are trained on an aesthetic subset of the [LAION-5B dataset](https://laion.ai/blog/laion-5b/) created by the DeepFloyd team at Stability AI, which is then further filtered to remove adult content using [LAION’s NSFW filter](https://openreview.net/forum?id=M3Y74vmsMcY).* + +For more details about how Stable Diffusion 2 works and how it differs from Stable Diffusion 1, please refer to the official [launch announcement post](https://stability.ai/blog/stable-diffusion-v2-release). + +## Tips + +### Available checkpoints: + +Note that the architecture is more or less identical to [Stable Diffusion 1](./stable_diffusion/overview) so please refer to [this page](./stable_diffusion/overview) for API documentation. + +- *Text-to-Image (512x512 resolution)*: [stabilityai/stable-diffusion-2-base](https://huggingface.co/stabilityai/stable-diffusion-2-base) with [`StableDiffusionPipeline`] +- *Text-to-Image (768x768 resolution)*: [stabilityai/stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) with [`StableDiffusionPipeline`] +- *Image Inpainting (512x512 resolution)*: [stabilityai/stable-diffusion-2-inpainting](https://huggingface.co/stabilityai/stable-diffusion-2-inpainting) with [`StableDiffusionInpaintPipeline`] +- *Super-Resolution (x4 resolution resolution)*: [stable-diffusion-x4-upscaler](https://huggingface.co/stabilityai/stable-diffusion-x4-upscaler) [`StableDiffusionUpscalePipeline`] +- *Depth-to-Image (512x512 resolution)*: [stabilityai/stable-diffusion-2-depth](https://huggingface.co/stabilityai/stable-diffusion-2-depth) with [`StableDiffusionDepth2ImagePipeline`] + +We recommend using the [`DPMSolverMultistepScheduler`] as it's currently the fastest scheduler there is. + + +### Text-to-Image + +- *Text-to-Image (512x512 resolution)*: [stabilityai/stable-diffusion-2-base](https://huggingface.co/stabilityai/stable-diffusion-2-base) with [`StableDiffusionPipeline`] + +```python +from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler +import torch + +repo_id = "stabilityai/stable-diffusion-2-base" +pipe = DiffusionPipeline.from_pretrained(repo_id, torch_dtype=torch.float16, revision="fp16") + +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +prompt = "High quality photo of an astronaut riding a horse in space" +image = pipe(prompt, num_inference_steps=25).images[0] +image.save("astronaut.png") +``` + +- *Text-to-Image (768x768 resolution)*: [stabilityai/stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) with [`StableDiffusionPipeline`] + +```python +from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler +import torch + +repo_id = "stabilityai/stable-diffusion-2" +pipe = DiffusionPipeline.from_pretrained(repo_id, torch_dtype=torch.float16, revision="fp16") + +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +prompt = "High quality photo of an astronaut riding a horse in space" +image = pipe(prompt, guidance_scale=9, num_inference_steps=25).images[0] +image.save("astronaut.png") +``` + +### Image Inpainting + +- *Image Inpainting (512x512 resolution)*: [stabilityai/stable-diffusion-2-inpainting](https://huggingface.co/stabilityai/stable-diffusion-2-inpainting) with [`StableDiffusionInpaintPipeline`] + +```python +import PIL +import requests +import torch +from io import BytesIO + +from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +repo_id = "stabilityai/stable-diffusion-2-inpainting" +pipe = DiffusionPipeline.from_pretrained(repo_id, torch_dtype=torch.float16, revision="fp16") + +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +image = pipe(prompt=prompt, image=init_image, mask_image=mask_image, num_inference_steps=25).images[0] + +image.save("yellow_cat.png") +``` + +### Super-Resolution + +- *Image Upscaling (x4 resolution resolution)*: [stable-diffusion-x4-upscaler](https://huggingface.co/stabilityai/stable-diffusion-x4-upscaler) with [`StableDiffusionUpscalePipeline`] + + +```python +import requests +from PIL import Image +from io import BytesIO +from diffusers import StableDiffusionUpscalePipeline +import torch + +# load model and scheduler +model_id = "stabilityai/stable-diffusion-x4-upscaler" +pipeline = StableDiffusionUpscalePipeline.from_pretrained(model_id, torch_dtype=torch.float16) +pipeline = pipeline.to("cuda") + +# let's download an image +url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-upscale/low_res_cat.png" +response = requests.get(url) +low_res_img = Image.open(BytesIO(response.content)).convert("RGB") +low_res_img = low_res_img.resize((128, 128)) +prompt = "a white cat" +upscaled_image = pipeline(prompt=prompt, image=low_res_img).images[0] +upscaled_image.save("upsampled_cat.png") +``` + +### Depth-to-Image + +- *Depth-Guided Text-to-Image*: [stabilityai/stable-diffusion-2-depth](https://huggingface.co/stabilityai/stable-diffusion-2-depth) [`StableDiffusionDepth2ImagePipeline`] + + +```python +import torch +import requests +from PIL import Image + +from diffusers import StableDiffusionDepth2ImgPipeline + +pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", + torch_dtype=torch.float16, +).to("cuda") + + +url = "http://images.cocodataset.org/val2017/000000039769.jpg" +init_image = Image.open(requests.get(url, stream=True).raw) +prompt = "two tigers" +n_propmt = "bad, deformed, ugly, bad anotomy" +image = pipe(prompt=prompt, image=init_image, negative_prompt=n_propmt, strength=0.7).images[0] +``` + +### How to load and use different schedulers. + +The stable diffusion pipeline uses [`DDIMScheduler`] scheduler by default. But `diffusers` provides many other schedulers that can be used with the stable diffusion pipeline such as [`PNDMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], [`EulerAncestralDiscreteScheduler`] etc. +To use a different scheduler, you can either change it via the [`ConfigMixin.from_config`] method or pass the `scheduler` argument to the `from_pretrained` method of the pipeline. For example, to use the [`EulerDiscreteScheduler`], you can do the following: + +```python +>>> from diffusers import StableDiffusionPipeline, EulerDiscreteScheduler + +>>> pipeline = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2") +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +>>> # or +>>> euler_scheduler = EulerDiscreteScheduler.from_pretrained("stabilityai/stable-diffusion-2", subfolder="scheduler") +>>> pipeline = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2", scheduler=euler_scheduler) +``` diff --git a/diffusers/docs/source/en/api/pipelines/stable_diffusion_safe.mdx b/diffusers/docs/source/en/api/pipelines/stable_diffusion_safe.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d2ae319a78e6daedb6e7a51090376374fca342f3 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stable_diffusion_safe.mdx @@ -0,0 +1,90 @@ + + +# Safe Stable Diffusion + +Safe Stable Diffusion was proposed in [Safe Latent Diffusion: Mitigating Inappropriate Degeneration in Diffusion Models](https://arxiv.org/abs/2211.05105) and mitigates the well known issue that models like Stable Diffusion that are trained on unfiltered, web-crawled datasets tend to suffer from inappropriate degeneration. For instance Stable Diffusion may unexpectedly generate nudity, violence, images depicting self-harm, or otherwise offensive content. +Safe Stable Diffusion is an extension to the Stable Diffusion that drastically reduces content like this. + +The abstract of the paper is the following: + +*Text-conditioned image generation models have recently achieved astonishing results in image quality and text alignment and are consequently employed in a fast-growing number of applications. Since they are highly data-driven, relying on billion-sized datasets randomly scraped from the internet, they also suffer, as we demonstrate, from degenerated and biased human behavior. In turn, they may even reinforce such biases. To help combat these undesired side effects, we present safe latent diffusion (SLD). Specifically, to measure the inappropriate degeneration due to unfiltered and imbalanced training sets, we establish a novel image generation test bed-inappropriate image prompts (I2P)-containing dedicated, real-world image-to-text prompts covering concepts such as nudity and violence. As our exhaustive empirical evaluation demonstrates, the introduced SLD removes and suppresses inappropriate image parts during the diffusion process, with no additional training required and no adverse effect on overall image quality or text alignment.* + + +*Overview*: + +| Pipeline | Tasks | Colab | Demo +|---|---|:---:|:---:| +| [pipeline_stable_diffusion_safe.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion_safe/pipeline_stable_diffusion_safe.py) | *Text-to-Image Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ml-research/safe-latent-diffusion/blob/main/examples/Safe%20Latent%20Diffusion.ipynb) | - + +## Tips + +- Safe Stable Diffusion may also be used with weights of [Stable Diffusion](./api/pipelines/stable_diffusion/text2img). + +### Run Safe Stable Diffusion + +Safe Stable Diffusion can be tested very easily with the [`StableDiffusionPipelineSafe`], and the `"AIML-TUDA/stable-diffusion-safe"` checkpoint exactly in the same way it is shown in the [Conditional Image Generation Guide](./using-diffusers/conditional_image_generation). + +### Interacting with the Safety Concept + +To check and edit the currently used safety concept, use the `safety_concept` property of [`StableDiffusionPipelineSafe`] +```python +>>> from diffusers import StableDiffusionPipelineSafe + +>>> pipeline = StableDiffusionPipelineSafe.from_pretrained("AIML-TUDA/stable-diffusion-safe") +>>> pipeline.safety_concept +``` +For each image generation the active concept is also contained in [`StableDiffusionSafePipelineOutput`]. + +### Using pre-defined safety configurations + +You may use the 4 configurations defined in the [Safe Latent Diffusion paper](https://arxiv.org/abs/2211.05105) as follows: + +```python +>>> from diffusers import StableDiffusionPipelineSafe +>>> from diffusers.pipelines.stable_diffusion_safe import SafetyConfig + +>>> pipeline = StableDiffusionPipelineSafe.from_pretrained("AIML-TUDA/stable-diffusion-safe") +>>> prompt = "the four horsewomen of the apocalypse, painting by tom of finland, gaston bussiere, craig mullins, j. c. leyendecker" +>>> out = pipeline(prompt=prompt, **SafetyConfig.MAX) +``` + +The following configurations are available: `SafetyConfig.WEAK`, `SafetyConfig.MEDIUM`, `SafetyConfig.STRONg`, and `SafetyConfig.MAX`. + +### How to load and use different schedulers. + +The safe stable diffusion pipeline uses [`PNDMScheduler`] scheduler by default. But `diffusers` provides many other schedulers that can be used with the stable diffusion pipeline such as [`DDIMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], [`EulerAncestralDiscreteScheduler`] etc. +To use a different scheduler, you can either change it via the [`ConfigMixin.from_config`] method or pass the `scheduler` argument to the `from_pretrained` method of the pipeline. For example, to use the [`EulerDiscreteScheduler`], you can do the following: + +```python +>>> from diffusers import StableDiffusionPipelineSafe, EulerDiscreteScheduler + +>>> pipeline = StableDiffusionPipelineSafe.from_pretrained("AIML-TUDA/stable-diffusion-safe") +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +>>> # or +>>> euler_scheduler = EulerDiscreteScheduler.from_pretrained("AIML-TUDA/stable-diffusion-safe", subfolder="scheduler") +>>> pipeline = StableDiffusionPipelineSafe.from_pretrained( +... "AIML-TUDA/stable-diffusion-safe", scheduler=euler_scheduler +... ) +``` + + +## StableDiffusionSafePipelineOutput +[[autodoc]] pipelines.stable_diffusion_safe.StableDiffusionSafePipelineOutput + - all + - __call__ + +## StableDiffusionPipelineSafe +[[autodoc]] StableDiffusionPipelineSafe + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/stochastic_karras_ve.mdx b/diffusers/docs/source/en/api/pipelines/stochastic_karras_ve.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ab185ec20d6cb060d4faa448f7be089a47d31611 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/stochastic_karras_ve.mdx @@ -0,0 +1,36 @@ + + +# Stochastic Karras VE + +## Overview + +[Elucidating the Design Space of Diffusion-Based Generative Models](https://arxiv.org/abs/2206.00364) by Tero Karras, Miika Aittala, Timo Aila and Samuli Laine. + +The abstract of the paper is the following: + +We argue that the theory and practice of diffusion-based generative models are currently unnecessarily convoluted and seek to remedy the situation by presenting a design space that clearly separates the concrete design choices. This lets us identify several changes to both the sampling and training processes, as well as preconditioning of the score networks. Together, our improvements yield new state-of-the-art FID of 1.79 for CIFAR-10 in a class-conditional setting and 1.97 in an unconditional setting, with much faster sampling (35 network evaluations per image) than prior designs. To further demonstrate their modular nature, we show that our design changes dramatically improve both the efficiency and quality obtainable with pre-trained score networks from previous work, including improving the FID of an existing ImageNet-64 model from 2.07 to near-SOTA 1.55. + +This pipeline implements the Stochastic sampling tailored to the Variance-Expanding (VE) models. + + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_stochastic_karras_ve.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stochastic_karras_ve/pipeline_stochastic_karras_ve.py) | *Unconditional Image Generation* | - | + + +## KarrasVePipeline +[[autodoc]] KarrasVePipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/unclip.mdx b/diffusers/docs/source/en/api/pipelines/unclip.mdx new file mode 100644 index 0000000000000000000000000000000000000000..87d44adc0d762c4c753e6ec04961da35792e654e --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/unclip.mdx @@ -0,0 +1,37 @@ + + +# unCLIP + +## Overview + +[Hierarchical Text-Conditional Image Generation with CLIP Latents](https://arxiv.org/abs/2204.06125) by Aditya Ramesh, Prafulla Dhariwal, Alex Nichol, Casey Chu, Mark Chen + +The abstract of the paper is the following: + +Contrastive models like CLIP have been shown to learn robust representations of images that capture both semantics and style. To leverage these representations for image generation, we propose a two-stage model: a prior that generates a CLIP image embedding given a text caption, and a decoder that generates an image conditioned on the image embedding. We show that explicitly generating image representations improves image diversity with minimal loss in photorealism and caption similarity. Our decoders conditioned on image representations can also produce variations of an image that preserve both its semantics and style, while varying the non-essential details absent from the image representation. Moreover, the joint embedding space of CLIP enables language-guided image manipulations in a zero-shot fashion. We use diffusion models for the decoder and experiment with both autoregressive and diffusion models for the prior, finding that the latter are computationally more efficient and produce higher-quality samples. + +The unCLIP model in diffusers comes from kakaobrain's karlo and the original codebase can be found [here](https://github.com/kakaobrain/karlo). Additionally, lucidrains has a DALL-E 2 recreation [here](https://github.com/lucidrains/DALLE2-pytorch). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_unclip.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/unclip/pipeline_unclip.py) | *Text-to-Image Generation* | - | +| [pipeline_unclip_image_variation.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/unclip/pipeline_unclip_image_variation.py) | *Image-Guided Image Generation* | - | + + +## UnCLIPPipeline +[[autodoc]] UnCLIPPipeline + - all + - __call__ + +[[autodoc]] UnCLIPImageVariationPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/versatile_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/versatile_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6231ce6a1aa6769d4100ef8e9681f8cec0a577eb --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/versatile_diffusion.mdx @@ -0,0 +1,70 @@ + + +# VersatileDiffusion + +VersatileDiffusion was proposed in [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) by Xingqian Xu, Zhangyang Wang, Eric Zhang, Kai Wang, Humphrey Shi . + +The abstract of the paper is the following: + +*The recent advances in diffusion models have set an impressive milestone in many generation tasks. Trending works such as DALL-E2, Imagen, and Stable Diffusion have attracted great interest in academia and industry. Despite the rapid landscape changes, recent new approaches focus on extensions and performance rather than capacity, thus requiring separate models for separate tasks. In this work, we expand the existing single-flow diffusion pipeline into a multi-flow network, dubbed Versatile Diffusion (VD), that handles text-to-image, image-to-text, image-variation, and text-variation in one unified model. Moreover, we generalize VD to a unified multi-flow multimodal diffusion framework with grouped layers, swappable streams, and other propositions that can process modalities beyond images and text. Through our experiments, we demonstrate that VD and its underlying framework have the following merits: a) VD handles all subtasks with competitive quality; b) VD initiates novel extensions and applications such as disentanglement of style and semantic, image-text dual-guided generation, etc.; c) Through these experiments and applications, VD provides more semantic insights of the generated outputs.* + +## Tips + +- VersatileDiffusion is conceptually very similar as [Stable Diffusion](./api/pipelines/stable_diffusion/overview), but instead of providing just a image data stream conditioned on text, VersatileDiffusion provides both a image and text data stream and can be conditioned on both text and image. + +### *Run VersatileDiffusion* + +You can both load the memory intensive "all-in-one" [`VersatileDiffusionPipeline`] that can run all tasks +with the same class as shown in [`VersatileDiffusionPipeline.text_to_image`], [`VersatileDiffusionPipeline.image_variation`], and [`VersatileDiffusionPipeline.dual_guided`] + +**or** + +You can run the individual pipelines which are much more memory efficient: + +- *Text-to-Image*: [`VersatileDiffusionTextToImagePipeline.__call__`] +- *Image Variation*: [`VersatileDiffusionImageVariationPipeline.__call__`] +- *Dual Text and Image Guided Generation*: [`VersatileDiffusionDualGuidedPipeline.__call__`] + +### *How to load and use different schedulers.* + +The versatile diffusion pipelines uses [`DDIMScheduler`] scheduler by default. But `diffusers` provides many other schedulers that can be used with the alt diffusion pipeline such as [`PNDMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], [`EulerAncestralDiscreteScheduler`] etc. +To use a different scheduler, you can either change it via the [`ConfigMixin.from_config`] method or pass the `scheduler` argument to the `from_pretrained` method of the pipeline. For example, to use the [`EulerDiscreteScheduler`], you can do the following: + +```python +>>> from diffusers import VersatileDiffusionPipeline, EulerDiscreteScheduler + +>>> pipeline = VersatileDiffusionPipeline.from_pretrained("shi-labs/versatile-diffusion") +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +>>> # or +>>> euler_scheduler = EulerDiscreteScheduler.from_pretrained("shi-labs/versatile-diffusion", subfolder="scheduler") +>>> pipeline = VersatileDiffusionPipeline.from_pretrained("shi-labs/versatile-diffusion", scheduler=euler_scheduler) +``` + +## VersatileDiffusionPipeline +[[autodoc]] VersatileDiffusionPipeline + +## VersatileDiffusionTextToImagePipeline +[[autodoc]] VersatileDiffusionTextToImagePipeline + - all + - __call__ + +## VersatileDiffusionImageVariationPipeline +[[autodoc]] VersatileDiffusionImageVariationPipeline + - all + - __call__ + +## VersatileDiffusionDualGuidedPipeline +[[autodoc]] VersatileDiffusionDualGuidedPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/pipelines/vq_diffusion.mdx b/diffusers/docs/source/en/api/pipelines/vq_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..459c652935895a9f4f9cbddec57fa6f9b475aad7 --- /dev/null +++ b/diffusers/docs/source/en/api/pipelines/vq_diffusion.mdx @@ -0,0 +1,35 @@ + + +# VQDiffusion + +## Overview + +[Vector Quantized Diffusion Model for Text-to-Image Synthesis](https://arxiv.org/abs/2111.14822) by Shuyang Gu, Dong Chen, Jianmin Bao, Fang Wen, Bo Zhang, Dongdong Chen, Lu Yuan, Baining Guo + +The abstract of the paper is the following: + +We present the vector quantized diffusion (VQ-Diffusion) model for text-to-image generation. This method is based on a vector quantized variational autoencoder (VQ-VAE) whose latent space is modeled by a conditional variant of the recently developed Denoising Diffusion Probabilistic Model (DDPM). We find that this latent-space method is well-suited for text-to-image generation tasks because it not only eliminates the unidirectional bias with existing methods but also allows us to incorporate a mask-and-replace diffusion strategy to avoid the accumulation of errors, which is a serious problem with existing methods. Our experiments show that the VQ-Diffusion produces significantly better text-to-image generation results when compared with conventional autoregressive (AR) models with similar numbers of parameters. Compared with previous GAN-based text-to-image methods, our VQ-Diffusion can handle more complex scenes and improve the synthesized image quality by a large margin. Finally, we show that the image generation computation in our method can be made highly efficient by reparameterization. With traditional AR methods, the text-to-image generation time increases linearly with the output image resolution and hence is quite time consuming even for normal size images. The VQ-Diffusion allows us to achieve a better trade-off between quality and speed. Our experiments indicate that the VQ-Diffusion model with the reparameterization is fifteen times faster than traditional AR methods while achieving a better image quality. + +The original codebase can be found [here](https://github.com/microsoft/VQ-Diffusion). + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_vq_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/vq_diffusion/pipeline_vq_diffusion.py) | *Text-to-Image Generation* | - | + + +## VQDiffusionPipeline +[[autodoc]] VQDiffusionPipeline + - all + - __call__ diff --git a/diffusers/docs/source/en/api/schedulers/ddim.mdx b/diffusers/docs/source/en/api/schedulers/ddim.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f7de240c606fdfd9323d691de03bf3cacd1918a1 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/ddim.mdx @@ -0,0 +1,27 @@ + + +# Denoising diffusion implicit models (DDIM) + +## Overview + +[Denoising Diffusion Implicit Models](https://arxiv.org/abs/2010.02502) (DDIM) by Jiaming Song, Chenlin Meng and Stefano Ermon. + +The abstract of the paper is the following: + +Denoising diffusion probabilistic models (DDPMs) have achieved high quality image generation without adversarial training, yet they require simulating a Markov chain for many steps to produce a sample. To accelerate sampling, we present denoising diffusion implicit models (DDIMs), a more efficient class of iterative implicit probabilistic models with the same training procedure as DDPMs. In DDPMs, the generative process is defined as the reverse of a Markovian diffusion process. We construct a class of non-Markovian diffusion processes that lead to the same training objective, but whose reverse process can be much faster to sample from. We empirically demonstrate that DDIMs can produce high quality samples 10× to 50× faster in terms of wall-clock time compared to DDPMs, allow us to trade off computation for sample quality, and can perform semantically meaningful image interpolation directly in the latent space. + +The original codebase of this paper can be found here: [ermongroup/ddim](https://github.com/ermongroup/ddim). +For questions, feel free to contact the author on [tsong.me](https://tsong.me/). + +## DDIMScheduler +[[autodoc]] DDIMScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/ddpm.mdx b/diffusers/docs/source/en/api/schedulers/ddpm.mdx new file mode 100644 index 0000000000000000000000000000000000000000..260e20aced0f49b9c8f41ace78176bdb4109db0c --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/ddpm.mdx @@ -0,0 +1,27 @@ + + +# Denoising diffusion probabilistic models (DDPM) + +## Overview + +[Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2006.11239) + (DDPM) by Jonathan Ho, Ajay Jain and Pieter Abbeel proposes the diffusion based model of the same name, but in the context of the 🤗 Diffusers library, DDPM refers to the discrete denoising scheduler from the paper as well as the pipeline. + +The abstract of the paper is the following: + +We present high quality image synthesis results using diffusion probabilistic models, a class of latent variable models inspired by considerations from nonequilibrium thermodynamics. Our best results are obtained by training on a weighted variational bound designed according to a novel connection between diffusion probabilistic models and denoising score matching with Langevin dynamics, and our models naturally admit a progressive lossy decompression scheme that can be interpreted as a generalization of autoregressive decoding. On the unconditional CIFAR10 dataset, we obtain an Inception score of 9.46 and a state-of-the-art FID score of 3.17. On 256x256 LSUN, we obtain sample quality similar to ProgressiveGAN. + +The original paper can be found [here](https://arxiv.org/abs/2010.02502). + +## DDPMScheduler +[[autodoc]] DDPMScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/deis.mdx b/diffusers/docs/source/en/api/schedulers/deis.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1841aba71d9ae0b1e38fd84a455de462f58c5249 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/deis.mdx @@ -0,0 +1,22 @@ + + +# DEIS + +Fast Sampling of Diffusion Models with Exponential Integrator. + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2204.13902). The original implementation can be found [here](https://github.com/qsh-zh/deis). + +## DEISMultistepScheduler +[[autodoc]] DEISMultistepScheduler diff --git a/diffusers/docs/source/en/api/schedulers/dpm_discrete.mdx b/diffusers/docs/source/en/api/schedulers/dpm_discrete.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c848195e7ebe2abbcf84fcba5de09daaea1c8324 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/dpm_discrete.mdx @@ -0,0 +1,22 @@ + + +# DPM Discrete Scheduler inspired by Karras et. al paper + +## Overview + +Inspired by [Karras et. al](https://arxiv.org/abs/2206.00364). Scheduler ported from @crowsonkb's https://github.com/crowsonkb/k-diffusion library: + +All credit for making this scheduler work goes to [Katherine Crowson](https://github.com/crowsonkb/) + +## KDPM2DiscreteScheduler +[[autodoc]] KDPM2DiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/dpm_discrete_ancestral.mdx b/diffusers/docs/source/en/api/schedulers/dpm_discrete_ancestral.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5fdc651dc1f9f5076931d5f2a5ff6483d2a7ca96 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/dpm_discrete_ancestral.mdx @@ -0,0 +1,22 @@ + + +# DPM Discrete Scheduler with ancestral sampling inspired by Karras et. al paper + +## Overview + +Inspired by [Karras et. al](https://arxiv.org/abs/2206.00364). Scheduler ported from @crowsonkb's https://github.com/crowsonkb/k-diffusion library: + +All credit for making this scheduler work goes to [Katherine Crowson](https://github.com/crowsonkb/) + +## KDPM2AncestralDiscreteScheduler +[[autodoc]] KDPM2AncestralDiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/euler.mdx b/diffusers/docs/source/en/api/schedulers/euler.mdx new file mode 100644 index 0000000000000000000000000000000000000000..718e7031dda35a46c9511f749564faa9f24df18b --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/euler.mdx @@ -0,0 +1,21 @@ + + +# Euler scheduler + +## Overview + +Euler scheduler (Algorithm 2) from the paper [Elucidating the Design Space of Diffusion-Based Generative Models](https://arxiv.org/abs/2206.00364) by Karras et al. (2022). Based on the original [k-diffusion](https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L51) implementation by Katherine Crowson. +Fast scheduler which often times generates good outputs with 20-30 steps. + +## EulerDiscreteScheduler +[[autodoc]] EulerDiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/euler_ancestral.mdx b/diffusers/docs/source/en/api/schedulers/euler_ancestral.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3f736fa5454d8be7ae0313a65f03877cc47b7ed5 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/euler_ancestral.mdx @@ -0,0 +1,21 @@ + + +# Euler Ancestral scheduler + +## Overview + +Ancestral sampling with Euler method steps. Based on the original (k-diffusion)[https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L72] implementation by Katherine Crowson. +Fast scheduler which often times generates good outputs with 20-30 steps. + +## EulerAncestralDiscreteScheduler +[[autodoc]] EulerAncestralDiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/heun.mdx b/diffusers/docs/source/en/api/schedulers/heun.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5539be881d1961939a029eb320da3951d4e0f260 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/heun.mdx @@ -0,0 +1,23 @@ + + +# Heun scheduler inspired by Karras et. al paper + +## Overview + +Algorithm 1 of [Karras et. al](https://arxiv.org/abs/2206.00364). +Scheduler ported from @crowsonkb's https://github.com/crowsonkb/k-diffusion library: + +All credit for making this scheduler work goes to [Katherine Crowson](https://github.com/crowsonkb/) + +## HeunDiscreteScheduler +[[autodoc]] HeunDiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/ipndm.mdx b/diffusers/docs/source/en/api/schedulers/ipndm.mdx new file mode 100644 index 0000000000000000000000000000000000000000..069928cbbc10e4c34a8ea9dc77a84d8cea5839ed --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/ipndm.mdx @@ -0,0 +1,20 @@ + + +# improved pseudo numerical methods for diffusion models (iPNDM) + +## Overview + +Original implementation can be found [here](https://github.com/crowsonkb/v-diffusion-pytorch/blob/987f8985e38208345c1959b0ea767a625831cc9b/diffusion/sampling.py#L296). + +## IPNDMScheduler +[[autodoc]] IPNDMScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/lms_discrete.mdx b/diffusers/docs/source/en/api/schedulers/lms_discrete.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d7fa87812602d26a06a848083d5253d8b864ddc8 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/lms_discrete.mdx @@ -0,0 +1,20 @@ + + +# Linear multistep scheduler for discrete beta schedules + +## Overview + +Original implementation can be found [here](https://arxiv.org/abs/2206.00364). + +## LMSDiscreteScheduler +[[autodoc]] LMSDiscreteScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/multistep_dpm_solver.mdx b/diffusers/docs/source/en/api/schedulers/multistep_dpm_solver.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e3f725fe4a7ef740e8dd2f071ea6586de76592a3 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/multistep_dpm_solver.mdx @@ -0,0 +1,20 @@ + + +# Multistep DPM-Solver + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2206.00927) and the [improved version](https://arxiv.org/abs/2211.01095). The original implementation can be found [here](https://github.com/LuChengTHU/dpm-solver). + +## DPMSolverMultistepScheduler +[[autodoc]] DPMSolverMultistepScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/overview.mdx b/diffusers/docs/source/en/api/schedulers/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d27fbe10c528ab16dff4ac2472504151960e732d --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/overview.mdx @@ -0,0 +1,86 @@ + + +# Schedulers + +Diffusers contains multiple pre-built schedule functions for the diffusion process. + +## What is a scheduler? + +The schedule functions, denoted *Schedulers* in the library take in the output of a trained model, a sample which the diffusion process is iterating on, and a timestep to return a denoised sample. That's why schedulers may also be called *Samplers* in other diffusion models implementations. + +- Schedulers define the methodology for iteratively adding noise to an image or for updating a sample based on model outputs. + - adding noise in different manners represent the algorithmic processes to train a diffusion model by adding noise to images. + - for inference, the scheduler defines how to update a sample based on an output from a pretrained model. +- Schedulers are often defined by a *noise schedule* and an *update rule* to solve the differential equation solution. + +### Discrete versus continuous schedulers + +All schedulers take in a timestep to predict the updated version of the sample being diffused. +The timesteps dictate where in the diffusion process the step is, where data is generated by iterating forward in time and inference is executed by propagating backwards through timesteps. +Different algorithms use timesteps that can be discrete (accepting `int` inputs), such as the [`DDPMScheduler`] or [`PNDMScheduler`], or continuous (accepting `float` inputs), such as the score-based schedulers [`ScoreSdeVeScheduler`] or [`ScoreSdeVpScheduler`]. + +## Designing Re-usable schedulers + +The core design principle between the schedule functions is to be model, system, and framework independent. +This allows for rapid experimentation and cleaner abstractions in the code, where the model prediction is separated from the sample update. +To this end, the design of schedulers is such that: + +- Schedulers can be used interchangeably between diffusion models in inference to find the preferred trade-off between speed and generation quality. +- Schedulers are currently by default in PyTorch, but are designed to be framework independent (partial Jax support currently exists). +- Many diffusion pipelines, such as [`StableDiffusionPipeline`] and [`DiTPipeline`] can use any of [`KarrasDiffusionSchedulers`] + +## Schedulers Summary + +The following table summarizes all officially supported schedulers, their corresponding paper + + +| Scheduler | Paper | +|---|---| +| [ddim](./ddim) | [**Denoising Diffusion Implicit Models**](https://arxiv.org/abs/2010.02502) | +| [ddpm](./ddpm) | [**Denoising Diffusion Probabilistic Models**](https://arxiv.org/abs/2006.11239) | +| [singlestep_dpm_solver](./singlestep_dpm_solver) | [**Singlestep DPM-Solver**](https://arxiv.org/abs/2206.00927) | +| [multistep_dpm_solver](./multistep_dpm_solver) | [**Multistep DPM-Solver**](https://arxiv.org/abs/2206.00927) | +| [heun](./heun) | [**Heun scheduler inspired by Karras et. al paper**](https://arxiv.org/abs/2206.00364) | +| [dpm_discrete](./dpm_discrete) | [**DPM Discrete Scheduler inspired by Karras et. al paper**](https://arxiv.org/abs/2206.00364) | +| [dpm_discrete_ancestral](./dpm_discrete_ancestral) | [**DPM Discrete Scheduler with ancestral sampling inspired by Karras et. al paper**](https://arxiv.org/abs/2206.00364) | +| [stochastic_karras_ve](./stochastic_karras_ve) | [**Variance exploding, stochastic sampling from Karras et. al**](https://arxiv.org/abs/2206.00364) | +| [lms_discrete](./lms_discrete) | [**Linear multistep scheduler for discrete beta schedules**](https://arxiv.org/abs/2206.00364) | +| [pndm](./pndm) | [**Pseudo numerical methods for diffusion models (PNDM)**](https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L181) | +| [score_sde_ve](./score_sde_ve) | [**variance exploding stochastic differential equation (VE-SDE) scheduler**](https://arxiv.org/abs/2011.13456) | +| [ipndm](./ipndm) | [**improved pseudo numerical methods for diffusion models (iPNDM)**](https://github.com/crowsonkb/v-diffusion-pytorch/blob/987f8985e38208345c1959b0ea767a625831cc9b/diffusion/sampling.py#L296) | +| [score_sde_vp](./score_sde_vp) | [**Variance preserving stochastic differential equation (VP-SDE) scheduler**](https://arxiv.org/abs/2011.13456) | +| [euler](./euler) | [**Euler scheduler**](https://arxiv.org/abs/2206.00364) | +| [euler_ancestral](./euler_ancestral) | [**Euler Ancestral scheduler**](https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L72) | +| [vq_diffusion](./vq_diffusion) | [**VQDiffusionScheduler**](https://arxiv.org/abs/2111.14822) | +| [repaint](./repaint) | [**RePaint scheduler**](https://arxiv.org/abs/2201.09865) | + +## API + +The core API for any new scheduler must follow a limited structure. +- Schedulers should provide one or more `def step(...)` functions that should be called to update the generated sample iteratively. +- Schedulers should provide a `set_timesteps(...)` method that configures the parameters of a schedule function for a specific inference task. +- Schedulers should be framework-specific. + +The base class [`SchedulerMixin`] implements low level utilities used by multiple schedulers. + +### SchedulerMixin +[[autodoc]] SchedulerMixin + +### SchedulerOutput +The class [`SchedulerOutput`] contains the outputs from any schedulers `step(...)` call. + +[[autodoc]] schedulers.scheduling_utils.SchedulerOutput + +### KarrasDiffusionSchedulers + +[[autodoc]] schedulers.scheduling_utils.KarrasDiffusionSchedulers diff --git a/diffusers/docs/source/en/api/schedulers/pndm.mdx b/diffusers/docs/source/en/api/schedulers/pndm.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ed6ae082c049e3d207035adb2372202d2eb74307 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/pndm.mdx @@ -0,0 +1,20 @@ + + +# Pseudo numerical methods for diffusion models (PNDM) + +## Overview + +Original implementation can be found [here](https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L181). + +## PNDMScheduler +[[autodoc]] PNDMScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/repaint.mdx b/diffusers/docs/source/en/api/schedulers/repaint.mdx new file mode 100644 index 0000000000000000000000000000000000000000..717a58372e73c22f979483acd99ce245c9b61c19 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/repaint.mdx @@ -0,0 +1,23 @@ + + +# RePaint scheduler + +## Overview + +DDPM-based inpainting scheduler for unsupervised inpainting with extreme masks. +Intended for use with [`RePaintPipeline`]. +Based on the paper [RePaint: Inpainting using Denoising Diffusion Probabilistic Models](https://arxiv.org/abs/2201.09865) +and the original implementation by Andreas Lugmayr et al.: https://github.com/andreas128/RePaint + +## RePaintScheduler +[[autodoc]] RePaintScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/score_sde_ve.mdx b/diffusers/docs/source/en/api/schedulers/score_sde_ve.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b8fb9325767fff7c9378d642b7925aeee0c21f2e --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/score_sde_ve.mdx @@ -0,0 +1,20 @@ + + +# variance exploding stochastic differential equation (VE-SDE) scheduler + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2011.13456). + +## ScoreSdeVeScheduler +[[autodoc]] ScoreSdeVeScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/score_sde_vp.mdx b/diffusers/docs/source/en/api/schedulers/score_sde_vp.mdx new file mode 100644 index 0000000000000000000000000000000000000000..1f45439ab7cc3cadb7bf93d5abee44f788d7593e --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/score_sde_vp.mdx @@ -0,0 +1,26 @@ + + +# Variance preserving stochastic differential equation (VP-SDE) scheduler + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2011.13456). + + + +Score SDE-VP is under construction. + + + +## ScoreSdeVpScheduler +[[autodoc]] schedulers.scheduling_sde_vp.ScoreSdeVpScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/singlestep_dpm_solver.mdx b/diffusers/docs/source/en/api/schedulers/singlestep_dpm_solver.mdx new file mode 100644 index 0000000000000000000000000000000000000000..44231c2d97d7702bb40ae99d1e1bbee1084d3cea --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/singlestep_dpm_solver.mdx @@ -0,0 +1,20 @@ + + +# Singlestep DPM-Solver + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2206.00927) and the [improved version](https://arxiv.org/abs/2211.01095). The original implementation can be found [here](https://github.com/LuChengTHU/dpm-solver). + +## DPMSolverSinglestepScheduler +[[autodoc]] DPMSolverSinglestepScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/stochastic_karras_ve.mdx b/diffusers/docs/source/en/api/schedulers/stochastic_karras_ve.mdx new file mode 100644 index 0000000000000000000000000000000000000000..95437f85da5d056db846d2c395064eddfb10b326 --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/stochastic_karras_ve.mdx @@ -0,0 +1,20 @@ + + +# Variance exploding, stochastic sampling from Karras et. al + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2206.00364). + +## KarrasVeScheduler +[[autodoc]] KarrasVeScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/api/schedulers/vq_diffusion.mdx b/diffusers/docs/source/en/api/schedulers/vq_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..921cc17d434d36ce43a941b24559d32f4654d8bd --- /dev/null +++ b/diffusers/docs/source/en/api/schedulers/vq_diffusion.mdx @@ -0,0 +1,20 @@ + + +# VQDiffusionScheduler + +## Overview + +Original paper can be found [here](https://arxiv.org/abs/2111.14822) + +## VQDiffusionScheduler +[[autodoc]] VQDiffusionScheduler \ No newline at end of file diff --git a/diffusers/docs/source/en/conceptual/contribution.mdx b/diffusers/docs/source/en/conceptual/contribution.mdx new file mode 100644 index 0000000000000000000000000000000000000000..99ea6e9d7e0636f1d087ca47bd5d6b3df253c1c4 --- /dev/null +++ b/diffusers/docs/source/en/conceptual/contribution.mdx @@ -0,0 +1,291 @@ + + +# How to contribute to Diffusers 🧨 + +We ❤️ contributions from the open-source community! Everyone is welcome, and all types of participation –not just code– are valued and appreciated. Answering questions, helping others, reaching out and improving the documentation are all immensely valuable to the community, so don't be afraid and get involved if you're up for it! + +It also helps us if you spread the word: reference the library from blog posts +on the awesome projects it made possible, shout out on Twitter every time it has +helped you, or simply star the repo to say "thank you". + +We encourage everyone to start by saying 👋 in our public Discord channel. We discuss the hottest trends about diffusion models, ask questions, show-off personal projects, help each other with contributions, or just hang out ☕. Join us on Discord + +Whichever way you choose to contribute, we strive to be part of an open, welcoming and kind community. Please, read our [code of conduct](https://github.com/huggingface/diffusers/blob/main/CODE_OF_CONDUCT.md) and be mindful to respect it during your interactions. + + +## Overview + +You can contribute in so many ways! Just to name a few: + +* Fixing outstanding issues with the existing code. +* Implementing [new diffusion pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines#contribution), [new schedulers](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers) or [new models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models). +* [Contributing to the examples](https://github.com/huggingface/diffusers/tree/main/examples). +* [Contributing to the documentation](https://github.com/huggingface/diffusers/tree/main/docs/source). +* Submitting issues related to bugs or desired new features. + +*All are equally valuable to the community.* + +### Browse GitHub issues for suggestions + +If you need inspiration, you can look out for [issues](https://github.com/huggingface/diffusers/issues) you'd like to tackle to contribute to the library. There are a few filters that can be helpful: + +- See [Good first issues](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) for general opportunities to contribute and getting started with the codebase. +- See [New pipeline/model](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22New+pipeline%2Fmodel%22) to contribute exciting new diffusion models or diffusion pipelines. +- See [New scheduler](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22New+scheduler%22) to work on new samplers and schedulers. + + +## Submitting a new issue or feature request + +Do your best to follow these guidelines when submitting an issue or a feature +request. It will make it easier for us to come back to you quickly and with good +feedback. + +### Did you find a bug? + +The 🧨 Diffusers library is robust and reliable thanks to the users who notify us of +the problems they encounter. So thank you for reporting an issue. + +First, we would really appreciate it if you could **make sure the bug was not +already reported** (use the search bar on GitHub under Issues). + +### Do you want to implement a new diffusion pipeline / diffusion model? + +Awesome! Please provide the following information: + +* Short description of the diffusion pipeline and link to the paper; +* Link to the implementation if it is open-source; +* Link to the model weights if they are available. + +If you are willing to contribute the model yourself, let us know so we can best +guide you. + +### Do you want a new feature (that is not a model)? + +A world-class feature request addresses the following points: + +1. Motivation first: + * Is it related to a problem/frustration with the library? If so, please explain + why. Providing a code snippet that demonstrates the problem is best. + * Is it related to something you would need for a project? We'd love to hear + about it! + * Is it something you worked on and think could benefit the community? + Awesome! Tell us what problem it solved for you. +2. Write a *full paragraph* describing the feature; +3. Provide a **code snippet** that demonstrates its future use; +4. In case this is related to a paper, please attach a link; +5. Attach any additional information (drawings, screenshots, etc.) you think may help. + +If your issue is well written we're already 80% of the way there by the time you +post it. + +## Start contributing! (Pull Requests) + +Before writing code, we strongly advise you to search through the existing PRs or +issues to make sure that nobody is already working on the same thing. If you are +unsure, it is always a good idea to open an issue to get some feedback. + +You will need basic `git` proficiency to be able to contribute to +🧨 Diffusers. `git` is not the easiest tool to use but it has the greatest +manual. Type `git --help` in a shell and enjoy. If you prefer books, [Pro +Git](https://git-scm.com/book/en/v2) is a very good reference. + +Follow these steps to start contributing ([supported Python versions](https://github.com/huggingface/diffusers/blob/main/setup.py#L212)): + +1. Fork the [repository](https://github.com/huggingface/diffusers) by + clicking on the 'Fork' button on the repository's page. This creates a copy of the code + under your GitHub user account. + +2. Clone your fork to your local disk, and add the base repository as a remote: + + ```bash + $ git clone git@github.com:/diffusers.git + $ cd diffusers + $ git remote add upstream https://github.com/huggingface/diffusers.git + ``` + +3. Create a new branch to hold your development changes: + + ```bash + $ git checkout -b a-descriptive-name-for-my-changes + ``` + + **Do not** work on the `main` branch. + +4. Set up a development environment by running the following command in a virtual environment: + + ```bash + $ pip install -e ".[dev]" + ``` + + (If Diffusers was already installed in the virtual environment, remove + it with `pip uninstall diffusers` before reinstalling it in editable + mode with the `-e` flag.) + + To run the full test suite, you might need the additional dependency on `transformers` and `datasets` which requires a separate source + install: + + ```bash + $ git clone https://github.com/huggingface/transformers + $ cd transformers + $ pip install -e . + ``` + + ```bash + $ git clone https://github.com/huggingface/datasets + $ cd datasets + $ pip install -e . + ``` + + If you have already cloned that repo, you might need to `git pull` to get the most recent changes in the `datasets` + library. + +5. Develop the features on your branch. + + As you work on the features, you should make sure that the test suite + passes. You should run the tests impacted by your changes like this: + + ```bash + $ pytest tests/.py + ``` + + You can also run the full suite with the following command, but it takes + a beefy machine to produce a result in a decent amount of time now that + Diffusers has grown a lot. Here is the command for it: + + ```bash + $ make test + ``` + + For more information about tests, check out the + [dedicated documentation](https://huggingface.co/docs/diffusers/testing) + + 🧨 Diffusers relies on `black` and `isort` to format its source code + consistently. After you make changes, apply automatic style corrections and code verifications + that can't be automated in one go with: + + ```bash + $ make style + ``` + + 🧨 Diffusers also uses `ruff` and a few custom scripts to check for coding mistakes. Quality + control runs in CI, however you can also run the same checks with: + + ```bash + $ make quality + ``` + + Once you're happy with your changes, add changed files using `git add` and + make a commit with `git commit` to record your changes locally: + + ```bash + $ git add modified_file.py + $ git commit + ``` + + It is a good idea to sync your copy of the code with the original + repository regularly. This way you can quickly account for changes: + + ```bash + $ git fetch upstream + $ git rebase upstream/main + ``` + + Push the changes to your account using: + + ```bash + $ git push -u origin a-descriptive-name-for-my-changes + ``` + +6. Once you are satisfied (**and the checklist below is happy too**), go to the + webpage of your fork on GitHub. Click on 'Pull request' to send your changes + to the project maintainers for review. + +7. It's ok if maintainers ask you for changes. It happens to core contributors + too! So everyone can see the changes in the Pull request, work in your local + branch and push the changes to your fork. They will automatically appear in + the pull request. + + +### Checklist + +1. The title of your pull request should be a summary of its contribution; +2. If your pull request addresses an issue, please mention the issue number in + the pull request description to make sure they are linked (and people + consulting the issue know you are working on it); +3. To indicate a work in progress please prefix the title with `[WIP]`. These + are useful to avoid duplicated work, and to differentiate it from PRs ready + to be merged; +4. Make sure existing tests pass; +5. Add high-coverage tests. No quality testing = no merge. + - If you are adding new `@slow` tests, make sure they pass using + `RUN_SLOW=1 python -m pytest tests/test_my_new_model.py`. + - If you are adding a new tokenizer, write tests, and make sure + `RUN_SLOW=1 python -m pytest tests/test_tokenization_{your_model_name}.py` passes. + CircleCI does not run the slow tests, but GitHub actions does every night! +6. All public methods must have informative docstrings that work nicely with sphinx. See `[pipeline_latent_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py)` for an example. +7. Due to the rapidly growing repository, it is important to make sure that no files that would significantly weigh down the repository are added. This includes images, videos and other non-text files. We prefer to leverage a hf.co hosted `dataset` like + the ones hosted on [`hf-internal-testing`](https://huggingface.co/hf-internal-testing) in which to place these files and reference or [huggingface/documentation-images](https://huggingface.co/datasets/huggingface/documentation-images). + If an external contribution, feel free to add the images to your PR and ask a Hugging Face member to migrate your images + to this dataset. + +### Tests + +An extensive test suite is included to test the library behavior and several examples. Library tests can be found in +the [tests folder](https://github.com/huggingface/diffusers/tree/main/tests). + +We like `pytest` and `pytest-xdist` because it's faster. From the root of the +repository, here's how to run tests with `pytest` for the library: + +```bash +$ python -m pytest -n auto --dist=loadfile -s -v ./tests/ +``` + +In fact, that's how `make test` is implemented! + +You can specify a smaller set of tests in order to test only the feature +you're working on. + +By default, slow tests are skipped. Set the `RUN_SLOW` environment variable to +`yes` to run them. This will download many gigabytes of models — make sure you +have enough disk space and a good Internet connection, or a lot of patience! + +```bash +$ RUN_SLOW=yes python -m pytest -n auto --dist=loadfile -s -v ./tests/ +``` + +`unittest` is fully supported, here's how to run tests with it: + +```bash +$ python -m unittest discover -s tests -t . -v +$ python -m unittest discover -s examples -t examples -v +``` + +### Syncing forked main with upstream (HuggingFace) main + +To avoid pinging the upstream repository which adds reference notes to each upstream PR and sends unnecessary notifications to the developers involved in these PRs, +when syncing the main branch of a forked repository, please, follow these steps: +1. When possible, avoid syncing with the upstream using a branch and PR on the forked repository. Instead, merge directly into the forked main. +2. If a PR is absolutely necessary, use the following steps after checking out your branch: +``` +$ git checkout -b your-branch-for-syncing +$ git pull --squash --no-commit upstream main +$ git commit -m '' +$ git push --set-upstream origin your-branch-for-syncing +``` + +### Style guide + +For documentation strings, 🧨 Diffusers follows the [google style](https://google.github.io/styleguide/pyguide.html). + + +**This guide was heavily inspired by the awesome [scikit-learn guide to contributing](https://github.com/scikit-learn/scikit-learn/blob/main/CONTRIBUTING.md).** diff --git a/diffusers/docs/source/en/conceptual/ethical_guidelines.mdx b/diffusers/docs/source/en/conceptual/ethical_guidelines.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f63f4b7a2a98e2e5bff7fe601e6d5920567653cf --- /dev/null +++ b/diffusers/docs/source/en/conceptual/ethical_guidelines.mdx @@ -0,0 +1,43 @@ +# 🧨 Diffusers’ Ethical Guidelines + +## Preamble + +[Diffusers](https://huggingface.co/docs/diffusers/index) provides pre-trained diffusion models and serves as a modular toolbox for inference and training. + +Given its real case applications in the world and potential negative impacts on society, we think it is important to provide the project with ethical guidelines to guide the development, users’ contributions, and usage of the Diffusers library. + +The risks associated with using this technology are still being examined, but to name a few: copyrights issues for artists; deep-fake exploitation; sexual content generation in inappropriate contexts; non-consensual impersonation; harmful social biases perpetuating the oppression of marginalized groups. +We will keep tracking risks and adapt the following guidelines based on the community's responsiveness and valuable feedback. + +## Scope +The Diffusers community will apply the following ethical guidelines to the project’s development and help coordinate how the community will integrate the contributions, especially concerning sensitive topics related to ethical concerns. + +## Ethical guidelines +The following ethical guidelines apply generally, but we will primarily implement them when dealing with ethically sensitive issues while making a technical choice. Furthermore, we commit to adapting those ethical principles over time following emerging harms related to the state of the art of the technology in question. + +- **Transparency**: we are committed to being transparent in managing PRs, explaining our choices to users, and making technical decisions. + +- **Consistency**: we are committed to guaranteeing our users the same level of attention in project management, keeping it technically stable and consistent. + +- **Simplicity**: with a desire to make it easy to use and exploit the Diffusers library, we are committed to keeping the project’s goals lean and coherent. + +- **Accessibility**: the Diffusers project helps lower the entry bar for contributors who can help run it even without technical expertise. Doing so makes research artifacts more accessible to the community. + +- **Reproducibility**: we aim to be transparent about the reproducibility of upstream code, models, and datasets when made available through the Diffusers library. + +- **Responsibility**: as a community and through teamwork, we hold a collective responsibility to our users by anticipating and mitigating this technology's potential risks and dangers. + +## Examples of implementations: Safety features and Mechanisms +The team works daily to make the technical and non-technical tools available to deal with the potential ethical and social risks associated with diffusion technology. Moreover, the community's input is invaluable in ensuring these features' implementation and raising awareness with us. + +- [**Community tab**](https://huggingface.co/docs/hub/repositories-pull-requests-discussions): it enables the community to discuss and better collaborate on a project. + +- **Bias exploration and evaluation**: the Hugging Face team provides a [space](https://huggingface.co/spaces/society-ethics/DiffusionBiasExplorer) to demonstrate the biases in Stable Diffusion interactively. In this sense, we support and encourage bias explorers and evaluations. + +- **Encouraging safety in deployment** + + - [**Safe Stable Diffusion**](https://huggingface.co/docs/diffusers/main/en/api/pipelines/stable_diffusion_safe): It mitigates the well-known issue that models, like Stable Diffusion, that are trained on unfiltered, web-crawled datasets tend to suffer from inappropriate degeneration. Related paper: [Safe Latent Diffusion: Mitigating Inappropriate Degeneration in Diffusion Models](https://arxiv.org/abs/2211.05105). + +- **Staged released on the Hub**: in particularly sensitive situations, access to some repositories should be restricted. This staged release is an intermediary step that allows the repository’s authors to have more control over its use. + +- **Licensing**: [OpenRAILs](https://huggingface.co/blog/open_rail), a new type of licensing, allow us to ensure free access while having a set of restrictions that ensure more responsible use. diff --git a/diffusers/docs/source/en/conceptual/philosophy.mdx b/diffusers/docs/source/en/conceptual/philosophy.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c090463b0eabfb1bb037e9a8514c411f18c17fd7 --- /dev/null +++ b/diffusers/docs/source/en/conceptual/philosophy.mdx @@ -0,0 +1,110 @@ + + +# Philosophy + +🧨 Diffusers provides **state-of-the-art** pretrained diffusion models across multiple modalities. +Its purpose is to serve as a **modular toolbox** for both inference and training. + +We aim at building a library that stands the test of time and therefore take API design very seriously. + +In a nutshell, Diffusers is built to be a natural extension of PyTorch. Therefore, most of our design choices are based on [PyTorch's Design Principles](https://pytorch.org/docs/stable/community/design.html#pytorch-design-philosophy). Let's go over the most important ones: + +## Usability over Performance + +- While Diffusers has many built-in performance-enhancing features (see [Memory and Speed](https://huggingface.co/docs/diffusers/optimization/fp16)), models are always loaded with the highest precision and lowest optimization. Therefore, by default diffusion pipelines are always instantiated on CPU with float32 precision if not otherwise defined by the user. This ensures usability across different platforms and accelerators and means that no complex installations are required to run the library. +- Diffusers aim at being a **light-weight** package and therefore has very few required dependencies, but many soft dependencies that can improve performance (such as `accelerate`, `safetensors`, `onnx`, etc...). We strive to keep the library as lightweight as possible so that it can be added without much concern as a dependency on other packages. +- Diffusers prefers simple, self-explainable code over condensed, magic code. This means that short-hand code syntaxes such as lambda functions, and advanced PyTorch operators are often not desired. + +## Simple over easy + +As PyTorch states, **explicit is better than implicit** and **simple is better than complex**. This design philosophy is reflected in multiple parts of the library: +- We follow PyTorch's API with methods like [`DiffusionPipeline.to`](https://huggingface.co/docs/diffusers/main/en/api/diffusion_pipeline#diffusers.DiffusionPipeline.to) to let the user handle device management. +- Raising concise error messages is preferred to silently correct erroneous input. Diffusers aims at teaching the user, rather than making the library as easy to use as possible. +- Complex model vs. scheduler logic is exposed instead of magically handled inside. Schedulers/Samplers are separated from diffusion models with minimal dependencies on each other. This forces the user to write the unrolled denoising loop. However, the separation allows for easier debugging and gives the user more control over adapting the denoising process or switching out diffusion models or schedulers. +- Separately trained components of the diffusion pipeline, *e.g.* the text encoder, the unet, and the variational autoencoder, each have their own model class. This forces the user to handle the interaction between the different model components, and the serialization format separates the model components into different files. However, this allows for easier debugging and customization. Dreambooth or textual inversion training +is very simple thanks to diffusers' ability to separate single components of the diffusion pipeline. + +## Tweakable, contributor-friendly over abstraction + +For large parts of the library, Diffusers adopts an important design principle of the [Transformers library](https://github.com/huggingface/transformers), which is to prefer copy-pasted code over hasty abstractions. This design principle is very opinionated and stands in stark contrast to popular design principles such as [Don't repeat yourself (DRY)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). +In short, just like Transformers does for modeling files, diffusers prefers to keep an extremely low level of abstraction and very self-contained code for pipelines and schedulers. +Functions, long code blocks, and even classes can be copied across multiple files which at first can look like a bad, sloppy design choice that makes the library unmaintainable. +**However**, this design has proven to be extremely successful for Transformers and makes a lot of sense for community-driven, open-source machine learning libraries because: +- Machine Learning is an extremely fast-moving field in which paradigms, model architectures, and algorithms are changing rapidly, which therefore makes it very difficult to define long-lasting code abstractions. +- Machine Learning practitioners like to be able to quickly tweak existing code for ideation and research and therefore prefer self-contained code over one that contains many abstractions. +- Open-source libraries rely on community contributions and therefore must build a library that is easy to contribute to. The more abstract the code, the more dependencies, the harder to read, and the harder to contribute to. Contributors simply stop contributing to very abstract libraries out of fear of breaking vital functionality. If contributing to a library cannot break other fundamental code, not only is it more inviting for potential new contributors, but it is also easier to review and contribute to multiple parts in parallel. + +At Hugging Face, we call this design the **single-file policy** which means that almost all of the code of a certain class should be written in a single, self-contained file. To read more about the philosophy, you can have a look +at [this blog post](https://huggingface.co/blog/transformers-design-philosophy). + +In diffusers, we follow this philosophy for both pipelines and schedulers, but only partly for diffusion models. The reason we don't follow this design fully for diffusion models is because almost all diffusion pipelines, such +as [DDPM](https://huggingface.co/docs/diffusers/v0.12.0/en/api/pipelines/ddpm), [Stable Diffusion](https://huggingface.co/docs/diffusers/v0.12.0/en/api/pipelines/stable_diffusion/overview#stable-diffusion-pipelines), [UnCLIP (Dalle-2)](https://huggingface.co/docs/diffusers/v0.12.0/en/api/pipelines/unclip#overview) and [Imagen](https://imagen.research.google/) all rely on the same diffusion model, the [UNet](https://huggingface.co/docs/diffusers/api/models#diffusers.UNet2DConditionModel). + +Great, now you should have generally understood why 🧨 Diffusers is designed the way it is 🤗. +We try to apply these design principles consistently across the library. Nevertheless, there are some minor exceptions to the philosophy or some unlucky design choices. If you have feedback regarding the design, we would ❤️ to hear it [directly on GitHub](https://github.com/huggingface/diffusers/issues/new?assignees=&labels=&template=feedback.md&title=). + +## Design Philosophy in Details + +Now, let's look a bit into the nitty-gritty details of the design philosophy. Diffusers essentially consist of three major classes, [pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines), [models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models), and [schedulers](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers). +Let's walk through more in-detail design decisions for each class. + +### Pipelines + +Pipelines are designed to be easy to use (therefore do not follow [*Simple over easy*](#simple-over-easy) 100%)), are not feature complete, and should loosely be seen as examples of how to use [models](#models) and [schedulers](#schedulers) for inference. + +The following design principles are followed: +- Pipelines follow the single-file policy. All pipelines can be found in individual directories under src/diffusers/pipelines. One pipeline folder corresponds to one diffusion paper/project/release. Multiple pipeline files can be gathered in one pipeline folder, as it’s done for [`src/diffusers/pipelines/stable-diffusion`](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/stable_diffusion). If pipelines share similar functionality, one can make use of the [#Copied from mechanism](https://github.com/huggingface/diffusers/blob/125d783076e5bd9785beb05367a2d2566843a271/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py#L251). +- Pipelines all inherit from [`DiffusionPipeline`] +- Every pipeline consists of different model and scheduler components, that are documented in the [`model_index.json` file](https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/model_index.json), are accessible under the same name as attributes of the pipeline and can be shared between pipelines with [`DiffusionPipeline.components`](https://huggingface.co/docs/diffusers/main/en/api/diffusion_pipeline#diffusers.DiffusionPipeline.components) function. +- Every pipeline should be loadable via the [`DiffusionPipeline.from_pretrained`](https://huggingface.co/docs/diffusers/main/en/api/diffusion_pipeline#diffusers.DiffusionPipeline.from_pretrained) function. +- Pipelines should be used **only** for inference. +- Pipelines should be very readable, self-explanatory, and easy to tweak. +- Pipelines should be designed to build on top of each other and be easy to integrate into higher-level APIs. +- Pipelines are **not** intended to be feature-complete user interfaces. For future complete user interfaces one should rather have a look at [InvokeAI](https://github.com/invoke-ai/InvokeAI), [Diffuzers](https://github.com/abhishekkrthakur/diffuzers), and [lama-cleaner](https://github.com/Sanster/lama-cleaner) +- Every pipeline should have one and only one way to run it via a `__call__` method. The naming of the `__call__` arguments should be shared across all pipelines. +- Pipelines should be named after the task they are intended to solve. +- In almost all cases, novel diffusion pipelines shall be implemented in a new pipeline folder/file. + +### Models + +Models are designed as configurable toolboxes that are natural extensions of [PyTorch's Module class](https://pytorch.org/docs/stable/generated/torch.nn.Module.html). They only partly follow the **single-file policy**. + +The following design principles are followed: +- Models correspond to **a type of model architecture**. *E.g.* the [`UNet2DConditionModel`] class is used for all UNet variations that expect 2D image inputs and are conditioned on some context. +- All models can be found in [`src/diffusers/models`](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models) and every model architecture shall be defined in its file, e.g. [`unet_2d_condition.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unet_2d_condition.py), [`transformer_2d.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformer_2d.py), etc... +- Models **do not** follow the single-file policy and should make use of smaller model building blocks, such as [`attention.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention.py), [`resnet.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/resnet.py), [`embeddings.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/embeddings.py), etc... **Note**: This is in stark contrast to Transformers' modeling files and shows that models do not really follow the single-file policy. +- Models intend to expose complexity, just like PyTorch's module does, and give clear error messages. +- Models all inherit from `ModelMixin` and `ConfigMixin`. +- Models can be optimized for performance when it doesn’t demand major code changes, keeps backward compatibility, and gives significant memory or compute gain. +- Models should by default have the highest precision and lowest performance setting. +- To integrate new model checkpoints whose general architecture can be classified as an architecture that already exists in Diffusers, the existing model architecture shall be adapted to make it work with the new checkpoint. One should only create a new file if the model architecture is fundamentally different. +- Models should be designed to be easily extendable to future changes. This can be achieved by limiting public function arguments, configuration arguments, and "foreseeing" future changes, *e.g.* it is usually better to add `string` "...type" arguments that can easily be extended to new future types instead of boolean `is_..._type` arguments. Only the minimum amount of changes shall be made to existing architectures to make a new model checkpoint work. +- The model design is a difficult trade-off between keeping code readable and concise and supporting many model checkpoints. For most parts of the modeling code, classes shall be adapted for new model checkpoints, while there are some exceptions where it is preferred to add new classes to make sure the code is kept concise and +readable longterm, such as [UNet blocks](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unet_2d_blocks.py) and [Attention processors](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py). + +### Schedulers + +Schedulers are responsible to guide the denoising process for inference as well as to define a noise schedule for training. They are designed as individual classes with loadable configuration files and strongly follow the **single-file policy**. + +The following design principles are followed: +- All schedulers are found in [`src/diffusers/schedulers`](https://github.com/huggingface/diffusers/tree/main/src/diffusers/schedulers). +- Schedulers are **not** allowed to import from large utils files and shall be kept very self-contained. +- One scheduler python file corresponds to one scheduler algorithm (as might be defined in a paper). +- If schedulers share similar functionalities, we can make use of the `#Copied from` mechanism. +- Schedulers all inherit from `SchedulerMixin` and `ConfigMixin`. +- Schedulers can be easily swapped out with the [`ConfigMixin.from_config`](https://huggingface.co/docs/diffusers/main/en/api/configuration#diffusers.ConfigMixin.from_config) method as explained in detail [here](./using-diffusers/schedulers.mdx). +- Every scheduler has to have a `set_num_inference_steps`, and a `step` function. `set_num_inference_steps(...)` has to be called before every denoising process, *i.e.* before `step(...)` is called. +- Every scheduler exposes the timesteps to be "looped over" via a `timesteps` attribute, which is an array of timesteps the model will be called upon +- The `step(...)` function takes a predicted model output and the "current" sample (x_t) and returns the "previous", slightly more denoised sample (x_t-1). +- Given the complexity of diffusion schedulers, the `step` function does not expose all the complexity and can be a bit of a "black box". +- In almost all cases, novel schedulers shall be implemented in a new scheduling file. diff --git a/diffusers/docs/source/en/imgs/access_request.png b/diffusers/docs/source/en/imgs/access_request.png new file mode 100644 index 0000000000000000000000000000000000000000..33c6abc88dfb226e929b44c30c173c787b407045 Binary files /dev/null and b/diffusers/docs/source/en/imgs/access_request.png differ diff --git a/diffusers/docs/source/en/imgs/diffusers_library.jpg b/diffusers/docs/source/en/imgs/diffusers_library.jpg new file mode 100644 index 0000000000000000000000000000000000000000..07ba9c6571a3f070d9d10b78dccfd4d4537dd539 Binary files /dev/null and b/diffusers/docs/source/en/imgs/diffusers_library.jpg differ diff --git a/diffusers/docs/source/en/index.mdx b/diffusers/docs/source/en/index.mdx new file mode 100644 index 0000000000000000000000000000000000000000..148ee53f411f51fffca08c03da0b0f94479f2748 --- /dev/null +++ b/diffusers/docs/source/en/index.mdx @@ -0,0 +1,64 @@ + + +

+
+ +
+

+ +# 🧨 Diffusers + +🤗 Diffusers provides pretrained vision and audio diffusion models, and serves as a modular toolbox for inference and training. + +More precisely, 🤗 Diffusers offers: + +- State-of-the-art diffusion pipelines that can be run in inference with just a couple of lines of code (see [**Using Diffusers**](./using-diffusers/conditional_image_generation)) or have a look at [**Pipelines**](#pipelines) to get an overview of all supported pipelines and their corresponding papers. +- Various noise schedulers that can be used interchangeably for the preferred speed vs. quality trade-off in inference. For more information see [**Schedulers**](./api/schedulers/overview). +- Multiple types of models, such as UNet, can be used as building blocks in an end-to-end diffusion system. See [**Models**](./api/models) for more details +- Training examples to show how to train the most popular diffusion model tasks. For more information see [**Training**](./training/overview). + +## 🧨 Diffusers Pipelines + +The following table summarizes all officially supported pipelines, their corresponding paper, and if +available a colab notebook to directly try them out. + +| Pipeline | Paper | Tasks | Colab +|---|---|:---:|:---:| +| [alt_diffusion](./api/pipelines/alt_diffusion) | [**AltDiffusion**](https://arxiv.org/abs/2211.06679) | Image-to-Image Text-Guided Generation | +| [audio_diffusion](./api/pipelines/audio_diffusion) | [**Audio Diffusion**](https://github.com/teticio/audio-diffusion.git) | Unconditional Audio Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/teticio/audio-diffusion/blob/master/notebooks/audio_diffusion_pipeline.ipynb) +| [cycle_diffusion](./api/pipelines/cycle_diffusion) | [**Cycle Diffusion**](https://arxiv.org/abs/2210.05559) | Image-to-Image Text-Guided Generation | +| [dance_diffusion](./api/pipelines/dance_diffusion) | [**Dance Diffusion**](https://github.com/williamberman/diffusers.git) | Unconditional Audio Generation | +| [ddpm](./api/pipelines/ddpm) | [**Denoising Diffusion Probabilistic Models**](https://arxiv.org/abs/2006.11239) | Unconditional Image Generation | +| [ddim](./api/pipelines/ddim) | [**Denoising Diffusion Implicit Models**](https://arxiv.org/abs/2010.02502) | Unconditional Image Generation | +| [latent_diffusion](./api/pipelines/latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Text-to-Image Generation | +| [latent_diffusion](./api/pipelines/latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Super Resolution Image-to-Image | +| [latent_diffusion_uncond](./api/pipelines/latent_diffusion_uncond) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752) | Unconditional Image Generation | +| [paint_by_example](./api/pipelines/paint_by_example) | [**Paint by Example: Exemplar-based Image Editing with Diffusion Models**](https://arxiv.org/abs/2211.13227) | Image-Guided Image Inpainting | +| [pndm](./api/pipelines/pndm) | [**Pseudo Numerical Methods for Diffusion Models on Manifolds**](https://arxiv.org/abs/2202.09778) | Unconditional Image Generation | +| [score_sde_ve](./api/pipelines/score_sde_ve) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [score_sde_vp](./api/pipelines/score_sde_vp) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [stable_diffusion](./api/pipelines/stable_diffusion/text2img) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-to-Image Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [stable_diffusion](./api/pipelines/stable_diffusion/img2img) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Image-to-Image Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) +| [stable_diffusion](./api/pipelines/stable_diffusion/inpaint) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-Guided Image Inpainting | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-to-Image Generation | +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Image Inpainting | +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Super Resolution Image-to-Image | +| [stable_diffusion_safe](./api/pipelines/stable_diffusion_safe) | [**Safe Stable Diffusion**](https://arxiv.org/abs/2211.05105) | Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ml-research/safe-latent-diffusion/blob/main/examples/Safe%20Latent%20Diffusion.ipynb) +| [stochastic_karras_ve](./api/pipelines/stochastic_karras_ve) | [**Elucidating the Design Space of Diffusion-Based Generative Models**](https://arxiv.org/abs/2206.00364) | Unconditional Image Generation | +| [unclip](./api/pipelines/unclip) | [Hierarchical Text-Conditional Image Generation with CLIP Latents](https://arxiv.org/abs/2204.06125) | Text-to-Image Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Text-to-Image Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Image Variations Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Dual Image and Text Guided Generation | +| [vq_diffusion](./api/pipelines/vq_diffusion) | [Vector Quantized Diffusion Model for Text-to-Image Synthesis](https://arxiv.org/abs/2111.14822) | Text-to-Image Generation | + +**Note**: Pipelines are simple examples of how to play around with the diffusion systems as described in the corresponding papers. diff --git a/diffusers/docs/source/en/installation.mdx b/diffusers/docs/source/en/installation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5015f5b7df08115985d5e79717f713898fdb1d5e --- /dev/null +++ b/diffusers/docs/source/en/installation.mdx @@ -0,0 +1,144 @@ + + +# Installation + +Install 🤗 Diffusers for whichever deep learning library you’re working with. + +🤗 Diffusers is tested on Python 3.7+, PyTorch 1.7.0+ and flax. Follow the installation instructions below for the deep learning library you are using: + +- [PyTorch](https://pytorch.org/get-started/locally/) installation instructions. +- [Flax](https://flax.readthedocs.io/en/latest/) installation instructions. + +## Install with pip + +You should install 🤗 Diffusers in a [virtual environment](https://docs.python.org/3/library/venv.html). +If you're unfamiliar with Python virtual environments, take a look at this [guide](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/). +A virtual environment makes it easier to manage different projects, and avoid compatibility issues between dependencies. + +Start by creating a virtual environment in your project directory: + +```bash +python -m venv .env +``` + +Activate the virtual environment: + +```bash +source .env/bin/activate +``` + +Now you're ready to install 🤗 Diffusers with the following command: + +**For PyTorch** + +```bash +pip install diffusers["torch"] +``` + +**For Flax** + +```bash +pip install diffusers["flax"] +``` + +## Install from source + +Before intsalling `diffusers` from source, make sure you have `torch` and `accelerate` installed. + +For `torch` installation refer to the `torch` [docs](https://pytorch.org/get-started/locally/#start-locally). + +To install `accelerate` + +```bash +pip install accelerate +``` + +Install 🤗 Diffusers from source with the following command: + +```bash +pip install git+https://github.com/huggingface/diffusers +``` + +This command installs the bleeding edge `main` version rather than the latest `stable` version. +The `main` version is useful for staying up-to-date with the latest developments. +For instance, if a bug has been fixed since the last official release but a new release hasn't been rolled out yet. +However, this means the `main` version may not always be stable. +We strive to keep the `main` version operational, and most issues are usually resolved within a few hours or a day. +If you run into a problem, please open an [Issue](https://github.com/huggingface/transformers/issues), so we can fix it even sooner! + +## Editable install + +You will need an editable install if you'd like to: + +* Use the `main` version of the source code. +* Contribute to 🤗 Diffusers and need to test changes in the code. + +Clone the repository and install 🤗 Diffusers with the following commands: + +```bash +git clone https://github.com/huggingface/diffusers.git +cd diffusers +``` + +**For PyTorch** + +``` +pip install -e ".[torch]" +``` + +**For Flax** + +``` +pip install -e ".[flax]" +``` + +These commands will link the folder you cloned the repository to and your Python library paths. +Python will now look inside the folder you cloned to in addition to the normal library paths. +For example, if your Python packages are typically installed in `~/anaconda3/envs/main/lib/python3.7/site-packages/`, Python will also search the folder you cloned to: `~/diffusers/`. + + + +You must keep the `diffusers` folder if you want to keep using the library. + + + +Now you can easily update your clone to the latest version of 🤗 Diffusers with the following command: + +```bash +cd ~/diffusers/ +git pull +``` + +Your Python environment will find the `main` version of 🤗 Diffusers on the next run. + +## Notice on telemetry logging + +Our library gathers telemetry information during `from_pretrained()` requests. +This data includes the version of Diffusers and PyTorch/Flax, the requested model or pipeline class, +and the path to a pretrained checkpoint if it is hosted on the Hub. +This usage data helps us debug issues and prioritize new features. +Telemetry is only sent when loading models and pipelines from the HuggingFace Hub, +and is not collected during local usage. + +We understand that not everyone wants to share additional information, and we respect your privacy, +so you can disable telemetry collection by setting the `DISABLE_TELEMETRY` environment variable from your terminal: + +On Linux/MacOS: +```bash +export DISABLE_TELEMETRY=YES +``` + +On Windows: +```bash +set DISABLE_TELEMETRY=YES +``` \ No newline at end of file diff --git a/diffusers/docs/source/en/optimization/fp16.mdx b/diffusers/docs/source/en/optimization/fp16.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ca245568717a81d31e54f04caf9ec08dd541c61b --- /dev/null +++ b/diffusers/docs/source/en/optimization/fp16.mdx @@ -0,0 +1,349 @@ + + +# Memory and speed + +We present some techniques and ideas to optimize 🤗 Diffusers _inference_ for memory or speed. As a general rule, we recommend the use of [xFormers](https://github.com/facebookresearch/xformers) for memory efficient attention, please see the recommended [installation instructions](xformers). + +We'll discuss how the following settings impact performance and memory. + +| | Latency | Speedup | +| ---------------- | ------- | ------- | +| original | 9.50s | x1 | +| cuDNN auto-tuner | 9.37s | x1.01 | +| fp16 | 3.61s | x2.63 | +| channels last | 3.30s | x2.88 | +| traced UNet | 3.21s | x2.96 | +| memory efficient attention | 2.63s | x3.61 | + + + obtained on NVIDIA TITAN RTX by generating a single image of size 512x512 from + the prompt "a photo of an astronaut riding a horse on mars" with 50 DDIM + steps. + + +## Enable cuDNN auto-tuner + +[NVIDIA cuDNN](https://developer.nvidia.com/cudnn) supports many algorithms to compute a convolution. Autotuner runs a short benchmark and selects the kernel with the best performance on a given hardware for a given input size. + +Since we’re using **convolutional networks** (other types currently not supported), we can enable cuDNN autotuner before launching the inference by setting: + +```python +import torch + +torch.backends.cudnn.benchmark = True +``` + +### Use tf32 instead of fp32 (on Ampere and later CUDA devices) + +On Ampere and later CUDA devices matrix multiplications and convolutions can use the TensorFloat32 (TF32) mode for faster but slightly less accurate computations. By default PyTorch enables TF32 mode for convolutions but not matrix multiplications, and unless a network requires full float32 precision we recommend enabling this setting for matrix multiplications, too. It can significantly speed up computations with typically negligible loss of numerical accuracy. You can read more about it [here](https://huggingface.co/docs/transformers/v4.18.0/en/performance#tf32). All you need to do is to add this before your inference: + +```python +import torch + +torch.backends.cuda.matmul.allow_tf32 = True +``` + +## Half precision weights + +To save more GPU memory and get more speed, you can load and run the model weights directly in half precision. This involves loading the float16 version of the weights, which was saved to a branch named `fp16`, and telling PyTorch to use the `float16` type when loading them: + +```Python +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] +``` + + + It is strongly discouraged to make use of [`torch.autocast`](https://pytorch.org/docs/stable/amp.html#torch.autocast) in any of the pipelines as it can lead to black images and is always slower than using pure + float16 precision. + + +## Sliced attention for additional memory savings + +For even additional memory savings, you can use a sliced version of attention that performs the computation in steps instead of all at once. + + + Attention slicing is useful even if a batch size of just 1 is used - as long + as the model uses more than one attention head. If there is more than one + attention head the *QK^T* attention matrix can be computed sequentially for + each head which can save a significant amount of memory. + + +To perform the attention computation sequentially over each head, you only need to invoke [`~StableDiffusionPipeline.enable_attention_slicing`] in your pipeline before inference, like here: + +```Python +import torch +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +pipe.enable_attention_slicing() +image = pipe(prompt).images[0] +``` + +There's a small performance penalty of about 10% slower inference times, but this method allows you to use Stable Diffusion in as little as 3.2 GB of VRAM! + + +## Sliced VAE decode for larger batches + +To decode large batches of images with limited VRAM, or to enable batches with 32 images or more, you can use sliced VAE decode that decodes the batch latents one image at a time. + +You likely want to couple this with [`~StableDiffusionPipeline.enable_attention_slicing`] or [`~StableDiffusionPipeline.enable_xformers_memory_efficient_attention`] to further minimize memory use. + +To perform the VAE decode one image at a time, invoke [`~StableDiffusionPipeline.enable_vae_slicing`] in your pipeline before inference. For example: + +```Python +import torch +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +pipe.enable_vae_slicing() +images = pipe([prompt] * 32).images +``` + +You may see a small performance boost in VAE decode on multi-image batches. There should be no performance impact on single-image batches. + + +## Offloading to CPU with accelerate for memory savings + +For additional memory savings, you can offload the weights to CPU and only load them to GPU when performing the forward pass. + +To perform CPU offloading, all you have to do is invoke [`~StableDiffusionPipeline.enable_sequential_cpu_offload`]: + +```Python +import torch +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + + torch_dtype=torch.float16, +) + +prompt = "a photo of an astronaut riding a horse on mars" +pipe.enable_sequential_cpu_offload() +image = pipe(prompt).images[0] +``` + +And you can get the memory consumption to < 3GB. + +If is also possible to chain it with attention slicing for minimal memory consumption (< 2GB). + +```Python +import torch +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + + torch_dtype=torch.float16, +) + +prompt = "a photo of an astronaut riding a horse on mars" +pipe.enable_sequential_cpu_offload() +pipe.enable_attention_slicing(1) + +image = pipe(prompt).images[0] +``` + +**Note**: When using `enable_sequential_cpu_offload()`, it is important to **not** move the pipeline to CUDA beforehand or else the gain in memory consumption will only be minimal. See [this issue](https://github.com/huggingface/diffusers/issues/1934) for more information. + +## Using Channels Last memory format + +Channels last memory format is an alternative way of ordering NCHW tensors in memory preserving dimensions ordering. Channels last tensors ordered in such a way that channels become the densest dimension (aka storing images pixel-per-pixel). Since not all operators currently support channels last format it may result in a worst performance, so it's better to try it and see if it works for your model. + +For example, in order to set the UNet model in our pipeline to use channels last format, we can use the following: + +```python +print(pipe.unet.conv_out.state_dict()["weight"].stride()) # (2880, 9, 3, 1) +pipe.unet.to(memory_format=torch.channels_last) # in-place operation +print( + pipe.unet.conv_out.state_dict()["weight"].stride() +) # (2880, 1, 960, 320) having a stride of 1 for the 2nd dimension proves that it works +``` + +## Tracing + +Tracing runs an example input tensor through your model, and captures the operations that are invoked as that input makes its way through the model's layers so that an executable or `ScriptFunction` is returned that will be optimized using just-in-time compilation. + +To trace our UNet model, we can use the following: + +```python +import time +import torch +from diffusers import StableDiffusionPipeline +import functools + +# torch disable grad +torch.set_grad_enabled(False) + +# set variables +n_experiments = 2 +unet_runs_per_experiment = 50 + + +# load inputs +def generate_inputs(): + sample = torch.randn(2, 4, 64, 64).half().cuda() + timestep = torch.rand(1).half().cuda() * 999 + encoder_hidden_states = torch.randn(2, 77, 768).half().cuda() + return sample, timestep, encoder_hidden_states + + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + torch_dtype=torch.float16, +).to("cuda") +unet = pipe.unet +unet.eval() +unet.to(memory_format=torch.channels_last) # use channels_last memory format +unet.forward = functools.partial(unet.forward, return_dict=False) # set return_dict=False as default + +# warmup +for _ in range(3): + with torch.inference_mode(): + inputs = generate_inputs() + orig_output = unet(*inputs) + +# trace +print("tracing..") +unet_traced = torch.jit.trace(unet, inputs) +unet_traced.eval() +print("done tracing") + + +# warmup and optimize graph +for _ in range(5): + with torch.inference_mode(): + inputs = generate_inputs() + orig_output = unet_traced(*inputs) + + +# benchmarking +with torch.inference_mode(): + for _ in range(n_experiments): + torch.cuda.synchronize() + start_time = time.time() + for _ in range(unet_runs_per_experiment): + orig_output = unet_traced(*inputs) + torch.cuda.synchronize() + print(f"unet traced inference took {time.time() - start_time:.2f} seconds") + for _ in range(n_experiments): + torch.cuda.synchronize() + start_time = time.time() + for _ in range(unet_runs_per_experiment): + orig_output = unet(*inputs) + torch.cuda.synchronize() + print(f"unet inference took {time.time() - start_time:.2f} seconds") + +# save the model +unet_traced.save("unet_traced.pt") +``` + +Then we can replace the `unet` attribute of the pipeline with the traced model like the following + +```python +from diffusers import StableDiffusionPipeline +import torch +from dataclasses import dataclass + + +@dataclass +class UNet2DConditionOutput: + sample: torch.FloatTensor + + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + torch_dtype=torch.float16, +).to("cuda") + +# use jitted unet +unet_traced = torch.jit.load("unet_traced.pt") + + +# del pipe.unet +class TracedUNet(torch.nn.Module): + def __init__(self): + super().__init__() + self.in_channels = pipe.unet.in_channels + self.device = pipe.unet.device + + def forward(self, latent_model_input, t, encoder_hidden_states): + sample = unet_traced(latent_model_input, t, encoder_hidden_states)[0] + return UNet2DConditionOutput(sample=sample) + + +pipe.unet = TracedUNet() + +with torch.inference_mode(): + image = pipe([prompt] * 1, num_inference_steps=50).images[0] +``` + + +## Memory Efficient Attention + +Recent work on optimizing the bandwitdh in the attention block has generated huge speed ups and gains in GPU memory usage. The most recent being Flash Attention from @tridao: [code](https://github.com/HazyResearch/flash-attention), [paper](https://arxiv.org/pdf/2205.14135.pdf). + +Here are the speedups we obtain on a few Nvidia GPUs when running the inference at 512x512 with a batch size of 1 (one prompt): + +| GPU | Base Attention FP16 | Memory Efficient Attention FP16 | +|------------------ |--------------------- |--------------------------------- | +| NVIDIA Tesla T4 | 3.5it/s | 5.5it/s | +| NVIDIA 3060 RTX | 4.6it/s | 7.8it/s | +| NVIDIA A10G | 8.88it/s | 15.6it/s | +| NVIDIA RTX A6000 | 11.7it/s | 21.09it/s | +| NVIDIA TITAN RTX | 12.51it/s | 18.22it/s | +| A100-SXM4-40GB | 18.6it/s | 29.it/s | +| A100-SXM-80GB | 18.7it/s | 29.5it/s | + +To leverage it just make sure you have: + - PyTorch > 1.12 + - Cuda available + - [Installed the xformers library](xformers). +```python +from diffusers import StableDiffusionPipeline +import torch + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + torch_dtype=torch.float16, +).to("cuda") + +pipe.enable_xformers_memory_efficient_attention() + +with torch.inference_mode(): + sample = pipe("a small cat") + +# optional: You can disable it via +# pipe.disable_xformers_memory_efficient_attention() +``` diff --git a/diffusers/docs/source/en/optimization/habana.mdx b/diffusers/docs/source/en/optimization/habana.mdx new file mode 100644 index 0000000000000000000000000000000000000000..064eb510ce24c1375425720085727dc6dc79892e --- /dev/null +++ b/diffusers/docs/source/en/optimization/habana.mdx @@ -0,0 +1,70 @@ + + +# How to use Stable Diffusion on Habana Gaudi + +🤗 Diffusers is compatible with Habana Gaudi through 🤗 [Optimum Habana](https://huggingface.co/docs/optimum/habana/usage_guides/stable_diffusion). + +## Requirements + +- Optimum Habana 1.3 or later, [here](https://huggingface.co/docs/optimum/habana/installation) is how to install it. +- SynapseAI 1.7. + + +## Inference Pipeline + +To generate images with Stable Diffusion 1 and 2 on Gaudi, you need to instantiate two instances: +- A pipeline with [`GaudiStableDiffusionPipeline`](https://huggingface.co/docs/optimum/habana/package_reference/stable_diffusion_pipeline). This pipeline supports *text-to-image generation*. +- A scheduler with [`GaudiDDIMScheduler`](https://huggingface.co/docs/optimum/habana/package_reference/stable_diffusion_pipeline#optimum.habana.diffusers.GaudiDDIMScheduler). This scheduler has been optimized for Habana Gaudi. + +When initializing the pipeline, you have to specify `use_habana=True` to deploy it on HPUs. +Furthermore, in order to get the fastest possible generations you should enable **HPU graphs** with `use_hpu_graphs=True`. +Finally, you will need to specify a [Gaudi configuration](https://huggingface.co/docs/optimum/habana/package_reference/gaudi_config) which can be downloaded from the [Hugging Face Hub](https://huggingface.co/Habana). + +```python +from optimum.habana import GaudiConfig +from optimum.habana.diffusers import GaudiDDIMScheduler, GaudiStableDiffusionPipeline + +model_name = "stabilityai/stable-diffusion-2-base" +scheduler = GaudiDDIMScheduler.from_pretrained(model_name, subfolder="scheduler") +pipeline = GaudiStableDiffusionPipeline.from_pretrained( + model_name, + scheduler=scheduler, + use_habana=True, + use_hpu_graphs=True, + gaudi_config="Habana/stable-diffusion", +) +``` + +You can then call the pipeline to generate images by batches from one or several prompts: +```python +outputs = pipeline( + prompt=[ + "High quality photo of an astronaut riding a horse in space", + "Face of a yellow cat, high resolution, sitting on a park bench", + ], + num_images_per_prompt=10, + batch_size=4, +) +``` + +For more information, check out Optimum Habana's [documentation](https://huggingface.co/docs/optimum/habana/usage_guides/stable_diffusion) and the [example](https://github.com/huggingface/optimum-habana/tree/main/examples/stable-diffusion) provided in the official Github repository. + + +## Benchmark + +Here are the latencies for Habana Gaudi 1 and Gaudi 2 with the [Habana/stable-diffusion](https://huggingface.co/Habana/stable-diffusion) Gaudi configuration (mixed precision bf16/fp32): + +| | Latency | Batch size | +| ------- |:-------:|:----------:| +| Gaudi 1 | 4.37s | 4/8 | +| Gaudi 2 | 1.19s | 4/8 | diff --git a/diffusers/docs/source/en/optimization/mps.mdx b/diffusers/docs/source/en/optimization/mps.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8a2d5ad763a22d98ecb2dbb9064684e90dbda1fd --- /dev/null +++ b/diffusers/docs/source/en/optimization/mps.mdx @@ -0,0 +1,63 @@ + + +# How to use Stable Diffusion in Apple Silicon (M1/M2) + +🤗 Diffusers is compatible with Apple silicon for Stable Diffusion inference, using the PyTorch `mps` device. These are the steps you need to follow to use your M1 or M2 computer with Stable Diffusion. + +## Requirements + +- Mac computer with Apple silicon (M1/M2) hardware. +- macOS 12.6 or later (13.0 or later recommended). +- arm64 version of Python. +- PyTorch 1.13. You can install it with `pip` or `conda` using the instructions in https://pytorch.org/get-started/locally/. + + +## Inference Pipeline + +The snippet below demonstrates how to use the `mps` backend using the familiar `to()` interface to move the Stable Diffusion pipeline to your M1 or M2 device. + +We recommend to "prime" the pipeline using an additional one-time pass through it. This is a temporary workaround for a weird issue we have detected: the first inference pass produces slightly different results than subsequent ones. You only need to do this pass once, and it's ok to use just one inference step and discard the result. + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +pipe = pipe.to("mps") + +# Recommended if your computer has < 64 GB of RAM +pipe.enable_attention_slicing() + +prompt = "a photo of an astronaut riding a horse on mars" + +# First-time "warmup" pass (see explanation above) +_ = pipe(prompt, num_inference_steps=1) + +# Results match those from the CPU device after the warmup pass. +image = pipe(prompt).images[0] +``` + +## Performance Recommendations + +M1/M2 performance is very sensitive to memory pressure. The system will automatically swap if it needs to, but performance will degrade significantly when it does. + +We recommend you use _attention slicing_ to reduce memory pressure during inference and prevent swapping, particularly if your computer has lass than 64 GB of system RAM, or if you generate images at non-standard resolutions larger than 512 × 512 pixels. Attention slicing performs the costly attention operation in multiple steps instead of all at once. It usually has a performance impact of ~20% in computers without universal memory, but we have observed _better performance_ in most Apple Silicon computers, unless you have 64 GB or more. + +```python +pipeline.enable_attention_slicing() +``` + +## Known Issues + +- As mentioned above, we are investigating a strange [first-time inference issue](https://github.com/huggingface/diffusers/issues/372). +- Generating multiple prompts in a batch [crashes or doesn't work reliably](https://github.com/huggingface/diffusers/issues/363). We believe this is related to the [`mps` backend in PyTorch](https://github.com/pytorch/pytorch/issues/84039). This is being resolved, but for now we recommend to iterate instead of batching. diff --git a/diffusers/docs/source/en/optimization/onnx.mdx b/diffusers/docs/source/en/optimization/onnx.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e79efbde0742a96957f2daf5f1e8cd3a5399facc --- /dev/null +++ b/diffusers/docs/source/en/optimization/onnx.mdx @@ -0,0 +1,42 @@ + + + +# How to use the ONNX Runtime for inference + +🤗 Diffusers provides a Stable Diffusion pipeline compatible with the ONNX Runtime. This allows you to run Stable Diffusion on any hardware that supports ONNX (including CPUs), and where an accelerated version of PyTorch is not available. + +## Installation + +- TODO + +## Stable Diffusion Inference + +The snippet below demonstrates how to use the ONNX runtime. You need to use `StableDiffusionOnnxPipeline` instead of `StableDiffusionPipeline`. You also need to download the weights from the `onnx` branch of the repository, and indicate the runtime provider you want to use. + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionOnnxPipeline + +pipe = StableDiffusionOnnxPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + provider="CUDAExecutionProvider", +) + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] +``` + +## Known Issues + +- Generating multiple prompts in a batch seems to take too much memory. While we look into it, you may need to iterate instead of batching. diff --git a/diffusers/docs/source/en/optimization/open_vino.mdx b/diffusers/docs/source/en/optimization/open_vino.mdx new file mode 100644 index 0000000000000000000000000000000000000000..da6878c124177caff71cdd445a6a567204cf21d6 --- /dev/null +++ b/diffusers/docs/source/en/optimization/open_vino.mdx @@ -0,0 +1,15 @@ + + +# OpenVINO + +Under construction 🚧 diff --git a/diffusers/docs/source/en/optimization/xformers.mdx b/diffusers/docs/source/en/optimization/xformers.mdx new file mode 100644 index 0000000000000000000000000000000000000000..15c62fe880014d0d17be93b7f2fbc3d6d3a6021c --- /dev/null +++ b/diffusers/docs/source/en/optimization/xformers.mdx @@ -0,0 +1,35 @@ + + +# Installing xFormers + +We recommend the use of [xFormers](https://github.com/facebookresearch/xformers) for both inference and training. In our tests, the optimizations performed in the attention blocks allow for both faster speed and reduced memory consumption. + +Starting from version `0.0.16` of xFormers, released on January 2023, installation can be easily performed using pre-built pip wheels: + +```bash +pip install xformers +``` + + + +The xFormers PIP package requires the latest version of PyTorch (1.13.1 as of xFormers 0.0.16). If you need to use a previous version of PyTorch, then we recommend you install xFormers from source using [the project instructions](https://github.com/facebookresearch/xformers#installing-xformers). + + + +After xFormers is installed, you can use `enable_xformers_memory_efficient_attention()` for faster inference and reduced memory consumption, as discussed [here](fp16#memory-efficient-attention). + + + +According to [this issue](https://github.com/huggingface/diffusers/issues/2234#issuecomment-1416931212), xFormers `v0.0.16` cannot be used for training (fine-tune or Dreambooth) in some GPUs. If you observe that problem, please install a development version as indicated in that comment. + + diff --git a/diffusers/docs/source/en/quicktour.mdx b/diffusers/docs/source/en/quicktour.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e9a95d3c74cb25c86bfaa0a3000a24552c15b1c4 --- /dev/null +++ b/diffusers/docs/source/en/quicktour.mdx @@ -0,0 +1,130 @@ + + +# Quicktour + +Get up and running with 🧨 Diffusers quickly! +Whether you're a developer or an everyday user, this quick tour will help you get started and show you how to use [`DiffusionPipeline`] for inference. + +Before you begin, make sure you have all the necessary libraries installed: + +```bash +pip install --upgrade diffusers accelerate transformers +``` + +- [`accelerate`](https://huggingface.co/docs/accelerate/index) speeds up model loading for inference and training +- [`transformers`](https://huggingface.co/docs/transformers/index) is required to run the most popular diffusion models, such as [Stable Diffusion](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/overview) + +## DiffusionPipeline + +The [`DiffusionPipeline`] is the easiest way to use a pre-trained diffusion system for inference. You can use the [`DiffusionPipeline`] out-of-the-box for many tasks across different modalities. Take a look at the table below for some supported tasks: + +| **Task** | **Description** | **Pipeline** +|------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------| +| Unconditional Image Generation | generate an image from gaussian noise | [unconditional_image_generation](./using-diffusers/unconditional_image_generation`) | +| Text-Guided Image Generation | generate an image given a text prompt | [conditional_image_generation](./using-diffusers/conditional_image_generation) | +| Text-Guided Image-to-Image Translation | adapt an image guided by a text prompt | [img2img](./using-diffusers/img2img) | +| Text-Guided Image-Inpainting | fill the masked part of an image given the image, the mask and a text prompt | [inpaint](./using-diffusers/inpaint) | +| Text-Guided Depth-to-Image Translation | adapt parts of an image guided by a text prompt while preserving structure via depth estimation | [depth2image](./using-diffusers/depth2image) | + +For more in-detail information on how diffusion pipelines function for the different tasks, please have a look at the [**Using Diffusers**](./using-diffusers/overview) section. + +As an example, start by creating an instance of [`DiffusionPipeline`] and specify which pipeline checkpoint you would like to download. +You can use the [`DiffusionPipeline`] for any [Diffusers' checkpoint](https://huggingface.co/models?library=diffusers&sort=downloads). +In this guide though, you'll use [`DiffusionPipeline`] for text-to-image generation with [Stable Diffusion](https://huggingface.co/CompVis/stable-diffusion). + +For [Stable Diffusion](https://huggingface.co/CompVis/stable-diffusion), please carefully read its [license](https://huggingface.co/spaces/CompVis/stable-diffusion-license) before running the model. +This is due to the improved image generation capabilities of the model and the potentially harmful content that could be produced with it. +Please, head over to your stable diffusion model of choice, *e.g.* [`runwayml/stable-diffusion-v1-5`](https://huggingface.co/runwayml/stable-diffusion-v1-5), and read the license. + +You can load the model as follows: + +```python +>>> from diffusers import DiffusionPipeline + +>>> pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +``` + +The [`DiffusionPipeline`] downloads and caches all modeling, tokenization, and scheduling components. +Because the model consists of roughly 1.4 billion parameters, we strongly recommend running it on GPU. +You can move the generator object to GPU, just like you would in PyTorch. + +```python +>>> pipeline.to("cuda") +``` + +Now you can use the `pipeline` on your text prompt: + +```python +>>> image = pipeline("An image of a squirrel in Picasso style").images[0] +``` + +The output is by default wrapped into a [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image#the-image-class). + +You can save the image by simply calling: + +```python +>>> image.save("image_of_squirrel_painting.png") +``` + +**Note**: You can also use the pipeline locally by downloading the weights via: + +``` +git lfs install +git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +``` + +and then loading the saved weights into the pipeline. + +```python +>>> pipeline = DiffusionPipeline.from_pretrained("./stable-diffusion-v1-5") +``` + +Running the pipeline is then identical to the code above as it's the same model architecture. + +```python +>>> generator.to("cuda") +>>> image = generator("An image of a squirrel in Picasso style").images[0] +>>> image.save("image_of_squirrel_painting.png") +``` + +Diffusion systems can be used with multiple different [schedulers](./api/schedulers/overview) each with their +pros and cons. By default, Stable Diffusion runs with [`PNDMScheduler`], but it's very simple to +use a different scheduler. *E.g.* if you would instead like to use the [`EulerDiscreteScheduler`] scheduler, +you could use it as follows: + +```python +>>> from diffusers import EulerDiscreteScheduler + +>>> pipeline = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + +>>> # change scheduler to Euler +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) +``` + +For more in-detail information on how to change between schedulers, please refer to the [Using Schedulers](./using-diffusers/schedulers) guide. + +[Stability AI's](https://stability.ai/) Stable Diffusion model is an impressive image generation model +and can do much more than just generating images from text. We have dedicated a whole documentation page, +just for Stable Diffusion [here](./conceptual/stable_diffusion). + +If you want to know how to optimize Stable Diffusion to run on less memory, higher inference speeds, on specific hardware, such as Mac, or with [ONNX Runtime](https://onnxruntime.ai/), please have a look at our +optimization pages: + +- [Optimized PyTorch on GPU](./optimization/fp16) +- [Mac OS with PyTorch](./optimization/mps) +- [ONNX](./optimization/onnx) +- [OpenVINO](./optimization/open_vino) + +If you want to fine-tune or train your diffusion model, please have a look at the [**training section**](./training/overview) + +Finally, please be considerate when distributing generated images publicly 🤗. diff --git a/diffusers/docs/source/en/stable_diffusion.mdx b/diffusers/docs/source/en/stable_diffusion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..56a3a48fbcb98a6772ed3dec9b3d818d5f1b589e --- /dev/null +++ b/diffusers/docs/source/en/stable_diffusion.mdx @@ -0,0 +1,333 @@ + + +# The Stable Diffusion Guide 🎨 + + Open In Colab + + +## Intro + +Stable Diffusion is a [Latent Diffusion model](https://github.com/CompVis/latent-diffusion) developed by researchers from the Machine Vision and Learning group at LMU Munich, *a.k.a* CompVis. +Model checkpoints were publicly released at the end of August 2022 by a collaboration of Stability AI, CompVis, and Runway with support from EleutherAI and LAION. For more information, you can check out [the official blog post](https://stability.ai/blog/stable-diffusion-public-release). + +Since its public release the community has done an incredible job at working together to make the stable diffusion checkpoints **faster**, **more memory efficient**, and **more performant**. + +🧨 Diffusers offers a simple API to run stable diffusion with all memory, computing, and quality improvements. + +This notebook walks you through the improvements one-by-one so you can best leverage [`StableDiffusionPipeline`] for **inference**. + +## Prompt Engineering 🎨 + +When running *Stable Diffusion* in inference, we usually want to generate a certain type, or style of image and then improve upon it. Improving upon a previously generated image means running inference over and over again with a different prompt and potentially a different seed until we are happy with our generation. + +So to begin with, it is most important to speed up stable diffusion as much as possible to generate as many pictures as possible in a given amount of time. + +This can be done by both improving the **computational efficiency** (speed) and the **memory efficiency** (GPU RAM). + +Let's start by looking into computational efficiency first. + +Throughout the notebook, we will focus on [runwayml/stable-diffusion-v1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5): + +``` python +model_id = "runwayml/stable-diffusion-v1-5" +``` + +Let's load the pipeline. + +## Speed Optimization + +``` python +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained(model_id) +``` + +We aim at generating a beautiful photograph of an *old warrior chief* and will later try to find the best prompt to generate such a photograph. For now, let's keep the prompt simple: + +``` python +prompt = "portrait photo of a old warrior chief" +``` + +To begin with, we should make sure we run inference on GPU, so let's move the pipeline to GPU, just like you would with any PyTorch module. + +``` python +pipe = pipe.to("cuda") +``` + +To generate an image, you should use the [~`StableDiffusionPipeline.__call__`] method. + +To make sure we can reproduce more or less the same image in every call, let's make use of the generator. See the documentation on reproducibility [here](./conceptual/reproducibility) for more information. + +``` python +generator = torch.Generator("cuda").manual_seed(0) +``` + +Now, let's take a spin on it. + +``` python +image = pipe(prompt, generator=generator).images[0] +image +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_1.png) + +Cool, this now took roughly 30 seconds on a T4 GPU (you might see faster inference if your allocated GPU is better than a T4). + +The default run we did above used full float32 precision and ran the default number of inference steps (50). The easiest speed-ups come from switching to float16 (or half) precision and simply running fewer inference steps. Let's load the model now in float16 instead. + +``` python +import torch + +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16) +pipe = pipe.to("cuda") +``` + +And we can again call the pipeline to generate an image. + +``` python +generator = torch.Generator("cuda").manual_seed(0) + +image = pipe(prompt, generator=generator).images[0] +image +``` +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_2.png) + +Cool, this is almost three times as fast for arguably the same image quality. + +We strongly suggest always running your pipelines in float16 as so far we have very rarely seen degradations in quality because of it. + +Next, let's see if we need to use 50 inference steps or whether we could use significantly fewer. The number of inference steps is associated with the denoising scheduler we use. Choosing a more efficient scheduler could help us decrease the number of steps. + +Let's have a look at all the schedulers the stable diffusion pipeline is compatible with. + +``` python +pipe.scheduler.compatibles +``` + +``` + [diffusers.schedulers.scheduling_dpmsolver_singlestep.DPMSolverSinglestepScheduler, + diffusers.schedulers.scheduling_lms_discrete.LMSDiscreteScheduler, + diffusers.schedulers.scheduling_heun_discrete.HeunDiscreteScheduler, + diffusers.schedulers.scheduling_pndm.PNDMScheduler, + diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler, + diffusers.schedulers.scheduling_euler_ancestral_discrete.EulerAncestralDiscreteScheduler, + diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler, + diffusers.schedulers.scheduling_ddpm.DDPMScheduler, + diffusers.schedulers.scheduling_ddim.DDIMScheduler] +``` + +Cool, that's a lot of schedulers. + +🧨 Diffusers is constantly adding a bunch of novel schedulers/samplers that can be used with Stable Diffusion. For more information, we recommend taking a look at the official documentation [here](https://huggingface.co/docs/diffusers/main/en/api/schedulers/overview). + +Alright, right now Stable Diffusion is using the `PNDMScheduler` which usually requires around 50 inference steps. However, other schedulers such as `DPMSolverMultistepScheduler` or `DPMSolverSinglestepScheduler` seem to get away with just 20 to 25 inference steps. Let's try them out. + +You can set a new scheduler by making use of the [from_config](https://huggingface.co/docs/diffusers/main/en/api/configuration#diffusers.ConfigMixin.from_config) function. + +``` python +from diffusers import DPMSolverMultistepScheduler + +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +``` + +Now, let's try to reduce the number of inference steps to just 20. + +``` python +generator = torch.Generator("cuda").manual_seed(0) + +image = pipe(prompt, generator=generator, num_inference_steps=20).images[0] +image +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_3.png) + +The image now does look a little different, but it's arguably still of equally high quality. We now cut inference time to just 4 seconds though 😍. + +## Memory Optimization + +Less memory used in generation indirectly implies more speed, since we're often trying to maximize how many images we can generate per second. Usually, the more images per inference run, the more images per second too. + +The easiest way to see how many images we can generate at once is to simply try it out, and see when we get a *"Out-of-memory (OOM)"* error. + +We can run batched inference by simply passing a list of prompts and generators. Let's define a quick function that generates a batch for us. + +``` python +def get_inputs(batch_size=1): + generator = [torch.Generator("cuda").manual_seed(i) for i in range(batch_size)] + prompts = batch_size * [prompt] + num_inference_steps = 20 + + return {"prompt": prompts, "generator": generator, "num_inference_steps": num_inference_steps} +``` +This function returns a list of prompts and a list of generators, so we can reuse the generator that produced a result we like. + +We also need a method that allows us to easily display a batch of images. + +``` python +from PIL import Image + +def image_grid(imgs, rows=2, cols=2): + w, h = imgs[0].size + grid = Image.new('RGB', size=(cols*w, rows*h)) + + for i, img in enumerate(imgs): + grid.paste(img, box=(i%cols*w, i//cols*h)) + return grid +``` + +Cool, let's see how much memory we can use starting with `batch_size=4`. + +``` python +images = pipe(**get_inputs(batch_size=4)).images +image_grid(images) +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_4.png) + +Going over a batch_size of 4 will error out in this notebook (assuming we are running it on a T4 GPU). Also, we can see we only generate slightly more images per second (3.75s/image) compared to 4s/image previously. + +However, the community has found some nice tricks to improve the memory constraints further. After stable diffusion was released, the community found improvements within days and shared them freely over GitHub - open-source at its finest! I believe the original idea came from [this](https://github.com/basujindal/stable-diffusion/pull/117) GitHub thread. + +By far most of the memory is taken up by the cross-attention layers. Instead of running this operation in batch, one can run it sequentially to save a significant amount of memory. + +It can easily be enabled by calling `enable_attention_slicing` as is documented [here](https://huggingface.co/docs/diffusers/main/en/api/pipelines/stable_diffusion/text2img#diffusers.StableDiffusionPipeline.enable_attention_slicing). + +``` python +pipe.enable_attention_slicing() +``` + +Great, now that attention slicing is enabled, let's try to double the batch size again, going for `batch_size=8`. + +``` python +images = pipe(**get_inputs(batch_size=8)).images +image_grid(images, rows=2, cols=4) +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_5.png) + +Nice, it works. However, the speed gain is again not very big (it might however be much more significant on other GPUs). + +We're at roughly 3.5 seconds per image 🔥 which is probably the fastest we can be with a simple T4 without sacrificing quality. + +Next, let's look into how to improve the quality! + +## Quality Improvements + +Now that our image generation pipeline is blazing fast, let's try to get maximum image quality. + +First of all, image quality is extremely subjective, so it's difficult to make general claims here. + +The most obvious step to take to improve quality is to use *better checkpoints*. Since the release of Stable Diffusion, many improved versions have been released, which are summarized here: + +- *Official Release - 22 Aug 2022*: [Stable-Diffusion 1.4](https://huggingface.co/CompVis/stable-diffusion-v1-4) +- *20 October 2022*: [Stable-Diffusion 1.5](https://huggingface.co/runwayml/stable-diffusion-v1-5) +- *24 Nov 2022*: [Stable-Diffusion 2.0](https://huggingface.co/stabilityai/stable-diffusion-2-0) +- *7 Dec 2022*: [Stable-Diffusion 2.1](https://huggingface.co/stabilityai/stable-diffusion-2-1) + +Newer versions don't necessarily mean better image quality with the same parameters. People mentioned that *2.0* is slightly worse than *1.5* for certain prompts, but given the right prompt engineering *2.0* and *2.1* seem to be better. + +Overall, we strongly recommend just trying the models out and reading up on advice online (e.g. it has been shown that using negative prompts is very important for 2.0 and 2.1 to get the highest possible quality. See for example [this nice blog post](https://minimaxir.com/2022/11/stable-diffusion-negative-prompt/). + +Additionally, the community has started fine-tuning many of the above versions on certain styles with some of them having an extremely high quality and gaining a lot of traction. + +We recommend having a look at all [diffusers checkpoints sorted by downloads and trying out the different checkpoints](https://huggingface.co/models?library=diffusers). + +For the following, we will stick to v1.5 for simplicity. + +Next, we can also try to optimize single components of the pipeline, e.g. switching out the latent decoder. For more details on how the whole Stable Diffusion pipeline works, please have a look at [this blog post](https://huggingface.co/blog/stable_diffusion). + +Let's load [stabilityai's newest auto-decoder](https://huggingface.co/stabilityai/stable-diffusion-2-1). + +``` python +from diffusers import AutoencoderKL + +vae = AutoencoderKL.from_pretrained("stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16).to("cuda") +``` + +Now we can set it to the vae of the pipeline to use it. + +``` python +pipe.vae = vae +``` + +Let's run the same prompt as before to compare quality. + +``` python +images = pipe(**get_inputs(batch_size=8)).images +image_grid(images, rows=2, cols=4) +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_6.png) + +Seems like the difference is only very minor, but the new generations are arguably a bit *sharper*. + +Cool, finally, let's look a bit into prompt engineering. + +Our goal was to generate a photo of an old warrior chief. Let's now try to bring a bit more color into the photos and make the look more impressive. + +Originally our prompt was "*portrait photo of an old warrior chief*". + +To improve the prompt, it often helps to add cues that could have been used online to save high-quality photos, as well as add more details. +Essentially, when doing prompt engineering, one has to think: + +- How was the photo or similar photos of the one I want probably stored on the internet? +- What additional detail can I give that steers the models into the style that I want? + +Cool, let's add more details. + +``` python +prompt += ", tribal panther make up, blue on red, side profile, looking away, serious eyes" +``` + +and let's also add some cues that usually help to generate higher quality images. + +``` python +prompt += " 50mm portrait photography, hard rim lighting photography--beta --ar 2:3 --beta --upbeta" +prompt +``` + +Cool, let's now try this prompt. + +``` python +images = pipe(**get_inputs(batch_size=8)).images +image_grid(images, rows=2, cols=4) +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_7.png) + +Pretty impressive! We got some very high-quality image generations there. The 2nd image is my personal favorite, so I'll re-use this seed and see whether I can tweak the prompts slightly by using "oldest warrior", "old", "", and "young" instead of "old". + +``` python +prompts = [ + "portrait photo of the oldest warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3 --beta --upbeta", + "portrait photo of a old warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3 --beta --upbeta", + "portrait photo of a warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3 --beta --upbeta", + "portrait photo of a young warrior chief, tribal panther make up, blue on red, side profile, looking away, serious eyes 50mm portrait photography, hard rim lighting photography--beta --ar 2:3 --beta --upbeta", +] + +generator = [torch.Generator("cuda").manual_seed(1) for _ in range(len(prompts))] # 1 because we want the 2nd image + +images = pipe(prompt=prompts, generator=generator, num_inference_steps=25).images +image_grid(images) +``` + +![img](https://huggingface.co/datasets/diffusers/docs-images/resolve/main/stable_diffusion_101/sd_101_8.png) + +The first picture looks nice! The eye movement slightly changed and looks nice. This finished up our 101-guide on how to use Stable Diffusion 🤗. + +For more information on optimization or other guides, I recommend taking a look at the following: + +- [Blog post about Stable Diffusion](https://huggingface.co/blog/stable_diffusion): In-detail blog post explaining Stable Diffusion. +- [FlashAttention](https://huggingface.co/docs/diffusers/optimization/xformers): XFormers flash attention can optimize your model even further with more speed and memory improvements. +- [Dreambooth](https://huggingface.co/docs/diffusers/training/dreambooth) - Quickly customize the model by fine-tuning it. +- [General info on Stable Diffusion](https://huggingface.co/docs/diffusers/main/en/api/pipelines/stable_diffusion/overview) - Info on other tasks that are powered by Stable Diffusion. diff --git a/diffusers/docs/source/en/training/dreambooth.mdx b/diffusers/docs/source/en/training/dreambooth.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5ff5cca4bf82cd20780fd5a93f3b3b4347176635 --- /dev/null +++ b/diffusers/docs/source/en/training/dreambooth.mdx @@ -0,0 +1,314 @@ + + +# DreamBooth fine-tuning example + +[DreamBooth](https://arxiv.org/abs/2208.12242) is a method to personalize text-to-image models like stable diffusion given just a few (3~5) images of a subject. + +![Dreambooth examples from the project's blog](https://dreambooth.github.io/DreamBooth_files/teaser_static.jpg) +_Dreambooth examples from the [project's blog](https://dreambooth.github.io)._ + +The [Dreambooth training script](https://github.com/huggingface/diffusers/tree/main/examples/dreambooth) shows how to implement this training procedure on a pre-trained Stable Diffusion model. + + + +Dreambooth fine-tuning is very sensitive to hyperparameters and easy to overfit. We recommend you take a look at our [in-depth analysis](https://huggingface.co/blog/dreambooth) with recommended settings for different subjects, and go from there. + + + +## Training locally + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies. We also recommend to install `diffusers` from the `main` github branch. + +```bash +pip install git+https://github.com/huggingface/diffusers +pip install -U -r diffusers/examples/dreambooth/requirements.txt +``` + +xFormers is not part of the training requirements, but [we recommend you install it if you can](../optimization/xformers). It could make your training faster and less memory intensive. + +After all dependencies have been set up you can configure a [🤗 Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +In this example we'll use model version `v1-4`, so please visit [its card](https://huggingface.co/CompVis/stable-diffusion-v1-4) and carefully read the license before proceeding. + +The command below will download and cache the model weights from the Hub because we use the model's Hub id `CompVis/stable-diffusion-v1-4`. You may also clone the repo locally and use the local path in your system where the checkout was saved. + +### Dog toy example + +In this example we'll use [these images](https://drive.google.com/drive/folders/1BO_dyz-p65qhBRRMRA4TbZ8qW4rB99JZ) to add a new concept to Stable Diffusion using the Dreambooth process. They will be our training data. Please, download them and place them somewhere in your system. + +Then you can launch the training script using: + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path_to_training_images" +export OUTPUT_DIR="path_to_saved_model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=400 +``` + +### Training with a prior-preserving loss + +Prior preservation is used to avoid overfitting and language-drift. Please, refer to the paper to learn more about it if you are interested. For prior preservation, we use other images of the same class as part of the training process. The nice thing is that we can generate those images using the Stable Diffusion model itself! The training script will save the generated images to a local path we specify. + +According to the paper, it's recommended to generate `num_epochs * num_samples` images for prior preservation. 200-300 works well for most cases. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path_to_training_images" +export CLASS_DIR="path_to_class_images" +export OUTPUT_DIR="path_to_saved_model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Saving checkpoints while training + +It's easy to overfit while training with Dreambooth, so sometimes it's useful to save regular checkpoints during the process. One of the intermediate checkpoints might work better than the final model! To use this feature you need to pass the following argument to the training script: + +```bash + --checkpointing_steps=500 +``` + +This will save the full training state in subfolders of your `output_dir`. Subfolder names begin with the prefix `checkpoint-`, and then the number of steps performed so far; for example: `checkpoint-1500` would be a checkpoint saved after 1500 training steps. + +#### Resuming training from a saved checkpoint + +If you want to resume training from any of the saved checkpoints, you can pass the argument `--resume_from_checkpoint` and then indicate the name of the checkpoint you want to use. You can also use the special string `"latest"` to resume from the last checkpoint saved (i.e., the one with the largest number of steps). For example, the following would resume training from the checkpoint saved after 1500 steps: + +```bash + --resume_from_checkpoint="checkpoint-1500" +``` + +This would be a good opportunity to tweak some of your hyperparameters if you wish. + +#### Performing inference using a saved checkpoint + +Saved checkpoints are stored in a format suitable for resuming training. They not only include the model weights, but also the state of the optimizer, data loaders and learning rate. + +**Note**: If you have installed `"accelerate>=0.16.0"` you can use the following code to run +inference from an intermediate checkpoint. + +```python +from diffusers import DiffusionPipeline, UNet2DConditionModel +from transformers import CLIPTextModel +import torch + +# Load the pipeline with the same arguments (model, revision) that were used for training +model_id = "CompVis/stable-diffusion-v1-4" + +unet = UNet2DConditionModel.from_pretrained("/sddata/dreambooth/daruma-v2-1/checkpoint-100/unet") + +# if you have trained with `--args.train_text_encoder` make sure to also load the text encoder +text_encoder = CLIPTextModel.from_pretrained("/sddata/dreambooth/daruma-v2-1/checkpoint-100/text_encoder") + +pipeline = DiffusionPipeline.from_pretrained(model_id, unet=unet, text_encoder=text_encoder, dtype=torch.float16) +pipeline.to("cuda") + +# Perform inference, or save, or push to the hub +pipeline.save_pretrained("dreambooth-pipeline") +``` + +If you have installed `"accelerate<0.16.0"` you need to first convert it to an inference pipeline. This is how you could do it: + +```python +from accelerate import Accelerator +from diffusers import DiffusionPipeline + +# Load the pipeline with the same arguments (model, revision) that were used for training +model_id = "CompVis/stable-diffusion-v1-4" +pipeline = DiffusionPipeline.from_pretrained(model_id) + +accelerator = Accelerator() + +# Use text_encoder if `--train_text_encoder` was used for the initial training +unet, text_encoder = accelerator.prepare(pipeline.unet, pipeline.text_encoder) + +# Restore state from a checkpoint path. You have to use the absolute path here. +accelerator.load_state("/sddata/dreambooth/daruma-v2-1/checkpoint-100") + +# Rebuild the pipeline with the unwrapped models (assignment to .unet and .text_encoder should work too) +pipeline = DiffusionPipeline.from_pretrained( + model_id, + unet=accelerator.unwrap_model(unet), + text_encoder=accelerator.unwrap_model(text_encoder), +) + +# Perform inference, or save, or push to the hub +pipeline.save_pretrained("dreambooth-pipeline") +``` + +### Training on a 16GB GPU + +With the help of gradient checkpointing and the 8-bit optimizer from [bitsandbytes](https://github.com/TimDettmers/bitsandbytes), it's possible to train dreambooth on a 16GB GPU. + +```bash +pip install bitsandbytes +``` + +Then pass the `--use_8bit_adam` option to the training script. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path_to_training_images" +export CLASS_DIR="path_to_class_images" +export OUTPUT_DIR="path_to_saved_model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=2 --gradient_checkpointing \ + --use_8bit_adam \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Fine-tune the text encoder in addition to the UNet + +The script also allows to fine-tune the `text_encoder` along with the `unet`. It has been observed experimentally that this gives much better results, especially on faces. Please, refer to [our blog](https://huggingface.co/blog/dreambooth) for more details. + +To enable this option, pass the `--train_text_encoder` argument to the training script. + + +Training the text encoder requires additional memory, so training won't fit on a 16GB GPU. You'll need at least 24GB VRAM to use this option. + + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path_to_training_images" +export CLASS_DIR="path_to_class_images" +export OUTPUT_DIR="path_to_saved_model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_text_encoder \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --use_8bit_adam + --gradient_checkpointing \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Training on a 8 GB GPU: + +Using [DeepSpeed](https://www.deepspeed.ai/) it's even possible to offload some +tensors from VRAM to either CPU or NVME, allowing training to proceed with less GPU memory. + +DeepSpeed needs to be enabled with `accelerate config`. During configuration, +answer yes to "Do you want to use DeepSpeed?". Combining DeepSpeed stage 2, fp16 +mixed precision, and offloading both the model parameters and the optimizer state to CPU, it's +possible to train on under 8 GB VRAM. The drawback is that this requires more system RAM (about 25 GB). See [the DeepSpeed documentation](https://huggingface.co/docs/accelerate/usage_guides/deepspeed) for more configuration options. + +Changing the default Adam optimizer to DeepSpeed's special version of Adam +`deepspeed.ops.adam.DeepSpeedCPUAdam` gives a substantial speedup, but enabling +it requires the system's CUDA toolchain version to be the same as the one installed with PyTorch. 8-bit optimizers don't seem to be compatible with DeepSpeed at the moment. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path_to_training_images" +export CLASS_DIR="path_to_class_images" +export OUTPUT_DIR="path_to_saved_model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --sample_batch_size=1 \ + --gradient_accumulation_steps=1 --gradient_checkpointing \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 \ + --mixed_precision=fp16 +``` + +## Inference + +Once you have trained a model, inference can be done using the `StableDiffusionPipeline`, by simply indicating the path where the model was saved. Make sure that your prompts include the special `identifier` used during training (`sks` in the previous examples). + +**Note**: If you have installed `"accelerate>=0.16.0"` you can use the following code to run +inference from an intermediate checkpoint. + + +```python +from diffusers import StableDiffusionPipeline +import torch + +model_id = "path_to_saved_model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A photo of sks dog in a bucket" +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("dog-bucket.png") +``` + +You may also run inference from [any of the saved training checkpoints](#performing-inference-using-a-saved-checkpoint). diff --git a/diffusers/docs/source/en/training/lora.mdx b/diffusers/docs/source/en/training/lora.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7006c4a9ecc7d80194bd381bf87e77128f7f3b61 --- /dev/null +++ b/diffusers/docs/source/en/training/lora.mdx @@ -0,0 +1,178 @@ + + +# LoRA Support in Diffusers + +Diffusers supports LoRA for faster fine-tuning of Stable Diffusion, allowing greater memory efficiency and easier portability. + +Low-Rank Adaption of Large Language Models was first introduced by Microsoft in +[LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685) by *Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu, Yuanzhi Li, Shean Wang, Lu Wang, Weizhu Chen*. + +In a nutshell, LoRA allows adapting pretrained models by adding pairs of rank-decomposition weight matrices (called **update matrices**) +to existing weights and **only** training those newly added weights. This has a couple of advantages: + +- Previous pretrained weights are kept frozen so that the model is not so prone to [catastrophic forgetting](https://www.pnas.org/doi/10.1073/pnas.1611835114). +- Rank-decomposition matrices have significantly fewer parameters than the original model, which means that trained LoRA weights are easily portable. +- LoRA matrices are generally added to the attention layers of the original model and they control to which extent the model is adapted toward new training images via a `scale` parameter. + +**__Note that the usage of LoRA is not just limited to attention layers. In the original LoRA work, the authors found out that just amending +the attention layers of a language model is sufficient to obtain good downstream performance with great efficiency. This is why, it's common +to just add the LoRA weights to the attention layers of a model.__** + +[cloneofsimo](https://github.com/cloneofsimo) was the first to try out LoRA training for Stable Diffusion in the popular [lora](https://github.com/cloneofsimo/lora) GitHub repository. + + + +LoRA allows us to achieve greater memory efficiency since the pretrained weights are kept frozen and only the LoRA weights are trained, thereby +allowing us to run fine-tuning on consumer GPUs like Tesla T4, RTX 3080 or even RTX 2080 Ti! One can get access to GPUs like T4 in the free +tiers of Kaggle Kernels and Google Colab Notebooks. + + + +## Getting started with LoRA for fine-tuning + +Stable Diffusion can be fine-tuned in different ways: + +* [Textual inversion](https://huggingface.co/docs/diffusers/main/en/training/text_inversion) +* [DreamBooth](https://huggingface.co/docs/diffusers/main/en/training/dreambooth) +* [Text2Image fine-tuning](https://huggingface.co/docs/diffusers/main/en/training/text2image) + +We provide two end-to-end examples that show how to run fine-tuning with LoRA: + +* [DreamBooth](https://github.com/huggingface/diffusers/tree/main/examples/dreambooth#training-with-low-rank-adaptation-of-large-language-models-lora) +* [Text2Image](https://github.com/huggingface/diffusers/tree/main/examples/text_to_image#training-with-lora) + +If you want to perform DreamBooth training with LoRA, for instance, you would run: + +```bash +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth_lora.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --checkpointing_steps=100 \ + --learning_rate=1e-4 \ + --report_to="wandb" \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=500 \ + --validation_prompt="A photo of sks dog in a bucket" \ + --validation_epochs=50 \ + --seed="0" \ + --push_to_hub +``` + +A similar process can be followed to fully fine-tune Stable Diffusion on a custom dataset using the +`examples/text_to_image/train_text_to_image_lora.py` script. + +Refer to the respective examples linked above to learn more. + + + +When using LoRA we can use a much higher learning rate (typically 1e-4 as opposed to ~1e-6) compared to non-LoRA Dreambooth fine-tuning. + + + +But there is no free lunch. For the given dataset and expected generation quality, you'd still need to experiment with +different hyperparameters. Here are some important ones: + +* Training time + * Learning rate + * Number of training steps +* Inference time + * Number of steps + * Scheduler type + +Additionally, you can follow [this blog](https://huggingface.co/blog/dreambooth) that documents some of our experimental +findings for performing DreamBooth training of Stable Diffusion. + +When fine-tuning, the LoRA update matrices are only added to the attention layers. To enable this, we added new weight +loading functionalities. Their details are available [here](https://huggingface.co/docs/diffusers/main/en/api/loaders). + +## Inference + +Assuming you used the `examples/text_to_image/train_text_to_image_lora.py` to fine-tune Stable Diffusion on the [Pokemon +dataset](https://huggingface.co/datasets/lambdalabs/pokemon-blip-captions), you can perform inference like so: + +```py +from diffusers import StableDiffusionPipeline +import torch + +model_path = "sayakpaul/sd-model-finetuned-lora-t4" +pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) +pipe.unet.load_attn_procs(model_path) +pipe.to("cuda") + +prompt = "A pokemon with blue eyes." +image = pipe(prompt, num_inference_steps=30, guidance_scale=7.5).images[0] +image.save("pokemon.png") +``` + +Here are some example images you can expect: + + + +[`sayakpaul/sd-model-finetuned-lora-t4`](https://huggingface.co/sayakpaul/sd-model-finetuned-lora-t4) contains [LoRA fine-tuned update matrices](https://huggingface.co/sayakpaul/sd-model-finetuned-lora-t4/blob/main/pytorch_lora_weights.bin) +which is only 3 MBs in size. During inference, the pre-trained Stable Diffusion checkpoints are loaded alongside these update +matrices and then they are combined to run inference. + +You can use the [`huggingface_hub`](https://github.com/huggingface/huggingface_hub) library to retrieve the base model +from [`sayakpaul/sd-model-finetuned-lora-t4`](https://huggingface.co/sayakpaul/sd-model-finetuned-lora-t4) like so: + +```py +from huggingface_hub.repocard import RepoCard + +card = RepoCard.load("sayakpaul/sd-model-finetuned-lora-t4") +base_model = card.data.to_dict()["base_model"] +# 'CompVis/stable-diffusion-v1-4' +``` + +And then you can use `pipe = StableDiffusionPipeline.from_pretrained(base_model, torch_dtype=torch.float16)`. + +This is especially useful when you don't want to hardcode the base model identifier during initializing the `StableDiffusionPipeline`. + +Inference for DreamBooth training remains the same. Check +[this section](https://github.com/huggingface/diffusers/tree/main/examples/dreambooth#inference-1) for more details. + +### Merging LoRA with original model + +When performing inference, you can merge the trained LoRA weights with the frozen pre-trained model weights, to interpolate between the original model's inference result (as if no fine-tuning had occurred) and the fully fine-tuned version. + +You can adjust the merging ratio with a parameter called α (alpha) in the paper, or `scale` in our implementation. You can tweak it with the following code, that passes `scale` as `cross_attention_kwargs` in the pipeline call: + +```py +from diffusers import StableDiffusionPipeline +import torch + +model_path = "sayakpaul/sd-model-finetuned-lora-t4" +pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) +pipe.unet.load_attn_procs(model_path) +pipe.to("cuda") + +prompt = "A pokemon with blue eyes." +image = pipe(prompt, num_inference_steps=30, guidance_scale=7.5, cross_attention_kwargs={"scale": 0.5}).images[0] +image.save("pokemon.png") +``` + +A value of `0` is the same as _not_ using the LoRA weights, whereas `1` means only the LoRA fine-tuned weights will be used. Values between 0 and 1 will interpolate between the two versions. + + +## Known limitations + +* Currently, we only support LoRA for the attention layers of [`UNet2DConditionModel`](https://huggingface.co/docs/diffusers/main/en/api/models#diffusers.UNet2DConditionModel). diff --git a/diffusers/docs/source/en/training/overview.mdx b/diffusers/docs/source/en/training/overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..49aab9aa3647ad809b79d175faf39474f2380c0d --- /dev/null +++ b/diffusers/docs/source/en/training/overview.mdx @@ -0,0 +1,73 @@ + + +# 🧨 Diffusers Training Examples + +Diffusers training examples are a collection of scripts to demonstrate how to effectively use the `diffusers` library +for a variety of use cases. + +**Note**: If you are looking for **official** examples on how to use `diffusers` for inference, +please have a look at [src/diffusers/pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines) + +Our examples aspire to be **self-contained**, **easy-to-tweak**, **beginner-friendly** and for **one-purpose-only**. +More specifically, this means: + +- **Self-contained**: An example script shall only depend on "pip-install-able" Python packages that can be found in a `requirements.txt` file. Example scripts shall **not** depend on any local files. This means that one can simply download an example script, *e.g.* [train_unconditional.py](https://github.com/huggingface/diffusers/blob/main/examples/unconditional_image_generation/train_unconditional.py), install the required dependencies, *e.g.* [requirements.txt](https://github.com/huggingface/diffusers/blob/main/examples/unconditional_image_generation/requirements.txt) and execute the example script. +- **Easy-to-tweak**: While we strive to present as many use cases as possible, the example scripts are just that - examples. It is expected that they won't work out-of-the box on your specific problem and that you will be required to change a few lines of code to adapt them to your needs. To help you with that, most of the examples fully expose the preprocessing of the data and the training loop to allow you to tweak and edit them as required. +- **Beginner-friendly**: We do not aim for providing state-of-the-art training scripts for the newest models, but rather examples that can be used as a way to better understand diffusion models and how to use them with the `diffusers` library. We often purposefully leave out certain state-of-the-art methods if we consider them too complex for beginners. +- **One-purpose-only**: Examples should show one task and one task only. Even if a task is from a modeling +point of view very similar, *e.g.* image super-resolution and image modification tend to use the same model and training method, we want examples to showcase only one task to keep them as readable and easy-to-understand as possible. + +We provide **official** examples that cover the most popular tasks of diffusion models. +*Official* examples are **actively** maintained by the `diffusers` maintainers and we try to rigorously follow our example philosophy as defined above. +If you feel like another important example should exist, we are more than happy to welcome a [Feature Request](https://github.com/huggingface/diffusers/issues/new?assignees=&labels=&template=feature_request.md&title=) or directly a [Pull Request](https://github.com/huggingface/diffusers/compare) from you! + +Training examples show how to pretrain or fine-tune diffusion models for a variety of tasks. Currently we support: + +- [Unconditional Training](./unconditional_training) +- [Text-to-Image Training](./text2image) +- [Text Inversion](./text_inversion) +- [Dreambooth](./dreambooth) +- [LoRA Support](./lora) + +If possible, please [install xFormers](../optimization/xformers) for memory efficient attention. This could help make your training faster and less memory intensive. + +| Task | 🤗 Accelerate | 🤗 Datasets | Colab +|---|---|:---:|:---:| +| [**Unconditional Image Generation**](./unconditional_training) | ✅ | ✅ | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [**Text-to-Image fine-tuning**](./text2image) | ✅ | ✅ | +| [**Textual Inversion**](./text_inversion) | ✅ | - | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_textual_inversion_training.ipynb) +| [**Dreambooth**](./dreambooth) | ✅ | - | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_dreambooth_training.ipynb) + +## Community + +In addition, we provide **community** examples, which are examples added and maintained by our community. +Community examples can consist of both *training* examples or *inference* pipelines. +For such examples, we are more lenient regarding the philosophy defined above and also cannot guarantee to provide maintenance for every issue. +Examples that are useful for the community, but are either not yet deemed popular or not yet following our above philosophy should go into the [community examples](https://github.com/huggingface/diffusers/tree/main/examples/community) folder. The community folder therefore includes training examples and inference pipelines. +**Note**: Community examples can be a [great first contribution](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) to show to the community how you like to use `diffusers` 🪄. + +## Important note + +To make sure you can successfully run the latest versions of the example scripts, you have to **install the library from source** and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: + +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder of your choice and run + +```bash +pip install -r requirements.txt +``` diff --git a/diffusers/docs/source/en/training/text2image.mdx b/diffusers/docs/source/en/training/text2image.mdx new file mode 100644 index 0000000000000000000000000000000000000000..eb71457cb758f168fb2b7c954219156670d87faf --- /dev/null +++ b/diffusers/docs/source/en/training/text2image.mdx @@ -0,0 +1,138 @@ + + + +# Stable Diffusion text-to-image fine-tuning + +The [`train_text_to_image.py`](https://github.com/huggingface/diffusers/tree/main/examples/text_to_image) script shows how to fine-tune the stable diffusion model on your own dataset. + + + +The text-to-image fine-tuning script is experimental. It's easy to overfit and run into issues like catastrophic forgetting. We recommend to explore different hyperparameters to get the best results on your dataset. + + + + +## Running locally + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install git+https://github.com/huggingface/diffusers.git +pip install -U -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-4`, so you'll need to visit [its card](https://huggingface.co/CompVis/stable-diffusion-v1-4), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. Instead, you can pass the path to your local checkout to the training script and it will be loaded from there. + +### Hardware Requirements for Fine-tuning + +Using `gradient_checkpointing` and `mixed_precision` it should be possible to fine tune the model on a single 24GB GPU. For higher `batch_size` and faster training it's better to use GPUs with more than 30GB of GPU memory. You can also use JAX / Flax for fine-tuning on TPUs or GPUs, see [below](#flax-jax-finetuning) for details. + +### Fine-tuning Example + +The following script will launch a fine-tuning run using [Justin Pinkneys' captioned Pokemon dataset](https://huggingface.co/datasets/lambdalabs/pokemon-blip-captions), available in Hugging Face Hub. + + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export dataset_name="lambdalabs/pokemon-blip-captions" + +accelerate launch train_text_to_image.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$dataset_name \ + --use_ema \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --gradient_checkpointing \ + --mixed_precision="fp16" \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --lr_scheduler="constant" --lr_warmup_steps=0 \ + --output_dir="sd-pokemon-model" +``` + +To run on your own training files you need to prepare the dataset according to the format required by `datasets`. You can upload your dataset to the Hub, or you can prepare a local folder with your files. [This documentation](https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder-with-metadata) explains how to do it. + +You should modify the script if you wish to use custom loading logic. We have left pointers in the code in the appropriate places :) + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export TRAIN_DIR="path_to_your_dataset" +export OUTPUT_DIR="path_to_save_model" + +accelerate launch train_text_to_image.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$TRAIN_DIR \ + --use_ema \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --gradient_checkpointing \ + --mixed_precision="fp16" \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --lr_scheduler="constant" --lr_warmup_steps=0 \ + --output_dir=${OUTPUT_DIR} +``` + +Once training is finished the model will be saved to the `OUTPUT_DIR` specified in the command. To load the fine-tuned model for inference, just pass that path to `StableDiffusionPipeline`: + +```python +from diffusers import StableDiffusionPipeline + +model_path = "path_to_saved_model" +pipe = StableDiffusionPipeline.from_pretrained(model_path, torch_dtype=torch.float16) +pipe.to("cuda") + +image = pipe(prompt="yoda").images[0] +image.save("yoda-pokemon.png") +``` + +### Flax / JAX fine-tuning + +Thanks to [@duongna211](https://github.com/duongna21) it's possible to fine-tune Stable Diffusion using Flax! This is very efficient on TPU hardware but works great on GPUs too. You can use the [Flax training script](https://github.com/huggingface/diffusers/blob/main/examples/text_to_image/train_text_to_image_flax.py) like this: + +```Python +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export dataset_name="lambdalabs/pokemon-blip-captions" + +python train_text_to_image_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$dataset_name \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --output_dir="sd-pokemon-model" +``` diff --git a/diffusers/docs/source/en/training/text_inversion.mdx b/diffusers/docs/source/en/training/text_inversion.mdx new file mode 100644 index 0000000000000000000000000000000000000000..7bc145299eace2f2ac1ddcb5c53ec86f2c763183 --- /dev/null +++ b/diffusers/docs/source/en/training/text_inversion.mdx @@ -0,0 +1,122 @@ + + + + +# Textual Inversion + +Textual Inversion is a technique for capturing novel concepts from a small number of example images in a way that can later be used to control text-to-image pipelines. It does so by learning new 'words' in the embedding space of the pipeline's text encoder. These special words can then be used within text prompts to achieve very fine-grained control of the resulting images. + +![Textual Inversion example](https://textual-inversion.github.io/static/images/editing/colorful_teapot.JPG) +_By using just 3-5 images you can teach new concepts to a model such as Stable Diffusion for personalized image generation ([image source](https://github.com/rinongal/textual_inversion))._ + +This technique was introduced in [An Image is Worth One Word: Personalizing Text-to-Image Generation using Textual Inversion](https://arxiv.org/abs/2208.01618). The paper demonstrated the concept using a [latent diffusion model](https://github.com/CompVis/latent-diffusion) but the idea has since been applied to other variants such as [Stable Diffusion](https://huggingface.co/docs/diffusers/main/en/conceptual/stable_diffusion). + + +## How It Works + +![Diagram from the paper showing overview](https://textual-inversion.github.io/static/images/training/training.JPG) +_Architecture Overview from the [textual inversion blog post](https://textual-inversion.github.io/)_ + +Before a text prompt can be used in a diffusion model, it must first be processed into a numerical representation. This typically involves tokenizing the text, converting each token to an embedding and then feeding those embeddings through a model (typically a transformer) whose output will be used as the conditioning for the diffusion model. + +Textual inversion learns a new token embedding (v* in the diagram above). A prompt (that includes a token which will be mapped to this new embedding) is used in conjunction with a noised version of one or more training images as inputs to the generator model, which attempts to predict the denoised version of the image. The embedding is optimized based on how well the model does at this task - an embedding that better captures the object or style shown by the training images will give more useful information to the diffusion model and thus result in a lower denoising loss. After many steps (typically several thousand) with a variety of prompt and image variants the learned embedding should hopefully capture the essence of the new concept being taught. + +## Usage + +To train your own textual inversions, see the [example script here](https://github.com/huggingface/diffusers/tree/main/examples/textual_inversion). + +There is also a notebook for training: +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_textual_inversion_training.ipynb) + +And one for inference: +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/stable_conceptualizer_inference.ipynb) + +In addition to using concepts you have trained yourself, there is a community-created collection of trained textual inversions in the new [Stable Diffusion public concepts library](https://huggingface.co/sd-concepts-library) which you can also use from the inference notebook above. Over time this will hopefully grow into a useful resource as more examples are added. + +## Example: Running locally + +The `textual_inversion.py` script [here](https://github.com/huggingface/diffusers/blob/main/examples/textual_inversion) shows how to implement the training procedure and adapt it for stable diffusion. + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies. + +```bash +pip install diffusers[training] accelerate transformers +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + + +### Cat toy example + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-4`, so you'll need to visit [its card](https://huggingface.co/CompVis/stable-diffusion-v1-4), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. + +
+ +Now let's get our dataset.Download 3-4 images from [here](https://drive.google.com/drive/folders/1fmJMs25nxS_rSNqS5hTcRdLem_YQXbq5) and save them in a directory. This will be our training data. + +And launch the training using + +```bash +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export DATA_DIR="path-to-dir-containing-images" + +accelerate launch textual_inversion.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --max_train_steps=3000 \ + --learning_rate=5.0e-04 --scale_lr \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --output_dir="textual_inversion_cat" +``` + +A full training run takes ~1 hour on one V100 GPU. + + +### Inference + +Once you have trained a model using above command, the inference can be done simply using the `StableDiffusionPipeline`. Make sure to include the `placeholder_token` in your prompt. + +```python +from diffusers import StableDiffusionPipeline + +model_id = "path-to-your-trained-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A backpack" + +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("cat-backpack.png") +``` diff --git a/diffusers/docs/source/en/training/unconditional_training.mdx b/diffusers/docs/source/en/training/unconditional_training.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e711e05973e1401852d2503f9a3cd51d650e63cf --- /dev/null +++ b/diffusers/docs/source/en/training/unconditional_training.mdx @@ -0,0 +1,149 @@ + + +# Unconditional Image-Generation + +In this section, we explain how one can train an unconditional image generation diffusion +model. "Unconditional" because the model is not conditioned on any context to generate an image - once trained the model will simply generate images that resemble its training data +distribution. + +## Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install diffusers[training] accelerate datasets +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +## Unconditional Flowers + +The command to train a DDPM UNet model on the Oxford Flowers dataset: + +```bash +accelerate launch train_unconditional.py \ + --dataset_name="huggan/flowers-102-categories" \ + --resolution=64 \ + --output_dir="ddpm-ema-flowers-64" \ + --train_batch_size=16 \ + --num_epochs=100 \ + --gradient_accumulation_steps=1 \ + --learning_rate=1e-4 \ + --lr_warmup_steps=500 \ + --mixed_precision=no \ + --push_to_hub +``` +An example trained model: https://huggingface.co/anton-l/ddpm-ema-flowers-64 + +A full training run takes 2 hours on 4xV100 GPUs. + + + +## Unconditional Pokemon + +The command to train a DDPM UNet model on the Pokemon dataset: + +```bash +accelerate launch train_unconditional.py \ + --dataset_name="huggan/pokemon" \ + --resolution=64 \ + --output_dir="ddpm-ema-pokemon-64" \ + --train_batch_size=16 \ + --num_epochs=100 \ + --gradient_accumulation_steps=1 \ + --learning_rate=1e-4 \ + --lr_warmup_steps=500 \ + --mixed_precision=no \ + --push_to_hub +``` +An example trained model: https://huggingface.co/anton-l/ddpm-ema-pokemon-64 + +A full training run takes 2 hours on 4xV100 GPUs. + + + + +## Using your own data + +To use your own dataset, there are 2 ways: +- you can either provide your own folder as `--train_data_dir` +- or you can upload your dataset to the hub (possibly as a private repo, if you prefer so), and simply pass the `--dataset_name` argument. + +**Note**: If you want to create your own training dataset please have a look at [this document](https://huggingface.co/docs/datasets/image_process#image-datasets). + +Below, we explain both in more detail. + +### Provide the dataset as a folder + +If you provide your own folders with images, the script expects the following directory structure: + +```bash +data_dir/xxx.png +data_dir/xxy.png +data_dir/[...]/xxz.png +``` + +In other words, the script will take care of gathering all images inside the folder. You can then run the script like this: + +```bash +accelerate launch train_unconditional.py \ + --train_data_dir \ + +``` + +Internally, the script will use the [`ImageFolder`](https://huggingface.co/docs/datasets/v2.0.0/en/image_process#imagefolder) feature which will automatically turn the folders into 🤗 Dataset objects. + +### Upload your data to the hub, as a (possibly private) repo + +It's very easy (and convenient) to upload your image dataset to the hub using the [`ImageFolder`](https://huggingface.co/docs/datasets/v2.0.0/en/image_process#imagefolder) feature available in 🤗 Datasets. Simply do the following: + +```python +from datasets import load_dataset + +# example 1: local folder +dataset = load_dataset("imagefolder", data_dir="path_to_your_folder") + +# example 2: local files (supported formats are tar, gzip, zip, xz, rar, zstd) +dataset = load_dataset("imagefolder", data_files="path_to_zip_file") + +# example 3: remote files (supported formats are tar, gzip, zip, xz, rar, zstd) +dataset = load_dataset( + "imagefolder", + data_files="https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.zip", +) + +# example 4: providing several splits +dataset = load_dataset( + "imagefolder", data_files={"train": ["path/to/file1", "path/to/file2"], "test": ["path/to/file3", "path/to/file4"]} +) +``` + +`ImageFolder` will create an `image` column containing the PIL-encoded images. + +Next, push it to the hub! + +```python +# assuming you have ran the huggingface-cli login command in a terminal +dataset.push_to_hub("name_of_your_dataset") + +# if you want to push to a private repo, simply pass private=True: +dataset.push_to_hub("name_of_your_dataset", private=True) +``` + +and that's it! You can now train your model by simply setting the `--dataset_name` argument to the name of your dataset on the hub. + +More on this can also be found in [this blog post](https://huggingface.co/blog/image-search-datasets). diff --git a/diffusers/docs/source/en/using-diffusers/audio.mdx b/diffusers/docs/source/en/using-diffusers/audio.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c895e2eb71d709fc81a883b699f317a7ab196009 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/audio.mdx @@ -0,0 +1,16 @@ + + +# Using Diffusers for audio + +[`DanceDiffusionPipeline`] and [`AudioDiffusionPipeline`] can be used to generate +audio rapidly! More coming soon! \ No newline at end of file diff --git a/diffusers/docs/source/en/using-diffusers/conditional_image_generation.mdx b/diffusers/docs/source/en/using-diffusers/conditional_image_generation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..5ed27ac9171cbe7c190d1ec831ce71350344f7f1 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/conditional_image_generation.mdx @@ -0,0 +1,46 @@ + + +# Conditional Image Generation + +The [`DiffusionPipeline`] is the easiest way to use a pre-trained diffusion system for inference + +Start by creating an instance of [`DiffusionPipeline`] and specify which pipeline checkpoint you would like to download. +You can use the [`DiffusionPipeline`] for any [Diffusers' checkpoint](https://huggingface.co/models?library=diffusers&sort=downloads). +In this guide though, you'll use [`DiffusionPipeline`] for text-to-image generation with [Latent Diffusion](https://huggingface.co/CompVis/ldm-text2im-large-256): + +```python +>>> from diffusers import DiffusionPipeline + +>>> generator = DiffusionPipeline.from_pretrained("CompVis/ldm-text2im-large-256") +``` +The [`DiffusionPipeline`] downloads and caches all modeling, tokenization, and scheduling components. +Because the model consists of roughly 1.4 billion parameters, we strongly recommend running it on GPU. +You can move the generator object to GPU, just like you would in PyTorch. + +```python +>>> generator.to("cuda") +``` + +Now you can use the `generator` on your text prompt: + +```python +>>> image = generator("An image of a squirrel in Picasso style").images[0] +``` + +The output is by default wrapped into a [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image#the-image-class). + +You can save the image by simply calling: + +```python +>>> image.save("image_of_squirrel_painting.png") +``` diff --git a/diffusers/docs/source/en/using-diffusers/configuration.mdx b/diffusers/docs/source/en/using-diffusers/configuration.mdx new file mode 100644 index 0000000000000000000000000000000000000000..36a2ad0d03949fdd94ca001a42bf8bfcb6b18947 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/configuration.mdx @@ -0,0 +1,21 @@ + + + + +# Configuration + +The handling of configurations in Diffusers is with the `ConfigMixin` class. + +[[autodoc]] ConfigMixin + +Under further construction 🚧, open a [PR](https://github.com/huggingface/diffusers/compare) if you want to contribute! diff --git a/diffusers/docs/source/en/using-diffusers/contribute_pipeline.mdx b/diffusers/docs/source/en/using-diffusers/contribute_pipeline.mdx new file mode 100644 index 0000000000000000000000000000000000000000..18e84cdfbc9491d7c72c55632cba0b807976b4c2 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/contribute_pipeline.mdx @@ -0,0 +1,169 @@ + + +# How to build a community pipeline + +*Note*: this page was built from the GitHub Issue on Community Pipelines [#841](https://github.com/huggingface/diffusers/issues/841). + +Let's make an example! +Say you want to define a pipeline that just does a single forward pass to a U-Net and then calls a scheduler only once (Note, this doesn't make any sense from a scientific point of view, but only represents an example of how things work under the hood). + +Cool! So you open your favorite IDE and start creating your pipeline 💻. +First, what model weights and configurations do we need? +We have a U-Net and a scheduler, so our pipeline should take a U-Net and a scheduler as an argument. +Also, as stated above, you'd like to be able to load weights and the scheduler config for Hub and share your code with others, so we'll inherit from `DiffusionPipeline`: + +```python +from diffusers import DiffusionPipeline +import torch + + +class UnetSchedulerOneForwardPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() +``` + +Now, we must save the `unet` and `scheduler` in a config file so that you can save your pipeline with `save_pretrained`. +Therefore, make sure you add every component that is save-able to the `register_modules` function: + +```python +from diffusers import DiffusionPipeline +import torch + + +class UnetSchedulerOneForwardPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() + + self.register_modules(unet=unet, scheduler=scheduler) +``` + +Cool, the init is done! 🔥 Now, let's go into the forward pass, which we recommend defining as `__call__` . Here you're given all the creative freedom there is. For our amazing "one-step" pipeline, we simply create a random image and call the unet once and the scheduler once: + +```python +from diffusers import DiffusionPipeline +import torch + + +class UnetSchedulerOneForwardPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() + + self.register_modules(unet=unet, scheduler=scheduler) + + def __call__(self): + image = torch.randn( + (1, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + ) + timestep = 1 + + model_output = self.unet(image, timestep).sample + scheduler_output = self.scheduler.step(model_output, timestep, image).prev_sample + + return scheduler_output +``` + +Cool, that's it! 🚀 You can now run this pipeline by passing a `unet` and a `scheduler` to the init: + +```python +from diffusers import DDPMScheduler, Unet2DModel + +scheduler = DDPMScheduler() +unet = UNet2DModel() + +pipeline = UnetSchedulerOneForwardPipeline(unet=unet, scheduler=scheduler) + +output = pipeline() +``` + +But what's even better is that you can load pre-existing weights into the pipeline if they match exactly your pipeline structure. This is e.g. the case for [https://huggingface.co/google/ddpm-cifar10-32](https://huggingface.co/google/ddpm-cifar10-32) so that we can do the following: + +```python +pipeline = UnetSchedulerOneForwardPipeline.from_pretrained("google/ddpm-cifar10-32") + +output = pipeline() +``` + +We want to share this amazing pipeline with the community, so we would open a PR request to add the following code under `one_step_unet.py` to [https://github.com/huggingface/diffusers/tree/main/examples/community](https://github.com/huggingface/diffusers/tree/main/examples/community) . + +```python +from diffusers import DiffusionPipeline +import torch + + +class UnetSchedulerOneForwardPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() + + self.register_modules(unet=unet, scheduler=scheduler) + + def __call__(self): + image = torch.randn( + (1, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + ) + timestep = 1 + + model_output = self.unet(image, timestep).sample + scheduler_output = self.scheduler.step(model_output, timestep, image).prev_sample + + return scheduler_output +``` + +Our amazing pipeline got merged here: [#840](https://github.com/huggingface/diffusers/pull/840). +Now everybody that has `diffusers >= 0.4.0` installed can use our pipeline magically 🪄 as follows: + +```python +from diffusers import DiffusionPipeline + +pipe = DiffusionPipeline.from_pretrained("google/ddpm-cifar10-32", custom_pipeline="one_step_unet") +pipe() +``` + +Another way to upload your custom_pipeline, besides sending a PR, is uploading the code that contains it to the Hugging Face Hub, [as exemplified here](https://huggingface.co/docs/diffusers/using-diffusers/custom_pipeline_overview#loading-custom-pipelines-from-the-hub). + +**Try it out now - it works!** + +In general, you will want to create much more sophisticated pipelines, so we recommend looking at existing pipelines here: [https://github.com/huggingface/diffusers/tree/main/examples/community](https://github.com/huggingface/diffusers/tree/main/examples/community). + +IMPORTANT: +You can use whatever package you want in your community pipeline file - as long as the user has it installed, everything will work fine. Make sure you have one and only one pipeline class that inherits from `DiffusionPipeline` as this will be automatically detected. + +## How do community pipelines work? +A community pipeline is a class that has to inherit from ['DiffusionPipeline']: +and that has been added to `examples/community` [files](https://github.com/huggingface/diffusers/tree/main/examples/community). +The community can load the pipeline code via the custom_pipeline argument from DiffusionPipeline. See docs [here](https://huggingface.co/docs/diffusers/api/diffusion_pipeline#diffusers.DiffusionPipeline.from_pretrained.custom_pipeline): + +This means: +The model weights and configs of the pipeline should be loaded from the `pretrained_model_name_or_path` [argument](https://huggingface.co/docs/diffusers/api/diffusion_pipeline#diffusers.DiffusionPipeline.from_pretrained.pretrained_model_name_or_path): +whereas the code that powers the community pipeline is defined in a file added in [`examples/community`](https://github.com/huggingface/diffusers/tree/main/examples/community). + +Now, it might very well be that only some of your pipeline components weights can be downloaded from an official repo. +The other components should then be passed directly to init as is the case for the ClIP guidance notebook [here](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/CLIP_Guided_Stable_diffusion_with_diffusers.ipynb#scrollTo=z9Kglma6hjki). + +The magic behind all of this is that we load the code directly from GitHub. You can check it out in more detail if you follow the functionality defined here: + +```python +# 2. Load the pipeline class, if using custom module then load it from the hub +# if we load from explicit class, let's use it +if custom_pipeline is not None: + pipeline_class = get_class_from_dynamic_module( + custom_pipeline, module_file=CUSTOM_PIPELINE_FILE_NAME, cache_dir=custom_pipeline + ) +elif cls != DiffusionPipeline: + pipeline_class = cls +else: + diffusers_module = importlib.import_module(cls.__module__.split(".")[0]) + pipeline_class = getattr(diffusers_module, config_dict["_class_name"]) +``` + +This is why a community pipeline merged to GitHub will be directly available to all `diffusers` packages. + diff --git a/diffusers/docs/source/en/using-diffusers/custom_pipeline_examples.mdx b/diffusers/docs/source/en/using-diffusers/custom_pipeline_examples.mdx new file mode 100644 index 0000000000000000000000000000000000000000..92132b228f5b1db6bd682cb059f8f84688ce906a --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/custom_pipeline_examples.mdx @@ -0,0 +1,280 @@ + + +# Custom Pipelines + +> **For more information about community pipelines, please have a look at [this issue](https://github.com/huggingface/diffusers/issues/841).** + +**Community** examples consist of both inference and training examples that have been added by the community. +Please have a look at the following table to get an overview of all community examples. Click on the **Code Example** to get a copy-and-paste ready code example that you can try out. +If a community doesn't work as expected, please open an issue and ping the author on it. + +| Example | Description | Code Example | Colab | Author | +|:---------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------:| +| CLIP Guided Stable Diffusion | Doing CLIP guidance for text to image generation with Stable Diffusion | [CLIP Guided Stable Diffusion](#clip-guided-stable-diffusion) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/CLIP_Guided_Stable_diffusion_with_diffusers.ipynb) | [Suraj Patil](https://github.com/patil-suraj/) | +| One Step U-Net (Dummy) | Example showcasing of how to use Community Pipelines (see https://github.com/huggingface/diffusers/issues/841) | [One Step U-Net](#one-step-unet) | - | [Patrick von Platen](https://github.com/patrickvonplaten/) | +| Stable Diffusion Interpolation | Interpolate the latent space of Stable Diffusion between different prompts/seeds | [Stable Diffusion Interpolation](#stable-diffusion-interpolation) | - | [Nate Raw](https://github.com/nateraw/) | +| Stable Diffusion Mega | **One** Stable Diffusion Pipeline with all functionalities of [Text2Image](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py), [Image2Image](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py) and [Inpainting](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) | [Stable Diffusion Mega](#stable-diffusion-mega) | - | [Patrick von Platen](https://github.com/patrickvonplaten/) | +| Long Prompt Weighting Stable Diffusion | **One** Stable Diffusion Pipeline without tokens length limit, and support parsing weighting in prompt. | [Long Prompt Weighting Stable Diffusion](#long-prompt-weighting-stable-diffusion) | - | [SkyTNT](https://github.com/SkyTNT) | +| Speech to Image | Using automatic-speech-recognition to transcribe text and Stable Diffusion to generate images | [Speech to Image](#speech-to-image) | - | [Mikail Duzenli](https://github.com/MikailINTech) + +To load a custom pipeline you just need to pass the `custom_pipeline` argument to `DiffusionPipeline`, as one of the files in `diffusers/examples/community`. Feel free to send a PR with your own pipelines, we will merge them quickly. +```py +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", custom_pipeline="filename_in_the_community_folder" +) +``` + +## Example usages + +### CLIP Guided Stable Diffusion + +CLIP guided stable diffusion can help to generate more realistic images +by guiding stable diffusion at every denoising step with an additional CLIP model. + +The following code requires roughly 12GB of GPU RAM. + +```python +from diffusers import DiffusionPipeline +from transformers import CLIPFeatureExtractor, CLIPModel +import torch + + +feature_extractor = CLIPFeatureExtractor.from_pretrained("laion/CLIP-ViT-B-32-laion2B-s34B-b79K") +clip_model = CLIPModel.from_pretrained("laion/CLIP-ViT-B-32-laion2B-s34B-b79K", torch_dtype=torch.float16) + + +guided_pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="clip_guided_stable_diffusion", + clip_model=clip_model, + feature_extractor=feature_extractor, + torch_dtype=torch.float16, +) +guided_pipeline.enable_attention_slicing() +guided_pipeline = guided_pipeline.to("cuda") + +prompt = "fantasy book cover, full moon, fantasy forest landscape, golden vector elements, fantasy magic, dark light night, intricate, elegant, sharp focus, illustration, highly detailed, digital painting, concept art, matte, art by WLOP and Artgerm and Albert Bierstadt, masterpiece" + +generator = torch.Generator(device="cuda").manual_seed(0) +images = [] +for i in range(4): + image = guided_pipeline( + prompt, + num_inference_steps=50, + guidance_scale=7.5, + clip_guidance_scale=100, + num_cutouts=4, + use_cutouts=False, + generator=generator, + ).images[0] + images.append(image) + +# save images locally +for i, img in enumerate(images): + img.save(f"./clip_guided_sd/image_{i}.png") +``` + +The `images` list contains a list of PIL images that can be saved locally or displayed directly in a google colab. +Generated images tend to be of higher qualtiy than natively using stable diffusion. E.g. the above script generates the following images: + +![clip_guidance](https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/clip_guidance/merged_clip_guidance.jpg). + +### One Step Unet + +The dummy "one-step-unet" can be run as follows: + +```python +from diffusers import DiffusionPipeline + +pipe = DiffusionPipeline.from_pretrained("google/ddpm-cifar10-32", custom_pipeline="one_step_unet") +pipe() +``` + +**Note**: This community pipeline is not useful as a feature, but rather just serves as an example of how community pipelines can be added (see https://github.com/huggingface/diffusers/issues/841). + +### Stable Diffusion Interpolation + +The following code can be run on a GPU of at least 8GB VRAM and should take approximately 5 minutes. + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + torch_dtype=torch.float16, + safety_checker=None, # Very important for videos...lots of false positives while interpolating + custom_pipeline="interpolate_stable_diffusion", +).to("cuda") +pipe.enable_attention_slicing() + +frame_filepaths = pipe.walk( + prompts=["a dog", "a cat", "a horse"], + seeds=[42, 1337, 1234], + num_interpolation_steps=16, + output_dir="./dreams", + batch_size=4, + height=512, + width=512, + guidance_scale=8.5, + num_inference_steps=50, +) +``` + +The output of the `walk(...)` function returns a list of images saved under the folder as defined in `output_dir`. You can use these images to create videos of stable diffusion. + +> **Please have a look at https://github.com/nateraw/stable-diffusion-videos for more in-detail information on how to create videos using stable diffusion as well as more feature-complete functionality.** + +### Stable Diffusion Mega + +The Stable Diffusion Mega Pipeline lets you use the main use cases of the stable diffusion pipeline in a single class. + +```python +#!/usr/bin/env python3 +from diffusers import DiffusionPipeline +import PIL +import requests +from io import BytesIO +import torch + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="stable_diffusion_mega", + torch_dtype=torch.float16, +) +pipe.to("cuda") +pipe.enable_attention_slicing() + + +### Text-to-Image + +images = pipe.text2img("An astronaut riding a horse").images + +### Image-to-Image + +init_image = download_image( + "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" +) + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe.img2img(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +### Inpainting + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +prompt = "a cat sitting on a bench" +images = pipe.inpaint(prompt=prompt, image=init_image, mask_image=mask_image, strength=0.75).images +``` + +As shown above this one pipeline can run all both "text-to-image", "image-to-image", and "inpainting" in one pipeline. + +### Long Prompt Weighting Stable Diffusion + +The Pipeline lets you input prompt without 77 token length limit. And you can increase words weighting by using "()" or decrease words weighting by using "[]" +The Pipeline also lets you use the main use cases of the stable diffusion pipeline in a single class. + +#### pytorch + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + "hakurei/waifu-diffusion", custom_pipeline="lpw_stable_diffusion", torch_dtype=torch.float16 +) +pipe = pipe.to("cuda") + +prompt = "best_quality (1girl:1.3) bow bride brown_hair closed_mouth frilled_bow frilled_hair_tubes frills (full_body:1.3) fox_ear hair_bow hair_tubes happy hood japanese_clothes kimono long_sleeves red_bow smile solo tabi uchikake white_kimono wide_sleeves cherry_blossoms" +neg_prompt = "lowres, bad_anatomy, error_body, error_hair, error_arm, error_hands, bad_hands, error_fingers, bad_fingers, missing_fingers, error_legs, bad_legs, multiple_legs, missing_legs, error_lighting, error_shadow, error_reflection, text, error, extra_digit, fewer_digits, cropped, worst_quality, low_quality, normal_quality, jpeg_artifacts, signature, watermark, username, blurry" + +pipe.text2img(prompt, negative_prompt=neg_prompt, width=512, height=512, max_embeddings_multiples=3).images[0] +``` + +#### onnxruntime + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="lpw_stable_diffusion_onnx", + revision="onnx", + provider="CUDAExecutionProvider", +) + +prompt = "a photo of an astronaut riding a horse on mars, best quality" +neg_prompt = "lowres, bad anatomy, error body, error hair, error arm, error hands, bad hands, error fingers, bad fingers, missing fingers, error legs, bad legs, multiple legs, missing legs, error lighting, error shadow, error reflection, text, error, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry" + +pipe.text2img(prompt, negative_prompt=neg_prompt, width=512, height=512, max_embeddings_multiples=3).images[0] +``` + +if you see `Token indices sequence length is longer than the specified maximum sequence length for this model ( *** > 77 ) . Running this sequence through the model will result in indexing errors`. Do not worry, it is normal. + +### Speech to Image + +The following code can generate an image from an audio sample using pre-trained OpenAI whisper-small and Stable Diffusion. + +```Python +import torch + +import matplotlib.pyplot as plt +from datasets import load_dataset +from diffusers import DiffusionPipeline +from transformers import ( + WhisperForConditionalGeneration, + WhisperProcessor, +) + + +device = "cuda" if torch.cuda.is_available() else "cpu" + +ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") + +audio_sample = ds[3] + +text = audio_sample["text"].lower() +speech_data = audio_sample["audio"]["array"] + +model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small").to(device) +processor = WhisperProcessor.from_pretrained("openai/whisper-small") + +diffuser_pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="speech_to_image_diffusion", + speech_model=model, + speech_processor=processor, + + torch_dtype=torch.float16, +) + +diffuser_pipeline.enable_attention_slicing() +diffuser_pipeline = diffuser_pipeline.to(device) + +output = diffuser_pipeline(speech_data) +plt.imshow(output.images[0]) +``` +This example produces the following image: + +![image](https://user-images.githubusercontent.com/45072645/196901736-77d9c6fc-63ee-4072-90b0-dc8b903d63e3.png) \ No newline at end of file diff --git a/diffusers/docs/source/en/using-diffusers/custom_pipeline_overview.mdx b/diffusers/docs/source/en/using-diffusers/custom_pipeline_overview.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ae5bad2d7bf20286890743cd06e2121ec022d7cf --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/custom_pipeline_overview.mdx @@ -0,0 +1,121 @@ + + +# Loading and Adding Custom Pipelines + +Diffusers allows you to conveniently load any custom pipeline from the Hugging Face Hub as well as any [official community pipeline](https://github.com/huggingface/diffusers/tree/main/examples/community) +via the [`DiffusionPipeline`] class. + +## Loading custom pipelines from the Hub + +Custom pipelines can be easily loaded from any model repository on the Hub that defines a diffusion pipeline in a `pipeline.py` file. +Let's load a dummy pipeline from [hf-internal-testing/diffusers-dummy-pipeline](https://huggingface.co/hf-internal-testing/diffusers-dummy-pipeline). + +All you need to do is pass the custom pipeline repo id with the `custom_pipeline` argument alongside the repo from where you wish to load the pipeline modules. + +```python +from diffusers import DiffusionPipeline + +pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="hf-internal-testing/diffusers-dummy-pipeline" +) +``` + +This will load the custom pipeline as defined in the [model repository](https://huggingface.co/hf-internal-testing/diffusers-dummy-pipeline/blob/main/pipeline.py). + + + +By loading a custom pipeline from the Hugging Face Hub, you are trusting that the code you are loading +is safe 🔒. Make sure to check out the code online before loading & running it automatically. + + + +## Loading official community pipelines + +Community pipelines are summarized in the [community examples folder](https://github.com/huggingface/diffusers/tree/main/examples/community) + +Similarly, you need to pass both the *repo id* from where you wish to load the weights as well as the `custom_pipeline` argument. Here the `custom_pipeline` argument should consist simply of the filename of the community pipeline excluding the `.py` suffix, *e.g.* `clip_guided_stable_diffusion`. + +Since community pipelines are often more complex, one can mix loading weights from an official *repo id* +and passing pipeline modules directly. + +```python +from diffusers import DiffusionPipeline +from transformers import CLIPFeatureExtractor, CLIPModel + +clip_model_id = "laion/CLIP-ViT-B-32-laion2B-s34B-b79K" + +feature_extractor = CLIPFeatureExtractor.from_pretrained(clip_model_id) +clip_model = CLIPModel.from_pretrained(clip_model_id) + +pipeline = DiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + custom_pipeline="clip_guided_stable_diffusion", + clip_model=clip_model, + feature_extractor=feature_extractor, +) +``` + +## Adding custom pipelines to the Hub + +To add a custom pipeline to the Hub, all you need to do is to define a pipeline class that inherits +from [`DiffusionPipeline`] in a `pipeline.py` file. +Make sure that the whole pipeline is encapsulated within a single class and that the `pipeline.py` file +has only one such class. + +Let's quickly define an example pipeline. + + +```python +import torch +from diffusers import DiffusionPipeline + + +class MyPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() + + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__(self, batch_size: int = 1, num_inference_steps: int = 50): + # Sample gaussian noise to begin loop + image = torch.randn((batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size)) + + image = image.to(self.device) + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(image, t).sample + + # 2. predict previous mean of image x_t-1 and add variance depending on eta + # eta corresponds to η in paper and should be between [0, 1] + # do x_t -> x_t-1 + image = self.scheduler.step(model_output, t, image, eta).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + + return image +``` + +Now you can upload this short file under the name `pipeline.py` in your preferred [model repository](https://huggingface.co/docs/hub/models-uploading). For Stable Diffusion pipelines, you may also [join the community organisation for shared pipelines](https://huggingface.co/organizations/sd-diffusers-pipelines-library/share/BUPyDUuHcciGTOKaExlqtfFcyCZsVFdrjr) to upload yours. +Finally, we can load the custom pipeline by passing the model repository name, *e.g.* `sd-diffusers-pipelines-library/my_custom_pipeline` alongside the model repository from where we want to load the `unet` and `scheduler` components. + +```python +my_pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="patrickvonplaten/my_custom_pipeline" +) +``` diff --git a/diffusers/docs/source/en/using-diffusers/depth2img.mdx b/diffusers/docs/source/en/using-diffusers/depth2img.mdx new file mode 100644 index 0000000000000000000000000000000000000000..afdf3a8cb79c7804f822ae330e4cd655da7d3da0 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/depth2img.mdx @@ -0,0 +1,35 @@ + + +# Text-Guided Image-to-Image Generation + +The [`StableDiffusionDepth2ImgPipeline`] lets you pass a text prompt and an initial image to condition the generation of new images as well as a `depth_map` to preserve the images' structure. If no `depth_map` is provided, the pipeline will automatically predict the depth via an integrated depth-estimation model. + +```python +import torch +import requests +from PIL import Image + +from diffusers import StableDiffusionDepth2ImgPipeline + +pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", + torch_dtype=torch.float16, +).to("cuda") + + +url = "http://images.cocodataset.org/val2017/000000039769.jpg" +init_image = Image.open(requests.get(url, stream=True).raw) +prompt = "two tigers" +n_prompt = "bad, deformed, ugly, bad anatomy" +image = pipe(prompt=prompt, image=init_image, negative_prompt=n_prompt, strength=0.7).images[0] +``` diff --git a/diffusers/docs/source/en/using-diffusers/img2img.mdx b/diffusers/docs/source/en/using-diffusers/img2img.mdx new file mode 100644 index 0000000000000000000000000000000000000000..c64d677686baea570964b68577c406a6d12fdabc --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/img2img.mdx @@ -0,0 +1,45 @@ + + +# Text-Guided Image-to-Image Generation + +The [`StableDiffusionImg2ImgPipeline`] lets you pass a text prompt and an initial image to condition the generation of new images. + +```python +import torch +import requests +from PIL import Image +from io import BytesIO + +from diffusers import StableDiffusionImg2ImgPipeline + +# load the pipeline +device = "cuda" +pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16).to( + device +) + +# let's download an initial image +url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image.thumbnail((768, 768)) + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +images[0].save("fantasy_landscape.png") +``` +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) + diff --git a/diffusers/docs/source/en/using-diffusers/inpaint.mdx b/diffusers/docs/source/en/using-diffusers/inpaint.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9b388c86436e5c0ce670d4f2f3f5e8f56ee5e61d --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/inpaint.mdx @@ -0,0 +1,56 @@ + + +# Text-Guided Image-Inpainting + +The [`StableDiffusionInpaintPipeline`] lets you edit specific parts of an image by providing a mask and a text prompt. It uses a version of Stable Diffusion specifically trained for in-painting tasks. + +```python +import PIL +import requests +import torch +from io import BytesIO + +from diffusers import StableDiffusionInpaintPipeline + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0] +``` + +`image` | `mask_image` | `prompt` | **Output** | +:-------------------------:|:-------------------------:|:-------------------------:|-------------------------:| +drawing | drawing | ***Face of a yellow cat, high resolution, sitting on a park bench*** | drawing | + + +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) + + +A previous experimental implementation of in-painting used a different, lower-quality process. To ensure backwards compatibility, loading a pretrained pipeline that doesn't contain the new model will still apply the old in-painting method. + diff --git a/diffusers/docs/source/en/using-diffusers/kerascv.mdx b/diffusers/docs/source/en/using-diffusers/kerascv.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9b5faeb18a220ac2826373e67d2de179c03a3712 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/kerascv.mdx @@ -0,0 +1,179 @@ + + +# Using KerasCV Stable Diffusion Checkpoints in Diffusers + + + +This is an experimental feature. + + + +[KerasCV](https://github.com/keras-team/keras-cv/) provides APIs for implementing various computer vision workflows. It +also provides the Stable Diffusion [v1 and v2](https://github.com/keras-team/keras-cv/blob/master/keras_cv/models/stable_diffusion) +models. Many practitioners find it easy to fine-tune the Stable Diffusion models shipped by KerasCV. However, as of this writing, KerasCV offers limited support to experiment with Stable Diffusion models for inference and deployment. On the other hand, +Diffusers provides tooling dedicated to this purpose (and more), such as different [noise schedulers](https://huggingface.co/docs/diffusers/using-diffusers/schedulers), [flash attention](https://huggingface.co/docs/diffusers/optimization/xformers), and [other +optimization techniques](https://huggingface.co/docs/diffusers/optimization/fp16). + +How about fine-tuning Stable Diffusion models in KerasCV and exporting them such that they become compatible with Diffusers to combine the +best of both worlds? We have created a [tool](https://huggingface.co/spaces/sayakpaul/convert-kerascv-sd-diffusers) that +lets you do just that! It takes KerasCV Stable Diffusion checkpoints and exports them to Diffusers-compatible checkpoints. +More specifically, it first converts the checkpoints to PyTorch and then wraps them into a +[`StableDiffusionPipeline`](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/overview) which is ready +for inference. Finally, it pushes the converted checkpoints to a repository on the Hugging Face Hub. + +We welcome you to try out the tool [here](https://huggingface.co/spaces/sayakpaul/convert-kerascv-sd-diffusers) +and share feedback via [discussions](https://huggingface.co/spaces/sayakpaul/convert-kerascv-sd-diffusers/discussions/new). + +## Getting Started + +First, you need to obtain the fine-tuned KerasCV Stable Diffusion checkpoints. We provide an +overview of the different ways Stable Diffusion models can be fine-tuned [using `diffusers`](https://huggingface.co/docs/diffusers/training/overview). For the Keras implementation of some of these methods, you can check out these resources: + +* [Teach StableDiffusion new concepts via Textual Inversion](https://keras.io/examples/generative/fine_tune_via_textual_inversion/) +* [Fine-tuning Stable Diffusion](https://keras.io/examples/generative/finetune_stable_diffusion/) +* [DreamBooth](https://keras.io/examples/generative/dreambooth/) +* [Prompt-to-Prompt editing](https://github.com/miguelCalado/prompt-to-prompt-tensorflow) + +Stable Diffusion is comprised of the following models: + +* Text encoder +* UNet +* VAE + +Depending on the fine-tuning task, we may fine-tune one or more of these components (the VAE is almost always left untouched). Here are some common combinations: + +* DreamBooth: UNet and text encoder +* Classical text to image fine-tuning: UNet +* Textual Inversion: Just the newly initialized embeddings in the text encoder + +### Performing the Conversion + +Let's use [this checkpoint](https://huggingface.co/sayakpaul/textual-inversion-kerasio/resolve/main/textual_inversion_kerasio.h5) which was generated +by conducting Textual Inversion with the following "placeholder token": ``. + +On the tool, we supply the following things: + +* Path(s) to download the fine-tuned checkpoint(s) (KerasCV) +* An HF token +* Placeholder token (only applicable for Textual Inversion) + +
+ +
+ +As soon as you hit "Submit", the conversion process will begin. Once it's complete, you should see the following: + +
+ +
+ +If you click the [link](https://huggingface.co/sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline/tree/main), you +should see something like so: + +
+ +
+ +If you head over to the [model card of the repository](https://huggingface.co/sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline), the +following should appear: + +
+ +
+ + + +Note that we're not specifying the UNet weights here since the UNet is not fine-tuned during Textual Inversion. + + + +And that's it! You now have your fine-tuned KerasCV Stable Diffusion model in Diffusers 🧨 + +## Using the Converted Model in Diffusers + +Just beside the model card of the [repository](https://huggingface.co/sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline), +you'd notice an inference widget to try out the model directly from the UI 🤗 + +
+ +
+ +On the top right hand side, we provide a "Use in Diffusers" button. If you click the button, you should see the following code-snippet: + +```py +from diffusers import DiffusionPipeline + +pipeline = DiffusionPipeline.from_pretrained("sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline") +``` + +The model is in standard `diffusers` format. Let's perform inference! + +```py +from diffusers import DiffusionPipeline + +pipeline = DiffusionPipeline.from_pretrained("sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline") +pipeline.to("cuda") + +placeholder_token = "" +prompt = f"two {placeholder_token} getting married, photorealistic, high quality" +image = pipeline(prompt, num_inference_steps=50).images[0] +``` + +And we get: + +
+ +
+ +_**Note that if you specified a `placeholder_token` while performing the conversion, the tool will log it accordingly. Refer +to the model card of [this repository](https://huggingface.co/sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline) +as an example.**_ + +We welcome you to use the tool for various Stable Diffusion fine-tuning scenarios and let us know your feedback! Here are some examples +of Diffusers checkpoints that were obtained using the tool: + +* [sayakpaul/text-unet-dogs-kerascv_sd_diffusers_pipeline](https://huggingface.co/sayakpaul/text-unet-dogs-kerascv_sd_diffusers_pipeline) (DreamBooth with both the text encoder and UNet fine-tuned) +* [sayakpaul/unet-dogs-kerascv_sd_diffusers_pipeline](https://huggingface.co/sayakpaul/unet-dogs-kerascv_sd_diffusers_pipeline) (DreamBooth with only the UNet fine-tuned) + +## Incorporating Diffusers Goodies 🎁 + +Diffusers provides various options that one can leverage to experiment with different inference setups. One particularly +useful option is the use of a different noise scheduler during inference other than what was used during fine-tuning. +Let's try out the [`DPMSolverMultistepScheduler`](https://huggingface.co/docs/diffusers/main/en/api/schedulers/multistep_dpm_solver) +which is different from the one ([`DDPMScheduler`](https://huggingface.co/docs/diffusers/main/en/api/schedulers/ddpm)) used during +fine-tuning. + +You can read more details about this process in [this section](https://huggingface.co/docs/diffusers/using-diffusers/schedulers). + +```py +from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler + +pipeline = DiffusionPipeline.from_pretrained("sayakpaul/textual-inversion-cat-kerascv_sd_diffusers_pipeline") +pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) +pipeline.to("cuda") + +placeholder_token = "" +prompt = f"two {placeholder_token} getting married, photorealistic, high quality" +image = pipeline(prompt, num_inference_steps=50).images[0] +``` + +
+ +
+ +One can also continue fine-tuning from these Diffusers checkpoints by leveraging some relevant tools from Diffusers. Refer [here](https://huggingface.co/docs/diffusers/training/overview) for +more details. For inference-specific optimizations, refer [here](https://huggingface.co/docs/diffusers/main/en/optimization/fp16). + +## Known Limitations + +* Only Stable Diffusion v1 checkpoints are supported for conversion in this tool. \ No newline at end of file diff --git a/diffusers/docs/source/en/using-diffusers/loading.mdx b/diffusers/docs/source/en/using-diffusers/loading.mdx new file mode 100644 index 0000000000000000000000000000000000000000..97bb7a0d037a522f1d0646ceac37150ebc8a6776 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/loading.mdx @@ -0,0 +1,380 @@ + + +# Loading + +A core premise of the diffusers library is to make diffusion models **as accessible as possible**. +Accessibility is therefore achieved by providing an API to load complete diffusion pipelines as well as individual components with a single line of code. + +In the following we explain in-detail how to easily load: + +- *Complete Diffusion Pipelines* via the [`DiffusionPipeline.from_pretrained`] +- *Diffusion Models* via [`ModelMixin.from_pretrained`] +- *Schedulers* via [`SchedulerMixin.from_pretrained`] + +## Loading pipelines + +The [`DiffusionPipeline`] class is the easiest way to access any diffusion model that is [available on the Hub](https://huggingface.co/models?library=diffusers). Let's look at an example on how to download [CompVis' Latent Diffusion model](https://huggingface.co/CompVis/ldm-text2im-large-256). + +```python +from diffusers import DiffusionPipeline + +repo_id = "CompVis/ldm-text2im-large-256" +ldm = DiffusionPipeline.from_pretrained(repo_id) +``` + +Here [`DiffusionPipeline`] automatically detects the correct pipeline (*i.e.* [`LDMTextToImagePipeline`]), downloads and caches all required configuration and weight files (if not already done so), and finally returns a pipeline instance, called `ldm`. +The pipeline instance can then be called using [`LDMTextToImagePipeline.__call__`] (i.e., `ldm("image of a astronaut riding a horse")`) for text-to-image generation. + +Instead of using the generic [`DiffusionPipeline`] class for loading, you can also load the appropriate pipeline class directly. The code snippet above yields the same instance as when doing: + +```python +from diffusers import LDMTextToImagePipeline + +repo_id = "CompVis/ldm-text2im-large-256" +ldm = LDMTextToImagePipeline.from_pretrained(repo_id) +``` + +Diffusion pipelines like `LDMTextToImagePipeline` often consist of multiple components. These components can be both parameterized models, such as `"unet"`, `"vqvae"` and "bert", tokenizers or schedulers. These components can interact in complex ways with each other when using the pipeline in inference, *e.g.* for [`LDMTextToImagePipeline`] or [`StableDiffusionPipeline`] the inference call is explained [here](https://huggingface.co/blog/stable_diffusion#how-does-stable-diffusion-work). +The purpose of the [pipeline classes](./api/overview#diffusers-summary) is to wrap the complexity of these diffusion systems and give the user an easy-to-use API while staying flexible for customization, as will be shown later. + +### Loading pipelines that require access request + +Due to the capabilities of diffusion models to generate extremely realistic images, there is a certain danger that such models might be misused for unwanted applications, *e.g.* generating pornography or violent images. +In order to minimize the possibility of such unsolicited use cases, some of the most powerful diffusion models require users to acknowledge a license before being able to use the model. If the user does not agree to the license, the pipeline cannot be downloaded. +If you try to load [`runwayml/stable-diffusion-v1-5`](https://huggingface.co/runwayml/stable-diffusion-v1-5) the same way as done previously: + +```python +from diffusers import DiffusionPipeline + +repo_id = "runwayml/stable-diffusion-v1-5" +stable_diffusion = DiffusionPipeline.from_pretrained(repo_id) +``` + +it will only work if you have both *click-accepted* the license on [the model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) and are logged into the Hugging Face Hub. Otherwise you will get an error message +such as the following: + +``` +OSError: runwayml/stable-diffusion-v1-5 is not a local folder and is not a valid model identifier listed on 'https://huggingface.co/models' +If this is a private repository, make sure to pass a token having permission to this repo with `use_auth_token` or log in with `huggingface-cli login` +``` + +Therefore, we need to make sure to *click-accept* the license. You can do this by simply visiting +the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) and clicking on "Agree and access repository": + +

+
+ +
+

+ +Second, you need to login with your access token: + +``` +huggingface-cli login +``` + +before trying to load the model. Or alternatively, you can pass [your access token](https://huggingface.co/docs/hub/security-tokens#user-access-tokens) directly via the flag `use_auth_token`. In this case you do **not** need +to run `huggingface-cli login` before: + +```python +from diffusers import DiffusionPipeline + +repo_id = "runwayml/stable-diffusion-v1-5" +stable_diffusion = DiffusionPipeline.from_pretrained(repo_id, use_auth_token="") +``` + +The final option to use pipelines that require access without having to rely on the Hugging Face Hub is to load the pipeline locally as explained in the next section. + +### Loading pipelines locally + +If you prefer to have complete control over the pipeline and its corresponding files or, as said before, if you want to use pipelines that require an access request without having to be connected to the Hugging Face Hub, +we recommend loading pipelines locally. + +To load a diffusion pipeline locally, you first need to manually download the whole folder structure on your local disk and then pass a local path to the [`DiffusionPipeline.from_pretrained`]. Let's again look at an example for +[CompVis' Latent Diffusion model](https://huggingface.co/CompVis/ldm-text2im-large-256). + +First, you should make use of [`git-lfs`](https://git-lfs.github.com/) to download the whole folder structure that has been uploaded to the [model repository](https://huggingface.co/CompVis/ldm-text2im-large-256/tree/main): + +``` +git lfs install +git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +``` + +The command above will create a local folder called `./stable-diffusion-v1-5` on your disk. +Now, all you have to do is to simply pass the local folder path to `from_pretrained`: + +```python +from diffusers import DiffusionPipeline + +repo_id = "./stable-diffusion-v1-5" +stable_diffusion = DiffusionPipeline.from_pretrained(repo_id) +``` + +If `repo_id` is a local path, as it is the case here, [`DiffusionPipeline.from_pretrained`] will automatically detect it and therefore not try to download any files from the Hub. +While we usually recommend to load weights directly from the Hub to be certain to stay up to date with the newest changes, loading pipelines locally should be preferred if one +wants to stay anonymous, self-contained applications, etc... + +### Loading customized pipelines + +Advanced users that want to load customized versions of diffusion pipelines can do so by swapping any of the default components, *e.g.* the scheduler, with other scheduler classes. +A classical use case of this functionality is to swap the scheduler. [Stable Diffusion v1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5) uses the [`PNDMScheduler`] by default which is generally not the most performant scheduler. Since the release +of stable diffusion, multiple improved schedulers have been published. To use those, the user has to manually load their preferred scheduler and pass it into [`DiffusionPipeline.from_pretrained`]. + +*E.g.* to use [`EulerDiscreteScheduler`] or [`DPMSolverMultistepScheduler`] to have a better quality vs. generation speed trade-off for inference, one could load them as follows: + +```python +from diffusers import DiffusionPipeline, EulerDiscreteScheduler, DPMSolverMultistepScheduler + +repo_id = "runwayml/stable-diffusion-v1-5" + +scheduler = EulerDiscreteScheduler.from_pretrained(repo_id, subfolder="scheduler") +# or +# scheduler = DPMSolverMultistepScheduler.from_pretrained(repo_id, subfolder="scheduler") + +stable_diffusion = DiffusionPipeline.from_pretrained(repo_id, scheduler=scheduler) +``` + +Three things are worth paying attention to here. +- First, the scheduler is loaded with [`SchedulerMixin.from_pretrained`] +- Second, the scheduler is loaded with a function argument, called `subfolder="scheduler"` as the configuration of stable diffusion's scheduling is defined in a [subfolder of the official pipeline repository](https://huggingface.co/runwayml/stable-diffusion-v1-5/tree/main/scheduler) +- Third, the scheduler instance can simply be passed with the `scheduler` keyword argument to [`DiffusionPipeline.from_pretrained`]. This works because the [`StableDiffusionPipeline`] defines its scheduler with the `scheduler` attribute. It's not possible to use a different name, such as `sampler=scheduler` since `sampler` is not a defined keyword for [`StableDiffusionPipeline.__init__`] + +Not only the scheduler components can be customized for diffusion pipelines; in theory, all components of a pipeline can be customized. In practice, however, it often only makes sense to switch out a component that has **compatible** alternatives to what the pipeline expects. +Many scheduler classes are compatible with each other as can be seen [here](https://github.com/huggingface/diffusers/blob/0dd8c6b4dbab4069de9ed1cafb53cbd495873879/src/diffusers/schedulers/scheduling_ddim.py#L112). This is not always the case for other components, such as the `"unet"`. + +One special case that can also be customized is the `"safety_checker"` of stable diffusion. If you believe the safety checker doesn't serve you any good, you can simply disable it by passing `None`: + +```python +from diffusers import DiffusionPipeline, EulerDiscreteScheduler, DPMSolverMultistepScheduler + +stable_diffusion = DiffusionPipeline.from_pretrained(repo_id, safety_checker=None) +``` + +Another common use case is to reuse the same components in multiple pipelines, *e.g.* the weights and configurations of [`"runwayml/stable-diffusion-v1-5"`](https://huggingface.co/runwayml/stable-diffusion-v1-5) can be used for both [`StableDiffusionPipeline`] and [`StableDiffusionImg2ImgPipeline`] and we might not want to +use the exact same weights into RAM twice. In this case, customizing all the input instances would help us +to only load the weights into RAM once: + +```python +from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline + +model_id = "runwayml/stable-diffusion-v1-5" +stable_diffusion_txt2img = StableDiffusionPipeline.from_pretrained(model_id) + +components = stable_diffusion_txt2img.components + +# weights are not reloaded into RAM +stable_diffusion_img2img = StableDiffusionImg2ImgPipeline(**components) +``` + +Note how the above code snippet makes use of [`DiffusionPipeline.components`]. + +### How does loading work? + +As a class method, [`DiffusionPipeline.from_pretrained`] is responsible for two things: +- Download the latest version of the folder structure required to run the `repo_id` with `diffusers` and cache them. If the latest folder structure is available in the local cache, [`DiffusionPipeline.from_pretrained`] will simply reuse the cache and **not** re-download the files. +- Load the cached weights into the _correct_ pipeline class – one of the [officially supported pipeline classes](./api/overview#diffusers-summary) - and return an instance of the class. The _correct_ pipeline class is thereby retrieved from the `model_index.json` file. + +The underlying folder structure of diffusion pipelines correspond 1-to-1 to their corresponding class instances, *e.g.* [`LDMTextToImagePipeline`] for [`CompVis/ldm-text2im-large-256`](https://huggingface.co/CompVis/ldm-text2im-large-256) +This can be understood better by looking at an example. Let's print out pipeline class instance `pipeline` we just defined: + +```python +from diffusers import DiffusionPipeline + +repo_id = "CompVis/ldm-text2im-large-256" +ldm = DiffusionPipeline.from_pretrained(repo_id) +print(ldm) +``` + +*Output*: +``` +LDMTextToImagePipeline { + "bert": [ + "latent_diffusion", + "LDMBertModel" + ], + "scheduler": [ + "diffusers", + "DDIMScheduler" + ], + "tokenizer": [ + "transformers", + "BertTokenizer" + ], + "unet": [ + "diffusers", + "UNet2DConditionModel" + ], + "vqvae": [ + "diffusers", + "AutoencoderKL" + ] +} +``` + +First, we see that the official pipeline is the [`LDMTextToImagePipeline`], and second we see that the `LDMTextToImagePipeline` consists of 5 components: +- `"bert"` of class `LDMBertModel` as defined [in the pipeline](https://github.com/huggingface/diffusers/blob/cd502b25cf0debac6f98d27a6638ef95208d1ea2/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py#L664) +- `"scheduler"` of class [`DDIMScheduler`] +- `"tokenizer"` of class `BertTokenizer` as defined [in `transformers`](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer) +- `"unet"` of class [`UNet2DConditionModel`] +- `"vqvae"` of class [`AutoencoderKL`] + +Let's now compare the pipeline instance to the folder structure of the model repository `CompVis/ldm-text2im-large-256`. Looking at the folder structure of [`CompVis/ldm-text2im-large-256`](https://huggingface.co/CompVis/ldm-text2im-large-256/tree/main) on the Hub, we can see it matches 1-to-1 the printed out instance of `LDMTextToImagePipeline` above: + +``` +. +├── bert +│   ├── config.json +│   └── pytorch_model.bin +├── model_index.json +├── scheduler +│   └── scheduler_config.json +├── tokenizer +│   ├── special_tokens_map.json +│   ├── tokenizer_config.json +│   └── vocab.txt +├── unet +│   ├── config.json +│   └── diffusion_pytorch_model.bin +└── vqvae + ├── config.json + └── diffusion_pytorch_model.bin +``` + +As we can see each attribute of the instance of `LDMTextToImagePipeline` has its configuration and possibly weights defined in a subfolder that is called **exactly** like the class attribute (`"bert"`, `"scheduler"`, `"tokenizer"`, `"unet"`, `"vqvae"`). Importantly, every pipeline expects a `model_index.json` file that tells the `DiffusionPipeline` both: +- which pipeline class should be loaded, and +- what sub-classes from which library are stored in which subfolders + +In the case of `CompVis/ldm-text2im-large-256` the `model_index.json` is therefore defined as follows: + +``` +{ + "_class_name": "LDMTextToImagePipeline", + "_diffusers_version": "0.0.4", + "bert": [ + "latent_diffusion", + "LDMBertModel" + ], + "scheduler": [ + "diffusers", + "DDIMScheduler" + ], + "tokenizer": [ + "transformers", + "BertTokenizer" + ], + "unet": [ + "diffusers", + "UNet2DConditionModel" + ], + "vqvae": [ + "diffusers", + "AutoencoderKL" + ] +} +``` + +- `_class_name` tells `DiffusionPipeline` which pipeline class should be loaded. +- `_diffusers_version` can be useful to know under which `diffusers` version this model was created. +- Every component of the pipeline is then defined under the form: +``` +"name" : [ + "library", + "class" +] +``` + - The `"name"` field corresponds both to the name of the subfolder in which the configuration and weights are stored as well as the attribute name of the pipeline class (as can be seen [here](https://huggingface.co/CompVis/ldm-text2im-large-256/tree/main/bert) and [here](https://github.com/huggingface/diffusers/blob/cd502b25cf0debac6f98d27a6638ef95208d1ea2/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py#L42) + - The `"library"` field corresponds to the name of the library, *e.g.* `diffusers` or `transformers` from which the `"class"` should be loaded + - The `"class"` field corresponds to the name of the class, *e.g.* [`BertTokenizer`](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer) or [`UNet2DConditionModel`] + + +## Loading models + +Models as defined under [src/diffusers/models](https://github.com/huggingface/diffusers/tree/main/src/diffusers/models) can be loaded via the [`ModelMixin.from_pretrained`] function. The API is very similar the [`DiffusionPipeline.from_pretrained`] and works in the same way: +- Download the latest version of the model weights and configuration with `diffusers` and cache them. If the latest files are available in the local cache, [`ModelMixin.from_pretrained`] will simply reuse the cache and **not** re-download the files. +- Load the cached weights into the _defined_ model class - one of [the existing model classes](./api/models) - and return an instance of the class. + +In constrast to [`DiffusionPipeline.from_pretrained`], models rely on fewer files that usually don't require a folder structure, but just a `diffusion_pytorch_model.bin` and `config.json` file. + +Let's look at an example: + +```python +from diffusers import UNet2DConditionModel + +repo_id = "CompVis/ldm-text2im-large-256" +model = UNet2DConditionModel.from_pretrained(repo_id, subfolder="unet") +``` + +Note how we have to define the `subfolder="unet"` argument to tell [`ModelMixin.from_pretrained`] that the model weights are located in a [subfolder of the repository](https://huggingface.co/CompVis/ldm-text2im-large-256/tree/main/unet). + +As explained in [Loading customized pipelines]("./using-diffusers/loading#loading-customized-pipelines"), one can pass a loaded model to a diffusion pipeline, via [`DiffusionPipeline.from_pretrained`]: + +```python +from diffusers import DiffusionPipeline + +repo_id = "CompVis/ldm-text2im-large-256" +ldm = DiffusionPipeline.from_pretrained(repo_id, unet=model) +``` + +If the model files can be found directly at the root level, which is usually only the case for some very simple diffusion models, such as [`google/ddpm-cifar10-32`](https://huggingface.co/google/ddpm-cifar10-32), we don't +need to pass a `subfolder` argument: + +```python +from diffusers import UNet2DModel + +repo_id = "google/ddpm-cifar10-32" +model = UNet2DModel.from_pretrained(repo_id) +``` + +## Loading schedulers + +Schedulers rely on [`SchedulerMixin.from_pretrained`]. Schedulers are **not parameterized** or **trained**, but instead purely defined by a configuration file. +For consistency, we use the same method name as we do for models or pipelines, but no weights are loaded in this case. + +In constrast to pipelines or models, loading schedulers does not consume any significant amount of memory and the same configuration file can often be used for a variety of different schedulers. +For example, all of: + +- [`DDPMScheduler`] +- [`DDIMScheduler`] +- [`PNDMScheduler`] +- [`LMSDiscreteScheduler`] +- [`EulerDiscreteScheduler`] +- [`EulerAncestralDiscreteScheduler`] +- [`DPMSolverMultistepScheduler`] + +are compatible with [`StableDiffusionPipeline`] and therefore the same scheduler configuration file can be loaded in any of those classes: + +```python +from diffusers import StableDiffusionPipeline +from diffusers import ( + DDPMScheduler, + DDIMScheduler, + PNDMScheduler, + LMSDiscreteScheduler, + EulerDiscreteScheduler, + EulerAncestralDiscreteScheduler, + DPMSolverMultistepScheduler, +) + +repo_id = "runwayml/stable-diffusion-v1-5" + +ddpm = DDPMScheduler.from_pretrained(repo_id, subfolder="scheduler") +ddim = DDIMScheduler.from_pretrained(repo_id, subfolder="scheduler") +pndm = PNDMScheduler.from_pretrained(repo_id, subfolder="scheduler") +lms = LMSDiscreteScheduler.from_pretrained(repo_id, subfolder="scheduler") +euler_anc = EulerAncestralDiscreteScheduler.from_pretrained(repo_id, subfolder="scheduler") +euler = EulerDiscreteScheduler.from_pretrained(repo_id, subfolder="scheduler") +dpm = DPMSolverMultistepScheduler.from_pretrained(repo_id, subfolder="scheduler") + +# replace `dpm` with any of `ddpm`, `ddim`, `pndm`, `lms`, `euler`, `euler_anc` +pipeline = StableDiffusionPipeline.from_pretrained(repo_id, scheduler=dpm) +``` diff --git a/diffusers/docs/source/en/using-diffusers/other-modalities.mdx b/diffusers/docs/source/en/using-diffusers/other-modalities.mdx new file mode 100644 index 0000000000000000000000000000000000000000..3e1cdbde80c4434e57a3e7f67d66b5b6f76baad1 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/other-modalities.mdx @@ -0,0 +1,21 @@ + + +# Using Diffusers with other modalities + +Diffusers is in the process of expanding to modalities other than images. + +Example type | Colab | Pipeline | +:-------------------------:|:-------------------------:|:-------------------------:| +[Molecule conformation](https://www.nature.com/subjects/molecular-conformation#:~:text=Definition,to%20changes%20in%20their%20environment.) generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/geodiff_molecule_conformation.ipynb) | ❌ + +More coming soon! \ No newline at end of file diff --git a/diffusers/docs/source/en/using-diffusers/reproducibility.mdx b/diffusers/docs/source/en/using-diffusers/reproducibility.mdx new file mode 100644 index 0000000000000000000000000000000000000000..03c6ae55998dc459dcbd3a5abcfa4a5977d8b728 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/reproducibility.mdx @@ -0,0 +1,159 @@ + + +# Reproducibility + +Before reading about reproducibility for Diffusers, it is strongly recommended to take a look at +[PyTorch's statement about reproducibility](https://pytorch.org/docs/stable/notes/randomness.html). + +PyTorch states that +> *completely reproducible results are not guaranteed across PyTorch releases, individual commits, or different platforms.* +While one can never expect the same results across platforms, one can expect results to be reproducible +across releases, platforms, etc... within a certain tolerance. However, this tolerance strongly varies +depending on the diffusion pipeline and checkpoint. + +In the following, we show how to best control sources of randomness for diffusion models. + +## Inference + +During inference, diffusion pipelines heavily rely on random sampling operations, such as the creating the +gaussian noise tensors to be denoised and adding noise to the scheduling step. + +Let's have a look at an example. We run the [DDIM pipeline](./api/pipelines/ddim.mdx) +for just two inference steps and return a numpy tensor to look into the numerical values of the output. + +```python +from diffusers import DDIMPipeline +import numpy as np + +model_id = "google/ddpm-cifar10-32" + +# load model and scheduler +ddim = DDIMPipeline.from_pretrained(model_id) + +# run pipeline for just two steps and return numpy tensor +image = ddim(num_inference_steps=2, output_type="np").images +print(np.abs(image).sum()) +``` + +Running the above prints a value of 1464.2076, but running it again prints a different +value of 1495.1768. What is going on here? Every time the pipeline is run, gaussian noise +is created and step-wise denoised. To create the gaussian noise with [`torch.randn`](https://pytorch.org/docs/stable/generated/torch.randn.html), a different random seed is taken every time, thus leading to a different result. +This is a desired property of diffusion pipelines, as it means that the pipeline can create a different random image every time it is run. In many cases, one would like to generate the exact same image of a certain +run, for which case an instance of a [PyTorch generator](https://pytorch.org/docs/stable/generated/torch.randn.html) has to be passed: + +```python +import torch +from diffusers import DDIMPipeline +import numpy as np + +model_id = "google/ddpm-cifar10-32" + +# load model and scheduler +ddim = DDIMPipeline.from_pretrained(model_id) + +# create a generator for reproducibility +generator = torch.Generator(device="cpu").manual_seed(0) + +# run pipeline for just two steps and return numpy tensor +image = ddim(num_inference_steps=2, output_type="np", generator=generator).images +print(np.abs(image).sum()) +``` + +Running the above always prints a value of 1491.1711 - also upon running it again because we +define the generator object to be passed to all random functions of the pipeline. + +If you run this code snippet on your specific hardware and version, you should get a similar, if not the same, result. + + + +It might be a bit unintuitive at first to pass `generator` objects to the pipelines instead of +just integer values representing the seed, but this is the recommended design when dealing with +probabilistic models in PyTorch as generators are *random states* that are advanced and can thus be +passed to multiple pipelines in a sequence. + + + +Great! Now, we know how to write reproducible pipelines, but it gets a bit trickier since the above example only runs on the CPU. How do we also achieve reproducibility on GPU? +In short, one should not expect full reproducibility across different hardware when running pipelines on GPU +as matrix multiplications are less deterministic on GPU than on CPU and diffusion pipelines tend to require +a lot of matrix multiplications. Let's see what we can do to keep the randomness within limits across +different GPU hardware. + +To achieve maximum speed performance, it is recommended to create the generator directly on GPU when running +the pipeline on GPU: + +```python +import torch +from diffusers import DDIMPipeline +import numpy as np + +model_id = "google/ddpm-cifar10-32" + +# load model and scheduler +ddim = DDIMPipeline.from_pretrained(model_id) +ddim.to("cuda") + +# create a generator for reproducibility +generator = torch.Generator(device="cuda").manual_seed(0) + +# run pipeline for just two steps and return numpy tensor +image = ddim(num_inference_steps=2, output_type="np", generator=generator).images +print(np.abs(image).sum()) +``` + +Running the above now prints a value of 1389.8634 - even though we're using the exact same seed! +This is unfortunate as it means we cannot reproduce the results we achieved on GPU, also on CPU. +Nevertheless, it should be expected since the GPU uses a different random number generator than the CPU. + +To circumvent this problem, we created a [`randn_tensor`](#diffusers.utils.randn_tensor) function, which can create random noise +on the CPU and then move the tensor to GPU if necessary. The function is used everywhere inside the pipelines allowing the user to **always** pass a CPU generator even if the pipeline is run on GPU: + +```python +import torch +from diffusers import DDIMPipeline +import numpy as np + +model_id = "google/ddpm-cifar10-32" + +# load model and scheduler +ddim = DDIMPipeline.from_pretrained(model_id) +ddim.to("cuda") + +# create a generator for reproducibility +generator = torch.manual_seed(0) + +# run pipeline for just two steps and return numpy tensor +image = ddim(num_inference_steps=2, output_type="np", generator=generator).images +print(np.abs(image).sum()) +``` + +Running the above now prints a value of 1491.1713, much closer to the value of 1491.1711 when +the pipeline is fully run on the CPU. + + + +As a consequence, we recommend always passing a CPU generator if Reproducibility is important. +The loss of performance is often neglectable, but one can be sure to generate much more similar +values than if the pipeline would have been run on CPU. + + + +Finally, we noticed that more complex pipelines, such as [`UnCLIPPipeline`] are often extremely +susceptible to precision error propagation and thus one cannot expect even similar results across +different GPU hardware or PyTorch versions. In such cases, one has to make sure to run +exactly the same hardware and PyTorch version for full Reproducibility. + +## Randomness utilities + +### randn_tensor +[[autodoc]] diffusers.utils.randn_tensor diff --git a/diffusers/docs/source/en/using-diffusers/reusing_seeds.mdx b/diffusers/docs/source/en/using-diffusers/reusing_seeds.mdx new file mode 100644 index 0000000000000000000000000000000000000000..54238cdf21b229bf5fa64505069c500c52d7c1f7 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/reusing_seeds.mdx @@ -0,0 +1,73 @@ + + +# Re-using seeds for fast prompt engineering + +A common use case when generating images is to generate a batch of images, select one image and improve it with a better, more detailed prompt in a second run. +To do this, one needs to make each generated image of the batch deterministic. +Images are generated by denoising gaussian random noise which can be instantiated by passing a [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html#generator). + +Now, for batched generation, we need to make sure that every single generated image in the batch is tied exactly to one seed. In 🧨 Diffusers, this can be achieved by not passing one `generator`, but a list +of `generators` to the pipeline. + +Let's go through an example using [`runwayml/stable-diffusion-v1-5`](runwayml/stable-diffusion-v1-5). +We want to generate several versions of the prompt: + +```py +prompt = "Labrador in the style of Vermeer" +``` + +Let's load the pipeline + +```python +>>> from diffusers import DiffusionPipeline + +>>> pipe = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) +>>> pipe = pipe.to("cuda") +``` + +Now, let's define 4 different generators, since we would like to reproduce a certain image. We'll use seeds `0` to `3` to create our generators. + +```python +>>> import torch + +>>> generator = [torch.Generator(device="cuda").manual_seed(i) for i in range(4)] +``` + +Let's generate 4 images: + +```python +>>> images = pipe(prompt, generator=generator, num_images_per_prompt=4).images +>>> images +``` + +![img](https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/reusabe_seeds.jpg) + +Ok, the last images has some double eyes, but the first image looks good! +Let's try to make the prompt a bit better **while keeping the first seed** +so that the images are similar to the first image. + +```python +prompt = [prompt + t for t in [", highly realistic", ", artsy", ", trending", ", colorful"]] +generator = [torch.Generator(device="cuda").manual_seed(0) for i in range(4)] +``` + +We create 4 generators with seed `0`, which is the first seed we used before. + +Let's run the pipeline again. + +```python +>>> images = pipe(prompt, generator=generator).images +>>> images +``` + +![img](https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/reusabe_seeds_2.jpg) diff --git a/diffusers/docs/source/en/using-diffusers/rl.mdx b/diffusers/docs/source/en/using-diffusers/rl.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e74cee742fbdac406ff009b0655fe77687d2665b --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/rl.mdx @@ -0,0 +1,25 @@ + + +# Using Diffusers for reinforcement learning + +Support for one RL model and related pipelines is included in the `experimental` source of diffusers. +More models and examples coming soon! + +# Diffuser Value-guided Planning + +You can run the model from [*Planning with Diffusion for Flexible Behavior Synthesis*](https://arxiv.org/abs/2205.09991) with Diffusers. +The script is located in the [RL Examples](https://github.com/huggingface/diffusers/tree/main/examples/rl) folder. + +Or, run this example in Colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/reinforcement_learning_with_diffusers.ipynb) + +[[autodoc]] diffusers.experimental.ValueGuidedRLPipeline \ No newline at end of file diff --git a/diffusers/docs/source/en/using-diffusers/schedulers.mdx b/diffusers/docs/source/en/using-diffusers/schedulers.mdx new file mode 100644 index 0000000000000000000000000000000000000000..caa80675a0c73d12a3f85ef425762f80a2d7d703 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/schedulers.mdx @@ -0,0 +1,314 @@ + + +# Schedulers + +Diffusion pipelines are inherently a collection of diffusion models and schedulers that are partly independent from each other. This means that one is able to switch out parts of the pipeline to better customize +a pipeline to one's use case. The best example of this are the [Schedulers](../api/schedulers/overview.mdx). + +Whereas diffusion models usually simply define the forward pass from noise to a less noisy sample, +schedulers define the whole denoising process, *i.e.*: +- How many denoising steps? +- Stochastic or deterministic? +- What algorithm to use to find the denoised sample + +They can be quite complex and often define a trade-off between **denoising speed** and **denoising quality**. +It is extremely difficult to measure quantitatively which scheduler works best for a given diffusion pipeline, so it is often recommended to simply try out which works best. + +The following paragraphs shows how to do so with the 🧨 Diffusers library. + +## Load pipeline + +Let's start by loading the stable diffusion pipeline. +Remember that you have to be a registered user on the 🤗 Hugging Face Hub, and have "click-accepted" the [license](https://huggingface.co/runwayml/stable-diffusion-v1-5) in order to use stable diffusion. + +```python +from huggingface_hub import login +from diffusers import DiffusionPipeline +import torch + +# first we need to login with our access token +login() + +# Now we can download the pipeline +pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) +``` + +Next, we move it to GPU: + +```python +pipeline.to("cuda") +``` + +## Access the scheduler + +The scheduler is always one of the components of the pipeline and is usually called `"scheduler"`. +So it can be accessed via the `"scheduler"` property. + +```python +pipeline.scheduler +``` + +**Output**: +``` +PNDMScheduler { + "_class_name": "PNDMScheduler", + "_diffusers_version": "0.8.0.dev0", + "beta_end": 0.012, + "beta_schedule": "scaled_linear", + "beta_start": 0.00085, + "clip_sample": false, + "num_train_timesteps": 1000, + "set_alpha_to_one": false, + "skip_prk_steps": true, + "steps_offset": 1, + "trained_betas": null +} +``` + +We can see that the scheduler is of type [`PNDMScheduler`]. +Cool, now let's compare the scheduler in its performance to other schedulers. +First we define a prompt on which we will test all the different schedulers: + +```python +prompt = "A photograph of an astronaut riding a horse on Mars, high resolution, high definition." +``` + +Next, we create a generator from a random seed that will ensure that we can generate similar images as well as run the pipeline: + +```python +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator).images[0] +image +``` + +

+
+ +
+

+ + +## Changing the scheduler + +Now we show how easy it is to change the scheduler of a pipeline. Every scheduler has a property [`SchedulerMixin.compatibles`] +which defines all compatible schedulers. You can take a look at all available, compatible schedulers for the Stable Diffusion pipeline as follows. + +```python +pipeline.scheduler.compatibles +``` + +**Output**: +``` +[diffusers.schedulers.scheduling_lms_discrete.LMSDiscreteScheduler, + diffusers.schedulers.scheduling_ddim.DDIMScheduler, + diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler, + diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler, + diffusers.schedulers.scheduling_pndm.PNDMScheduler, + diffusers.schedulers.scheduling_ddpm.DDPMScheduler, + diffusers.schedulers.scheduling_euler_ancestral_discrete.EulerAncestralDiscreteScheduler] +``` + +Cool, lots of schedulers to look at. Feel free to have a look at their respective class definitions: + +- [`LMSDiscreteScheduler`], +- [`DDIMScheduler`], +- [`DPMSolverMultistepScheduler`], +- [`EulerDiscreteScheduler`], +- [`PNDMScheduler`], +- [`DDPMScheduler`], +- [`EulerAncestralDiscreteScheduler`]. + +We will now compare the input prompt with all other schedulers. To change the scheduler of the pipeline you can make use of the +convenient [`ConfigMixin.config`] property in combination with the [`ConfigMixin.from_config`] function. + +```python +pipeline.scheduler.config +``` + +returns a dictionary of the configuration of the scheduler: + +**Output**: +``` +FrozenDict([('num_train_timesteps', 1000), + ('beta_start', 0.00085), + ('beta_end', 0.012), + ('beta_schedule', 'scaled_linear'), + ('trained_betas', None), + ('skip_prk_steps', True), + ('set_alpha_to_one', False), + ('steps_offset', 1), + ('_class_name', 'PNDMScheduler'), + ('_diffusers_version', '0.8.0.dev0'), + ('clip_sample', False)]) +``` + +This configuration can then be used to instantiate a scheduler +of a different class that is compatible with the pipeline. Here, +we change the scheduler to the [`DDIMScheduler`]. + +```python +from diffusers import DDIMScheduler + +pipeline.scheduler = DDIMScheduler.from_config(pipeline.scheduler.config) +``` + +Cool, now we can run the pipeline again to compare the generation quality. + +```python +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator).images[0] +image +``` + +

+
+ +
+

+ +If you are a JAX/Flax user, please check [this section](#changing-the-scheduler-in-flax) instead. + +## Compare schedulers + +So far we have tried running the stable diffusion pipeline with two schedulers: [`PNDMScheduler`] and [`DDIMScheduler`]. +A number of better schedulers have been released that can be run with much fewer steps, let's compare them here: + +[`LMSDiscreteScheduler`] usually leads to better results: + +```python +from diffusers import LMSDiscreteScheduler + +pipeline.scheduler = LMSDiscreteScheduler.from_config(pipeline.scheduler.config) + +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator).images[0] +image +``` + +

+
+ +
+

+ + +[`EulerDiscreteScheduler`] and [`EulerAncestralDiscreteScheduler`] can generate high quality results with as little as 30 steps. + +```python +from diffusers import EulerDiscreteScheduler + +pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) + +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator, num_inference_steps=30).images[0] +image +``` + +

+
+ +
+

+ + +and: + +```python +from diffusers import EulerAncestralDiscreteScheduler + +pipeline.scheduler = EulerAncestralDiscreteScheduler.from_config(pipeline.scheduler.config) + +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator, num_inference_steps=30).images[0] +image +``` + +

+
+ +
+

+ + +At the time of writing this doc [`DPMSolverMultistepScheduler`] gives arguably the best speed/quality trade-off and can be run with as little +as 20 steps. + +```python +from diffusers import DPMSolverMultistepScheduler + +pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) + +generator = torch.Generator(device="cuda").manual_seed(8) +image = pipeline(prompt, generator=generator, num_inference_steps=20).images[0] +image +``` + +

+
+ +
+

+ +As you can see most images look very similar and are arguably of very similar quality. It often really depends on the specific use case which scheduler to choose. A good approach is always to run multiple different +schedulers to compare results. + +## Changing the Scheduler in Flax + +If you are a JAX/Flax user, you can also change the default pipeline scheduler. This is a complete example of how to run inference using the Flax Stable Diffusion pipeline and the super-fast [DDPM-Solver++ scheduler](../api/schedulers/multistep_dpm_solver): + +```Python +import jax +import numpy as np +from flax.jax_utils import replicate +from flax.training.common_utils import shard + +from diffusers import FlaxStableDiffusionPipeline, FlaxDPMSolverMultistepScheduler + +model_id = "runwayml/stable-diffusion-v1-5" +scheduler, scheduler_state = FlaxDPMSolverMultistepScheduler.from_pretrained( + model_id, + subfolder="scheduler" +) +pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + model_id, + scheduler=scheduler, + revision="bf16", + dtype=jax.numpy.bfloat16, +) +params["scheduler"] = scheduler_state + +# Generate 1 image per parallel device (8 on TPUv2-8 or TPUv3-8) +prompt = "a photo of an astronaut riding a horse on mars" +num_samples = jax.device_count() +prompt_ids = pipeline.prepare_inputs([prompt] * num_samples) + +prng_seed = jax.random.PRNGKey(0) +num_inference_steps = 25 + +# shard inputs and rng +params = replicate(params) +prng_seed = jax.random.split(prng_seed, jax.device_count()) +prompt_ids = shard(prompt_ids) + +images = pipeline(prompt_ids, params, prng_seed, num_inference_steps, jit=True).images +images = pipeline.numpy_to_pil(np.asarray(images.reshape((num_samples,) + images.shape[-3:]))) +``` + + + +The following Flax schedulers are _not yet compatible_ with the Flax Stable Diffusion Pipeline: + +- `FlaxLMSDiscreteScheduler` +- `FlaxDDPMScheduler` + + diff --git a/diffusers/docs/source/en/using-diffusers/unconditional_image_generation.mdx b/diffusers/docs/source/en/using-diffusers/unconditional_image_generation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ba119defb85f174cf9f08e981155a1fb7090f0e8 --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/unconditional_image_generation.mdx @@ -0,0 +1,52 @@ + + + + +# Unconditional Image Generation + +The [`DiffusionPipeline`] is the easiest way to use a pre-trained diffusion system for inference + +Start by creating an instance of [`DiffusionPipeline`] and specify which pipeline checkpoint you would like to download. +You can use the [`DiffusionPipeline`] for any [Diffusers' checkpoint](https://huggingface.co/models?library=diffusers&sort=downloads). +In this guide though, you'll use [`DiffusionPipeline`] for unconditional image generation with [DDPM](https://arxiv.org/abs/2006.11239): + +```python +>>> from diffusers import DiffusionPipeline + +>>> generator = DiffusionPipeline.from_pretrained("google/ddpm-celebahq-256") +``` +The [`DiffusionPipeline`] downloads and caches all modeling, tokenization, and scheduling components. +Because the model consists of roughly 1.4 billion parameters, we strongly recommend running it on GPU. +You can move the generator object to GPU, just like you would in PyTorch. + +```python +>>> generator.to("cuda") +``` + +Now you can use the `generator` on your text prompt: + +```python +>>> image = generator().images[0] +``` + +The output is by default wrapped into a [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image#the-image-class). + +You can save the image by simply calling: + +```python +>>> image.save("generated_image.png") +``` + + + + diff --git a/diffusers/docs/source/en/using-diffusers/using_safetensors b/diffusers/docs/source/en/using-diffusers/using_safetensors new file mode 100644 index 0000000000000000000000000000000000000000..b6b165dabc728b885d8f7f097af808d8a2270b2c --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/using_safetensors @@ -0,0 +1,19 @@ +# What is safetensors ? + +[safetensors](https://github.com/huggingface/safetensors) is a different format +from the classic `.bin` which uses Pytorch which uses pickle. + +Pickle is notoriously unsafe which allow any malicious file to execute arbitrary code. +The hub itself tries to prevent issues from it, but it's not a silver bullet. + +`safetensors` first and foremost goal is to make loading machine learning models *safe* +in the sense that no takeover of your computer can be done. + +# Why use safetensors ? + +**Safety** can be one reason, if you're attempting to use a not well known model and +you're not sure about the source of the file. + +And a secondary reason, is **the speed of loading**. Safetensors can load models much faster +than regular pickle files. If you spend a lot of times switching models, this can be +a huge timesave. diff --git a/diffusers/docs/source/en/using-diffusers/using_safetensors.mdx b/diffusers/docs/source/en/using-diffusers/using_safetensors.mdx new file mode 100644 index 0000000000000000000000000000000000000000..029d1e84f7d97a3c23cab77c0a52b240fad9f63a --- /dev/null +++ b/diffusers/docs/source/en/using-diffusers/using_safetensors.mdx @@ -0,0 +1,87 @@ +# What is safetensors ? + +[safetensors](https://github.com/huggingface/safetensors) is a different format +from the classic `.bin` which uses Pytorch which uses pickle. It contains the +exact same data, which is just the model weights (or tensors). + +Pickle is notoriously unsafe which allow any malicious file to execute arbitrary code. +The hub itself tries to prevent issues from it, but it's not a silver bullet. + +`safetensors` first and foremost goal is to make loading machine learning models *safe* +in the sense that no takeover of your computer can be done. + +Hence the name. + +# Why use safetensors ? + +**Safety** can be one reason, if you're attempting to use a not well known model and +you're not sure about the source of the file. + +And a secondary reason, is **the speed of loading**. Safetensors can load models much faster +than regular pickle files. If you spend a lot of times switching models, this can be +a huge timesave. + +Numbers taken AMD EPYC 7742 64-Core Processor +``` +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1") + +# Loaded in safetensors 0:00:02.033658 +# Loaded in Pytorch 0:00:02.663379 +``` + +This is for the entire loading time, the actual weights loading time to load 500MB: + +``` +Safetensors: 3.4873ms +PyTorch: 172.7537ms +``` + +Performance in general is a tricky business, and there are a few things to understand: + +- If you're using the model for the first time from the hub, you will have to download the weights. + That's extremely likely to be much slower than any loading method, therefore you will not see any difference +- If you're loading the model for the first time (let's say after a reboot) then your machine will have to + actually read the disk. It's likely to be as slow in both cases. Again the speed difference may not be as visible (this depends on hardware and the actual model). +- The best performance benefit is when the model was already loaded previously on your computer and you're switching from one model to another. Your OS, is trying really hard not to read from disk, since this is slow, so it will keep the files around in RAM, making it loading again much faster. Since safetensors is doing zero-copy of the tensors, reloading will be faster than pytorch since it has at least once extra copy to do. + +# How to use safetensors ? + +If you have `safetensors` installed, and all the weights are available in `safetensors` format, \ +then by default it will use that instead of the pytorch weights. + +If you are really paranoid about this, the ultimate weapon would be disabling `torch.load`: +```python +import torch + + +def _raise(): + raise RuntimeError("I don't want to use pickle") + + +torch.load = lambda *args, **kwargs: _raise() +``` + +# I want to use model X but it doesn't have safetensors weights. + +Just go to this [space](https://huggingface.co/spaces/safetensors/convert). +This will create a new PR with the weights, let's say `refs/pr/22`. + +This space will download the pickled version, convert it, and upload it on the hub as a PR. +If anything bad is contained in the file, it's Huggingface hub that will get issues, not your own computer. +And we're equipped with dealing with it. + +Then in order to use the model, even before the branch gets accepted by the original author you can do: + +```python +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1", revision="refs/pr/22") +``` + +And that's it ! + +Anything unclear, concerns, or found a bugs ? [Open an issue](https://github.com/huggingface/diffusers/issues/new/choose) + + diff --git a/diffusers/docs/source/ko/_toctree.yml b/diffusers/docs/source/ko/_toctree.yml new file mode 100644 index 0000000000000000000000000000000000000000..a1c0c690eb94c5963bf1c4d6fd374ea19339a316 --- /dev/null +++ b/diffusers/docs/source/ko/_toctree.yml @@ -0,0 +1,193 @@ +- sections: + - local: index + title: "🧨 Diffusers" + - local: quicktour + title: "훑어보기" + - local: installation + title: "설치" + title: "시작하기" +- sections: + - sections: + - local: in_translation + title: "Loading Pipelines, Models, and Schedulers" + - local: in_translation + title: "Using different Schedulers" + - local: in_translation + title: "Configuring Pipelines, Models, and Schedulers" + - local: in_translation + title: "Loading and Adding Custom Pipelines" + title: "불러오기 & 허브 (번역 예정)" + - sections: + - local: in_translation + title: "Unconditional Image Generation" + - local: in_translation + title: "Text-to-Image Generation" + - local: in_translation + title: "Text-Guided Image-to-Image" + - local: in_translation + title: "Text-Guided Image-Inpainting" + - local: in_translation + title: "Text-Guided Depth-to-Image" + - local: in_translation + title: "Reusing seeds for deterministic generation" + - local: in_translation + title: "Community Pipelines" + - local: in_translation + title: "How to contribute a Pipeline" + title: "추론을 위한 파이프라인 (번역 예정)" + - sections: + - local: in_translation + title: "Reinforcement Learning" + - local: in_translation + title: "Audio" + - local: in_translation + title: "Other Modalities" + title: "Taking Diffusers Beyond Images" + title: "Diffusers 사용법 (번역 예정)" +- sections: + - local: in_translation + title: "Memory and Speed" + - local: in_translation + title: "xFormers" + - local: in_translation + title: "ONNX" + - local: in_translation + title: "OpenVINO" + - local: in_translation + title: "MPS" + - local: in_translation + title: "Habana Gaudi" + title: "최적화/특수 하드웨어 (번역 예정)" +- sections: + - local: in_translation + title: "Overview" + - local: in_translation + title: "Unconditional Image Generation" + - local: in_translation + title: "Textual Inversion" + - local: in_translation + title: "Dreambooth" + - local: in_translation + title: "Text-to-image fine-tuning" + title: "학습 (번역 예정)" +- sections: + - local: in_translation + title: "Stable Diffusion" + - local: in_translation + title: "Philosophy" + - local: in_translation + title: "How to contribute?" + title: "개념 설명 (번역 예정)" +- sections: + - sections: + - local: in_translation + title: "Models" + - local: in_translation + title: "Diffusion Pipeline" + - local: in_translation + title: "Logging" + - local: in_translation + title: "Configuration" + - local: in_translation + title: "Outputs" + title: "Main Classes" + + - sections: + - local: in_translation + title: "Overview" + - local: in_translation + title: "AltDiffusion" + - local: in_translation + title: "Cycle Diffusion" + - local: in_translation + title: "DDIM" + - local: in_translation + title: "DDPM" + - local: in_translation + title: "Latent Diffusion" + - local: in_translation + title: "Unconditional Latent Diffusion" + - local: in_translation + title: "PaintByExample" + - local: in_translation + title: "PNDM" + - local: in_translation + title: "Score SDE VE" + - sections: + - local: in_translation + title: "Overview" + - local: in_translation + title: "Text-to-Image" + - local: in_translation + title: "Image-to-Image" + - local: in_translation + title: "Inpaint" + - local: in_translation + title: "Depth-to-Image" + - local: in_translation + title: "Image-Variation" + - local: in_translation + title: "Super-Resolution" + title: "Stable Diffusion" + - local: in_translation + title: "Stable Diffusion 2" + - local: in_translation + title: "Safe Stable Diffusion" + - local: in_translation + title: "Stochastic Karras VE" + - local: in_translation + title: "Dance Diffusion" + - local: in_translation + title: "UnCLIP" + - local: in_translation + title: "Versatile Diffusion" + - local: in_translation + title: "VQ Diffusion" + - local: in_translation + title: "RePaint" + - local: in_translation + title: "Audio Diffusion" + title: "파이프라인 (번역 예정)" + - sections: + - local: in_translation + title: "Overview" + - local: in_translation + title: "DDIM" + - local: in_translation + title: "DDPM" + - local: in_translation + title: "Singlestep DPM-Solver" + - local: in_translation + title: "Multistep DPM-Solver" + - local: in_translation + title: "Heun Scheduler" + - local: in_translation + title: "DPM Discrete Scheduler" + - local: in_translation + title: "DPM Discrete Scheduler with ancestral sampling" + - local: in_translation + title: "Stochastic Kerras VE" + - local: in_translation + title: "Linear Multistep" + - local: in_translation + title: "PNDM" + - local: in_translation + title: "VE-SDE" + - local: in_translation + title: "IPNDM" + - local: in_translation + title: "VP-SDE" + - local: in_translation + title: "Euler scheduler" + - local: in_translation + title: "Euler Ancestral Scheduler" + - local: in_translation + title: "VQDiffusionScheduler" + - local: in_translation + title: "RePaint Scheduler" + title: "스케줄러 (번역 예정)" + - sections: + - local: in_translation + title: "RL Planning" + title: "Experimental Features" + title: "API (번역 예정)" diff --git a/diffusers/docs/source/ko/in_translation.mdx b/diffusers/docs/source/ko/in_translation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9ed1bdc3acf02008a6fa0ec2f4f7a7d03105c65f --- /dev/null +++ b/diffusers/docs/source/ko/in_translation.mdx @@ -0,0 +1,16 @@ + + +# 번역중 + +열심히 번역을 진행중입니다. 조금만 기다려주세요. +감사합니다! \ No newline at end of file diff --git a/diffusers/docs/source/ko/index.mdx b/diffusers/docs/source/ko/index.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ab81f94ff9c5d5e72253ab749e5901c88338be3e --- /dev/null +++ b/diffusers/docs/source/ko/index.mdx @@ -0,0 +1,63 @@ + + +

+
+ +
+

+ +# 🧨 Diffusers + +🤗 Diffusers는 사전학습된 비전 및 오디오 확산 모델을 제공하고, 추론 및 학습을 위한 모듈식 도구 상자 역할을 합니다. + +보다 정확하게, 🤗 Diffusers는 다음을 제공합니다: + +- 단 몇 줄의 코드로 추론을 실행할 수 있는 최신 확산 파이프라인을 제공합니다. ([**Using Diffusers**](./using-diffusers/conditional_image_generation)를 살펴보세요) 지원되는 모든 파이프라인과 해당 논문에 대한 개요를 보려면 [**Pipelines**](#pipelines)을 살펴보세요. +- 추론에서 속도 vs 품질의 절충을 위해 상호교환적으로 사용할 수 있는 다양한 노이즈 스케줄러를 제공합니다. 자세한 내용은 [**Schedulers**](./api/schedulers/overview)를 참고하세요. +- UNet과 같은 여러 유형의 모델을 end-to-end 확산 시스템의 구성 요소로 사용할 수 있습니다. 자세한 내용은 [**Models**](./api/models)을 참고하세요. +- 가장 인기있는 확산 모델 테스크를 학습하는 방법을 보여주는 예제들을 제공합니다. 자세한 내용은 [**Training**](./training/overview)를 참고하세요. + +## 🧨 Diffusers 파이프라인 + +다음 표에는 공시적으로 지원되는 모든 파이프라인, 관련 논문, 직접 사용해 볼 수 있는 Colab 노트북(사용 가능한 경우)이 요약되어 있습니다. + +| Pipeline | Paper | Tasks | Colab +|---|---|:---:|:---:| +| [alt_diffusion](./api/pipelines/alt_diffusion) | [**AltDiffusion**](https://arxiv.org/abs/2211.06679) | Image-to-Image Text-Guided Generation | +| [audio_diffusion](./api/pipelines/audio_diffusion) | [**Audio Diffusion**](https://github.com/teticio/audio-diffusion.git) | Unconditional Audio Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/teticio/audio-diffusion/blob/master/notebooks/audio_diffusion_pipeline.ipynb) +| [cycle_diffusion](./api/pipelines/cycle_diffusion) | [**Cycle Diffusion**](https://arxiv.org/abs/2210.05559) | Image-to-Image Text-Guided Generation | +| [dance_diffusion](./api/pipelines/dance_diffusion) | [**Dance Diffusion**](https://github.com/williamberman/diffusers.git) | Unconditional Audio Generation | +| [ddpm](./api/pipelines/ddpm) | [**Denoising Diffusion Probabilistic Models**](https://arxiv.org/abs/2006.11239) | Unconditional Image Generation | +| [ddim](./api/pipelines/ddim) | [**Denoising Diffusion Implicit Models**](https://arxiv.org/abs/2010.02502) | Unconditional Image Generation | +| [latent_diffusion](./api/pipelines/latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Text-to-Image Generation | +| [latent_diffusion](./api/pipelines/latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752)| Super Resolution Image-to-Image | +| [latent_diffusion_uncond](./api/pipelines/latent_diffusion_uncond) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752) | Unconditional Image Generation | +| [paint_by_example](./api/pipelines/paint_by_example) | [**Paint by Example: Exemplar-based Image Editing with Diffusion Models**](https://arxiv.org/abs/2211.13227) | Image-Guided Image Inpainting | +| [pndm](./api/pipelines/pndm) | [**Pseudo Numerical Methods for Diffusion Models on Manifolds**](https://arxiv.org/abs/2202.09778) | Unconditional Image Generation | +| [score_sde_ve](./api/pipelines/score_sde_ve) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [score_sde_vp](./api/pipelines/score_sde_vp) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | Unconditional Image Generation | +| [stable_diffusion](./api/pipelines/stable_diffusion/text2img) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-to-Image Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [stable_diffusion](./api/pipelines/stable_diffusion/img2img) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Image-to-Image Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) +| [stable_diffusion](./api/pipelines/stable_diffusion/inpaint) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | Text-Guided Image Inpainting | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-to-Image Generation | +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Image Inpainting | +| [stable_diffusion_2](./api/pipelines/stable_diffusion_2) | [**Stable Diffusion 2**](https://stability.ai/blog/stable-diffusion-v2-release) | Text-Guided Super Resolution Image-to-Image | +| [stable_diffusion_safe](./api/pipelines/stable_diffusion_safe) | [**Safe Stable Diffusion**](https://arxiv.org/abs/2211.05105) | Text-Guided Generation | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ml-research/safe-latent-diffusion/blob/main/examples/Safe%20Latent%20Diffusion.ipynb) +| [stochastic_karras_ve](./api/pipelines/stochastic_karras_ve) | [**Elucidating the Design Space of Diffusion-Based Generative Models**](https://arxiv.org/abs/2206.00364) | Unconditional Image Generation | +| [unclip](./api/pipelines/unclip) | [Hierarchical Text-Conditional Image Generation with CLIP Latents](https://arxiv.org/abs/2204.06125) | Text-to-Image Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Text-to-Image Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Image Variations Generation | +| [versatile_diffusion](./api/pipelines/versatile_diffusion) | [Versatile Diffusion: Text, Images and Variations All in One Diffusion Model](https://arxiv.org/abs/2211.08332) | Dual Image and Text Guided Generation | +| [vq_diffusion](./api/pipelines/vq_diffusion) | [Vector Quantized Diffusion Model for Text-to-Image Synthesis](https://arxiv.org/abs/2111.14822) | Text-to-Image Generation | + +**참고**: 파이프라인은 해당 문서에 설명된 대로 확산 시스템을 사용한 방법에 대한 간단한 예입니다. diff --git a/diffusers/docs/source/ko/installation.mdx b/diffusers/docs/source/ko/installation.mdx new file mode 100644 index 0000000000000000000000000000000000000000..20d0e082372720eafc3c411f3d6bed8e847cc1df --- /dev/null +++ b/diffusers/docs/source/ko/installation.mdx @@ -0,0 +1,142 @@ + + +# 설치 + +사용하시는 라이브러리에 맞는 🤗 Diffusers를 설치하세요. + +🤗 Diffusers는 Python 3.7+, PyTorch 1.7.0+ 및 flax에서 테스트되었습니다. 사용중인 딥러닝 라이브러리에 대한 아래의 설치 안내를 따르세요. + +- [PyTorch 설치 안내](https://pytorch.org/get-started/locally/) +- [Flax 설치 안내](https://flax.readthedocs.io/en/latest/) + +## pip를 이용한 설치 + +[가상 환경](https://docs.python.org/3/library/venv.html)에 🤗 Diffusers를 설치해야 합니다. +Python 가상 환경에 익숙하지 않은 경우 [가상환경 pip 설치 가이드](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/)를 살펴보세요. +가상 환경을 사용하면 서로 다른 프로젝트를 더 쉽게 관리하고, 종속성간의 호환성 문제를 피할 수 있습니다. + +프로젝트 디렉토리에 가상 환경을 생성하는 것으로 시작하세요: + +```bash +python -m venv .env +``` + +그리고 가상 환경을 활성화합니다: + +```bash +source .env/bin/activate +``` + +이제 다음의 명령어로 🤗 Diffusers를 설치할 준비가 되었습니다: + +**PyTorch의 경우** + +```bash +pip install diffusers["torch"] +``` + +**Flax의 경우** + +```bash +pip install diffusers["flax"] +``` + +## 소스로부터 설치 + +소스에서 `diffusers`를 설치하기 전에, `torch` 및 `accelerate`이 설치되어 있는지 확인하세요. + +`torch` 설치에 대해서는 [torch docs](https://pytorch.org/get-started/locally/#start-locally)를 참고하세요. + +다음과 같이 `accelerate`을 설치하세요. + +```bash +pip install accelerate +``` + +다음 명령어를 사용하여 소스에서 🤗 Diffusers를 설치하세요: + +```bash +pip install git+https://github.com/huggingface/diffusers +``` + +이 명령어는 최신 `stable` 버전이 아닌 최첨단 `main` 버전을 설치합니다. +`main` 버전은 최신 개발 정보를 최신 상태로 유지하는 데 유용합니다. +예를 들어 마지막 공식 릴리즈 이후 버그가 수정되었지만, 새 릴리즈가 아직 출시되지 않은 경우입니다. +그러나 이는 `main` 버전이 항상 안정적이지 않을 수 있음을 의미합니다. +우리는 `main` 버전이 지속적으로 작동하도록 노력하고 있으며, 대부분의 문제는 보통 몇 시간 또는 하루 안에 해결됩니다. +문제가 발생하면 더 빨리 해결할 수 있도록 [Issue](https://github.com/huggingface/transformers/issues)를 열어주세요! + + +## 편집가능한 설치 + +다음을 수행하려면 편집가능한 설치가 필요합니다: + +* 소스 코드의 `main` 버전을 사용 +* 🤗 Diffusers에 기여 (코드의 변경 사항을 테스트하기 위해 필요) + +저장소를 복제하고 다음 명령어를 사용하여 🤗 Diffusers를 설치합니다: + +```bash +git clone https://github.com/huggingface/diffusers.git +cd diffusers +``` + +**PyTorch의 경우** + +``` +pip install -e ".[torch]" +``` + +**Flax의 경우** + +``` +pip install -e ".[flax]" +``` + +이러한 명령어들은 저장소를 복제한 폴더와 Python 라이브러리 경로를 연결합니다. +Python은 이제 일반 라이브러리 경로에 더하여 복제한 폴더 내부를 살펴봅니다. +예를들어 Python 패키지가 `~/anaconda3/envs/main/lib/python3.7/site-packages/`에 설치되어 있는 경우 Python은 복제한 폴더인 `~/diffusers/`도 검색합니다. + + + +라이브러리를 계속 사용하려면 `diffusers` 폴더를 유지해야 합니다. + + + +이제 다음 명령어를 사용하여 최신 버전의 🤗 Diffusers로 쉽게 업데이트할 수 있습니다: + +```bash +cd ~/diffusers/ +git pull +``` + +이렇게 하면, 다음에 실행할 때 Python 환경이 🤗 Diffusers의 `main` 버전을 찾게 됩니다. + +## 텔레메트리 로깅에 대한 알림 + +우리 라이브러리는 `from_pretrained()` 요청 중에 텔레메트리 정보를 원격으로 수집합니다. +이 데이터에는 Diffusers 및 PyTorch/Flax의 버전, 요청된 모델 또는 파이프라인 클래스, 그리고 허브에서 호스팅되는 경우 사전학습된 체크포인트에 대한 경로를 포함합니다. +이 사용 데이터는 문제를 디버깅하고 새로운 기능의 우선순위를 지정하는데 도움이 됩니다. +텔레메트리는 HuggingFace 허브에서 모델과 파이프라인을 불러올 때만 전송되며, 로컬 사용 중에는 수집되지 않습니다. + +우리는 추가 정보를 공유하지 않기를 원하는 사람이 있다는 것을 이해하고 개인 정보를 존중하므로, 터미널에서 `DISABLE_TELEMETRY` 환경 변수를 설정하여 텔레메트리 수집을 비활성화할 수 있습니다. + +Linux/MacOS에서: +```bash +export DISABLE_TELEMETRY=YES +``` + +Windows에서: +```bash +set DISABLE_TELEMETRY=YES +``` \ No newline at end of file diff --git a/diffusers/docs/source/ko/quicktour.mdx b/diffusers/docs/source/ko/quicktour.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2ba4e99970a0887dbb9a0b7f85a433f2046f2ce6 --- /dev/null +++ b/diffusers/docs/source/ko/quicktour.mdx @@ -0,0 +1,123 @@ + + +# 훑어보기 + +🧨 Diffusers로 빠르게 시작하고 실행하세요! +이 훑어보기는 여러분이 개발자, 일반사용자 상관없이 시작하는 데 도움을 주며, 추론을 위해 [`DiffusionPipeline`] 사용하는 방법을 보여줍니다. + +시작하기에 앞서서, 필요한 모든 라이브러리가 설치되어 있는지 확인하세요: + +```bash +pip install --upgrade diffusers accelerate transformers +``` + +- [`accelerate`](https://huggingface.co/docs/accelerate/index)은 추론 및 학습을 위한 모델 불러오기 속도를 높입니다. +- [`transformers`](https://huggingface.co/docs/transformers/index)는 [Stable Diffusion](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/overview)과 같이 가장 널리 사용되는 확산 모델을 실행하기 위해 필요합니다. + +## DiffusionPipeline + +[`DiffusionPipeline`]은 추론을 위해 사전학습된 확산 시스템을 사용하는 가장 쉬운 방법입니다. 다양한 양식의 많은 작업에 [`DiffusionPipeline`]을 바로 사용할 수 있습니다. 지원되는 작업은 아래의 표를 참고하세요: + +| **Task** | **Description** | **Pipeline** +|------------------------------|--------------------------------------------------------------------------------------------------------------|-----------------| +| Unconditional Image Generation | 가우시안 노이즈에서 이미지 생성 | [unconditional_image_generation](./using-diffusers/unconditional_image_generation`) | +| Text-Guided Image Generation | 텍스트 프롬프트로 이미지 생성 | [conditional_image_generation](./using-diffusers/conditional_image_generation) | +| Text-Guided Image-to-Image Translation | 텍스트 프롬프트에 따라 이미지 조정 | [img2img](./using-diffusers/img2img) | +| Text-Guided Image-Inpainting | 마스크 및 텍스트 프롬프트가 주어진 이미지의 마스킹된 부분을 채우기 | [inpaint](./using-diffusers/inpaint) | +| Text-Guided Depth-to-Image Translation | 깊이 추정을 통해 구조를 유지하면서 텍스트 프롬프트에 따라 이미지의 일부를 조정 | [depth2image](./using-diffusers/depth2image) | + +확산 파이프라인이 다양한 작업에 대해 어떻게 작동하는지는 [**Using Diffusers**](./using-diffusers/overview)를 참고하세요. + +예를들어, [`DiffusionPipeline`] 인스턴스를 생성하여 시작하고, 다운로드하려는 파이프라인 체크포인트를 지정합니다. +모든 [Diffusers' checkpoint](https://huggingface.co/models?library=diffusers&sort=downloads)에 대해 [`DiffusionPipeline`]을 사용할 수 있습니다. +하지만, 이 가이드에서는 [Stable Diffusion](https://huggingface.co/CompVis/stable-diffusion)을 사용하여 text-to-image를 하는데 [`DiffusionPipeline`]을 사용합니다. + +[Stable Diffusion](https://huggingface.co/CompVis/stable-diffusion) 기반 모델을 실행하기 전에 [license](https://huggingface.co/spaces/CompVis/stable-diffusion-license)를 주의 깊게 읽으세요. +이는 모델의 향상된 이미지 생성 기능과 이것으로 생성될 수 있는 유해한 콘텐츠 때문입니다. 선택한 Stable Diffusion 모델(*예*: [`runwayml/stable-diffusion-v1-5`](https://huggingface.co/runwayml/stable-diffusion-v1-5))로 이동하여 라이센스를 읽으세요. + +다음과 같이 모델을 로드할 수 있습니다: + +```python +>>> from diffusers import DiffusionPipeline + +>>> pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +``` + +[`DiffusionPipeline`]은 모든 모델링, 토큰화 및 스케줄링 구성요소를 다운로드하고 캐시합니다. +모델은 약 14억개의 매개변수로 구성되어 있으므로 GPU에서 실행하는 것이 좋습니다. +PyTorch에서와 마찬가지로 생성기 객체를 GPU로 옮길 수 있습니다. + +```python +>>> pipeline.to("cuda") +``` + +이제 `pipeline`을 사용할 수 있습니다: + +```python +>>> image = pipeline("An image of a squirrel in Picasso style").images[0] +``` + +출력은 기본적으로 [PIL Image object](https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=image#the-image-class)로 래핑됩니다. + +다음과 같이 함수를 호출하여 이미지를 저장할 수 있습니다: + +```python +>>> image.save("image_of_squirrel_painting.png") +``` + +**참고**: 다음을 통해 가중치를 다운로드하여 로컬에서 파이프라인을 사용할 수도 있습니다: + +``` +git lfs install +git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +``` + +그리고 저장된 가중치를 파이프라인에 불러옵니다. + +```python +>>> pipeline = DiffusionPipeline.from_pretrained("./stable-diffusion-v1-5") +``` + +파이프라인 실행은 동일한 모델 아키텍처이므로 위의 코드와 동일합니다. + +```python +>>> generator.to("cuda") +>>> image = generator("An image of a squirrel in Picasso style").images[0] +>>> image.save("image_of_squirrel_painting.png") +``` + +확산 시스템은 각각 장점이 있는 여러 다른 [schedulers](./api/schedulers/overview)와 함께 사용할 수 있습니다. 기본적으로 Stable Diffusion은 `PNDMScheduler`로 실행되지만 다른 스케줄러를 사용하는 방법은 매우 간단합니다. *예* [`EulerDiscreteScheduler`] 스케줄러를 사용하려는 경우, 다음과 같이 사용할 수 있습니다: + +```python +>>> from diffusers import EulerDiscreteScheduler + +>>> pipeline = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + +>>> # change scheduler to Euler +>>> pipeline.scheduler = EulerDiscreteScheduler.from_config(pipeline.scheduler.config) +``` + +스케줄러 변경 방법에 대한 자세한 내용은 [Using Schedulers](./using-diffusers/schedulers) 가이드를 참고하세요. + +[Stability AI's](https://stability.ai/)의 Stable Diffusion 모델은 인상적인 이미지 생성 모델이며 텍스트에서 이미지를 생성하는 것보다 훨씬 더 많은 작업을 수행할 수 있습니다. 우리는 Stable Diffusion만을 위한 전체 문서 페이지를 제공합니다 [link](./conceptual/stable_diffusion). + +만약 더 적은 메모리, 더 높은 추론 속도, Mac과 같은 특정 하드웨어 또는 ONNX 런타임에서 실행되도록 Stable Diffusion을 최적화하는 방법을 알고 싶다면 최적화 페이지를 살펴보세요: + +- [Optimized PyTorch on GPU](./optimization/fp16) +- [Mac OS with PyTorch](./optimization/mps) +- [ONNX](./optimization/onnx) +- [OpenVINO](./optimization/open_vino) + +확산 모델을 미세조정하거나 학습시키려면, [**training section**](./training/overview)을 살펴보세요. + +마지막으로, 생성된 이미지를 공개적으로 배포할 때 신중을 기해 주세요 🤗. \ No newline at end of file diff --git a/diffusers/examples/README.md b/diffusers/examples/README.md new file mode 100644 index 0000000000000000000000000000000000000000..045e77473c09e637ed477e0654b1d84c4b1053ee --- /dev/null +++ b/diffusers/examples/README.md @@ -0,0 +1,70 @@ + + +# 🧨 Diffusers Examples + +Diffusers examples are a collection of scripts to demonstrate how to effectively use the `diffusers` library +for a variety of use cases involving training or fine-tuning. + +**Note**: If you are looking for **official** examples on how to use `diffusers` for inference, +please have a look at [src/diffusers/pipelines](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines) + +Our examples aspire to be **self-contained**, **easy-to-tweak**, **beginner-friendly** and for **one-purpose-only**. +More specifically, this means: + +- **Self-contained**: An example script shall only depend on "pip-install-able" Python packages that can be found in a `requirements.txt` file. Example scripts shall **not** depend on any local files. This means that one can simply download an example script, *e.g.* [train_unconditional.py](https://github.com/huggingface/diffusers/blob/main/examples/unconditional_image_generation/train_unconditional.py), install the required dependencies, *e.g.* [requirements.txt](https://github.com/huggingface/diffusers/blob/main/examples/unconditional_image_generation/requirements.txt) and execute the example script. +- **Easy-to-tweak**: While we strive to present as many use cases as possible, the example scripts are just that - examples. It is expected that they won't work out-of-the box on your specific problem and that you will be required to change a few lines of code to adapt them to your needs. To help you with that, most of the examples fully expose the preprocessing of the data and the training loop to allow you to tweak and edit them as required. +- **Beginner-friendly**: We do not aim for providing state-of-the-art training scripts for the newest models, but rather examples that can be used as a way to better understand diffusion models and how to use them with the `diffusers` library. We often purposefully leave out certain state-of-the-art methods if we consider them too complex for beginners. +- **One-purpose-only**: Examples should show one task and one task only. Even if a task is from a modeling +point of view very similar, *e.g.* image super-resolution and image modification tend to use the same model and training method, we want examples to showcase only one task to keep them as readable and easy-to-understand as possible. + +We provide **official** examples that cover the most popular tasks of diffusion models. +*Official* examples are **actively** maintained by the `diffusers` maintainers and we try to rigorously follow our example philosophy as defined above. +If you feel like another important example should exist, we are more than happy to welcome a [Feature Request](https://github.com/huggingface/diffusers/issues/new?assignees=&labels=&template=feature_request.md&title=) or directly a [Pull Request](https://github.com/huggingface/diffusers/compare) from you! + +Training examples show how to pretrain or fine-tune diffusion models for a variety of tasks. Currently we support: + +| Task | 🤗 Accelerate | 🤗 Datasets | Colab +|---|---|:---:|:---:| +| [**Unconditional Image Generation**](./unconditional_image_generation) | ✅ | ✅ | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [**Text-to-Image fine-tuning**](./text_to_image) | ✅ | ✅ | +| [**Textual Inversion**](./textual_inversion) | ✅ | - | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_textual_inversion_training.ipynb) +| [**Dreambooth**](./dreambooth) | ✅ | - | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_dreambooth_training.ipynb) +| [**Reinforcement Learning for Control**](https://github.com/huggingface/diffusers/blob/main/examples/rl/run_diffusers_locomotion.py) | - | - | coming soon. + +## Community + +In addition, we provide **community** examples, which are examples added and maintained by our community. +Community examples can consist of both *training* examples or *inference* pipelines. +For such examples, we are more lenient regarding the philosophy defined above and also cannot guarantee to provide maintenance for every issue. +Examples that are useful for the community, but are either not yet deemed popular or not yet following our above philosophy should go into the [community examples](https://github.com/huggingface/diffusers/tree/main/examples/community) folder. The community folder therefore includes training examples and inference pipelines. +**Note**: Community examples can be a [great first contribution](https://github.com/huggingface/diffusers/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) to show to the community how you like to use `diffusers` 🪄. + +## Research Projects + +We also provide **research_projects** examples that are maintained by the community as defined in the respective research project folders. These examples are useful and offer the extended capabilities which are complementary to the official examples. You may refer to [research_projects](https://github.com/huggingface/diffusers/tree/main/examples/research_projects) for details. + +## Important note + +To make sure you can successfully run the latest versions of the example scripts, you have to **install the library from source** and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` +Then cd in the example folder of your choice and run +```bash +pip install -r requirements.txt +``` diff --git a/diffusers/examples/community/README.md b/diffusers/examples/community/README.md new file mode 100644 index 0000000000000000000000000000000000000000..905f7b887b46f5be9734f4936613cd0dc6227797 --- /dev/null +++ b/diffusers/examples/community/README.md @@ -0,0 +1,991 @@ +# Community Examples + +> **For more information about community pipelines, please have a look at [this issue](https://github.com/huggingface/diffusers/issues/841).** + +**Community** examples consist of both inference and training examples that have been added by the community. +Please have a look at the following table to get an overview of all community examples. Click on the **Code Example** to get a copy-and-paste ready code example that you can try out. +If a community doesn't work as expected, please open an issue and ping the author on it. + +| Example | Description | Code Example | Colab | Author | +|:---------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------:| +| CLIP Guided Stable Diffusion | Doing CLIP guidance for text to image generation with Stable Diffusion | [CLIP Guided Stable Diffusion](#clip-guided-stable-diffusion) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/CLIP_Guided_Stable_diffusion_with_diffusers.ipynb) | [Suraj Patil](https://github.com/patil-suraj/) | +| One Step U-Net (Dummy) | Example showcasing of how to use Community Pipelines (see https://github.com/huggingface/diffusers/issues/841) | [One Step U-Net](#one-step-unet) | - | [Patrick von Platen](https://github.com/patrickvonplaten/) | +| Stable Diffusion Interpolation | Interpolate the latent space of Stable Diffusion between different prompts/seeds | [Stable Diffusion Interpolation](#stable-diffusion-interpolation) | - | [Nate Raw](https://github.com/nateraw/) | +| Stable Diffusion Mega | **One** Stable Diffusion Pipeline with all functionalities of [Text2Image](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py), [Image2Image](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py) and [Inpainting](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) | [Stable Diffusion Mega](#stable-diffusion-mega) | - | [Patrick von Platen](https://github.com/patrickvonplaten/) | +| Long Prompt Weighting Stable Diffusion | **One** Stable Diffusion Pipeline without tokens length limit, and support parsing weighting in prompt. | [Long Prompt Weighting Stable Diffusion](#long-prompt-weighting-stable-diffusion) | - | [SkyTNT](https://github.com/SkyTNT) | +| Speech to Image | Using automatic-speech-recognition to transcribe text and Stable Diffusion to generate images | [Speech to Image](#speech-to-image) | - | [Mikail Duzenli](https://github.com/MikailINTech) +| Wild Card Stable Diffusion | Stable Diffusion Pipeline that supports prompts that contain wildcard terms (indicated by surrounding double underscores), with values instantiated randomly from a corresponding txt file or a dictionary of possible values | [Wildcard Stable Diffusion](#wildcard-stable-diffusion) | - | [Shyam Sudhakaran](https://github.com/shyamsn97) | +| [Composable Stable Diffusion](https://energy-based-model.github.io/Compositional-Visual-Generation-with-Composable-Diffusion-Models/) | Stable Diffusion Pipeline that supports prompts that contain "|" in prompts (as an AND condition) and weights (separated by "|" as well) to positively / negatively weight prompts. | [Composable Stable Diffusion](#composable-stable-diffusion) | - | [Mark Rich](https://github.com/MarkRich) | +| Seed Resizing Stable Diffusion| Stable Diffusion Pipeline that supports resizing an image and retaining the concepts of the 512 by 512 generation. | [Seed Resizing](#seed-resizing) | - | [Mark Rich](https://github.com/MarkRich) | +| Imagic Stable Diffusion | Stable Diffusion Pipeline that enables writing a text prompt to edit an existing image| [Imagic Stable Diffusion](#imagic-stable-diffusion) | - | [Mark Rich](https://github.com/MarkRich) | +| Multilingual Stable Diffusion| Stable Diffusion Pipeline that supports prompts in 50 different languages. | [Multilingual Stable Diffusion](#multilingual-stable-diffusion-pipeline) | - | [Juan Carlos Piñeros](https://github.com/juancopi81) | +| Image to Image Inpainting Stable Diffusion | Stable Diffusion Pipeline that enables the overlaying of two images and subsequent inpainting| [Image to Image Inpainting Stable Diffusion](#image-to-image-inpainting-stable-diffusion) | - | [Alex McKinney](https://github.com/vvvm23) | +| Text Based Inpainting Stable Diffusion | Stable Diffusion Inpainting Pipeline that enables passing a text prompt to generate the mask for inpainting| [Text Based Inpainting Stable Diffusion](#image-to-image-inpainting-stable-diffusion) | - | [Dhruv Karan](https://github.com/unography) | +| Bit Diffusion | Diffusion on discrete data | [Bit Diffusion](#bit-diffusion) | - |[Stuti R.](https://github.com/kingstut) | +| K-Diffusion Stable Diffusion | Run Stable Diffusion with any of [K-Diffusion's samplers](https://github.com/crowsonkb/k-diffusion/blob/master/k_diffusion/sampling.py) | [Stable Diffusion with K Diffusion](#stable-diffusion-with-k-diffusion) | - | [Patrick von Platen](https://github.com/patrickvonplaten/) | +| Checkpoint Merger Pipeline | Diffusion Pipeline that enables merging of saved model checkpoints | [Checkpoint Merger Pipeline](#checkpoint-merger-pipeline) | - | [Naga Sai Abhinay Devarinti](https://github.com/Abhinay1997/) | +Stable Diffusion v1.1-1.4 Comparison | Run all 4 model checkpoints for Stable Diffusion and compare their results together | [Stable Diffusion Comparison](#stable-diffusion-comparisons) | - | [Suvaditya Mukherjee](https://github.com/suvadityamuk) | +MagicMix | Diffusion Pipeline for semantic mixing of an image and a text prompt | [MagicMix](#magic-mix) | - | [Partho Das](https://github.com/daspartho) | +| Stable UnCLIP | Diffusion Pipeline for combining prior model (generate clip image embedding from text, UnCLIPPipeline `"kakaobrain/karlo-v1-alpha"`) and decoder pipeline (decode clip image embedding to image, StableDiffusionImageVariationPipeline `"lambdalabs/sd-image-variations-diffusers"` ). | [Stable UnCLIP](#stable-unclip) | - |[Ray Wang](https://wrong.wang) | +| UnCLIP Text Interpolation Pipeline | Diffusion Pipeline that allows passing two prompts and produces images while interpolating between the text-embeddings of the two prompts | [UnCLIP Text Interpolation Pipeline](#unclip-text-interpolation-pipeline) | - | [Naga Sai Abhinay Devarinti](https://github.com/Abhinay1997/) | + + + + +To load a custom pipeline you just need to pass the `custom_pipeline` argument to `DiffusionPipeline`, as one of the files in `diffusers/examples/community`. Feel free to send a PR with your own pipelines, we will merge them quickly. +```py +pipe = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", custom_pipeline="filename_in_the_community_folder") +``` + +## Example usages + +### CLIP Guided Stable Diffusion + +CLIP guided stable diffusion can help to generate more realistic images +by guiding stable diffusion at every denoising step with an additional CLIP model. + +The following code requires roughly 12GB of GPU RAM. + +```python +from diffusers import DiffusionPipeline +from transformers import CLIPFeatureExtractor, CLIPModel +import torch + + +feature_extractor = CLIPFeatureExtractor.from_pretrained("laion/CLIP-ViT-B-32-laion2B-s34B-b79K") +clip_model = CLIPModel.from_pretrained("laion/CLIP-ViT-B-32-laion2B-s34B-b79K", torch_dtype=torch.float16) + + +guided_pipeline = DiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + custom_pipeline="clip_guided_stable_diffusion", + clip_model=clip_model, + feature_extractor=feature_extractor, + + torch_dtype=torch.float16, +) +guided_pipeline.enable_attention_slicing() +guided_pipeline = guided_pipeline.to("cuda") + +prompt = "fantasy book cover, full moon, fantasy forest landscape, golden vector elements, fantasy magic, dark light night, intricate, elegant, sharp focus, illustration, highly detailed, digital painting, concept art, matte, art by WLOP and Artgerm and Albert Bierstadt, masterpiece" + +generator = torch.Generator(device="cuda").manual_seed(0) +images = [] +for i in range(4): + image = guided_pipeline( + prompt, + num_inference_steps=50, + guidance_scale=7.5, + clip_guidance_scale=100, + num_cutouts=4, + use_cutouts=False, + generator=generator, + ).images[0] + images.append(image) + +# save images locally +for i, img in enumerate(images): + img.save(f"./clip_guided_sd/image_{i}.png") +``` + +The `images` list contains a list of PIL images that can be saved locally or displayed directly in a google colab. +Generated images tend to be of higher qualtiy than natively using stable diffusion. E.g. the above script generates the following images: + +![clip_guidance](https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/clip_guidance/merged_clip_guidance.jpg). + +### One Step Unet + +The dummy "one-step-unet" can be run as follows: + +```python +from diffusers import DiffusionPipeline + +pipe = DiffusionPipeline.from_pretrained("google/ddpm-cifar10-32", custom_pipeline="one_step_unet") +pipe() +``` + +**Note**: This community pipeline is not useful as a feature, but rather just serves as an example of how community pipelines can be added (see https://github.com/huggingface/diffusers/issues/841). + +### Stable Diffusion Interpolation + +The following code can be run on a GPU of at least 8GB VRAM and should take approximately 5 minutes. + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + revision='fp16', + torch_dtype=torch.float16, + safety_checker=None, # Very important for videos...lots of false positives while interpolating + custom_pipeline="interpolate_stable_diffusion", +).to('cuda') +pipe.enable_attention_slicing() + +frame_filepaths = pipe.walk( + prompts=['a dog', 'a cat', 'a horse'], + seeds=[42, 1337, 1234], + num_interpolation_steps=16, + output_dir='./dreams', + batch_size=4, + height=512, + width=512, + guidance_scale=8.5, + num_inference_steps=50, +) +``` + +The output of the `walk(...)` function returns a list of images saved under the folder as defined in `output_dir`. You can use these images to create videos of stable diffusion. + +> **Please have a look at https://github.com/nateraw/stable-diffusion-videos for more in-detail information on how to create videos using stable diffusion as well as more feature-complete functionality.** + +### Stable Diffusion Mega + +The Stable Diffusion Mega Pipeline lets you use the main use cases of the stable diffusion pipeline in a single class. + +```python +#!/usr/bin/env python3 +from diffusers import DiffusionPipeline +import PIL +import requests +from io import BytesIO +import torch + + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + +pipe = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", custom_pipeline="stable_diffusion_mega", torch_dtype=torch.float16, revision="fp16") +pipe.to("cuda") +pipe.enable_attention_slicing() + + +### Text-to-Image + +images = pipe.text2img("An astronaut riding a horse").images + +### Image-to-Image + +init_image = download_image("https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg") + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe.img2img(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +### Inpainting + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +prompt = "a cat sitting on a bench" +images = pipe.inpaint(prompt=prompt, image=init_image, mask_image=mask_image, strength=0.75).images +``` + +As shown above this one pipeline can run all both "text-to-image", "image-to-image", and "inpainting" in one pipeline. + +### Long Prompt Weighting Stable Diffusion +Features of this custom pipeline: +- Input a prompt without the 77 token length limit. +- Includes tx2img, img2img. and inpainting pipelines. +- Emphasize/weigh part of your prompt with parentheses as so: `a baby deer with (big eyes)` +- De-emphasize part of your prompt as so: `a [baby] deer with big eyes` +- Precisely weigh part of your prompt as so: `a baby deer with (big eyes:1.3)` + +Prompt weighting equivalents: +- `a baby deer with` == `(a baby deer with:1.0)` +- `(big eyes)` == `(big eyes:1.1)` +- `((big eyes))` == `(big eyes:1.21)` +- `[big eyes]` == `(big eyes:0.91)` + +You can run this custom pipeline as so: + +#### pytorch + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + 'hakurei/waifu-diffusion', + custom_pipeline="lpw_stable_diffusion", + + torch_dtype=torch.float16 +) +pipe=pipe.to("cuda") + +prompt = "best_quality (1girl:1.3) bow bride brown_hair closed_mouth frilled_bow frilled_hair_tubes frills (full_body:1.3) fox_ear hair_bow hair_tubes happy hood japanese_clothes kimono long_sleeves red_bow smile solo tabi uchikake white_kimono wide_sleeves cherry_blossoms" +neg_prompt = "lowres, bad_anatomy, error_body, error_hair, error_arm, error_hands, bad_hands, error_fingers, bad_fingers, missing_fingers, error_legs, bad_legs, multiple_legs, missing_legs, error_lighting, error_shadow, error_reflection, text, error, extra_digit, fewer_digits, cropped, worst_quality, low_quality, normal_quality, jpeg_artifacts, signature, watermark, username, blurry" + +pipe.text2img(prompt, negative_prompt=neg_prompt, width=512,height=512,max_embeddings_multiples=3).images[0] + +``` + +#### onnxruntime + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + 'CompVis/stable-diffusion-v1-4', + custom_pipeline="lpw_stable_diffusion_onnx", + revision="onnx", + provider="CUDAExecutionProvider" +) + +prompt = "a photo of an astronaut riding a horse on mars, best quality" +neg_prompt = "lowres, bad anatomy, error body, error hair, error arm, error hands, bad hands, error fingers, bad fingers, missing fingers, error legs, bad legs, multiple legs, missing legs, error lighting, error shadow, error reflection, text, error, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry" + +pipe.text2img(prompt,negative_prompt=neg_prompt, width=512, height=512, max_embeddings_multiples=3).images[0] + +``` + +if you see `Token indices sequence length is longer than the specified maximum sequence length for this model ( *** > 77 ) . Running this sequence through the model will result in indexing errors`. Do not worry, it is normal. + +### Speech to Image + +The following code can generate an image from an audio sample using pre-trained OpenAI whisper-small and Stable Diffusion. + +```Python +import torch + +import matplotlib.pyplot as plt +from datasets import load_dataset +from diffusers import DiffusionPipeline +from transformers import ( + WhisperForConditionalGeneration, + WhisperProcessor, +) + + +device = "cuda" if torch.cuda.is_available() else "cpu" + +ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation") + +audio_sample = ds[3] + +text = audio_sample["text"].lower() +speech_data = audio_sample["audio"]["array"] + +model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small").to(device) +processor = WhisperProcessor.from_pretrained("openai/whisper-small") + +diffuser_pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="speech_to_image_diffusion", + speech_model=model, + speech_processor=processor, + + torch_dtype=torch.float16, +) + +diffuser_pipeline.enable_attention_slicing() +diffuser_pipeline = diffuser_pipeline.to(device) + +output = diffuser_pipeline(speech_data) +plt.imshow(output.images[0]) +``` +This example produces the following image: + +![image](https://user-images.githubusercontent.com/45072645/196901736-77d9c6fc-63ee-4072-90b0-dc8b903d63e3.png) + +### Wildcard Stable Diffusion +Following the great examples from https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py and https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Custom-Scripts#wildcards, here's a minimal implementation that allows for users to add "wildcards", denoted by `__wildcard__` to prompts that are used as placeholders for randomly sampled values given by either a dictionary or a `.txt` file. For example: + +Say we have a prompt: + +``` +prompt = "__animal__ sitting on a __object__ wearing a __clothing__" +``` + +We can then define possible values to be sampled for `animal`, `object`, and `clothing`. These can either be from a `.txt` with the same name as the category. + +The possible values can also be defined / combined by using a dictionary like: `{"animal":["dog", "cat", mouse"]}`. + +The actual pipeline works just like `StableDiffusionPipeline`, except the `__call__` method takes in: + +`wildcard_files`: list of file paths for wild card replacement +`wildcard_option_dict`: dict with key as `wildcard` and values as a list of possible replacements +`num_prompt_samples`: number of prompts to sample, uniformly sampling wildcards + +A full example: + +create `animal.txt`, with contents like: + +``` +dog +cat +mouse +``` + +create `object.txt`, with contents like: + +``` +chair +sofa +bench +``` + +```python +from diffusers import DiffusionPipeline +import torch + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="wildcard_stable_diffusion", + + torch_dtype=torch.float16, +) +prompt = "__animal__ sitting on a __object__ wearing a __clothing__" +out = pipe( + prompt, + wildcard_option_dict={ + "clothing":["hat", "shirt", "scarf", "beret"] + }, + wildcard_files=["object.txt", "animal.txt"], + num_prompt_samples=1 +) +``` + +### Composable Stable diffusion + +[Composable Stable Diffusion](https://energy-based-model.github.io/Compositional-Visual-Generation-with-Composable-Diffusion-Models/) proposes conjunction and negation (negative prompts) operators for compositional generation with conditional diffusion models. + +```python +import torch as th +import numpy as np +import torchvision.utils as tvu + +from diffusers import DiffusionPipeline + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--prompt", type=str, default="mystical trees | A magical pond | dark", + help="use '|' as the delimiter to compose separate sentences.") +parser.add_argument("--steps", type=int, default=50) +parser.add_argument("--scale", type=float, default=7.5) +parser.add_argument("--weights", type=str, default="7.5 | 7.5 | -7.5") +parser.add_argument("--seed", type=int, default=2) +parser.add_argument("--model_path", type=str, default="CompVis/stable-diffusion-v1-4") +parser.add_argument("--num_images", type=int, default=1) +args = parser.parse_args() + +has_cuda = th.cuda.is_available() +device = th.device('cpu' if not has_cuda else 'cuda') + +prompt = args.prompt +scale = args.scale +steps = args.steps + +pipe = DiffusionPipeline.from_pretrained( + args.model_path, + custom_pipeline="composable_stable_diffusion", +).to(device) + +pipe.safety_checker = None + +images = [] +generator = th.Generator("cuda").manual_seed(args.seed) +for i in range(args.num_images): + image = pipe(prompt, guidance_scale=scale, num_inference_steps=steps, + weights=args.weights, generator=generator).images[0] + images.append(th.from_numpy(np.array(image)).permute(2, 0, 1) / 255.) +grid = tvu.make_grid(th.stack(images, dim=0), nrow=4, padding=0) +tvu.save_image(grid, f'{prompt}_{args.weights}' + '.png') + +``` + +### Imagic Stable Diffusion +Allows you to edit an image using stable diffusion. + +```python +import requests +from PIL import Image +from io import BytesIO +import torch +import os +from diffusers import DiffusionPipeline, DDIMScheduler +has_cuda = torch.cuda.is_available() +device = torch.device('cpu' if not has_cuda else 'cuda') +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + safety_checker=None, + use_auth_token=True, + custom_pipeline="imagic_stable_diffusion", + scheduler = DDIMScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", clip_sample=False, set_alpha_to_one=False) +).to(device) +generator = torch.Generator("cuda").manual_seed(0) +seed = 0 +prompt = "A photo of Barack Obama smiling with a big grin" +url = 'https://www.dropbox.com/s/6tlwzr73jd1r9yk/obama.png?dl=1' +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((512, 512)) +res = pipe.train( + prompt, + image=init_image, + generator=generator) +res = pipe(alpha=1, guidance_scale=7.5, num_inference_steps=50) +os.makedirs("imagic", exist_ok=True) +image = res.images[0] +image.save('./imagic/imagic_image_alpha_1.png') +res = pipe(alpha=1.5, guidance_scale=7.5, num_inference_steps=50) +image = res.images[0] +image.save('./imagic/imagic_image_alpha_1_5.png') +res = pipe(alpha=2, guidance_scale=7.5, num_inference_steps=50) +image = res.images[0] +image.save('./imagic/imagic_image_alpha_2.png') +``` + +### Seed Resizing +Test seed resizing. Originally generate an image in 512 by 512, then generate image with same seed at 512 by 592 using seed resizing. Finally, generate 512 by 592 using original stable diffusion pipeline. + +```python +import torch as th +import numpy as np +from diffusers import DiffusionPipeline + +has_cuda = th.cuda.is_available() +device = th.device('cpu' if not has_cuda else 'cuda') + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + use_auth_token=True, + custom_pipeline="seed_resize_stable_diffusion" +).to(device) + +def dummy(images, **kwargs): + return images, False + +pipe.safety_checker = dummy + + +images = [] +th.manual_seed(0) +generator = th.Generator("cuda").manual_seed(0) + +seed = 0 +prompt = "A painting of a futuristic cop" + +width = 512 +height = 512 + +res = pipe( + prompt, + guidance_scale=7.5, + num_inference_steps=50, + height=height, + width=width, + generator=generator) +image = res.images[0] +image.save('./seed_resize/seed_resize_{w}_{h}_image.png'.format(w=width, h=height)) + + +th.manual_seed(0) +generator = th.Generator("cuda").manual_seed(0) + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + use_auth_token=True, + custom_pipeline="/home/mark/open_source/diffusers/examples/community/" +).to(device) + +width = 512 +height = 592 + +res = pipe( + prompt, + guidance_scale=7.5, + num_inference_steps=50, + height=height, + width=width, + generator=generator) +image = res.images[0] +image.save('./seed_resize/seed_resize_{w}_{h}_image.png'.format(w=width, h=height)) + +pipe_compare = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + use_auth_token=True, + custom_pipeline="/home/mark/open_source/diffusers/examples/community/" +).to(device) + +res = pipe_compare( + prompt, + guidance_scale=7.5, + num_inference_steps=50, + height=height, + width=width, + generator=generator +) + +image = res.images[0] +image.save('./seed_resize/seed_resize_{w}_{h}_image_compare.png'.format(w=width, h=height)) +``` + +### Multilingual Stable Diffusion Pipeline + +The following code can generate an images from texts in different languages using the pre-trained [mBART-50 many-to-one multilingual machine translation model](https://huggingface.co/facebook/mbart-large-50-many-to-one-mmt) and Stable Diffusion. + +```python +from PIL import Image + +import torch + +from diffusers import DiffusionPipeline +from transformers import ( + pipeline, + MBart50TokenizerFast, + MBartForConditionalGeneration, +) +device = "cuda" if torch.cuda.is_available() else "cpu" +device_dict = {"cuda": 0, "cpu": -1} + +# helper function taken from: https://huggingface.co/blog/stable_diffusion +def image_grid(imgs, rows, cols): + assert len(imgs) == rows*cols + + w, h = imgs[0].size + grid = Image.new('RGB', size=(cols*w, rows*h)) + grid_w, grid_h = grid.size + + for i, img in enumerate(imgs): + grid.paste(img, box=(i%cols*w, i//cols*h)) + return grid + +# Add language detection pipeline +language_detection_model_ckpt = "papluca/xlm-roberta-base-language-detection" +language_detection_pipeline = pipeline("text-classification", + model=language_detection_model_ckpt, + device=device_dict[device]) + +# Add model for language translation +trans_tokenizer = MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-one-mmt") +trans_model = MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-one-mmt").to(device) + +diffuser_pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="multilingual_stable_diffusion", + detection_pipeline=language_detection_pipeline, + translation_model=trans_model, + translation_tokenizer=trans_tokenizer, + + torch_dtype=torch.float16, +) + +diffuser_pipeline.enable_attention_slicing() +diffuser_pipeline = diffuser_pipeline.to(device) + +prompt = ["a photograph of an astronaut riding a horse", + "Una casa en la playa", + "Ein Hund, der Orange isst", + "Un restaurant parisien"] + +output = diffuser_pipeline(prompt) + +images = output.images + +grid = image_grid(images, rows=2, cols=2) +``` + +This example produces the following images: +![image](https://user-images.githubusercontent.com/4313860/198328706-295824a4-9856-4ce5-8e66-278ceb42fd29.png) + +### Image to Image Inpainting Stable Diffusion + +Similar to the standard stable diffusion inpainting example, except with the addition of an `inner_image` argument. + +`image`, `inner_image`, and `mask` should have the same dimensions. `inner_image` should have an alpha (transparency) channel. + +The aim is to overlay two images, then mask out the boundary between `image` and `inner_image` to allow stable diffusion to make the connection more seamless. +For example, this could be used to place a logo on a shirt and make it blend seamlessly. + +```python +import PIL +import torch + +from diffusers import DiffusionPipeline + +image_path = "./path-to-image.png" +inner_image_path = "./path-to-inner-image.png" +mask_path = "./path-to-mask.png" + +init_image = PIL.Image.open(image_path).convert("RGB").resize((512, 512)) +inner_image = PIL.Image.open(inner_image_path).convert("RGBA").resize((512, 512)) +mask_image = PIL.Image.open(mask_path).convert("RGB").resize((512, 512)) + +pipe = DiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + custom_pipeline="img2img_inpainting", + + torch_dtype=torch.float16 +) +pipe = pipe.to("cuda") + +prompt = "Your prompt here!" +image = pipe(prompt=prompt, image=init_image, inner_image=inner_image, mask_image=mask_image).images[0] +``` + +![2 by 2 grid demonstrating image to image inpainting.](https://user-images.githubusercontent.com/44398246/203506577-ec303be4-887e-4ebd-a773-c83fcb3dd01a.png) + +### Text Based Inpainting Stable Diffusion + +Use a text prompt to generate the mask for the area to be inpainted. +Currently uses the CLIPSeg model for mask generation, then calls the standard Stable Diffusion Inpainting pipeline to perform the inpainting. + +```python +from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation +from diffusers import DiffusionPipeline + +from PIL import Image +import requests + +processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined") +model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined") + +pipe = DiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + custom_pipeline="text_inpainting", + segmentation_model=model, + segmentation_processor=processor +) +pipe = pipe.to("cuda") + + +url = "https://github.com/timojl/clipseg/blob/master/example_image.jpg?raw=true" +image = Image.open(requests.get(url, stream=True).raw).resize((512, 512)) +text = "a glass" # will mask out this text +prompt = "a cup" # the masked out region will be replaced with this + +image = pipe(image=image, text=text, prompt=prompt).images[0] +``` + +### Bit Diffusion +Based https://arxiv.org/abs/2208.04202, this is used for diffusion on discrete data - eg, discreate image data, DNA sequence data. An unconditional discreate image can be generated like this: + +```python +from diffusers import DiffusionPipeline +pipe = DiffusionPipeline.from_pretrained("google/ddpm-cifar10-32", custom_pipeline="bit_diffusion") +image = pipe().images[0] + +``` + +### Stable Diffusion with K Diffusion + +Make sure you have @crowsonkb's https://github.com/crowsonkb/k-diffusion installed: + +``` +pip install k-diffusion +``` + +You can use the community pipeline as follows: + +```python +from diffusers import DiffusionPipeline + +pipe = DiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", custom_pipeline="sd_text2img_k_diffusion") +pipe = pipe.to("cuda") + +prompt = "an astronaut riding a horse on mars" +pipe.set_scheduler("sample_heun") +generator = torch.Generator(device="cuda").manual_seed(seed) +image = pipe(prompt, generator=generator, num_inference_steps=20).images[0] + +image.save("./astronaut_heun_k_diffusion.png") +``` + +To make sure that K Diffusion and `diffusers` yield the same results: + +**Diffusers**: +```python +from diffusers import DiffusionPipeline, EulerDiscreteScheduler + +seed = 33 + +pipe = DiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4") +pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +generator = torch.Generator(device="cuda").manual_seed(seed) +image = pipe(prompt, generator=generator, num_inference_steps=50).images[0] +``` + +![diffusers_euler](https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/k_diffusion/astronaut_euler.png) + +**K Diffusion**: +```python +from diffusers import DiffusionPipeline, EulerDiscreteScheduler + +seed = 33 + +pipe = DiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", custom_pipeline="sd_text2img_k_diffusion") +pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config) +pipe = pipe.to("cuda") + +pipe.set_scheduler("sample_euler") +generator = torch.Generator(device="cuda").manual_seed(seed) +image = pipe(prompt, generator=generator, num_inference_steps=50).images[0] +``` + +![diffusers_euler](https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/k_diffusion/astronaut_euler_k_diffusion.png) + +### Checkpoint Merger Pipeline +Based on the AUTOMATIC1111/webui for checkpoint merging. This is a custom pipeline that merges upto 3 pretrained model checkpoints as long as they are in the HuggingFace model_index.json format. + +The checkpoint merging is currently memory intensive as it modifies the weights of a DiffusionPipeline object in place. Expect atleast 13GB RAM Usage on Kaggle GPU kernels and +on colab you might run out of the 12GB memory even while merging two checkpoints. + +Usage:- +```python +from diffusers import DiffusionPipeline + +#Return a CheckpointMergerPipeline class that allows you to merge checkpoints. +#The checkpoint passed here is ignored. But still pass one of the checkpoints you plan to +#merge for convenience +pipe = DiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", custom_pipeline="checkpoint_merger") + +#There are multiple possible scenarios: +#The pipeline with the merged checkpoints is returned in all the scenarios + +#Compatible checkpoints a.k.a matched model_index.json files. Ignores the meta attributes in model_index.json during comparision.( attrs with _ as prefix ) +merged_pipe = pipe.merge(["CompVis/stable-diffusion-v1-4","CompVis/stable-diffusion-v1-2"], interp = "sigmoid", alpha = 0.4) + +#Incompatible checkpoints in model_index.json but merge might be possible. Use force = True to ignore model_index.json compatibility +merged_pipe_1 = pipe.merge(["CompVis/stable-diffusion-v1-4","hakurei/waifu-diffusion"], force = True, interp = "sigmoid", alpha = 0.4) + +#Three checkpoint merging. Only "add_difference" method actually works on all three checkpoints. Using any other options will ignore the 3rd checkpoint. +merged_pipe_2 = pipe.merge(["CompVis/stable-diffusion-v1-4","hakurei/waifu-diffusion","prompthero/openjourney"], force = True, interp = "add_difference", alpha = 0.4) + +prompt = "An astronaut riding a horse on Mars" + +image = merged_pipe(prompt).images[0] + +``` +Some examples along with the merge details: + +1. "CompVis/stable-diffusion-v1-4" + "hakurei/waifu-diffusion" ; Sigmoid interpolation; alpha = 0.8 + +![Stable plus Waifu Sigmoid 0.8](https://huggingface.co/datasets/NagaSaiAbhinay/CheckpointMergerSamples/resolve/main/stability_v1_4_waifu_sig_0.8.png) + +2. "hakurei/waifu-diffusion" + "prompthero/openjourney" ; Inverse Sigmoid interpolation; alpha = 0.8 + +![Stable plus Waifu Sigmoid 0.8](https://huggingface.co/datasets/NagaSaiAbhinay/CheckpointMergerSamples/resolve/main/waifu_openjourney_inv_sig_0.8.png) + + +3. "CompVis/stable-diffusion-v1-4" + "hakurei/waifu-diffusion" + "prompthero/openjourney"; Add Difference interpolation; alpha = 0.5 + +![Stable plus Waifu plus openjourney add_diff 0.5](https://huggingface.co/datasets/NagaSaiAbhinay/CheckpointMergerSamples/resolve/main/stable_waifu_openjourney_add_diff_0.5.png) + + +### Stable Diffusion Comparisons + +This Community Pipeline enables the comparison between the 4 checkpoints that exist for Stable Diffusion. They can be found through the following links: +1. [Stable Diffusion v1.1](https://huggingface.co/CompVis/stable-diffusion-v1-1) +2. [Stable Diffusion v1.2](https://huggingface.co/CompVis/stable-diffusion-v1-2) +3. [Stable Diffusion v1.3](https://huggingface.co/CompVis/stable-diffusion-v1-3) +4. [Stable Diffusion v1.4](https://huggingface.co/CompVis/stable-diffusion-v1-4) + +```python +from diffusers import DiffusionPipeline +import matplotlib.pyplot as plt + +pipe = DiffusionPipeline.from_pretrained('CompVis/stable-diffusion-v1-4', custom_pipeline='suvadityamuk/StableDiffusionComparison') +pipe.enable_attention_slicing() +pipe = pipe.to('cuda') +prompt = "an astronaut riding a horse on mars" +output = pipe(prompt) + +plt.subplots(2,2,1) +plt.imshow(output.images[0]) +plt.title('Stable Diffusion v1.1') +plt.axis('off') +plt.subplots(2,2,2) +plt.imshow(output.images[1]) +plt.title('Stable Diffusion v1.2') +plt.axis('off') +plt.subplots(2,2,3) +plt.imshow(output.images[2]) +plt.title('Stable Diffusion v1.3') +plt.axis('off') +plt.subplots(2,2,4) +plt.imshow(output.images[3]) +plt.title('Stable Diffusion v1.4') +plt.axis('off') + +plt.show() +``` + +As a result, you can look at a grid of all 4 generated images being shown together, that captures a difference the advancement of the training between the 4 checkpoints. + +### Magic Mix + +Implementation of the [MagicMix: Semantic Mixing with Diffusion Models](https://arxiv.org/abs/2210.16056) paper. This is a Diffusion Pipeline for semantic mixing of an image and a text prompt to create a new concept while preserving the spatial layout and geometry of the subject in the image. The pipeline takes an image that provides the layout semantics and a prompt that provides the content semantics for the mixing process. + +There are 3 parameters for the method- +- `mix_factor`: It is the interpolation constant used in the layout generation phase. The greater the value of `mix_factor`, the greater the influence of the prompt on the layout generation process. +- `kmax` and `kmin`: These determine the range for the layout and content generation process. A higher value of kmax results in loss of more information about the layout of the original image and a higher value of kmin results in more steps for content generation process. + +Here is an example usage- + +```python +from diffusers import DiffusionPipeline, DDIMScheduler +from PIL import Image + +pipe = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="magic_mix", + scheduler = DDIMScheduler.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="scheduler"), +).to('cuda') + +img = Image.open('phone.jpg') +mix_img = pipe( + img, + prompt = 'bed', + kmin = 0.3, + kmax = 0.5, + mix_factor = 0.5, + ) +mix_img.save('phone_bed_mix.jpg') +``` +The `mix_img` is a PIL image that can be saved locally or displayed directly in a google colab. Generated image is a mix of the layout semantics of the given image and the content semantics of the prompt. + +E.g. the above script generates the following image: + +`phone.jpg` + +![206903102-34e79b9f-9ed2-4fac-bb38-82871343c655](https://user-images.githubusercontent.com/59410571/209578593-141467c7-d831-4792-8b9a-b17dc5e47816.jpg) + +`phone_bed_mix.jpg` + +![206903104-913a671d-ef53-4ae4-919d-64c3059c8f67](https://user-images.githubusercontent.com/59410571/209578602-70f323fa-05b7-4dd6-b055-e40683e37914.jpg) + +For more example generations check out this [demo notebook](https://github.com/daspartho/MagicMix/blob/main/demo.ipynb). + + +### Stable UnCLIP + +UnCLIPPipeline("kakaobrain/karlo-v1-alpha") provide a prior model that can generate clip image embedding from text. +StableDiffusionImageVariationPipeline("lambdalabs/sd-image-variations-diffusers") provide a decoder model than can generate images from clip image embedding. + +```python +import torch +from diffusers import DiffusionPipeline + +device = torch.device("cpu" if not torch.cuda.is_available() else "cuda") + +pipeline = DiffusionPipeline.from_pretrained( + "kakaobrain/karlo-v1-alpha", + torch_dtype=torch.float16, + custom_pipeline="stable_unclip", + decoder_pipe_kwargs=dict( + image_encoder=None, + ), +) +pipeline.to(device) + +prompt = "a shiba inu wearing a beret and black turtleneck" +random_generator = torch.Generator(device=device).manual_seed(1000) +output = pipeline( + prompt=prompt, + width=512, + height=512, + generator=random_generator, + prior_guidance_scale=4, + prior_num_inference_steps=25, + decoder_guidance_scale=8, + decoder_num_inference_steps=50, +) + +image = output.images[0] +image.save("./shiba-inu.jpg") + +# debug + +# `pipeline.decoder_pipe` is a regular StableDiffusionImageVariationPipeline instance. +# It is used to convert clip image embedding to latents, then fed into VAE decoder. +print(pipeline.decoder_pipe.__class__) +# + +# this pipeline only use prior module in "kakaobrain/karlo-v1-alpha" +# It is used to convert clip text embedding to clip image embedding. +print(pipeline) +# StableUnCLIPPipeline { +# "_class_name": "StableUnCLIPPipeline", +# "_diffusers_version": "0.12.0.dev0", +# "prior": [ +# "diffusers", +# "PriorTransformer" +# ], +# "prior_scheduler": [ +# "diffusers", +# "UnCLIPScheduler" +# ], +# "text_encoder": [ +# "transformers", +# "CLIPTextModelWithProjection" +# ], +# "tokenizer": [ +# "transformers", +# "CLIPTokenizer" +# ] +# } + +# pipeline.prior_scheduler is the scheduler used for prior in UnCLIP. +print(pipeline.prior_scheduler) +# UnCLIPScheduler { +# "_class_name": "UnCLIPScheduler", +# "_diffusers_version": "0.12.0.dev0", +# "clip_sample": true, +# "clip_sample_range": 5.0, +# "num_train_timesteps": 1000, +# "prediction_type": "sample", +# "variance_type": "fixed_small_log" +# } +``` + + +`shiba-inu.jpg` + + +![shiba-inu](https://user-images.githubusercontent.com/16448529/209185639-6e5ec794-ce9d-4883-aa29-bd6852a2abad.jpg) + +### UnCLIP Text Interpolation Pipeline + +This Diffusion Pipeline takes two prompts and interpolates between the two input prompts using spherical interpolation ( slerp ). The input prompts are converted to text embeddings by the pipeline's text_encoder and the interpolation is done on the resulting text_embeddings over the number of steps specified. Defaults to 5 steps. + +```python +import torch +from diffusers import DiffusionPipeline + +device = torch.device("cpu" if not torch.cuda.is_available() else "cuda") + +pipe = DiffusionPipeline.from_pretrained( + "kakaobrain/karlo-v1-alpha", + torch_dtype=torch.float16, + custom_pipeline="unclip_text_interpolation" +) +pipe.to(device) + +start_prompt = "A photograph of an adult lion" +end_prompt = "A photograph of a lion cub" +#For best results keep the prompts close in length to each other. Of course, feel free to try out with differing lengths. +generator = torch.Generator(device=device).manual_seed(42) + +output = pipe(start_prompt, end_prompt, steps = 6, generator = generator, enable_sequential_cpu_offload=False) + +for i,image in enumerate(output.images): + img.save('result%s.jpg' % i) +``` + +The resulting images in order:- + +![result_0](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_0.png) +![result_1](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_1.png) +![result_2](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_2.png) +![result_3](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_3.png) +![result_4](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_4.png) +![result_5](https://huggingface.co/datasets/NagaSaiAbhinay/UnCLIPTextInterpolationSamples/resolve/main/lion_to_cub_5.png) diff --git a/diffusers/examples/community/bit_diffusion.py b/diffusers/examples/community/bit_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..c778b6cc6c71ed1a38a0da54c6e65c18ab04a6a1 --- /dev/null +++ b/diffusers/examples/community/bit_diffusion.py @@ -0,0 +1,264 @@ +from typing import Optional, Tuple, Union + +import torch +from einops import rearrange, reduce + +from diffusers import DDIMScheduler, DDPMScheduler, DiffusionPipeline, ImagePipelineOutput, UNet2DConditionModel +from diffusers.schedulers.scheduling_ddim import DDIMSchedulerOutput +from diffusers.schedulers.scheduling_ddpm import DDPMSchedulerOutput + + +BITS = 8 + + +# convert to bit representations and back taken from https://github.com/lucidrains/bit-diffusion/blob/main/bit_diffusion/bit_diffusion.py +def decimal_to_bits(x, bits=BITS): + """expects image tensor ranging from 0 to 1, outputs bit tensor ranging from -1 to 1""" + device = x.device + + x = (x * 255).int().clamp(0, 255) + + mask = 2 ** torch.arange(bits - 1, -1, -1, device=device) + mask = rearrange(mask, "d -> d 1 1") + x = rearrange(x, "b c h w -> b c 1 h w") + + bits = ((x & mask) != 0).float() + bits = rearrange(bits, "b c d h w -> b (c d) h w") + bits = bits * 2 - 1 + return bits + + +def bits_to_decimal(x, bits=BITS): + """expects bits from -1 to 1, outputs image tensor from 0 to 1""" + device = x.device + + x = (x > 0).int() + mask = 2 ** torch.arange(bits - 1, -1, -1, device=device, dtype=torch.int32) + + mask = rearrange(mask, "d -> d 1 1") + x = rearrange(x, "b (c d) h w -> b c d h w", d=8) + dec = reduce(x * mask, "b c d h w -> b c h w", "sum") + return (dec / 255).clamp(0.0, 1.0) + + +# modified scheduler step functions for clamping the predicted x_0 between -bit_scale and +bit_scale +def ddim_bit_scheduler_step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + eta: float = 0.0, + use_clipped_model_output: bool = True, + generator=None, + return_dict: bool = True, +) -> Union[DDIMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + eta (`float`): weight of noise for added noise in diffusion step. + use_clipped_model_output (`bool`): TODO + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than DDIMSchedulerOutput class + Returns: + [`~schedulers.scheduling_utils.DDIMSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.DDIMSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + # See formulas (12) and (16) of DDIM paper https://arxiv.org/pdf/2010.02502.pdf + # Ideally, read DDIM paper in-detail understanding + + # Notation ( -> + # - pred_noise_t -> e_theta(x_t, t) + # - pred_original_sample -> f_theta(x_t, t) or x_0 + # - std_dev_t -> sigma_t + # - eta -> η + # - pred_sample_direction -> "direction pointing to x_t" + # - pred_prev_sample -> "x_t-1" + + # 1. get previous step value (=t-1) + prev_timestep = timestep - self.config.num_train_timesteps // self.num_inference_steps + + # 2. compute alphas, betas + alpha_prod_t = self.alphas_cumprod[timestep] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + + beta_prod_t = 1 - alpha_prod_t + + # 3. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + + # 4. Clip "predicted x_0" + scale = self.bit_scale + if self.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -scale, scale) + + # 5. compute variance: "sigma_t(η)" -> see formula (16) + # σ_t = sqrt((1 − α_t−1)/(1 − α_t)) * sqrt(1 − α_t/α_t−1) + variance = self._get_variance(timestep, prev_timestep) + std_dev_t = eta * variance ** (0.5) + + if use_clipped_model_output: + # the model_output is always re-derived from the clipped x_0 in Glide + model_output = (sample - alpha_prod_t ** (0.5) * pred_original_sample) / beta_prod_t ** (0.5) + + # 6. compute "direction pointing to x_t" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * model_output + + # 7. compute x_t without "random noise" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction + + if eta > 0: + # randn_like does not support generator https://github.com/pytorch/pytorch/issues/27072 + device = model_output.device if torch.is_tensor(model_output) else "cpu" + noise = torch.randn(model_output.shape, dtype=model_output.dtype, generator=generator).to(device) + variance = self._get_variance(timestep, prev_timestep) ** (0.5) * eta * noise + + prev_sample = prev_sample + variance + + if not return_dict: + return (prev_sample,) + + return DDIMSchedulerOutput(prev_sample=prev_sample, pred_original_sample=pred_original_sample) + + +def ddpm_bit_scheduler_step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + prediction_type="epsilon", + generator=None, + return_dict: bool = True, +) -> Union[DDPMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the samples (`sample`). + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than DDPMSchedulerOutput class + Returns: + [`~schedulers.scheduling_utils.DDPMSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.DDPMSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + t = timestep + + if model_output.shape[1] == sample.shape[1] * 2 and self.variance_type in ["learned", "learned_range"]: + model_output, predicted_variance = torch.split(model_output, sample.shape[1], dim=1) + else: + predicted_variance = None + + # 1. compute alphas, betas + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[t - 1] if t > 0 else self.one + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + # 2. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf + if prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif prediction_type == "sample": + pred_original_sample = model_output + else: + raise ValueError(f"Unsupported prediction_type {prediction_type}.") + + # 3. Clip "predicted x_0" + scale = self.bit_scale + if self.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -scale, scale) + + # 4. Compute coefficients for pred_original_sample x_0 and current sample x_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * self.betas[t]) / beta_prod_t + current_sample_coeff = self.alphas[t] ** (0.5) * beta_prod_t_prev / beta_prod_t + + # 5. Compute predicted previous sample µ_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_prev_sample = pred_original_sample_coeff * pred_original_sample + current_sample_coeff * sample + + # 6. Add noise + variance = 0 + if t > 0: + noise = torch.randn( + model_output.size(), dtype=model_output.dtype, layout=model_output.layout, generator=generator + ).to(model_output.device) + variance = (self._get_variance(t, predicted_variance=predicted_variance) ** 0.5) * noise + + pred_prev_sample = pred_prev_sample + variance + + if not return_dict: + return (pred_prev_sample,) + + return DDPMSchedulerOutput(prev_sample=pred_prev_sample, pred_original_sample=pred_original_sample) + + +class BitDiffusion(DiffusionPipeline): + def __init__( + self, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, DDPMScheduler], + bit_scale: Optional[float] = 1.0, + ): + super().__init__() + self.bit_scale = bit_scale + self.scheduler.step = ( + ddim_bit_scheduler_step if isinstance(scheduler, DDIMScheduler) else ddpm_bit_scheduler_step + ) + + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + height: Optional[int] = 256, + width: Optional[int] = 256, + num_inference_steps: Optional[int] = 50, + generator: Optional[torch.Generator] = None, + batch_size: Optional[int] = 1, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[Tuple, ImagePipelineOutput]: + latents = torch.randn( + (batch_size, self.unet.in_channels, height, width), + generator=generator, + ) + latents = decimal_to_bits(latents) * self.bit_scale + latents = latents.to(self.device) + + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # predict the noise residual + noise_pred = self.unet(latents, t).sample + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents).prev_sample + + image = bits_to_decimal(latents) + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/examples/community/checkpoint_merger.py b/diffusers/examples/community/checkpoint_merger.py new file mode 100644 index 0000000000000000000000000000000000000000..576c2cdb4d160772a4831b0907e2427a975ab245 --- /dev/null +++ b/diffusers/examples/community/checkpoint_merger.py @@ -0,0 +1,286 @@ +import glob +import os +from typing import Dict, List, Union + +import torch + +from diffusers.utils import is_safetensors_available + + +if is_safetensors_available(): + import safetensors.torch + +from huggingface_hub import snapshot_download + +from diffusers import DiffusionPipeline, __version__ +from diffusers.schedulers.scheduling_utils import SCHEDULER_CONFIG_NAME +from diffusers.utils import CONFIG_NAME, DIFFUSERS_CACHE, ONNX_WEIGHTS_NAME, WEIGHTS_NAME + + +class CheckpointMergerPipeline(DiffusionPipeline): + """ + A class that that supports merging diffusion models based on the discussion here: + https://github.com/huggingface/diffusers/issues/877 + + Example usage:- + + pipe = DiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", custom_pipeline="checkpoint_merger.py") + + merged_pipe = pipe.merge(["CompVis/stable-diffusion-v1-4","prompthero/openjourney"], interp = 'inv_sigmoid', alpha = 0.8, force = True) + + merged_pipe.to('cuda') + + prompt = "An astronaut riding a unicycle on Mars" + + results = merged_pipe(prompt) + + ## For more details, see the docstring for the merge method. + + """ + + def __init__(self): + self.register_to_config() + super().__init__() + + def _compare_model_configs(self, dict0, dict1): + if dict0 == dict1: + return True + else: + config0, meta_keys0 = self._remove_meta_keys(dict0) + config1, meta_keys1 = self._remove_meta_keys(dict1) + if config0 == config1: + print(f"Warning !: Mismatch in keys {meta_keys0} and {meta_keys1}.") + return True + return False + + def _remove_meta_keys(self, config_dict: Dict): + meta_keys = [] + temp_dict = config_dict.copy() + for key in config_dict.keys(): + if key.startswith("_"): + temp_dict.pop(key) + meta_keys.append(key) + return (temp_dict, meta_keys) + + @torch.no_grad() + def merge(self, pretrained_model_name_or_path_list: List[Union[str, os.PathLike]], **kwargs): + """ + Returns a new pipeline object of the class 'DiffusionPipeline' with the merged checkpoints(weights) of the models passed + in the argument 'pretrained_model_name_or_path_list' as a list. + + Parameters: + ----------- + pretrained_model_name_or_path_list : A list of valid pretrained model names in the HuggingFace hub or paths to locally stored models in the HuggingFace format. + + **kwargs: + Supports all the default DiffusionPipeline.get_config_dict kwargs viz.. + + cache_dir, resume_download, force_download, proxies, local_files_only, use_auth_token, revision, torch_dtype, device_map. + + alpha - The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha + would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 + + interp - The interpolation method to use for the merging. Supports "sigmoid", "inv_sigmoid", "add_difference" and None. + Passing None uses the default interpolation which is weighted sum interpolation. For merging three checkpoints, only "add_difference" is supported. + + force - Whether to ignore mismatch in model_config.json for the current models. Defaults to False. + + """ + # Default kwargs from DiffusionPipeline + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + resume_download = kwargs.pop("resume_download", False) + force_download = kwargs.pop("force_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", False) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + torch_dtype = kwargs.pop("torch_dtype", None) + device_map = kwargs.pop("device_map", None) + + alpha = kwargs.pop("alpha", 0.5) + interp = kwargs.pop("interp", None) + + print("Received list", pretrained_model_name_or_path_list) + print(f"Combining with alpha={alpha}, interpolation mode={interp}") + + checkpoint_count = len(pretrained_model_name_or_path_list) + # Ignore result from model_index_json comparision of the two checkpoints + force = kwargs.pop("force", False) + + # If less than 2 checkpoints, nothing to merge. If more than 3, not supported for now. + if checkpoint_count > 3 or checkpoint_count < 2: + raise ValueError( + "Received incorrect number of checkpoints to merge. Ensure that either 2 or 3 checkpoints are being" + " passed." + ) + + print("Received the right number of checkpoints") + # chkpt0, chkpt1 = pretrained_model_name_or_path_list[0:2] + # chkpt2 = pretrained_model_name_or_path_list[2] if checkpoint_count == 3 else None + + # Validate that the checkpoints can be merged + # Step 1: Load the model config and compare the checkpoints. We'll compare the model_index.json first while ignoring the keys starting with '_' + config_dicts = [] + for pretrained_model_name_or_path in pretrained_model_name_or_path_list: + config_dict = DiffusionPipeline.load_config( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + force_download=force_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + ) + config_dicts.append(config_dict) + + comparison_result = True + for idx in range(1, len(config_dicts)): + comparison_result &= self._compare_model_configs(config_dicts[idx - 1], config_dicts[idx]) + if not force and comparison_result is False: + raise ValueError("Incompatible checkpoints. Please check model_index.json for the models.") + print(config_dicts[0], config_dicts[1]) + print("Compatible model_index.json files found") + # Step 2: Basic Validation has succeeded. Let's download the models and save them into our local files. + cached_folders = [] + for pretrained_model_name_or_path, config_dict in zip(pretrained_model_name_or_path_list, config_dicts): + folder_names = [k for k in config_dict.keys() if not k.startswith("_")] + allow_patterns = [os.path.join(k, "*") for k in folder_names] + allow_patterns += [ + WEIGHTS_NAME, + SCHEDULER_CONFIG_NAME, + CONFIG_NAME, + ONNX_WEIGHTS_NAME, + DiffusionPipeline.config_name, + ] + requested_pipeline_class = config_dict.get("_class_name") + user_agent = {"diffusers": __version__, "pipeline_class": requested_pipeline_class} + + cached_folder = ( + pretrained_model_name_or_path + if os.path.isdir(pretrained_model_name_or_path) + else snapshot_download( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + allow_patterns=allow_patterns, + user_agent=user_agent, + ) + ) + print("Cached Folder", cached_folder) + cached_folders.append(cached_folder) + + # Step 3:- + # Load the first checkpoint as a diffusion pipeline and modify its module state_dict in place + final_pipe = DiffusionPipeline.from_pretrained( + cached_folders[0], torch_dtype=torch_dtype, device_map=device_map + ) + final_pipe.to(self.device) + + checkpoint_path_2 = None + if len(cached_folders) > 2: + checkpoint_path_2 = os.path.join(cached_folders[2]) + + if interp == "sigmoid": + theta_func = CheckpointMergerPipeline.sigmoid + elif interp == "inv_sigmoid": + theta_func = CheckpointMergerPipeline.inv_sigmoid + elif interp == "add_diff": + theta_func = CheckpointMergerPipeline.add_difference + else: + theta_func = CheckpointMergerPipeline.weighted_sum + + # Find each module's state dict. + for attr in final_pipe.config.keys(): + if not attr.startswith("_"): + checkpoint_path_1 = os.path.join(cached_folders[1], attr) + if os.path.exists(checkpoint_path_1): + files = list( + ( + *glob.glob(os.path.join(checkpoint_path_1, "*.safetensors")), + *glob.glob(os.path.join(checkpoint_path_1, "*.bin")), + ) + ) + checkpoint_path_1 = files[0] if len(files) > 0 else None + if checkpoint_path_2 is not None and os.path.exists(checkpoint_path_2): + files = list( + ( + *glob.glob(os.path.join(checkpoint_path_2, "*.safetensors")), + *glob.glob(os.path.join(checkpoint_path_2, "*.bin")), + ) + ) + checkpoint_path_2 = files[0] if len(files) > 0 else None + # For an attr if both checkpoint_path_1 and 2 are None, ignore. + # If atleast one is present, deal with it according to interp method, of course only if the state_dict keys match. + if checkpoint_path_1 is None and checkpoint_path_2 is None: + print(f"Skipping {attr}: not present in 2nd or 3d model") + continue + try: + module = getattr(final_pipe, attr) + if isinstance(module, bool): # ignore requires_safety_checker boolean + continue + theta_0 = getattr(module, "state_dict") + theta_0 = theta_0() + + update_theta_0 = getattr(module, "load_state_dict") + theta_1 = ( + safetensors.torch.load_file(checkpoint_path_1) + if (is_safetensors_available() and checkpoint_path_1.endswith(".safetensors")) + else torch.load(checkpoint_path_1, map_location="cpu") + ) + theta_2 = None + if checkpoint_path_2: + theta_2 = ( + safetensors.torch.load_file(checkpoint_path_2) + if (is_safetensors_available() and checkpoint_path_2.endswith(".safetensors")) + else torch.load(checkpoint_path_2, map_location="cpu") + ) + + if not theta_0.keys() == theta_1.keys(): + print(f"Skipping {attr}: key mismatch") + continue + if theta_2 and not theta_1.keys() == theta_2.keys(): + print(f"Skipping {attr}:y mismatch") + except Exception as e: + print(f"Skipping {attr} do to an unexpected error: {str(e)}") + continue + print(f"MERGING {attr}") + + for key in theta_0.keys(): + if theta_2: + theta_0[key] = theta_func(theta_0[key], theta_1[key], theta_2[key], alpha) + else: + theta_0[key] = theta_func(theta_0[key], theta_1[key], None, alpha) + + del theta_1 + del theta_2 + update_theta_0(theta_0) + + del theta_0 + return final_pipe + + @staticmethod + def weighted_sum(theta0, theta1, theta2, alpha): + return ((1 - alpha) * theta0) + (alpha * theta1) + + # Smoothstep (https://en.wikipedia.org/wiki/Smoothstep) + @staticmethod + def sigmoid(theta0, theta1, theta2, alpha): + alpha = alpha * alpha * (3 - (2 * alpha)) + return theta0 + ((theta1 - theta0) * alpha) + + # Inverse Smoothstep (https://en.wikipedia.org/wiki/Smoothstep) + @staticmethod + def inv_sigmoid(theta0, theta1, theta2, alpha): + import math + + alpha = 0.5 - math.sin(math.asin(1.0 - 2.0 * alpha) / 3.0) + return theta0 + ((theta1 - theta0) * alpha) + + @staticmethod + def add_difference(theta0, theta1, theta2, alpha): + return theta0 + (theta1 - theta2) * (1.0 - alpha) diff --git a/diffusers/examples/community/clip_guided_stable_diffusion.py b/diffusers/examples/community/clip_guided_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..68bdf22f945406b40a9a288177e6b3b0020815c6 --- /dev/null +++ b/diffusers/examples/community/clip_guided_stable_diffusion.py @@ -0,0 +1,351 @@ +import inspect +from typing import List, Optional, Union + +import torch +from torch import nn +from torch.nn import functional as F +from torchvision import transforms +from transformers import CLIPFeatureExtractor, CLIPModel, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DiffusionPipeline, + LMSDiscreteScheduler, + PNDMScheduler, + UNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipelineOutput + + +class MakeCutouts(nn.Module): + def __init__(self, cut_size, cut_power=1.0): + super().__init__() + + self.cut_size = cut_size + self.cut_power = cut_power + + def forward(self, pixel_values, num_cutouts): + sideY, sideX = pixel_values.shape[2:4] + max_size = min(sideX, sideY) + min_size = min(sideX, sideY, self.cut_size) + cutouts = [] + for _ in range(num_cutouts): + size = int(torch.rand([]) ** self.cut_power * (max_size - min_size) + min_size) + offsetx = torch.randint(0, sideX - size + 1, ()) + offsety = torch.randint(0, sideY - size + 1, ()) + cutout = pixel_values[:, :, offsety : offsety + size, offsetx : offsetx + size] + cutouts.append(F.adaptive_avg_pool2d(cutout, self.cut_size)) + return torch.cat(cutouts) + + +def spherical_dist_loss(x, y): + x = F.normalize(x, dim=-1) + y = F.normalize(y, dim=-1) + return (x - y).norm(dim=-1).div(2).arcsin().pow(2).mul(2) + + +def set_requires_grad(model, value): + for param in model.parameters(): + param.requires_grad = value + + +class CLIPGuidedStableDiffusion(DiffusionPipeline): + """CLIP guided stable diffusion based on the amazing repo by @crowsonkb and @Jack000 + - https://github.com/Jack000/glid-3-xl + - https://github.dev/crowsonkb/k-diffusion + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + clip_model: CLIPModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[PNDMScheduler, LMSDiscreteScheduler, DDIMScheduler], + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + self.register_modules( + vae=vae, + text_encoder=text_encoder, + clip_model=clip_model, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + feature_extractor=feature_extractor, + ) + + self.normalize = transforms.Normalize(mean=feature_extractor.image_mean, std=feature_extractor.image_std) + self.cut_out_size = ( + feature_extractor.size + if isinstance(feature_extractor.size, int) + else feature_extractor.size["shortest_edge"] + ) + self.make_cutouts = MakeCutouts(self.cut_out_size) + + set_requires_grad(self.text_encoder, False) + set_requires_grad(self.clip_model, False) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + self.enable_attention_slicing(None) + + def freeze_vae(self): + set_requires_grad(self.vae, False) + + def unfreeze_vae(self): + set_requires_grad(self.vae, True) + + def freeze_unet(self): + set_requires_grad(self.unet, False) + + def unfreeze_unet(self): + set_requires_grad(self.unet, True) + + @torch.enable_grad() + def cond_fn( + self, + latents, + timestep, + index, + text_embeddings, + noise_pred_original, + text_embeddings_clip, + clip_guidance_scale, + num_cutouts, + use_cutouts=True, + ): + latents = latents.detach().requires_grad_() + + if isinstance(self.scheduler, LMSDiscreteScheduler): + sigma = self.scheduler.sigmas[index] + # the model input needs to be scaled to match the continuous ODE formulation in K-LMS + latent_model_input = latents / ((sigma**2 + 1) ** 0.5) + else: + latent_model_input = latents + + # predict the noise residual + noise_pred = self.unet(latent_model_input, timestep, encoder_hidden_states=text_embeddings).sample + + if isinstance(self.scheduler, (PNDMScheduler, DDIMScheduler)): + alpha_prod_t = self.scheduler.alphas_cumprod[timestep] + beta_prod_t = 1 - alpha_prod_t + # compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_original_sample = (latents - beta_prod_t ** (0.5) * noise_pred) / alpha_prod_t ** (0.5) + + fac = torch.sqrt(beta_prod_t) + sample = pred_original_sample * (fac) + latents * (1 - fac) + elif isinstance(self.scheduler, LMSDiscreteScheduler): + sigma = self.scheduler.sigmas[index] + sample = latents - sigma * noise_pred + else: + raise ValueError(f"scheduler type {type(self.scheduler)} not supported") + + sample = 1 / self.vae.config.scaling_factor * sample + image = self.vae.decode(sample).sample + image = (image / 2 + 0.5).clamp(0, 1) + + if use_cutouts: + image = self.make_cutouts(image, num_cutouts) + else: + image = transforms.Resize(self.cut_out_size)(image) + image = self.normalize(image).to(latents.dtype) + + image_embeddings_clip = self.clip_model.get_image_features(image) + image_embeddings_clip = image_embeddings_clip / image_embeddings_clip.norm(p=2, dim=-1, keepdim=True) + + if use_cutouts: + dists = spherical_dist_loss(image_embeddings_clip, text_embeddings_clip) + dists = dists.view([num_cutouts, sample.shape[0], -1]) + loss = dists.sum(2).mean(0).sum() * clip_guidance_scale + else: + loss = spherical_dist_loss(image_embeddings_clip, text_embeddings_clip).mean() * clip_guidance_scale + + grads = -torch.autograd.grad(loss, latents)[0] + + if isinstance(self.scheduler, LMSDiscreteScheduler): + latents = latents.detach() + grads * (sigma**2) + noise_pred = noise_pred_original + else: + noise_pred = noise_pred_original - torch.sqrt(beta_prod_t) * grads + return noise_pred, latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = 512, + width: Optional[int] = 512, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + clip_guidance_scale: Optional[float] = 100, + clip_prompt: Optional[Union[str, List[str]]] = None, + num_cutouts: Optional[int] = 4, + use_cutouts: Optional[bool] = True, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ): + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # get prompt text embeddings + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_embeddings = self.text_encoder(text_input.input_ids.to(self.device))[0] + # duplicate text embeddings for each generation per prompt + text_embeddings = text_embeddings.repeat_interleave(num_images_per_prompt, dim=0) + + if clip_guidance_scale > 0: + if clip_prompt is not None: + clip_text_input = self.tokenizer( + clip_prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ).input_ids.to(self.device) + else: + clip_text_input = text_input.input_ids.to(self.device) + text_embeddings_clip = self.clip_model.get_text_features(clip_text_input) + text_embeddings_clip = text_embeddings_clip / text_embeddings_clip.norm(p=2, dim=-1, keepdim=True) + # duplicate text embeddings clip for each generation per prompt + text_embeddings_clip = text_embeddings_clip.repeat_interleave(num_images_per_prompt, dim=0) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + max_length = text_input.input_ids.shape[-1] + uncond_input = self.tokenizer([""], padding="max_length", max_length=max_length, return_tensors="pt") + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + # duplicate unconditional embeddings for each generation per prompt + uncond_embeddings = uncond_embeddings.repeat_interleave(num_images_per_prompt, dim=0) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # set timesteps + accepts_offset = "offset" in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys()) + extra_set_kwargs = {} + if accepts_offset: + extra_set_kwargs["offset"] = 1 + + self.scheduler.set_timesteps(num_inference_steps, **extra_set_kwargs) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform classifier free guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # perform clip guidance + if clip_guidance_scale > 0: + text_embeddings_for_guidance = ( + text_embeddings.chunk(2)[1] if do_classifier_free_guidance else text_embeddings + ) + noise_pred, latents = self.cond_fn( + latents, + t, + i, + text_embeddings_for_guidance, + noise_pred, + text_embeddings_clip, + clip_guidance_scale, + num_cutouts, + use_cutouts, + ) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # scale and decode the image latents with vae + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, None) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=None) diff --git a/diffusers/examples/community/composable_stable_diffusion.py b/diffusers/examples/community/composable_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..966227b466ca8fbd42ba769714462ca35dc7c941 --- /dev/null +++ b/diffusers/examples/community/composable_stable_diffusion.py @@ -0,0 +1,582 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.schedulers import ( + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, +) +from diffusers.utils import is_accelerate_available + +from ...utils import deprecate, logging +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class ComposableStableDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[ + DDIMScheduler, + PNDMScheduler, + LMSDiscreteScheduler, + EulerDiscreteScheduler, + EulerAncestralDiscreteScheduler, + DPMSolverMultistepScheduler, + ], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. + + When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several + steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously invoked, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + # TODO(Patrick) - there is currently a bug with cpu offload of nn.Parameter in accelerate + # fix by only offloading self.safety_checker for now + cpu_offload(self.safety_checker.vision_model, device) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `list(int)`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + text_embeddings = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + text_embeddings = text_embeddings[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + uncond_embeddings = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + uncond_embeddings = uncond_embeddings[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + return text_embeddings + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs(self, prompt, height, width, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if latents is None: + if device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(shape, generator=generator, device="cpu", dtype=dtype).to(device) + else: + latents = torch.randn(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + weights: Optional[str] = "", + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + if "|" in prompt: + prompt = [x.strip() for x in prompt.split("|")] + print(f"composing {prompt}...") + + if not weights: + # specify weights for prompts (excluding the unconditional score) + print("using equal positive weights (conjunction) for all prompts...") + weights = torch.tensor([guidance_scale] * len(prompt), device=self.device).reshape(-1, 1, 1, 1) + else: + # set prompt weight for each + num_prompts = len(prompt) if isinstance(prompt, list) else 1 + weights = [float(w.strip()) for w in weights.split("|")] + # guidance scale as the default + if len(weights) < num_prompts: + weights.append(guidance_scale) + else: + weights = weights[:num_prompts] + assert len(weights) == len(prompt), "weights specified are not equal to the number of prompts" + weights = torch.tensor(weights, device=self.device).reshape(-1, 1, 1, 1) + else: + weights = guidance_scale + + # 3. Encode input prompt + text_embeddings = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + text_embeddings.dtype, + device, + generator, + latents, + ) + + # composable diffusion + if isinstance(prompt, list) and batch_size == 1: + # remove extra unconditional embedding + # N = one unconditional embed + conditional embeds + text_embeddings = text_embeddings[len(prompt) - 1 :] + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = [] + for j in range(text_embeddings.shape[0]): + noise_pred.append( + self.unet(latent_model_input[:1], t, encoder_hidden_states=text_embeddings[j : j + 1]).sample + ) + noise_pred = torch.cat(noise_pred, dim=0) + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred[:1], noise_pred[1:] + noise_pred = noise_pred_uncond + (weights * (noise_pred_text - noise_pred_uncond)).sum( + dim=0, keepdims=True + ) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, text_embeddings.dtype) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/imagic_stable_diffusion.py b/diffusers/examples/community/imagic_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..3ca0da0ec0613489d81602f369bdeacfc11c3d43 --- /dev/null +++ b/diffusers/examples/community/imagic_stable_diffusion.py @@ -0,0 +1,501 @@ +""" + modeled after the textual_inversion.py / train_dreambooth.py and the work + of justinpinkney here: https://github.com/justinpinkney/stable-diffusion/blob/main/notebooks/imagic.ipynb +""" +import inspect +import warnings +from typing import List, Optional, Union + +import numpy as np +import PIL +import torch +import torch.nn.functional as F +from accelerate import Accelerator + +# TODO: remove and import from diffusers.utils when the new version of diffusers is released +from packaging import version +from tqdm.auto import tqdm +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, logging + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def preprocess(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image) + return 2.0 * image - 1.0 + + +class ImagicStableDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for imagic image editing. + See paper here: https://arxiv.org/pdf/2210.09276.pdf + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offsensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + def train( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image], + height: Optional[int] = 512, + width: Optional[int] = 512, + generator: Optional[torch.Generator] = None, + embedding_learning_rate: float = 0.001, + diffusion_model_learning_rate: float = 2e-6, + text_embedding_optimization_steps: int = 500, + model_fine_tuning_optimization_steps: int = 1000, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `nd.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + accelerator = Accelerator( + gradient_accumulation_steps=1, + mixed_precision="fp16", + ) + + if "torch_device" in kwargs: + device = kwargs.pop("torch_device") + warnings.warn( + "`torch_device` is deprecated as an input argument to `__call__` and will be removed in v0.3.0." + " Consider using `pipe.to(torch_device)` instead." + ) + + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + self.to(device) + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # Freeze vae and unet + self.vae.requires_grad_(False) + self.unet.requires_grad_(False) + self.text_encoder.requires_grad_(False) + self.unet.eval() + self.vae.eval() + self.text_encoder.eval() + + if accelerator.is_main_process: + accelerator.init_trackers( + "imagic", + config={ + "embedding_learning_rate": embedding_learning_rate, + "text_embedding_optimization_steps": text_embedding_optimization_steps, + }, + ) + + # get text embeddings for prompt + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_embeddings = torch.nn.Parameter( + self.text_encoder(text_input.input_ids.to(self.device))[0], requires_grad=True + ) + text_embeddings = text_embeddings.detach() + text_embeddings.requires_grad_() + text_embeddings_orig = text_embeddings.clone() + + # Initialize the optimizer + optimizer = torch.optim.Adam( + [text_embeddings], # only optimize the embeddings + lr=embedding_learning_rate, + ) + + if isinstance(image, PIL.Image.Image): + image = preprocess(image) + + latents_dtype = text_embeddings.dtype + image = image.to(device=self.device, dtype=latents_dtype) + init_latent_image_dist = self.vae.encode(image).latent_dist + image_latents = init_latent_image_dist.sample(generator=generator) + image_latents = 0.18215 * image_latents + + progress_bar = tqdm(range(text_embedding_optimization_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + global_step = 0 + + logger.info("First optimizing the text embedding to better reconstruct the init image") + for _ in range(text_embedding_optimization_steps): + with accelerator.accumulate(text_embeddings): + # Sample noise that we'll add to the latents + noise = torch.randn(image_latents.shape).to(image_latents.device) + timesteps = torch.randint(1000, (1,), device=image_latents.device) + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = self.scheduler.add_noise(image_latents, noise, timesteps) + + # Predict the noise residual + noise_pred = self.unet(noisy_latents, timesteps, text_embeddings).sample + + loss = F.mse_loss(noise_pred, noise, reduction="none").mean([1, 2, 3]).mean() + accelerator.backward(loss) + + optimizer.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + logs = {"loss": loss.detach().item()} # , "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + accelerator.wait_for_everyone() + + text_embeddings.requires_grad_(False) + + # Now we fine tune the unet to better reconstruct the image + self.unet.requires_grad_(True) + self.unet.train() + optimizer = torch.optim.Adam( + self.unet.parameters(), # only optimize unet + lr=diffusion_model_learning_rate, + ) + progress_bar = tqdm(range(model_fine_tuning_optimization_steps), disable=not accelerator.is_local_main_process) + + logger.info("Next fine tuning the entire model to better reconstruct the init image") + for _ in range(model_fine_tuning_optimization_steps): + with accelerator.accumulate(self.unet.parameters()): + # Sample noise that we'll add to the latents + noise = torch.randn(image_latents.shape).to(image_latents.device) + timesteps = torch.randint(1000, (1,), device=image_latents.device) + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = self.scheduler.add_noise(image_latents, noise, timesteps) + + # Predict the noise residual + noise_pred = self.unet(noisy_latents, timesteps, text_embeddings).sample + + loss = F.mse_loss(noise_pred, noise, reduction="none").mean([1, 2, 3]).mean() + accelerator.backward(loss) + + optimizer.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + logs = {"loss": loss.detach().item()} # , "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + accelerator.wait_for_everyone() + self.text_embeddings_orig = text_embeddings_orig + self.text_embeddings = text_embeddings + + @torch.no_grad() + def __call__( + self, + alpha: float = 1.2, + height: Optional[int] = 512, + width: Optional[int] = 512, + num_inference_steps: Optional[int] = 50, + generator: Optional[torch.Generator] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + guidance_scale: float = 7.5, + eta: float = 0.0, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `nd.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + if self.text_embeddings is None: + raise ValueError("Please run the pipe.train() before trying to generate an image.") + if self.text_embeddings_orig is None: + raise ValueError("Please run the pipe.train() before trying to generate an image.") + + text_embeddings = alpha * self.text_embeddings_orig + (1 - alpha) * self.text_embeddings + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens = [""] + max_length = self.tokenizer.model_max_length + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.view(1, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (1, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if self.device.type == "mps": + # randn does not exist on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/img2img_inpainting.py b/diffusers/examples/community/img2img_inpainting.py new file mode 100644 index 0000000000000000000000000000000000000000..71cc22de4b4f11780d1f2d72b4a1a092cbacc39d --- /dev/null +++ b/diffusers/examples/community/img2img_inpainting.py @@ -0,0 +1,463 @@ +import inspect +from typing import Callable, List, Optional, Tuple, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def prepare_mask_and_masked_image(image, mask): + image = np.array(image.convert("RGB")) + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + + mask = np.array(mask.convert("L")) + mask = mask.astype(np.float32) / 255.0 + mask = mask[None, None] + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + + masked_image = image * (mask < 0.5) + + return mask, masked_image + + +def check_size(image, height, width): + if isinstance(image, PIL.Image.Image): + w, h = image.size + elif isinstance(image, torch.Tensor): + *_, h, w = image.shape + + if h != height or w != width: + raise ValueError(f"Image size should be {height}x{width}, but got {h}x{w}") + + +def overlay_inner_image(image, inner_image, paste_offset: Tuple[int] = (0, 0)): + inner_image = inner_image.convert("RGBA") + image = image.convert("RGB") + + image.paste(inner_image, paste_offset, inner_image) + image = image.convert("RGB") + + return image + + +class ImageToImageInpaintingPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image-to-image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latens. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image], + inner_image: Union[torch.FloatTensor, PIL.Image.Image], + mask_image: Union[torch.FloatTensor, PIL.Image.Image], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`torch.Tensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be inpainted, *i.e.* parts of the image will + be masked out with `mask_image` and repainted according to `prompt`. + inner_image (`torch.Tensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be overlayed onto `image`. Non-transparent + regions of `inner_image` must fit inside white pixels in `mask_image`. Expects four channels, with + the last channel representing the alpha channel, which will be used to blend `inner_image` with + `image`. If not provided, it will be forcibly cast to RGBA. + mask_image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + repainted, while black pixels will be preserved. If `mask_image` is a PIL image, it will be converted + to a single channel (luminance) before use. If it's a tensor, it should contain one color channel (L) + instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # check if input sizes are correct + check_size(image, height, width) + check_size(inner_image, height, width) + check_size(mask_image, height, width) + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(batch_size, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + num_channels_latents = self.vae.config.latent_channels + latents_shape = (batch_size * num_images_per_prompt, num_channels_latents, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not exist on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # overlay the inner image + image = overlay_inner_image(image, inner_image) + + # prepare mask and masked_image + mask, masked_image = prepare_mask_and_masked_image(image, mask_image) + mask = mask.to(device=self.device, dtype=text_embeddings.dtype) + masked_image = masked_image.to(device=self.device, dtype=text_embeddings.dtype) + + # resize the mask to latents shape as we concatenate the mask to the latents + mask = torch.nn.functional.interpolate(mask, size=(height // 8, width // 8)) + + # encode the mask image into latents space so we can concatenate it to the latents + masked_image_latents = self.vae.encode(masked_image).latent_dist.sample(generator=generator) + masked_image_latents = 0.18215 * masked_image_latents + + # duplicate mask and masked_image_latents for each generation per prompt, using mps friendly method + mask = mask.repeat(batch_size * num_images_per_prompt, 1, 1, 1) + masked_image_latents = masked_image_latents.repeat(batch_size * num_images_per_prompt, 1, 1, 1) + + mask = torch.cat([mask] * 2) if do_classifier_free_guidance else mask + masked_image_latents = ( + torch.cat([masked_image_latents] * 2) if do_classifier_free_guidance else masked_image_latents + ) + + num_channels_mask = mask.shape[1] + num_channels_masked_image = masked_image_latents.shape[1] + + if num_channels_latents + num_channels_mask + num_channels_masked_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_mask`: {num_channels_mask} + `num_channels_masked_image`: {num_channels_masked_image}" + f" = {num_channels_latents+num_channels_masked_image+num_channels_mask}. Please verify the config of" + " `pipeline.unet` or your `mask_image` or `image` input." + ) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + + # concat latents, mask, masked_image_latents in the channel dimension + latent_model_input = torch.cat([latent_model_input, mask, masked_image_latents], dim=1) + + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/interpolate_stable_diffusion.py b/diffusers/examples/community/interpolate_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..27d0488760d71df49614415b6d09ead8960a78ca --- /dev/null +++ b/diffusers/examples/community/interpolate_stable_diffusion.py @@ -0,0 +1,524 @@ +import inspect +import time +from pathlib import Path +from typing import Callable, List, Optional, Union + +import numpy as np +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def slerp(t, v0, v1, DOT_THRESHOLD=0.9995): + """helper function to spherically interpolate two arrays v1 v2""" + + if not isinstance(v0, np.ndarray): + inputs_are_torch = True + input_device = v0.device + v0 = v0.cpu().numpy() + v1 = v1.cpu().numpy() + + dot = np.sum(v0 * v1 / (np.linalg.norm(v0) * np.linalg.norm(v1))) + if np.abs(dot) > DOT_THRESHOLD: + v2 = (1 - t) * v0 + t * v1 + else: + theta_0 = np.arccos(dot) + sin_theta_0 = np.sin(theta_0) + theta_t = theta_0 * t + sin_theta_t = np.sin(theta_t) + s0 = np.sin(theta_0 - theta_t) / sin_theta_0 + s1 = sin_theta_t / sin_theta_0 + v2 = s0 * v0 + s1 * v1 + + if inputs_are_torch: + v2 = torch.from_numpy(v2).to(input_device) + + return v2 + + +class StableDiffusionWalkPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def __call__( + self, + prompt: Optional[Union[str, List[str]]] = None, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + text_embeddings: Optional[torch.FloatTensor] = None, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*, defaults to `None`): + The prompt or prompts to guide the image generation. If not provided, `text_embeddings` is required. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + text_embeddings (`torch.FloatTensor`, *optional*, defaults to `None`): + Pre-generated text embeddings to be used as inputs for image generation. Can be used in place of + `prompt` to avoid re-computing the embeddings. If not provided, the embeddings will be generated from + the supplied `prompt`. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if text_embeddings is None: + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + print( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + else: + batch_size = text_embeddings.shape[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = self.tokenizer.model_max_length + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) + + def embed_text(self, text): + """takes in text and turns it into text embeddings""" + text_input = self.tokenizer( + text, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + with torch.no_grad(): + embed = self.text_encoder(text_input.input_ids.to(self.device))[0] + return embed + + def get_noise(self, seed, dtype=torch.float32, height=512, width=512): + """Takes in random seed and returns corresponding noise vector""" + return torch.randn( + (1, self.unet.in_channels, height // 8, width // 8), + generator=torch.Generator(device=self.device).manual_seed(seed), + device=self.device, + dtype=dtype, + ) + + def walk( + self, + prompts: List[str], + seeds: List[int], + num_interpolation_steps: Optional[int] = 6, + output_dir: Optional[str] = "./dreams", + name: Optional[str] = None, + batch_size: Optional[int] = 1, + height: Optional[int] = 512, + width: Optional[int] = 512, + guidance_scale: Optional[float] = 7.5, + num_inference_steps: Optional[int] = 50, + eta: Optional[float] = 0.0, + ) -> List[str]: + """ + Walks through a series of prompts and seeds, interpolating between them and saving the results to disk. + + Args: + prompts (`List[str]`): + List of prompts to generate images for. + seeds (`List[int]`): + List of seeds corresponding to provided prompts. Must be the same length as prompts. + num_interpolation_steps (`int`, *optional*, defaults to 6): + Number of interpolation steps to take between prompts. + output_dir (`str`, *optional*, defaults to `./dreams`): + Directory to save the generated images to. + name (`str`, *optional*, defaults to `None`): + Subdirectory of `output_dir` to save the generated images to. If `None`, the name will + be the current time. + batch_size (`int`, *optional*, defaults to 1): + Number of images to generate at once. + height (`int`, *optional*, defaults to 512): + Height of the generated images. + width (`int`, *optional*, defaults to 512): + Width of the generated images. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + + Returns: + `List[str]`: List of paths to the generated images. + """ + if not len(prompts) == len(seeds): + raise ValueError( + f"Number of prompts and seeds must be equalGot {len(prompts)} prompts and {len(seeds)} seeds" + ) + + name = name or time.strftime("%Y%m%d-%H%M%S") + save_path = Path(output_dir) / name + save_path.mkdir(exist_ok=True, parents=True) + + frame_idx = 0 + frame_filepaths = [] + for prompt_a, prompt_b, seed_a, seed_b in zip(prompts, prompts[1:], seeds, seeds[1:]): + # Embed Text + embed_a = self.embed_text(prompt_a) + embed_b = self.embed_text(prompt_b) + + # Get Noise + noise_dtype = embed_a.dtype + noise_a = self.get_noise(seed_a, noise_dtype, height, width) + noise_b = self.get_noise(seed_b, noise_dtype, height, width) + + noise_batch, embeds_batch = None, None + T = np.linspace(0.0, 1.0, num_interpolation_steps) + for i, t in enumerate(T): + noise = slerp(float(t), noise_a, noise_b) + embed = torch.lerp(embed_a, embed_b, t) + + noise_batch = noise if noise_batch is None else torch.cat([noise_batch, noise], dim=0) + embeds_batch = embed if embeds_batch is None else torch.cat([embeds_batch, embed], dim=0) + + batch_is_ready = embeds_batch.shape[0] == batch_size or i + 1 == T.shape[0] + if batch_is_ready: + outputs = self( + latents=noise_batch, + text_embeddings=embeds_batch, + height=height, + width=width, + guidance_scale=guidance_scale, + eta=eta, + num_inference_steps=num_inference_steps, + ) + noise_batch, embeds_batch = None, None + + for image in outputs["images"]: + frame_filepath = str(save_path / f"frame_{frame_idx:06d}.png") + image.save(frame_filepath) + frame_filepaths.append(frame_filepath) + frame_idx += 1 + return frame_filepaths diff --git a/diffusers/examples/community/lpw_stable_diffusion.py b/diffusers/examples/community/lpw_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..b4602cc2b905608ddcadd700baf4741878981797 --- /dev/null +++ b/diffusers/examples/community/lpw_stable_diffusion.py @@ -0,0 +1,1162 @@ +import inspect +import re +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import SchedulerMixin, StableDiffusionPipeline +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput, StableDiffusionSafetyChecker +from diffusers.utils import deprecate, logging + + +try: + from diffusers.utils import PIL_INTERPOLATION +except ImportError: + if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } + else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +re_attention = re.compile( + r""" +\\\(| +\\\)| +\\\[| +\\]| +\\\\| +\\| +\(| +\[| +:([+-]?[.\d]+)\)| +\)| +]| +[^\\()\[\]:]+| +: +""", + re.X, +) + + +def parse_prompt_attention(text): + """ + Parses a string with attention tokens and returns a list of pairs: text and its associated weight. + Accepted tokens are: + (abc) - increases attention to abc by a multiplier of 1.1 + (abc:3.12) - increases attention to abc by a multiplier of 3.12 + [abc] - decreases attention to abc by a multiplier of 1.1 + \( - literal character '(' + \[ - literal character '[' + \) - literal character ')' + \] - literal character ']' + \\ - literal character '\' + anything else - just text + >>> parse_prompt_attention('normal text') + [['normal text', 1.0]] + >>> parse_prompt_attention('an (important) word') + [['an ', 1.0], ['important', 1.1], [' word', 1.0]] + >>> parse_prompt_attention('(unbalanced') + [['unbalanced', 1.1]] + >>> parse_prompt_attention('\(literal\]') + [['(literal]', 1.0]] + >>> parse_prompt_attention('(unnecessary)(parens)') + [['unnecessaryparens', 1.1]] + >>> parse_prompt_attention('a (((house:1.3)) [on] a (hill:0.5), sun, (((sky))).') + [['a ', 1.0], + ['house', 1.5730000000000004], + [' ', 1.1], + ['on', 1.0], + [' a ', 1.1], + ['hill', 0.55], + [', sun, ', 1.1], + ['sky', 1.4641000000000006], + ['.', 1.1]] + """ + + res = [] + round_brackets = [] + square_brackets = [] + + round_bracket_multiplier = 1.1 + square_bracket_multiplier = 1 / 1.1 + + def multiply_range(start_position, multiplier): + for p in range(start_position, len(res)): + res[p][1] *= multiplier + + for m in re_attention.finditer(text): + text = m.group(0) + weight = m.group(1) + + if text.startswith("\\"): + res.append([text[1:], 1.0]) + elif text == "(": + round_brackets.append(len(res)) + elif text == "[": + square_brackets.append(len(res)) + elif weight is not None and len(round_brackets) > 0: + multiply_range(round_brackets.pop(), float(weight)) + elif text == ")" and len(round_brackets) > 0: + multiply_range(round_brackets.pop(), round_bracket_multiplier) + elif text == "]" and len(square_brackets) > 0: + multiply_range(square_brackets.pop(), square_bracket_multiplier) + else: + res.append([text, 1.0]) + + for pos in round_brackets: + multiply_range(pos, round_bracket_multiplier) + + for pos in square_brackets: + multiply_range(pos, square_bracket_multiplier) + + if len(res) == 0: + res = [["", 1.0]] + + # merge runs of identical weights + i = 0 + while i + 1 < len(res): + if res[i][1] == res[i + 1][1]: + res[i][0] += res[i + 1][0] + res.pop(i + 1) + else: + i += 1 + + return res + + +def get_prompts_with_weights(pipe: StableDiffusionPipeline, prompt: List[str], max_length: int): + r""" + Tokenize a list of prompts and return its tokens with weights of each token. + + No padding, starting or ending token is included. + """ + tokens = [] + weights = [] + truncated = False + for text in prompt: + texts_and_weights = parse_prompt_attention(text) + text_token = [] + text_weight = [] + for word, weight in texts_and_weights: + # tokenize and discard the starting and the ending token + token = pipe.tokenizer(word).input_ids[1:-1] + text_token += token + # copy the weight by length of token + text_weight += [weight] * len(token) + # stop if the text is too long (longer than truncation limit) + if len(text_token) > max_length: + truncated = True + break + # truncate + if len(text_token) > max_length: + truncated = True + text_token = text_token[:max_length] + text_weight = text_weight[:max_length] + tokens.append(text_token) + weights.append(text_weight) + if truncated: + logger.warning("Prompt was truncated. Try to shorten the prompt or increase max_embeddings_multiples") + return tokens, weights + + +def pad_tokens_and_weights(tokens, weights, max_length, bos, eos, no_boseos_middle=True, chunk_length=77): + r""" + Pad the tokens (with starting and ending tokens) and weights (with 1.0) to max_length. + """ + max_embeddings_multiples = (max_length - 2) // (chunk_length - 2) + weights_length = max_length if no_boseos_middle else max_embeddings_multiples * chunk_length + for i in range(len(tokens)): + tokens[i] = [bos] + tokens[i] + [eos] * (max_length - 1 - len(tokens[i])) + if no_boseos_middle: + weights[i] = [1.0] + weights[i] + [1.0] * (max_length - 1 - len(weights[i])) + else: + w = [] + if len(weights[i]) == 0: + w = [1.0] * weights_length + else: + for j in range(max_embeddings_multiples): + w.append(1.0) # weight for starting token in this chunk + w += weights[i][j * (chunk_length - 2) : min(len(weights[i]), (j + 1) * (chunk_length - 2))] + w.append(1.0) # weight for ending token in this chunk + w += [1.0] * (weights_length - len(w)) + weights[i] = w[:] + + return tokens, weights + + +def get_unweighted_text_embeddings( + pipe: StableDiffusionPipeline, + text_input: torch.Tensor, + chunk_length: int, + no_boseos_middle: Optional[bool] = True, +): + """ + When the length of tokens is a multiple of the capacity of the text encoder, + it should be split into chunks and sent to the text encoder individually. + """ + max_embeddings_multiples = (text_input.shape[1] - 2) // (chunk_length - 2) + if max_embeddings_multiples > 1: + text_embeddings = [] + for i in range(max_embeddings_multiples): + # extract the i-th chunk + text_input_chunk = text_input[:, i * (chunk_length - 2) : (i + 1) * (chunk_length - 2) + 2].clone() + + # cover the head and the tail by the starting and the ending tokens + text_input_chunk[:, 0] = text_input[0, 0] + text_input_chunk[:, -1] = text_input[0, -1] + text_embedding = pipe.text_encoder(text_input_chunk)[0] + + if no_boseos_middle: + if i == 0: + # discard the ending token + text_embedding = text_embedding[:, :-1] + elif i == max_embeddings_multiples - 1: + # discard the starting token + text_embedding = text_embedding[:, 1:] + else: + # discard both starting and ending tokens + text_embedding = text_embedding[:, 1:-1] + + text_embeddings.append(text_embedding) + text_embeddings = torch.concat(text_embeddings, axis=1) + else: + text_embeddings = pipe.text_encoder(text_input)[0] + return text_embeddings + + +def get_weighted_text_embeddings( + pipe: StableDiffusionPipeline, + prompt: Union[str, List[str]], + uncond_prompt: Optional[Union[str, List[str]]] = None, + max_embeddings_multiples: Optional[int] = 3, + no_boseos_middle: Optional[bool] = False, + skip_parsing: Optional[bool] = False, + skip_weighting: Optional[bool] = False, + **kwargs, +): + r""" + Prompts can be assigned with local weights using brackets. For example, + prompt 'A (very beautiful) masterpiece' highlights the words 'very beautiful', + and the embedding tokens corresponding to the words get multiplied by a constant, 1.1. + + Also, to regularize of the embedding, the weighted embedding would be scaled to preserve the original mean. + + Args: + pipe (`StableDiffusionPipeline`): + Pipe to provide access to the tokenizer and the text encoder. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + uncond_prompt (`str` or `List[str]`): + The unconditional prompt or prompts for guide the image generation. If unconditional prompt + is provided, the embeddings of prompt and uncond_prompt are concatenated. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + no_boseos_middle (`bool`, *optional*, defaults to `False`): + If the length of text token is multiples of the capacity of text encoder, whether reserve the starting and + ending token in each of the chunk in the middle. + skip_parsing (`bool`, *optional*, defaults to `False`): + Skip the parsing of brackets. + skip_weighting (`bool`, *optional*, defaults to `False`): + Skip the weighting. When the parsing is skipped, it is forced True. + """ + max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2 + if isinstance(prompt, str): + prompt = [prompt] + + if not skip_parsing: + prompt_tokens, prompt_weights = get_prompts_with_weights(pipe, prompt, max_length - 2) + if uncond_prompt is not None: + if isinstance(uncond_prompt, str): + uncond_prompt = [uncond_prompt] + uncond_tokens, uncond_weights = get_prompts_with_weights(pipe, uncond_prompt, max_length - 2) + else: + prompt_tokens = [ + token[1:-1] for token in pipe.tokenizer(prompt, max_length=max_length, truncation=True).input_ids + ] + prompt_weights = [[1.0] * len(token) for token in prompt_tokens] + if uncond_prompt is not None: + if isinstance(uncond_prompt, str): + uncond_prompt = [uncond_prompt] + uncond_tokens = [ + token[1:-1] + for token in pipe.tokenizer(uncond_prompt, max_length=max_length, truncation=True).input_ids + ] + uncond_weights = [[1.0] * len(token) for token in uncond_tokens] + + # round up the longest length of tokens to a multiple of (model_max_length - 2) + max_length = max([len(token) for token in prompt_tokens]) + if uncond_prompt is not None: + max_length = max(max_length, max([len(token) for token in uncond_tokens])) + + max_embeddings_multiples = min( + max_embeddings_multiples, + (max_length - 1) // (pipe.tokenizer.model_max_length - 2) + 1, + ) + max_embeddings_multiples = max(1, max_embeddings_multiples) + max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2 + + # pad the length of tokens and weights + bos = pipe.tokenizer.bos_token_id + eos = pipe.tokenizer.eos_token_id + prompt_tokens, prompt_weights = pad_tokens_and_weights( + prompt_tokens, + prompt_weights, + max_length, + bos, + eos, + no_boseos_middle=no_boseos_middle, + chunk_length=pipe.tokenizer.model_max_length, + ) + prompt_tokens = torch.tensor(prompt_tokens, dtype=torch.long, device=pipe.device) + if uncond_prompt is not None: + uncond_tokens, uncond_weights = pad_tokens_and_weights( + uncond_tokens, + uncond_weights, + max_length, + bos, + eos, + no_boseos_middle=no_boseos_middle, + chunk_length=pipe.tokenizer.model_max_length, + ) + uncond_tokens = torch.tensor(uncond_tokens, dtype=torch.long, device=pipe.device) + + # get the embeddings + text_embeddings = get_unweighted_text_embeddings( + pipe, + prompt_tokens, + pipe.tokenizer.model_max_length, + no_boseos_middle=no_boseos_middle, + ) + prompt_weights = torch.tensor(prompt_weights, dtype=text_embeddings.dtype, device=pipe.device) + if uncond_prompt is not None: + uncond_embeddings = get_unweighted_text_embeddings( + pipe, + uncond_tokens, + pipe.tokenizer.model_max_length, + no_boseos_middle=no_boseos_middle, + ) + uncond_weights = torch.tensor(uncond_weights, dtype=uncond_embeddings.dtype, device=pipe.device) + + # assign weights to the prompts and normalize in the sense of mean + # TODO: should we normalize by chunk or in a whole (current implementation)? + if (not skip_parsing) and (not skip_weighting): + previous_mean = text_embeddings.float().mean(axis=[-2, -1]).to(text_embeddings.dtype) + text_embeddings *= prompt_weights.unsqueeze(-1) + current_mean = text_embeddings.float().mean(axis=[-2, -1]).to(text_embeddings.dtype) + text_embeddings *= (previous_mean / current_mean).unsqueeze(-1).unsqueeze(-1) + if uncond_prompt is not None: + previous_mean = uncond_embeddings.float().mean(axis=[-2, -1]).to(uncond_embeddings.dtype) + uncond_embeddings *= uncond_weights.unsqueeze(-1) + current_mean = uncond_embeddings.float().mean(axis=[-2, -1]).to(uncond_embeddings.dtype) + uncond_embeddings *= (previous_mean / current_mean).unsqueeze(-1).unsqueeze(-1) + + if uncond_prompt is not None: + return text_embeddings, uncond_embeddings + return text_embeddings, None + + +def preprocess_image(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image) + return 2.0 * image - 1.0 + + +def preprocess_mask(mask, scale_factor=8): + mask = mask.convert("L") + w, h = mask.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = mask.resize((w // scale_factor, h // scale_factor), resample=PIL_INTERPOLATION["nearest"]) + mask = np.array(mask).astype(np.float32) / 255.0 + mask = np.tile(mask, (4, 1, 1)) + mask = mask[None].transpose(0, 1, 2, 3) # what does this step do? + mask = 1 - mask # repaint white, keep black + mask = torch.from_numpy(mask) + return mask + + +class StableDiffusionLongPromptWeightingPipeline(StableDiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion without tokens length limit, and support parsing + weighting in prompt. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + if version.parse(version.parse(diffusers.__version__).base_version) >= version.parse("0.9.0"): + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: SchedulerMixin, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + requires_safety_checker=requires_safety_checker, + ) + self.__init__additional__() + + else: + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: SchedulerMixin, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.__init__additional__() + + def __init__additional__(self): + if not hasattr(self, "vae_scale_factor"): + setattr(self, "vae_scale_factor", 2 ** (len(self.vae.config.block_out_channels) - 1)) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + max_embeddings_multiples, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `list(int)`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + if negative_prompt is None: + negative_prompt = [""] * batch_size + elif isinstance(negative_prompt, str): + negative_prompt = [negative_prompt] * batch_size + if batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + + text_embeddings, uncond_embeddings = get_weighted_text_embeddings( + pipe=self, + prompt=prompt, + uncond_prompt=negative_prompt if do_classifier_free_guidance else None, + max_embeddings_multiples=max_embeddings_multiples, + ) + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + if do_classifier_free_guidance: + bs_embed, seq_len, _ = uncond_embeddings.shape + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + return text_embeddings + + def check_inputs(self, prompt, height, width, strength, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def get_timesteps(self, num_inference_steps, strength, device, is_text2img): + if is_text2img: + return self.scheduler.timesteps.to(device), num_inference_steps + else: + # get the original timestep using init_timestep + offset = self.scheduler.config.get("steps_offset", 0) + init_timestep = int(num_inference_steps * strength) + offset + init_timestep = min(init_timestep, num_inference_steps) + + t_start = max(num_inference_steps - init_timestep + offset, 0) + timesteps = self.scheduler.timesteps[t_start:].to(device) + return timesteps, num_inference_steps - t_start + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def prepare_latents(self, image, timestep, batch_size, height, width, dtype, device, generator, latents=None): + if image is None: + shape = ( + batch_size, + self.unet.in_channels, + height // self.vae_scale_factor, + width // self.vae_scale_factor, + ) + + if latents is None: + if device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(shape, generator=generator, device="cpu", dtype=dtype).to(device) + else: + latents = torch.randn(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents, None, None + else: + init_latent_dist = self.vae.encode(image).latent_dist + init_latents = init_latent_dist.sample(generator=generator) + init_latents = 0.18215 * init_latents + init_latents = torch.cat([init_latents] * batch_size, dim=0) + init_latents_orig = init_latents + shape = init_latents.shape + + # add noise to latents using the timesteps + if device.type == "mps": + noise = torch.randn(shape, generator=generator, device="cpu", dtype=dtype).to(device) + else: + noise = torch.randn(shape, generator=generator, device=device, dtype=dtype) + latents = self.scheduler.add_noise(init_latents, noise, timestep) + return latents, init_latents_orig, noise + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + mask_image: Union[torch.FloatTensor, PIL.Image.Image] = None, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + strength: float = 0.8, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + is_cancelled_callback: Optional[Callable[[], bool]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + mask_image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If it's a tensor, it should + contain one color channel (L) instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. + `image` will be used as a starting point, adding more noise to it the larger the `strength`. The + number of denoising steps depends on the amount of noise initially added. When `strength` is 1, added + noise will be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + is_cancelled_callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. If the function returns + `True`, the inference will be cancelled. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + `None` if cancelled by `is_cancelled_callback`, + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, strength, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + text_embeddings = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + max_embeddings_multiples, + ) + dtype = text_embeddings.dtype + + # 4. Preprocess image and mask + if isinstance(image, PIL.Image.Image): + image = preprocess_image(image) + if image is not None: + image = image.to(device=self.device, dtype=dtype) + if isinstance(mask_image, PIL.Image.Image): + mask_image = preprocess_mask(mask_image, self.vae_scale_factor) + if mask_image is not None: + mask = mask_image.to(device=self.device, dtype=dtype) + mask = torch.cat([mask] * batch_size * num_images_per_prompt) + else: + mask = None + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device, image is None) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + latents, init_latents_orig, noise = self.prepare_latents( + image, + latent_timestep, + batch_size * num_images_per_prompt, + height, + width, + dtype, + device, + generator, + latents, + ) + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 8. Denoising loop + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + if mask is not None: + # masking + init_latents_proper = self.scheduler.add_noise(init_latents_orig, noise, torch.tensor([t])) + latents = (init_latents_proper * mask) + (latents * (1 - mask)) + + # call the callback, if provided + if i % callback_steps == 0: + if callback is not None: + callback(i, t, latents) + if is_cancelled_callback is not None and is_cancelled_callback(): + return None + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, text_embeddings.dtype) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return image, has_nsfw_concept + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) + + def text2img( + self, + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + is_cancelled_callback: Optional[Callable[[], bool]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for text-to-image generation. + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + is_cancelled_callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. If the function returns + `True`, the inference will be cancelled. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + is_cancelled_callback=is_cancelled_callback, + callback_steps=callback_steps, + **kwargs, + ) + + def img2img( + self, + image: Union[torch.FloatTensor, PIL.Image.Image], + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + is_cancelled_callback: Optional[Callable[[], bool]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for image-to-image generation. + Args: + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. + `image` will be used as a starting point, adding more noise to it the larger the `strength`. The + number of denoising steps depends on the amount of noise initially added. When `strength` is 1, added + noise will be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + is_cancelled_callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. If the function returns + `True`, the inference will be cancelled. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + image=image, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + strength=strength, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + is_cancelled_callback=is_cancelled_callback, + callback_steps=callback_steps, + **kwargs, + ) + + def inpaint( + self, + image: Union[torch.FloatTensor, PIL.Image.Image], + mask_image: Union[torch.FloatTensor, PIL.Image.Image], + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + is_cancelled_callback: Optional[Callable[[], bool]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for inpaint. + Args: + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. This is the image whose masked region will be inpainted. + mask_image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If it's a tensor, it should + contain one color channel (L) instead of 3, so the expected shape would be `(B, H, W, 1)`. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to inpaint the masked area. Must be between 0 and 1. When `strength` + is 1, the denoising process will be run on the masked area for the full number of iterations specified + in `num_inference_steps`. `image` will be used as a reference for the masked area, adding more + noise to that region the larger the `strength`. If `strength` is 0, no inpainting will occur. + num_inference_steps (`int`, *optional*, defaults to 50): + The reference number of denoising steps. More denoising steps usually lead to a higher quality image at + the expense of slower inference. This parameter will be modulated by `strength`, as explained above. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + is_cancelled_callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. If the function returns + `True`, the inference will be cancelled. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + image=image, + mask_image=mask_image, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + strength=strength, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + is_cancelled_callback=is_cancelled_callback, + callback_steps=callback_steps, + **kwargs, + ) diff --git a/diffusers/examples/community/lpw_stable_diffusion_onnx.py b/diffusers/examples/community/lpw_stable_diffusion_onnx.py new file mode 100644 index 0000000000000000000000000000000000000000..eae130867056cff0c630a865fa5b05ced4e1b3dd --- /dev/null +++ b/diffusers/examples/community/lpw_stable_diffusion_onnx.py @@ -0,0 +1,1147 @@ +import inspect +import re +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTokenizer + +import diffusers +from diffusers import OnnxRuntimeModel, OnnxStableDiffusionPipeline, SchedulerMixin +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.utils import deprecate, logging + + +try: + from diffusers.pipelines.onnx_utils import ORT_TO_NP_TYPE +except ImportError: + ORT_TO_NP_TYPE = { + "tensor(bool)": np.bool_, + "tensor(int8)": np.int8, + "tensor(uint8)": np.uint8, + "tensor(int16)": np.int16, + "tensor(uint16)": np.uint16, + "tensor(int32)": np.int32, + "tensor(uint32)": np.uint32, + "tensor(int64)": np.int64, + "tensor(uint64)": np.uint64, + "tensor(float16)": np.float16, + "tensor(float)": np.float32, + "tensor(double)": np.float64, + } + +try: + from diffusers.utils import PIL_INTERPOLATION +except ImportError: + if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } + else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +re_attention = re.compile( + r""" +\\\(| +\\\)| +\\\[| +\\]| +\\\\| +\\| +\(| +\[| +:([+-]?[.\d]+)\)| +\)| +]| +[^\\()\[\]:]+| +: +""", + re.X, +) + + +def parse_prompt_attention(text): + """ + Parses a string with attention tokens and returns a list of pairs: text and its associated weight. + Accepted tokens are: + (abc) - increases attention to abc by a multiplier of 1.1 + (abc:3.12) - increases attention to abc by a multiplier of 3.12 + [abc] - decreases attention to abc by a multiplier of 1.1 + \( - literal character '(' + \[ - literal character '[' + \) - literal character ')' + \] - literal character ']' + \\ - literal character '\' + anything else - just text + >>> parse_prompt_attention('normal text') + [['normal text', 1.0]] + >>> parse_prompt_attention('an (important) word') + [['an ', 1.0], ['important', 1.1], [' word', 1.0]] + >>> parse_prompt_attention('(unbalanced') + [['unbalanced', 1.1]] + >>> parse_prompt_attention('\(literal\]') + [['(literal]', 1.0]] + >>> parse_prompt_attention('(unnecessary)(parens)') + [['unnecessaryparens', 1.1]] + >>> parse_prompt_attention('a (((house:1.3)) [on] a (hill:0.5), sun, (((sky))).') + [['a ', 1.0], + ['house', 1.5730000000000004], + [' ', 1.1], + ['on', 1.0], + [' a ', 1.1], + ['hill', 0.55], + [', sun, ', 1.1], + ['sky', 1.4641000000000006], + ['.', 1.1]] + """ + + res = [] + round_brackets = [] + square_brackets = [] + + round_bracket_multiplier = 1.1 + square_bracket_multiplier = 1 / 1.1 + + def multiply_range(start_position, multiplier): + for p in range(start_position, len(res)): + res[p][1] *= multiplier + + for m in re_attention.finditer(text): + text = m.group(0) + weight = m.group(1) + + if text.startswith("\\"): + res.append([text[1:], 1.0]) + elif text == "(": + round_brackets.append(len(res)) + elif text == "[": + square_brackets.append(len(res)) + elif weight is not None and len(round_brackets) > 0: + multiply_range(round_brackets.pop(), float(weight)) + elif text == ")" and len(round_brackets) > 0: + multiply_range(round_brackets.pop(), round_bracket_multiplier) + elif text == "]" and len(square_brackets) > 0: + multiply_range(square_brackets.pop(), square_bracket_multiplier) + else: + res.append([text, 1.0]) + + for pos in round_brackets: + multiply_range(pos, round_bracket_multiplier) + + for pos in square_brackets: + multiply_range(pos, square_bracket_multiplier) + + if len(res) == 0: + res = [["", 1.0]] + + # merge runs of identical weights + i = 0 + while i + 1 < len(res): + if res[i][1] == res[i + 1][1]: + res[i][0] += res[i + 1][0] + res.pop(i + 1) + else: + i += 1 + + return res + + +def get_prompts_with_weights(pipe, prompt: List[str], max_length: int): + r""" + Tokenize a list of prompts and return its tokens with weights of each token. + + No padding, starting or ending token is included. + """ + tokens = [] + weights = [] + truncated = False + for text in prompt: + texts_and_weights = parse_prompt_attention(text) + text_token = [] + text_weight = [] + for word, weight in texts_and_weights: + # tokenize and discard the starting and the ending token + token = pipe.tokenizer(word, return_tensors="np").input_ids[0, 1:-1] + text_token += list(token) + # copy the weight by length of token + text_weight += [weight] * len(token) + # stop if the text is too long (longer than truncation limit) + if len(text_token) > max_length: + truncated = True + break + # truncate + if len(text_token) > max_length: + truncated = True + text_token = text_token[:max_length] + text_weight = text_weight[:max_length] + tokens.append(text_token) + weights.append(text_weight) + if truncated: + logger.warning("Prompt was truncated. Try to shorten the prompt or increase max_embeddings_multiples") + return tokens, weights + + +def pad_tokens_and_weights(tokens, weights, max_length, bos, eos, no_boseos_middle=True, chunk_length=77): + r""" + Pad the tokens (with starting and ending tokens) and weights (with 1.0) to max_length. + """ + max_embeddings_multiples = (max_length - 2) // (chunk_length - 2) + weights_length = max_length if no_boseos_middle else max_embeddings_multiples * chunk_length + for i in range(len(tokens)): + tokens[i] = [bos] + tokens[i] + [eos] * (max_length - 1 - len(tokens[i])) + if no_boseos_middle: + weights[i] = [1.0] + weights[i] + [1.0] * (max_length - 1 - len(weights[i])) + else: + w = [] + if len(weights[i]) == 0: + w = [1.0] * weights_length + else: + for j in range(max_embeddings_multiples): + w.append(1.0) # weight for starting token in this chunk + w += weights[i][j * (chunk_length - 2) : min(len(weights[i]), (j + 1) * (chunk_length - 2))] + w.append(1.0) # weight for ending token in this chunk + w += [1.0] * (weights_length - len(w)) + weights[i] = w[:] + + return tokens, weights + + +def get_unweighted_text_embeddings( + pipe, + text_input: np.array, + chunk_length: int, + no_boseos_middle: Optional[bool] = True, +): + """ + When the length of tokens is a multiple of the capacity of the text encoder, + it should be split into chunks and sent to the text encoder individually. + """ + max_embeddings_multiples = (text_input.shape[1] - 2) // (chunk_length - 2) + if max_embeddings_multiples > 1: + text_embeddings = [] + for i in range(max_embeddings_multiples): + # extract the i-th chunk + text_input_chunk = text_input[:, i * (chunk_length - 2) : (i + 1) * (chunk_length - 2) + 2].copy() + + # cover the head and the tail by the starting and the ending tokens + text_input_chunk[:, 0] = text_input[0, 0] + text_input_chunk[:, -1] = text_input[0, -1] + + text_embedding = pipe.text_encoder(input_ids=text_input_chunk)[0] + + if no_boseos_middle: + if i == 0: + # discard the ending token + text_embedding = text_embedding[:, :-1] + elif i == max_embeddings_multiples - 1: + # discard the starting token + text_embedding = text_embedding[:, 1:] + else: + # discard both starting and ending tokens + text_embedding = text_embedding[:, 1:-1] + + text_embeddings.append(text_embedding) + text_embeddings = np.concatenate(text_embeddings, axis=1) + else: + text_embeddings = pipe.text_encoder(input_ids=text_input)[0] + return text_embeddings + + +def get_weighted_text_embeddings( + pipe, + prompt: Union[str, List[str]], + uncond_prompt: Optional[Union[str, List[str]]] = None, + max_embeddings_multiples: Optional[int] = 4, + no_boseos_middle: Optional[bool] = False, + skip_parsing: Optional[bool] = False, + skip_weighting: Optional[bool] = False, + **kwargs, +): + r""" + Prompts can be assigned with local weights using brackets. For example, + prompt 'A (very beautiful) masterpiece' highlights the words 'very beautiful', + and the embedding tokens corresponding to the words get multiplied by a constant, 1.1. + + Also, to regularize of the embedding, the weighted embedding would be scaled to preserve the original mean. + + Args: + pipe (`OnnxStableDiffusionPipeline`): + Pipe to provide access to the tokenizer and the text encoder. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + uncond_prompt (`str` or `List[str]`): + The unconditional prompt or prompts for guide the image generation. If unconditional prompt + is provided, the embeddings of prompt and uncond_prompt are concatenated. + max_embeddings_multiples (`int`, *optional*, defaults to `1`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + no_boseos_middle (`bool`, *optional*, defaults to `False`): + If the length of text token is multiples of the capacity of text encoder, whether reserve the starting and + ending token in each of the chunk in the middle. + skip_parsing (`bool`, *optional*, defaults to `False`): + Skip the parsing of brackets. + skip_weighting (`bool`, *optional*, defaults to `False`): + Skip the weighting. When the parsing is skipped, it is forced True. + """ + max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2 + if isinstance(prompt, str): + prompt = [prompt] + + if not skip_parsing: + prompt_tokens, prompt_weights = get_prompts_with_weights(pipe, prompt, max_length - 2) + if uncond_prompt is not None: + if isinstance(uncond_prompt, str): + uncond_prompt = [uncond_prompt] + uncond_tokens, uncond_weights = get_prompts_with_weights(pipe, uncond_prompt, max_length - 2) + else: + prompt_tokens = [ + token[1:-1] + for token in pipe.tokenizer(prompt, max_length=max_length, truncation=True, return_tensors="np").input_ids + ] + prompt_weights = [[1.0] * len(token) for token in prompt_tokens] + if uncond_prompt is not None: + if isinstance(uncond_prompt, str): + uncond_prompt = [uncond_prompt] + uncond_tokens = [ + token[1:-1] + for token in pipe.tokenizer( + uncond_prompt, + max_length=max_length, + truncation=True, + return_tensors="np", + ).input_ids + ] + uncond_weights = [[1.0] * len(token) for token in uncond_tokens] + + # round up the longest length of tokens to a multiple of (model_max_length - 2) + max_length = max([len(token) for token in prompt_tokens]) + if uncond_prompt is not None: + max_length = max(max_length, max([len(token) for token in uncond_tokens])) + + max_embeddings_multiples = min( + max_embeddings_multiples, + (max_length - 1) // (pipe.tokenizer.model_max_length - 2) + 1, + ) + max_embeddings_multiples = max(1, max_embeddings_multiples) + max_length = (pipe.tokenizer.model_max_length - 2) * max_embeddings_multiples + 2 + + # pad the length of tokens and weights + bos = pipe.tokenizer.bos_token_id + eos = pipe.tokenizer.eos_token_id + prompt_tokens, prompt_weights = pad_tokens_and_weights( + prompt_tokens, + prompt_weights, + max_length, + bos, + eos, + no_boseos_middle=no_boseos_middle, + chunk_length=pipe.tokenizer.model_max_length, + ) + prompt_tokens = np.array(prompt_tokens, dtype=np.int32) + if uncond_prompt is not None: + uncond_tokens, uncond_weights = pad_tokens_and_weights( + uncond_tokens, + uncond_weights, + max_length, + bos, + eos, + no_boseos_middle=no_boseos_middle, + chunk_length=pipe.tokenizer.model_max_length, + ) + uncond_tokens = np.array(uncond_tokens, dtype=np.int32) + + # get the embeddings + text_embeddings = get_unweighted_text_embeddings( + pipe, + prompt_tokens, + pipe.tokenizer.model_max_length, + no_boseos_middle=no_boseos_middle, + ) + prompt_weights = np.array(prompt_weights, dtype=text_embeddings.dtype) + if uncond_prompt is not None: + uncond_embeddings = get_unweighted_text_embeddings( + pipe, + uncond_tokens, + pipe.tokenizer.model_max_length, + no_boseos_middle=no_boseos_middle, + ) + uncond_weights = np.array(uncond_weights, dtype=uncond_embeddings.dtype) + + # assign weights to the prompts and normalize in the sense of mean + # TODO: should we normalize by chunk or in a whole (current implementation)? + if (not skip_parsing) and (not skip_weighting): + previous_mean = text_embeddings.mean(axis=(-2, -1)) + text_embeddings *= prompt_weights[:, :, None] + text_embeddings *= (previous_mean / text_embeddings.mean(axis=(-2, -1)))[:, None, None] + if uncond_prompt is not None: + previous_mean = uncond_embeddings.mean(axis=(-2, -1)) + uncond_embeddings *= uncond_weights[:, :, None] + uncond_embeddings *= (previous_mean / uncond_embeddings.mean(axis=(-2, -1)))[:, None, None] + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + if uncond_prompt is not None: + return text_embeddings, uncond_embeddings + + return text_embeddings + + +def preprocess_image(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + return 2.0 * image - 1.0 + + +def preprocess_mask(mask, scale_factor=8): + mask = mask.convert("L") + w, h = mask.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = mask.resize((w // scale_factor, h // scale_factor), resample=PIL_INTERPOLATION["nearest"]) + mask = np.array(mask).astype(np.float32) / 255.0 + mask = np.tile(mask, (4, 1, 1)) + mask = mask[None].transpose(0, 1, 2, 3) # what does this step do? + mask = 1 - mask # repaint white, keep black + return mask + + +class OnnxStableDiffusionLongPromptWeightingPipeline(OnnxStableDiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion without tokens length limit, and support parsing + weighting in prompt. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + """ + if version.parse(version.parse(diffusers.__version__).base_version) >= version.parse("0.9.0"): + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: SchedulerMixin, + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + requires_safety_checker=requires_safety_checker, + ) + self.__init__additional__() + + else: + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: SchedulerMixin, + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.__init__additional__() + + def __init__additional__(self): + self.unet_in_channels = 4 + self.vae_scale_factor = 8 + + def _encode_prompt( + self, + prompt, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + max_embeddings_multiples, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `list(int)`): + prompt to be encoded + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + if negative_prompt is None: + negative_prompt = [""] * batch_size + elif isinstance(negative_prompt, str): + negative_prompt = [negative_prompt] * batch_size + if batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + + text_embeddings, uncond_embeddings = get_weighted_text_embeddings( + pipe=self, + prompt=prompt, + uncond_prompt=negative_prompt if do_classifier_free_guidance else None, + max_embeddings_multiples=max_embeddings_multiples, + ) + + text_embeddings = text_embeddings.repeat(num_images_per_prompt, 0) + if do_classifier_free_guidance: + uncond_embeddings = uncond_embeddings.repeat(num_images_per_prompt, 0) + text_embeddings = np.concatenate([uncond_embeddings, text_embeddings]) + + return text_embeddings + + def check_inputs(self, prompt, height, width, strength, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def get_timesteps(self, num_inference_steps, strength, is_text2img): + if is_text2img: + return self.scheduler.timesteps, num_inference_steps + else: + # get the original timestep using init_timestep + offset = self.scheduler.config.get("steps_offset", 0) + init_timestep = int(num_inference_steps * strength) + offset + init_timestep = min(init_timestep, num_inference_steps) + + t_start = max(num_inference_steps - init_timestep + offset, 0) + timesteps = self.scheduler.timesteps[t_start:] + return timesteps, num_inference_steps - t_start + + def run_safety_checker(self, image): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor( + self.numpy_to_pil(image), return_tensors="np" + ).pixel_values.astype(image.dtype) + # There will throw an error if use safety_checker directly and batchsize>1 + images, has_nsfw_concept = [], [] + for i in range(image.shape[0]): + image_i, has_nsfw_concept_i = self.safety_checker( + clip_input=safety_checker_input[i : i + 1], images=image[i : i + 1] + ) + images.append(image_i) + has_nsfw_concept.append(has_nsfw_concept_i[0]) + image = np.concatenate(images) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / 0.18215 * latents + # image = self.vae_decoder(latent_sample=latents)[0] + # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 + image = np.concatenate( + [self.vae_decoder(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])] + ) + image = np.clip(image / 2 + 0.5, 0, 1) + image = image.transpose((0, 2, 3, 1)) + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def prepare_latents(self, image, timestep, batch_size, height, width, dtype, generator, latents=None): + if image is None: + shape = ( + batch_size, + self.unet_in_channels, + height // self.vae_scale_factor, + width // self.vae_scale_factor, + ) + + if latents is None: + latents = torch.randn(shape, generator=generator, device="cpu").numpy().astype(dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + + # scale the initial noise by the standard deviation required by the scheduler + latents = (torch.from_numpy(latents) * self.scheduler.init_noise_sigma).numpy() + return latents, None, None + else: + init_latents = self.vae_encoder(sample=image)[0] + init_latents = 0.18215 * init_latents + init_latents = np.concatenate([init_latents] * batch_size, axis=0) + init_latents_orig = init_latents + shape = init_latents.shape + + # add noise to latents using the timesteps + noise = torch.randn(shape, generator=generator, device="cpu").numpy().astype(dtype) + latents = self.scheduler.add_noise( + torch.from_numpy(init_latents), torch.from_numpy(noise), timestep + ).numpy() + return latents, init_latents_orig, noise + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + image: Union[np.ndarray, PIL.Image.Image] = None, + mask_image: Union[np.ndarray, PIL.Image.Image] = None, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + strength: float = 0.8, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[np.ndarray] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + is_cancelled_callback: Optional[Callable[[], bool]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + mask_image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If it's a tensor, it should + contain one color channel (L) instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. + `image` will be used as a starting point, adding more noise to it the larger the `strength`. The + number of denoising steps depends on the amount of noise initially added. When `strength` is 1, added + noise will be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`np.ndarray`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + is_cancelled_callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. If the function returns + `True`, the inference will be cancelled. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + `None` if cancelled by `is_cancelled_callback`, + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, strength, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + text_embeddings = self._encode_prompt( + prompt, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + max_embeddings_multiples, + ) + dtype = text_embeddings.dtype + + # 4. Preprocess image and mask + if isinstance(image, PIL.Image.Image): + image = preprocess_image(image) + if image is not None: + image = image.astype(dtype) + if isinstance(mask_image, PIL.Image.Image): + mask_image = preprocess_mask(mask_image, self.vae_scale_factor) + if mask_image is not None: + mask = mask_image.astype(dtype) + mask = np.concatenate([mask] * batch_size * num_images_per_prompt) + else: + mask = None + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps) + timestep_dtype = next( + (input.type for input in self.unet.model.get_inputs() if input.name == "timestep"), "tensor(float)" + ) + timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, image is None) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + latents, init_latents_orig, noise = self.prepare_latents( + image, + latent_timestep, + batch_size * num_images_per_prompt, + height, + width, + dtype, + generator, + latents, + ) + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 8. Denoising loop + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(torch.from_numpy(latent_model_input), t) + latent_model_input = latent_model_input.numpy() + + # predict the noise residual + noise_pred = self.unet( + sample=latent_model_input, + timestep=np.array([t], dtype=timestep_dtype), + encoder_hidden_states=text_embeddings, + ) + noise_pred = noise_pred[0] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + scheduler_output = self.scheduler.step( + torch.from_numpy(noise_pred), t, torch.from_numpy(latents), **extra_step_kwargs + ) + latents = scheduler_output.prev_sample.numpy() + + if mask is not None: + # masking + init_latents_proper = self.scheduler.add_noise( + torch.from_numpy(init_latents_orig), + torch.from_numpy(noise), + t, + ).numpy() + latents = (init_latents_proper * mask) + (latents * (1 - mask)) + + # call the callback, if provided + if i % callback_steps == 0: + if callback is not None: + callback(i, t, latents) + if is_cancelled_callback is not None and is_cancelled_callback(): + return None + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return image, has_nsfw_concept + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) + + def text2img( + self, + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[np.ndarray] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for text-to-image generation. + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`np.ndarray`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + def img2img( + self, + image: Union[np.ndarray, PIL.Image.Image], + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for image-to-image generation. + Args: + image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or ndarray representing an image batch, that will be used as the starting point for the + process. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. + `image` will be used as a starting point, adding more noise to it the larger the `strength`. The + number of denoising steps depends on the amount of noise initially added. When `strength` is 1, added + noise will be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + image=image, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + strength=strength, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + def inpaint( + self, + image: Union[np.ndarray, PIL.Image.Image], + mask_image: Union[np.ndarray, PIL.Image.Image], + prompt: Union[str, List[str]], + negative_prompt: Optional[Union[str, List[str]]] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + max_embeddings_multiples: Optional[int] = 3, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function for inpaint. + Args: + image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. This is the image whose masked region will be inpainted. + mask_image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If it's a tensor, it should + contain one color channel (L) instead of 3, so the expected shape would be `(B, H, W, 1)`. + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to inpaint the masked area. Must be between 0 and 1. When `strength` + is 1, the denoising process will be run on the masked area for the full number of iterations specified + in `num_inference_steps`. `image` will be used as a reference for the masked area, adding more + noise to that region the larger the `strength`. If `strength` is 0, no inpainting will occur. + num_inference_steps (`int`, *optional*, defaults to 50): + The reference number of denoising steps. More denoising steps usually lead to a higher quality image at + the expense of slower inference. This parameter will be modulated by `strength`, as explained above. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + max_embeddings_multiples (`int`, *optional*, defaults to `3`): + The max multiple length of prompt embeddings compared to the max output length of text encoder. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + return self.__call__( + prompt=prompt, + negative_prompt=negative_prompt, + image=image, + mask_image=mask_image, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + strength=strength, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + max_embeddings_multiples=max_embeddings_multiples, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) diff --git a/diffusers/examples/community/magic_mix.py b/diffusers/examples/community/magic_mix.py new file mode 100644 index 0000000000000000000000000000000000000000..b1d69ec8457617653a4dcb17f0bb2b5b0313dd87 --- /dev/null +++ b/diffusers/examples/community/magic_mix.py @@ -0,0 +1,152 @@ +from typing import Union + +import torch +from PIL import Image +from torchvision import transforms as tfms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DiffusionPipeline, + LMSDiscreteScheduler, + PNDMScheduler, + UNet2DConditionModel, +) + + +class MagicMixPipeline(DiffusionPipeline): + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[PNDMScheduler, LMSDiscreteScheduler, DDIMScheduler], + ): + super().__init__() + + self.register_modules(vae=vae, text_encoder=text_encoder, tokenizer=tokenizer, unet=unet, scheduler=scheduler) + + # convert PIL image to latents + def encode(self, img): + with torch.no_grad(): + latent = self.vae.encode(tfms.ToTensor()(img).unsqueeze(0).to(self.device) * 2 - 1) + latent = 0.18215 * latent.latent_dist.sample() + return latent + + # convert latents to PIL image + def decode(self, latent): + latent = (1 / 0.18215) * latent + with torch.no_grad(): + img = self.vae.decode(latent).sample + img = (img / 2 + 0.5).clamp(0, 1) + img = img.detach().cpu().permute(0, 2, 3, 1).numpy() + img = (img * 255).round().astype("uint8") + return Image.fromarray(img[0]) + + # convert prompt into text embeddings, also unconditional embeddings + def prep_text(self, prompt): + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + + text_embedding = self.text_encoder(text_input.input_ids.to(self.device))[0] + + uncond_input = self.tokenizer( + "", + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + + uncond_embedding = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + return torch.cat([uncond_embedding, text_embedding]) + + def __call__( + self, + img: Image.Image, + prompt: str, + kmin: float = 0.3, + kmax: float = 0.6, + mix_factor: float = 0.5, + seed: int = 42, + steps: int = 50, + guidance_scale: float = 7.5, + ) -> Image.Image: + tmin = steps - int(kmin * steps) + tmax = steps - int(kmax * steps) + + text_embeddings = self.prep_text(prompt) + + self.scheduler.set_timesteps(steps) + + width, height = img.size + encoded = self.encode(img) + + torch.manual_seed(seed) + noise = torch.randn( + (1, self.unet.in_channels, height // 8, width // 8), + ).to(self.device) + + latents = self.scheduler.add_noise( + encoded, + noise, + timesteps=self.scheduler.timesteps[tmax], + ) + + input = torch.cat([latents] * 2) + + input = self.scheduler.scale_model_input(input, self.scheduler.timesteps[tmax]) + + with torch.no_grad(): + pred = self.unet( + input, + self.scheduler.timesteps[tmax], + encoder_hidden_states=text_embeddings, + ).sample + + pred_uncond, pred_text = pred.chunk(2) + pred = pred_uncond + guidance_scale * (pred_text - pred_uncond) + + latents = self.scheduler.step(pred, self.scheduler.timesteps[tmax], latents).prev_sample + + for i, t in enumerate(tqdm(self.scheduler.timesteps)): + if i > tmax: + if i < tmin: # layout generation phase + orig_latents = self.scheduler.add_noise( + encoded, + noise, + timesteps=t, + ) + + input = (mix_factor * latents) + ( + 1 - mix_factor + ) * orig_latents # interpolating between layout noise and conditionally generated noise to preserve layout sematics + input = torch.cat([input] * 2) + + else: # content generation phase + input = torch.cat([latents] * 2) + + input = self.scheduler.scale_model_input(input, t) + + with torch.no_grad(): + pred = self.unet( + input, + t, + encoder_hidden_states=text_embeddings, + ).sample + + pred_uncond, pred_text = pred.chunk(2) + pred = pred_uncond + guidance_scale * (pred_text - pred_uncond) + + latents = self.scheduler.step(pred, t, latents).prev_sample + + return self.decode(latents) diff --git a/diffusers/examples/community/multilingual_stable_diffusion.py b/diffusers/examples/community/multilingual_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..587ec3be01fb8df2dd1be4ca66fa44014323980e --- /dev/null +++ b/diffusers/examples/community/multilingual_stable_diffusion.py @@ -0,0 +1,436 @@ +import inspect +from typing import Callable, List, Optional, Union + +import torch +from transformers import ( + CLIPFeatureExtractor, + CLIPTextModel, + CLIPTokenizer, + MBart50TokenizerFast, + MBartForConditionalGeneration, + pipeline, +) + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def detect_language(pipe, prompt, batch_size): + """helper function to detect language(s) of prompt""" + + if batch_size == 1: + preds = pipe(prompt, top_k=1, truncation=True, max_length=128) + return preds[0]["label"] + else: + detected_languages = [] + for p in prompt: + preds = pipe(p, top_k=1, truncation=True, max_length=128) + detected_languages.append(preds[0]["label"]) + + return detected_languages + + +def translate_prompt(prompt, translation_tokenizer, translation_model, device): + """helper function to translate prompt to English""" + + encoded_prompt = translation_tokenizer(prompt, return_tensors="pt").to(device) + generated_tokens = translation_model.generate(**encoded_prompt, max_new_tokens=1000) + en_trans = translation_tokenizer.batch_decode(generated_tokens, skip_special_tokens=True) + + return en_trans[0] + + +class MultilingualStableDiffusion(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion in different languages. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + detection_pipeline ([`pipeline`]): + Transformers pipeline to detect prompt's language. + translation_model ([`MBartForConditionalGeneration`]): + Model to translate prompt to English, if necessary. Please refer to the + [model card](https://huggingface.co/docs/transformers/model_doc/mbart) for details. + translation_tokenizer ([`MBart50TokenizerFast`]): + Tokenizer of the translation model. + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latens. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + detection_pipeline: pipeline, + translation_model: MBartForConditionalGeneration, + translation_tokenizer: MBart50TokenizerFast, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + detection_pipeline=detection_pipeline, + translation_model=translation_model, + translation_tokenizer=translation_tokenizer, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. Can be in different languages. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # detect language and translate if necessary + prompt_language = detect_language(self.detection_pipeline, prompt, batch_size) + if batch_size == 1 and prompt_language != "en": + prompt = translate_prompt(prompt, self.translation_tokenizer, self.translation_model, self.device) + + if isinstance(prompt, list): + for index in range(batch_size): + if prompt_language[index] != "en": + p = translate_prompt( + prompt[index], self.translation_tokenizer, self.translation_model, self.device + ) + prompt[index] = p + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + # detect language and translate it if necessary + negative_prompt_language = detect_language(self.detection_pipeline, negative_prompt, batch_size) + if negative_prompt_language != "en": + negative_prompt = translate_prompt( + negative_prompt, self.translation_tokenizer, self.translation_model, self.device + ) + if isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + # detect language and translate it if necessary + if isinstance(negative_prompt, list): + negative_prompt_languages = detect_language(self.detection_pipeline, negative_prompt, batch_size) + for index in range(batch_size): + if negative_prompt_languages[index] != "en": + p = translate_prompt( + negative_prompt[index], self.translation_tokenizer, self.translation_model, self.device + ) + negative_prompt[index] = p + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/one_step_unet.py b/diffusers/examples/community/one_step_unet.py new file mode 100644 index 0000000000000000000000000000000000000000..f3eaf1e0eb7a4efd7b2a2839954eaaacbc399b41 --- /dev/null +++ b/diffusers/examples/community/one_step_unet.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import torch + +from diffusers import DiffusionPipeline + + +class UnetSchedulerOneForwardPipeline(DiffusionPipeline): + def __init__(self, unet, scheduler): + super().__init__() + + self.register_modules(unet=unet, scheduler=scheduler) + + def __call__(self): + image = torch.randn( + (1, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + ) + timestep = 1 + + model_output = self.unet(image, timestep).sample + scheduler_output = self.scheduler.step(model_output, timestep, image).prev_sample + + result = scheduler_output - scheduler_output + torch.ones_like(scheduler_output) + + return result diff --git a/diffusers/examples/community/sd_text2img_k_diffusion.py b/diffusers/examples/community/sd_text2img_k_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..c68162475cc4e140da3218a9b1854174d50bd71b --- /dev/null +++ b/diffusers/examples/community/sd_text2img_k_diffusion.py @@ -0,0 +1,475 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import importlib +import warnings +from typing import Callable, List, Optional, Union + +import torch +from k_diffusion.external import CompVisDenoiser, CompVisVDenoiser + +from diffusers import DiffusionPipeline, LMSDiscreteScheduler +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.utils import is_accelerate_available, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class ModelWrapper: + def __init__(self, model, alphas_cumprod): + self.model = model + self.alphas_cumprod = alphas_cumprod + + def apply_model(self, *args, **kwargs): + if len(args) == 3: + encoder_hidden_states = args[-1] + args = args[:2] + if kwargs.get("cond", None) is not None: + encoder_hidden_states = kwargs.pop("cond") + return self.model(*args, encoder_hidden_states=encoder_hidden_states, **kwargs).sample + + +class StableDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae, + text_encoder, + tokenizer, + unet, + scheduler, + safety_checker, + feature_extractor, + ): + super().__init__() + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + # get correct sigmas from LMS + scheduler = LMSDiscreteScheduler.from_config(scheduler.config) + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + model = ModelWrapper(unet, scheduler.alphas_cumprod) + if scheduler.prediction_type == "v_prediction": + self.k_diffusion_model = CompVisVDenoiser(model) + else: + self.k_diffusion_model = CompVisDenoiser(model) + + def set_sampler(self, scheduler_type: str): + warnings.warn("The `set_sampler` method is deprecated, please use `set_scheduler` instead.") + return self.set_scheduler(scheduler_type) + + def set_scheduler(self, scheduler_type: str): + library = importlib.import_module("k_diffusion") + sampling = getattr(library, "sampling") + self.sampler = getattr(sampling, scheduler_type) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae, self.safety_checker]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `list(int)`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="pt").input_ids + + if not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + text_embeddings = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + text_embeddings = text_embeddings[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + uncond_embeddings = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + uncond_embeddings = uncond_embeddings[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + return text_embeddings + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def check_inputs(self, prompt, height, width, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // 8, width // 8) + if latents is None: + if device.type == "mps": + # randn does not work reproducibly on mps + latents = torch.randn(shape, generator=generator, device="cpu", dtype=dtype).to(device) + else: + latents = torch.randn(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = True + if guidance_scale <= 1.0: + raise ValueError("has to use guidance_scale") + + # 3. Encode input prompt + text_embeddings = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=text_embeddings.device) + sigmas = self.scheduler.sigmas + sigmas = sigmas.to(text_embeddings.dtype) + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + text_embeddings.dtype, + device, + generator, + latents, + ) + latents = latents * sigmas[0] + self.k_diffusion_model.sigmas = self.k_diffusion_model.sigmas.to(latents.device) + self.k_diffusion_model.log_sigmas = self.k_diffusion_model.log_sigmas.to(latents.device) + + def model_fn(x, t): + latent_model_input = torch.cat([x] * 2) + + noise_pred = self.k_diffusion_model(latent_model_input, t, cond=text_embeddings) + + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + return noise_pred + + latents = self.sampler(model_fn, latents, sigmas) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, text_embeddings.dtype) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/seed_resize_stable_diffusion.py b/diffusers/examples/community/seed_resize_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..293a4adf92ca251ac6f1f38e36a063b257d0a4af --- /dev/null +++ b/diffusers/examples/community/seed_resize_stable_diffusion.py @@ -0,0 +1,366 @@ +""" + modified based on diffusion library from Huggingface: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py +""" +import inspect +from typing import Callable, List, Optional, Union + +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class SeedResizeStableDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + text_embeddings: Optional[torch.FloatTensor] = None, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + + if text_embeddings is None: + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(batch_size, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_shape_reference = (batch_size * num_images_per_prompt, self.unet.in_channels, 64, 64) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not exist on mps + latents_reference = torch.randn( + latents_shape_reference, generator=generator, device="cpu", dtype=latents_dtype + ).to(self.device) + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents_reference = torch.randn( + latents_shape_reference, generator=generator, device=self.device, dtype=latents_dtype + ) + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents_reference.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents_reference = latents_reference.to(self.device) + latents = latents.to(self.device) + + # This is the key part of the pipeline where we + # try to ensure that the generated images w/ the same seed + # but different sizes actually result in similar images + dx = (latents_shape[3] - latents_shape_reference[3]) // 2 + dy = (latents_shape[2] - latents_shape_reference[2]) // 2 + w = latents_shape_reference[3] if dx >= 0 else latents_shape_reference[3] + 2 * dx + h = latents_shape_reference[2] if dy >= 0 else latents_shape_reference[2] + 2 * dy + tx = 0 if dx < 0 else dx + ty = 0 if dy < 0 else dy + dx = max(-dx, 0) + dy = max(-dy, 0) + # import pdb + # pdb.set_trace() + latents[:, :, ty : ty + h, tx : tx + w] = latents_reference[:, :, dy : dy + h, dx : dx + w] + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/examples/community/speech_to_image_diffusion.py b/diffusers/examples/community/speech_to_image_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..b22418fb39cc182b68055cba62e13ece99c9e69e --- /dev/null +++ b/diffusers/examples/community/speech_to_image_diffusion.py @@ -0,0 +1,261 @@ +import inspect +from typing import Callable, List, Optional, Union + +import torch +from transformers import ( + CLIPFeatureExtractor, + CLIPTextModel, + CLIPTokenizer, + WhisperForConditionalGeneration, + WhisperProcessor, +) + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DiffusionPipeline, + LMSDiscreteScheduler, + PNDMScheduler, + UNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.utils import logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class SpeechToImagePipeline(DiffusionPipeline): + def __init__( + self, + speech_model: WhisperForConditionalGeneration, + speech_processor: WhisperProcessor, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + speech_model=speech_model, + speech_processor=speech_processor, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + if slice_size == "auto": + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + self.enable_attention_slicing(None) + + @torch.no_grad() + def __call__( + self, + audio, + sampling_rate=16_000, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + inputs = self.speech_processor.feature_extractor( + audio, return_tensors="pt", sampling_rate=sampling_rate + ).input_features.to(self.device) + predicted_ids = self.speech_model.generate(inputs, max_length=480_000) + + prompt = self.speech_processor.tokenizer.batch_decode(predicted_ids, skip_special_tokens=True, normalize=True)[ + 0 + ] + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not exist on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return image + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=None) diff --git a/diffusers/examples/community/stable_diffusion_comparison.py b/diffusers/examples/community/stable_diffusion_comparison.py new file mode 100644 index 0000000000000000000000000000000000000000..d7e6138da12f1be92a4f1122476b4e9b3c948655 --- /dev/null +++ b/diffusers/examples/community/stable_diffusion_comparison.py @@ -0,0 +1,405 @@ +from typing import Any, Callable, Dict, List, Optional, Union + +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DiffusionPipeline, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker + + +pipe1_model_id = "CompVis/stable-diffusion-v1-1" +pipe2_model_id = "CompVis/stable-diffusion-v1-2" +pipe3_model_id = "CompVis/stable-diffusion-v1-3" +pipe4_model_id = "CompVis/stable-diffusion-v1-4" + + +class StableDiffusionComparisonPipeline(DiffusionPipeline): + r""" + Pipeline for parallel comparison of Stable Diffusion v1-v4 + This pipeline inherits from DiffusionPipeline and depends on the use of an Auth Token for + downloading pre-trained checkpoints from Hugging Face Hub. + If using Hugging Face Hub, pass the Model ID for Stable Diffusion v1.4 as the previous 3 checkpoints will be loaded + automatically. + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionMegaSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super()._init_() + + self.pipe1 = StableDiffusionPipeline.from_pretrained(pipe1_model_id) + self.pipe2 = StableDiffusionPipeline.from_pretrained(pipe2_model_id) + self.pipe3 = StableDiffusionPipeline.from_pretrained(pipe3_model_id) + self.pipe4 = StableDiffusionPipeline( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + requires_safety_checker=requires_safety_checker, + ) + + self.register_modules(pipeline1=self.pipe1, pipeline2=self.pipe2, pipeline3=self.pipe3, pipeline4=self.pipe4) + + @property + def layers(self) -> Dict[str, Any]: + return {k: getattr(self, k) for k in self.config.keys() if not k.startswith("_")} + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def text2img_sd1_1( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + return self.pipe1( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + @torch.no_grad() + def text2img_sd1_2( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + return self.pipe2( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + @torch.no_grad() + def text2img_sd1_3( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + return self.pipe3( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + @torch.no_grad() + def text2img_sd1_4( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + return self.pipe4( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + @torch.no_grad() + def _call_( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. This function will generate 4 results as part + of running all the 4 pipelines for SD1.1-1.4 together in a serial-processing, parallel-invocation fashion. + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, optional, defaults to 512): + The height in pixels of the generated image. + width (`int`, optional, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, optional, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, optional, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + eta (`float`, optional, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, optional): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, optional): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, optional, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, optional, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + device = "cuda" if torch.cuda.is_available() else "cpu" + self.to(device) + + # Checks if the height and width are divisible by 8 or not + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` must be divisible by 8 but are {height} and {width}.") + + # Get first result from Stable Diffusion Checkpoint v1.1 + res1 = self.text2img_sd1_1( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + # Get first result from Stable Diffusion Checkpoint v1.2 + res2 = self.text2img_sd1_2( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + # Get first result from Stable Diffusion Checkpoint v1.3 + res3 = self.text2img_sd1_3( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + # Get first result from Stable Diffusion Checkpoint v1.4 + res4 = self.text2img_sd1_4( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + **kwargs, + ) + + # Get all result images into a single list and pass it via StableDiffusionPipelineOutput for final result + return StableDiffusionPipelineOutput([res1[0], res2[0], res3[0], res4[0]]) diff --git a/diffusers/examples/community/stable_diffusion_mega.py b/diffusers/examples/community/stable_diffusion_mega.py new file mode 100644 index 0000000000000000000000000000000000000000..44b54dd5208d511f9c3682f0f8578f9932b48d6b --- /dev/null +++ b/diffusers/examples/community/stable_diffusion_mega.py @@ -0,0 +1,227 @@ +from typing import Any, Callable, Dict, List, Optional, Union + +import PIL.Image +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DiffusionPipeline, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipelineLegacy, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.configuration_utils import FrozenDict +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.utils import deprecate, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class StableDiffusionMegaPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionMegaSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + @property + def components(self) -> Dict[str, Any]: + return {k: getattr(self, k) for k in self.config.keys() if not k.startswith("_")} + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + @torch.no_grad() + def inpaint( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image], + mask_image: Union[torch.FloatTensor, PIL.Image.Image], + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + # For more information on how this function works, please see: https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion#diffusers.StableDiffusionImg2ImgPipeline + return StableDiffusionInpaintPipelineLegacy(**self.components)( + prompt=prompt, + image=image, + mask_image=mask_image, + strength=strength, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + output_type=output_type, + return_dict=return_dict, + callback=callback, + ) + + @torch.no_grad() + def img2img( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image], + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[torch.Generator] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + # For more information on how this function works, please see: https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion#diffusers.StableDiffusionImg2ImgPipeline + return StableDiffusionImg2ImgPipeline(**self.components)( + prompt=prompt, + image=image, + strength=strength, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) + + @torch.no_grad() + def text2img( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + # For more information on how this function https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion#diffusers.StableDiffusionPipeline + return StableDiffusionPipeline(**self.components)( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) diff --git a/diffusers/examples/community/stable_unclip.py b/diffusers/examples/community/stable_unclip.py new file mode 100644 index 0000000000000000000000000000000000000000..8ff9c44d19fdbf365fb80bbfabb0af1910689089 --- /dev/null +++ b/diffusers/examples/community/stable_unclip.py @@ -0,0 +1,287 @@ +import types +from typing import List, Optional, Tuple, Union + +import torch +from transformers import CLIPTextModelWithProjection, CLIPTokenizer +from transformers.models.clip.modeling_clip import CLIPTextModelOutput + +from diffusers.models import PriorTransformer +from diffusers.pipelines import DiffusionPipeline, StableDiffusionImageVariationPipeline +from diffusers.schedulers import UnCLIPScheduler +from diffusers.utils import logging, randn_tensor + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def _encode_image(self, image, device, num_images_per_prompt, do_classifier_free_guidance): + image = image.to(device=device) + image_embeddings = image # take image as image_embeddings + image_embeddings = image_embeddings.unsqueeze(1) + + # duplicate image embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = image_embeddings.shape + image_embeddings = image_embeddings.repeat(1, num_images_per_prompt, 1) + image_embeddings = image_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + if do_classifier_free_guidance: + uncond_embeddings = torch.zeros_like(image_embeddings) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + image_embeddings = torch.cat([uncond_embeddings, image_embeddings]) + + return image_embeddings + + +class StableUnCLIPPipeline(DiffusionPipeline): + def __init__( + self, + prior: PriorTransformer, + tokenizer: CLIPTokenizer, + text_encoder: CLIPTextModelWithProjection, + prior_scheduler: UnCLIPScheduler, + decoder_pipe_kwargs: Optional[dict] = None, + ): + super().__init__() + + decoder_pipe_kwargs = dict(image_encoder=None) if decoder_pipe_kwargs is None else decoder_pipe_kwargs + + decoder_pipe_kwargs["torch_dtype"] = decoder_pipe_kwargs.get("torch_dtype", None) or prior.dtype + + self.decoder_pipe = StableDiffusionImageVariationPipeline.from_pretrained( + "lambdalabs/sd-image-variations-diffusers", **decoder_pipe_kwargs + ) + + # replace `_encode_image` method + self.decoder_pipe._encode_image = types.MethodType(_encode_image, self.decoder_pipe) + + self.register_modules( + prior=prior, + tokenizer=tokenizer, + text_encoder=text_encoder, + prior_scheduler=prior_scheduler, + ) + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + text_model_output: Optional[Union[CLIPTextModelOutput, Tuple]] = None, + text_attention_mask: Optional[torch.Tensor] = None, + ): + if text_model_output is None: + batch_size = len(prompt) if isinstance(prompt, list) else 1 + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + text_mask = text_inputs.attention_mask.bool().to(device) + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + + text_encoder_output = self.text_encoder(text_input_ids.to(device)) + + text_embeddings = text_encoder_output.text_embeds + text_encoder_hidden_states = text_encoder_output.last_hidden_state + + else: + batch_size = text_model_output[0].shape[0] + text_embeddings, text_encoder_hidden_states = text_model_output[0], text_model_output[1] + text_mask = text_attention_mask + + text_embeddings = text_embeddings.repeat_interleave(num_images_per_prompt, dim=0) + text_encoder_hidden_states = text_encoder_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + text_mask = text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + if do_classifier_free_guidance: + uncond_tokens = [""] * batch_size + + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + uncond_text_mask = uncond_input.attention_mask.bool().to(device) + uncond_embeddings_text_encoder_output = self.text_encoder(uncond_input.input_ids.to(device)) + + uncond_embeddings = uncond_embeddings_text_encoder_output.text_embeds + uncond_text_encoder_hidden_states = uncond_embeddings_text_encoder_output.last_hidden_state + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len) + + seq_len = uncond_text_encoder_hidden_states.shape[1] + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.repeat(1, num_images_per_prompt, 1) + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.view( + batch_size * num_images_per_prompt, seq_len, -1 + ) + uncond_text_mask = uncond_text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + # done duplicates + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + text_encoder_hidden_states = torch.cat([uncond_text_encoder_hidden_states, text_encoder_hidden_states]) + + text_mask = torch.cat([uncond_text_mask, text_mask]) + + return text_embeddings, text_encoder_hidden_states, text_mask + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.prior, "_hf_hook"): + return self.device + for module in self.prior.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def prepare_latents(self, shape, dtype, device, generator, latents, scheduler): + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + latents = latents * scheduler.init_noise_sigma + return latents + + def to(self, torch_device: Optional[Union[str, torch.device]] = None): + self.decoder_pipe.to(torch_device) + super().to(torch_device) + + @torch.no_grad() + def __call__( + self, + prompt: Optional[Union[str, List[str]]] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_images_per_prompt: int = 1, + prior_num_inference_steps: int = 25, + generator: Optional[torch.Generator] = None, + prior_latents: Optional[torch.FloatTensor] = None, + text_model_output: Optional[Union[CLIPTextModelOutput, Tuple]] = None, + text_attention_mask: Optional[torch.Tensor] = None, + prior_guidance_scale: float = 4.0, + decoder_guidance_scale: float = 8.0, + decoder_num_inference_steps: int = 50, + decoder_num_images_per_prompt: Optional[int] = 1, + decoder_eta: float = 0.0, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ): + if prompt is not None: + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + else: + batch_size = text_model_output[0].shape[0] + + device = self._execution_device + + batch_size = batch_size * num_images_per_prompt + + do_classifier_free_guidance = prior_guidance_scale > 1.0 or decoder_guidance_scale > 1.0 + + text_embeddings, text_encoder_hidden_states, text_mask = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, text_model_output, text_attention_mask + ) + + # prior + + self.prior_scheduler.set_timesteps(prior_num_inference_steps, device=device) + prior_timesteps_tensor = self.prior_scheduler.timesteps + + embedding_dim = self.prior.config.embedding_dim + + prior_latents = self.prepare_latents( + (batch_size, embedding_dim), + text_embeddings.dtype, + device, + generator, + prior_latents, + self.prior_scheduler, + ) + + for i, t in enumerate(self.progress_bar(prior_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([prior_latents] * 2) if do_classifier_free_guidance else prior_latents + + predicted_image_embedding = self.prior( + latent_model_input, + timestep=t, + proj_embedding=text_embeddings, + encoder_hidden_states=text_encoder_hidden_states, + attention_mask=text_mask, + ).predicted_image_embedding + + if do_classifier_free_guidance: + predicted_image_embedding_uncond, predicted_image_embedding_text = predicted_image_embedding.chunk(2) + predicted_image_embedding = predicted_image_embedding_uncond + prior_guidance_scale * ( + predicted_image_embedding_text - predicted_image_embedding_uncond + ) + + if i + 1 == prior_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = prior_timesteps_tensor[i + 1] + + prior_latents = self.prior_scheduler.step( + predicted_image_embedding, + timestep=t, + sample=prior_latents, + generator=generator, + prev_timestep=prev_timestep, + ).prev_sample + + prior_latents = self.prior.post_process_latents(prior_latents) + + image_embeddings = prior_latents + + output = self.decoder_pipe( + image=image_embeddings, + height=height, + width=width, + num_inference_steps=decoder_num_inference_steps, + guidance_scale=decoder_guidance_scale, + generator=generator, + output_type=output_type, + return_dict=return_dict, + num_images_per_prompt=decoder_num_images_per_prompt, + eta=decoder_eta, + ) + return output diff --git a/diffusers/examples/community/text_inpainting.py b/diffusers/examples/community/text_inpainting.py new file mode 100644 index 0000000000000000000000000000000000000000..12598138fffcf90459cb489f550a4b079b8aeea0 --- /dev/null +++ b/diffusers/examples/community/text_inpainting.py @@ -0,0 +1,302 @@ +from typing import Callable, List, Optional, Union + +import PIL +import torch +from transformers import ( + CLIPFeatureExtractor, + CLIPSegForImageSegmentation, + CLIPSegProcessor, + CLIPTextModel, + CLIPTokenizer, +) + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion import StableDiffusionInpaintPipeline +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, is_accelerate_available, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class TextInpainting(DiffusionPipeline): + r""" + Pipeline for text based inpainting using Stable Diffusion. + Uses CLIPSeg to get a mask from the given text, then calls the Inpainting pipeline with the generated mask + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + segmentation_model ([`CLIPSegForImageSegmentation`]): + CLIPSeg Model to generate mask from the given text. Please refer to the [model card]() for details. + segmentation_processor ([`CLIPSegProcessor`]): + CLIPSeg processor to get image, text features to translate prompt to English, if necessary. Please refer to the + [model card](https://huggingface.co/docs/transformers/model_doc/clipseg) for details. + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latens. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + segmentation_model: CLIPSegForImageSegmentation, + segmentation_processor: CLIPSegProcessor, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "skip_prk_steps") and scheduler.config.skip_prk_steps is False: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration" + " `skip_prk_steps`. `skip_prk_steps` should be set to True in the configuration file. Please make" + " sure to update the config accordingly as not setting `skip_prk_steps` in the config might lead to" + " incorrect results in future versions. If you have downloaded this checkpoint from the Hugging Face" + " Hub, it would be very nice if you could open a Pull request for the" + " `scheduler/scheduler_config.json` file" + ) + deprecate("skip_prk_steps not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["skip_prk_steps"] = True + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + segmentation_model=segmentation_model, + segmentation_processor=segmentation_processor, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + a number is provided, uses as many slices as `attention_head_dim // slice_size`. In this case, + `attention_head_dim` must be a multiple of `slice_size`. + """ + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = self.unet.config.attention_head_dim // 2 + self.unet.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + def enable_sequential_cpu_offload(self): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device("cuda") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae, self.safety_checker]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image], + text: str, + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be inpainted, *i.e.* parts of the image will + be masked out with `mask_image` and repainted according to `prompt`. + text (`str``): + The text to use to generate the mask. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + # We use the input text to generate the mask + inputs = self.segmentation_processor( + text=[text], images=[image], padding="max_length", return_tensors="pt" + ).to(self.device) + outputs = self.segmentation_model(**inputs) + mask = torch.sigmoid(outputs.logits).cpu().detach().unsqueeze(-1).numpy() + mask_pil = self.numpy_to_pil(mask)[0].resize(image.size) + + # Run inpainting pipeline with the generated mask + inpainting_pipeline = StableDiffusionInpaintPipeline( + vae=self.vae, + text_encoder=self.text_encoder, + tokenizer=self.tokenizer, + unet=self.unet, + scheduler=self.scheduler, + safety_checker=self.safety_checker, + feature_extractor=self.feature_extractor, + ) + return inpainting_pipeline( + prompt=prompt, + image=image, + mask_image=mask_pil, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) diff --git a/diffusers/examples/community/tiled_upscaling.py b/diffusers/examples/community/tiled_upscaling.py new file mode 100644 index 0000000000000000000000000000000000000000..8dc92f5ae8180b0c0914f97f73869626ab69bed6 --- /dev/null +++ b/diffusers/examples/community/tiled_upscaling.py @@ -0,0 +1,298 @@ +# Copyright 2022 Peter Willemsen . All rights reserved. +# +# 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. + +import math +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from PIL import Image +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_upscale import StableDiffusionUpscalePipeline +from diffusers.schedulers import DDIMScheduler, DDPMScheduler, LMSDiscreteScheduler, PNDMScheduler + + +def make_transparency_mask(size, overlap_pixels, remove_borders=[]): + size_x = size[0] - overlap_pixels * 2 + size_y = size[1] - overlap_pixels * 2 + for letter in ["l", "r"]: + if letter in remove_borders: + size_x += overlap_pixels + for letter in ["t", "b"]: + if letter in remove_borders: + size_y += overlap_pixels + mask = np.ones((size_y, size_x), dtype=np.uint8) * 255 + mask = np.pad(mask, mode="linear_ramp", pad_width=overlap_pixels, end_values=0) + + if "l" in remove_borders: + mask = mask[:, overlap_pixels : mask.shape[1]] + if "r" in remove_borders: + mask = mask[:, 0 : mask.shape[1] - overlap_pixels] + if "t" in remove_borders: + mask = mask[overlap_pixels : mask.shape[0], :] + if "b" in remove_borders: + mask = mask[0 : mask.shape[0] - overlap_pixels, :] + return mask + + +def clamp(n, smallest, largest): + return max(smallest, min(n, largest)) + + +def clamp_rect(rect: [int], min: [int], max: [int]): + return ( + clamp(rect[0], min[0], max[0]), + clamp(rect[1], min[1], max[1]), + clamp(rect[2], min[0], max[0]), + clamp(rect[3], min[1], max[1]), + ) + + +def add_overlap_rect(rect: [int], overlap: int, image_size: [int]): + rect = list(rect) + rect[0] -= overlap + rect[1] -= overlap + rect[2] += overlap + rect[3] += overlap + rect = clamp_rect(rect, [0, 0], [image_size[0], image_size[1]]) + return rect + + +def squeeze_tile(tile, original_image, original_slice, slice_x): + result = Image.new("RGB", (tile.size[0] + original_slice, tile.size[1])) + result.paste( + original_image.resize((tile.size[0], tile.size[1]), Image.BICUBIC).crop( + (slice_x, 0, slice_x + original_slice, tile.size[1]) + ), + (0, 0), + ) + result.paste(tile, (original_slice, 0)) + return result + + +def unsqueeze_tile(tile, original_image_slice): + crop_rect = (original_image_slice * 4, 0, tile.size[0], tile.size[1]) + tile = tile.crop(crop_rect) + return tile + + +def next_divisible(n, d): + divisor = n % d + return n - divisor + + +class StableDiffusionTiledUpscalePipeline(StableDiffusionUpscalePipeline): + r""" + Pipeline for tile-based text-guided image super-resolution using Stable Diffusion 2, trading memory for compute + to create gigantic images. + + This model inherits from [`StableDiffusionUpscalePipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + low_res_scheduler ([`SchedulerMixin`]): + A scheduler used to add initial noise to the low res conditioning image. It must be an instance of + [`DDPMScheduler`]. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + low_res_scheduler: DDPMScheduler, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + max_noise_level: int = 350, + ): + super().__init__( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + low_res_scheduler=low_res_scheduler, + scheduler=scheduler, + max_noise_level=max_noise_level, + ) + + def _process_tile(self, original_image_slice, x, y, tile_size, tile_border, image, final_image, **kwargs): + torch.manual_seed(0) + crop_rect = ( + min(image.size[0] - (tile_size + original_image_slice), x * tile_size), + min(image.size[1] - (tile_size + original_image_slice), y * tile_size), + min(image.size[0], (x + 1) * tile_size), + min(image.size[1], (y + 1) * tile_size), + ) + crop_rect_with_overlap = add_overlap_rect(crop_rect, tile_border, image.size) + tile = image.crop(crop_rect_with_overlap) + translated_slice_x = ((crop_rect[0] + ((crop_rect[2] - crop_rect[0]) / 2)) / image.size[0]) * tile.size[0] + translated_slice_x = translated_slice_x - (original_image_slice / 2) + translated_slice_x = max(0, translated_slice_x) + to_input = squeeze_tile(tile, image, original_image_slice, translated_slice_x) + orig_input_size = to_input.size + to_input = to_input.resize((tile_size, tile_size), Image.BICUBIC) + upscaled_tile = super(StableDiffusionTiledUpscalePipeline, self).__call__(image=to_input, **kwargs).images[0] + upscaled_tile = upscaled_tile.resize((orig_input_size[0] * 4, orig_input_size[1] * 4), Image.BICUBIC) + upscaled_tile = unsqueeze_tile(upscaled_tile, original_image_slice) + upscaled_tile = upscaled_tile.resize((tile.size[0] * 4, tile.size[1] * 4), Image.BICUBIC) + remove_borders = [] + if x == 0: + remove_borders.append("l") + elif crop_rect[2] == image.size[0]: + remove_borders.append("r") + if y == 0: + remove_borders.append("t") + elif crop_rect[3] == image.size[1]: + remove_borders.append("b") + transparency_mask = Image.fromarray( + make_transparency_mask( + (upscaled_tile.size[0], upscaled_tile.size[1]), tile_border * 4, remove_borders=remove_borders + ), + mode="L", + ) + final_image.paste( + upscaled_tile, (crop_rect_with_overlap[0] * 4, crop_rect_with_overlap[1] * 4), transparency_mask + ) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[PIL.Image.Image, List[PIL.Image.Image]], + num_inference_steps: int = 75, + guidance_scale: float = 9.0, + noise_level: int = 50, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + tile_size: int = 128, + tile_border: int = 32, + original_image_slice: int = 32, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`PIL.Image.Image` or List[`PIL.Image.Image`] or `torch.FloatTensor`): + `Image`, or tensor representing an image batch which will be upscaled. * + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + tile_size (`int`, *optional*): + The size of the tiles. Too big can result in an OOM-error. + tile_border (`int`, *optional*): + The number of pixels around a tile to consider (bigger means less seams, too big can lead to an OOM-error). + original_image_slice (`int`, *optional*): + The amount of pixels of the original image to calculate with the current tile (bigger means more depth + is preserved, less blur occurs in the final image, too big can lead to an OOM-error or loss in detail). + callback (`Callable`, *optional*): + A function that take a callback function with a single argument, a dict, + that contains the (partially) processed image under "image", + as well as the progress (0 to 1, where 1 is completed) under "progress". + + Returns: A PIL.Image that is 4 times larger than the original input image. + + """ + + final_image = Image.new("RGB", (image.size[0] * 4, image.size[1] * 4)) + tcx = math.ceil(image.size[0] / tile_size) + tcy = math.ceil(image.size[1] / tile_size) + total_tile_count = tcx * tcy + current_count = 0 + for y in range(tcy): + for x in range(tcx): + self._process_tile( + original_image_slice, + x, + y, + tile_size, + tile_border, + image, + final_image, + prompt=prompt, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + noise_level=noise_level, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + ) + current_count += 1 + if callback is not None: + callback({"progress": current_count / total_tile_count, "image": final_image}) + return final_image + + +def main(): + # Run a demo + model_id = "stabilityai/stable-diffusion-x4-upscaler" + pipe = StableDiffusionTiledUpscalePipeline.from_pretrained(model_id, revision="fp16", torch_dtype=torch.float16) + pipe = pipe.to("cuda") + image = Image.open("../../docs/source/imgs/diffusers_library.jpg") + + def callback(obj): + print(f"progress: {obj['progress']:.4f}") + obj["image"].save("diffusers_library_progress.jpg") + + final_image = pipe(image=image, prompt="Black font, white background, vector", noise_level=40, callback=callback) + final_image.save("diffusers_library.jpg") + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/community/unclip_text_interpolation.py b/diffusers/examples/community/unclip_text_interpolation.py new file mode 100644 index 0000000000000000000000000000000000000000..ac6b73d974b6e0fd37434083ed923256b4f5db22 --- /dev/null +++ b/diffusers/examples/community/unclip_text_interpolation.py @@ -0,0 +1,573 @@ +import inspect +from typing import List, Optional, Tuple, Union + +import torch +from torch.nn import functional as F +from transformers import CLIPTextModelWithProjection, CLIPTokenizer +from transformers.models.clip.modeling_clip import CLIPTextModelOutput + +from diffusers import ( + DiffusionPipeline, + ImagePipelineOutput, + PriorTransformer, + UnCLIPScheduler, + UNet2DConditionModel, + UNet2DModel, +) +from diffusers.pipelines.unclip import UnCLIPTextProjModel +from diffusers.utils import is_accelerate_available, logging, randn_tensor + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def slerp(val, low, high): + """ + Find the interpolation point between the 'low' and 'high' values for the given 'val'. See https://en.wikipedia.org/wiki/Slerp for more details on the topic. + """ + low_norm = low / torch.norm(low) + high_norm = high / torch.norm(high) + omega = torch.acos((low_norm * high_norm)) + so = torch.sin(omega) + res = (torch.sin((1.0 - val) * omega) / so) * low + (torch.sin(val * omega) / so) * high + return res + + +class UnCLIPTextInterpolationPipeline(DiffusionPipeline): + + """ + Pipeline for prompt-to-prompt interpolation on CLIP text embeddings and using the UnCLIP / Dall-E to decode them to images. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + text_encoder ([`CLIPTextModelWithProjection`]): + Frozen text-encoder. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + prior ([`PriorTransformer`]): + The canonincal unCLIP prior to approximate the image embedding from the text embedding. + text_proj ([`UnCLIPTextProjModel`]): + Utility class to prepare and combine the embeddings before they are passed to the decoder. + decoder ([`UNet2DConditionModel`]): + The decoder to invert the image embedding into an image. + super_res_first ([`UNet2DModel`]): + Super resolution unet. Used in all but the last step of the super resolution diffusion process. + super_res_last ([`UNet2DModel`]): + Super resolution unet. Used in the last step of the super resolution diffusion process. + prior_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the prior denoising process. Just a modified DDPMScheduler. + decoder_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the decoder denoising process. Just a modified DDPMScheduler. + super_res_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the super resolution denoising process. Just a modified DDPMScheduler. + + """ + + prior: PriorTransformer + decoder: UNet2DConditionModel + text_proj: UnCLIPTextProjModel + text_encoder: CLIPTextModelWithProjection + tokenizer: CLIPTokenizer + super_res_first: UNet2DModel + super_res_last: UNet2DModel + + prior_scheduler: UnCLIPScheduler + decoder_scheduler: UnCLIPScheduler + super_res_scheduler: UnCLIPScheduler + + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline.__init__ + def __init__( + self, + prior: PriorTransformer, + decoder: UNet2DConditionModel, + text_encoder: CLIPTextModelWithProjection, + tokenizer: CLIPTokenizer, + text_proj: UnCLIPTextProjModel, + super_res_first: UNet2DModel, + super_res_last: UNet2DModel, + prior_scheduler: UnCLIPScheduler, + decoder_scheduler: UnCLIPScheduler, + super_res_scheduler: UnCLIPScheduler, + ): + super().__init__() + + self.register_modules( + prior=prior, + decoder=decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + text_proj=text_proj, + super_res_first=super_res_first, + super_res_last=super_res_last, + prior_scheduler=prior_scheduler, + decoder_scheduler=decoder_scheduler, + super_res_scheduler=super_res_scheduler, + ) + + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline.prepare_latents + def prepare_latents(self, shape, dtype, device, generator, latents, scheduler): + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + latents = latents * scheduler.init_noise_sigma + return latents + + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + text_model_output: Optional[Union[CLIPTextModelOutput, Tuple]] = None, + text_attention_mask: Optional[torch.Tensor] = None, + ): + if text_model_output is None: + batch_size = len(prompt) if isinstance(prompt, list) else 1 + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + text_mask = text_inputs.attention_mask.bool().to(device) + + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + + text_encoder_output = self.text_encoder(text_input_ids.to(device)) + + prompt_embeds = text_encoder_output.text_embeds + text_encoder_hidden_states = text_encoder_output.last_hidden_state + + else: + batch_size = text_model_output[0].shape[0] + prompt_embeds, text_encoder_hidden_states = text_model_output[0], text_model_output[1] + text_mask = text_attention_mask + + prompt_embeds = prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) + text_encoder_hidden_states = text_encoder_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + text_mask = text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + if do_classifier_free_guidance: + uncond_tokens = [""] * batch_size + + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + uncond_text_mask = uncond_input.attention_mask.bool().to(device) + negative_prompt_embeds_text_encoder_output = self.text_encoder(uncond_input.input_ids.to(device)) + + negative_prompt_embeds = negative_prompt_embeds_text_encoder_output.text_embeds + uncond_text_encoder_hidden_states = negative_prompt_embeds_text_encoder_output.last_hidden_state + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len) + + seq_len = uncond_text_encoder_hidden_states.shape[1] + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.repeat(1, num_images_per_prompt, 1) + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.view( + batch_size * num_images_per_prompt, seq_len, -1 + ) + uncond_text_mask = uncond_text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + # done duplicates + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + text_encoder_hidden_states = torch.cat([uncond_text_encoder_hidden_states, text_encoder_hidden_states]) + + text_mask = torch.cat([uncond_text_mask, text_mask]) + + return prompt_embeds, text_encoder_hidden_states, text_mask + + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, the pipeline's + models have their state dicts saved to CPU and then are moved to a `torch.device('meta') and loaded to GPU only + when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + # TODO: self.prior.post_process_latents is not covered by the offload hooks, so it fails if added to the list + models = [ + self.decoder, + self.text_proj, + self.text_encoder, + self.super_res_first, + self.super_res_last, + ] + for cpu_offloaded_model in models: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.decoder, "_hf_hook"): + return self.device + for module in self.decoder.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + @torch.no_grad() + def __call__( + self, + start_prompt: str, + end_prompt: str, + steps: int = 5, + prior_num_inference_steps: int = 25, + decoder_num_inference_steps: int = 25, + super_res_num_inference_steps: int = 7, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prior_guidance_scale: float = 4.0, + decoder_guidance_scale: float = 8.0, + enable_sequential_cpu_offload=True, + gpu_id=0, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ): + """ + Function invoked when calling the pipeline for generation. + + Args: + start_prompt (`str`): + The prompt to start the image generation interpolation from. + end_prompt (`str`): + The prompt to end the image generation interpolation at. + steps (`int`, *optional*, defaults to 5): + The number of steps over which to interpolate from start_prompt to end_prompt. The pipeline returns + the same number of images as this value. + prior_num_inference_steps (`int`, *optional*, defaults to 25): + The number of denoising steps for the prior. More denoising steps usually lead to a higher quality + image at the expense of slower inference. + decoder_num_inference_steps (`int`, *optional*, defaults to 25): + The number of denoising steps for the decoder. More denoising steps usually lead to a higher quality + image at the expense of slower inference. + super_res_num_inference_steps (`int`, *optional*, defaults to 7): + The number of denoising steps for super resolution. More denoising steps usually lead to a higher + quality image at the expense of slower inference. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prior_guidance_scale (`float`, *optional*, defaults to 4.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + decoder_guidance_scale (`float`, *optional*, defaults to 4.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + enable_sequential_cpu_offload (`bool`, *optional*, defaults to `True`): + If True, offloads all models to CPU using accelerate, significantly reducing memory usage. When called, the pipeline's + models have their state dicts saved to CPU and then are moved to a `torch.device('meta') and loaded to GPU only + when their specific submodule has its `forward` method called. + gpu_id (`int`, *optional*, defaults to `0`): + The gpu_id to be passed to enable_sequential_cpu_offload. Only works when enable_sequential_cpu_offload is set to True. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + """ + + if not isinstance(start_prompt, str) or not isinstance(end_prompt, str): + raise ValueError( + f"`start_prompt` and `end_prompt` should be of type `str` but got {type(start_prompt)} and" + f" {type(end_prompt)} instead" + ) + + if enable_sequential_cpu_offload: + self.enable_sequential_cpu_offload(gpu_id=gpu_id) + + device = self._execution_device + + # Turn the prompts into embeddings. + inputs = self.tokenizer( + [start_prompt, end_prompt], + padding="max_length", + truncation=True, + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + inputs.to(device) + text_model_output = self.text_encoder(**inputs) + + text_attention_mask = torch.max(inputs.attention_mask[0], inputs.attention_mask[1]) + text_attention_mask = torch.cat([text_attention_mask.unsqueeze(0)] * steps).to(device) + + # Interpolate from the start to end prompt using slerp and add the generated images to an image output pipeline + batch_text_embeds = [] + batch_last_hidden_state = [] + + for interp_val in torch.linspace(0, 1, steps): + text_embeds = slerp(interp_val, text_model_output.text_embeds[0], text_model_output.text_embeds[1]) + last_hidden_state = slerp( + interp_val, text_model_output.last_hidden_state[0], text_model_output.last_hidden_state[1] + ) + batch_text_embeds.append(text_embeds.unsqueeze(0)) + batch_last_hidden_state.append(last_hidden_state.unsqueeze(0)) + + batch_text_embeds = torch.cat(batch_text_embeds) + batch_last_hidden_state = torch.cat(batch_last_hidden_state) + + text_model_output = CLIPTextModelOutput( + text_embeds=batch_text_embeds, last_hidden_state=batch_last_hidden_state + ) + + batch_size = text_model_output[0].shape[0] + + do_classifier_free_guidance = prior_guidance_scale > 1.0 or decoder_guidance_scale > 1.0 + + prompt_embeds, text_encoder_hidden_states, text_mask = self._encode_prompt( + prompt=None, + device=device, + num_images_per_prompt=1, + do_classifier_free_guidance=do_classifier_free_guidance, + text_model_output=text_model_output, + text_attention_mask=text_attention_mask, + ) + + # prior + + self.prior_scheduler.set_timesteps(prior_num_inference_steps, device=device) + prior_timesteps_tensor = self.prior_scheduler.timesteps + + embedding_dim = self.prior.config.embedding_dim + + prior_latents = self.prepare_latents( + (batch_size, embedding_dim), + prompt_embeds.dtype, + device, + generator, + None, + self.prior_scheduler, + ) + + for i, t in enumerate(self.progress_bar(prior_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([prior_latents] * 2) if do_classifier_free_guidance else prior_latents + + predicted_image_embedding = self.prior( + latent_model_input, + timestep=t, + proj_embedding=prompt_embeds, + encoder_hidden_states=text_encoder_hidden_states, + attention_mask=text_mask, + ).predicted_image_embedding + + if do_classifier_free_guidance: + predicted_image_embedding_uncond, predicted_image_embedding_text = predicted_image_embedding.chunk(2) + predicted_image_embedding = predicted_image_embedding_uncond + prior_guidance_scale * ( + predicted_image_embedding_text - predicted_image_embedding_uncond + ) + + if i + 1 == prior_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = prior_timesteps_tensor[i + 1] + + prior_latents = self.prior_scheduler.step( + predicted_image_embedding, + timestep=t, + sample=prior_latents, + generator=generator, + prev_timestep=prev_timestep, + ).prev_sample + + prior_latents = self.prior.post_process_latents(prior_latents) + + image_embeddings = prior_latents + + # done prior + + # decoder + + text_encoder_hidden_states, additive_clip_time_embeddings = self.text_proj( + image_embeddings=image_embeddings, + prompt_embeds=prompt_embeds, + text_encoder_hidden_states=text_encoder_hidden_states, + do_classifier_free_guidance=do_classifier_free_guidance, + ) + + if device.type == "mps": + # HACK: MPS: There is a panic when padding bool tensors, + # so cast to int tensor for the pad and back to bool afterwards + text_mask = text_mask.type(torch.int) + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=1) + decoder_text_mask = decoder_text_mask.type(torch.bool) + else: + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=True) + + self.decoder_scheduler.set_timesteps(decoder_num_inference_steps, device=device) + decoder_timesteps_tensor = self.decoder_scheduler.timesteps + + num_channels_latents = self.decoder.in_channels + height = self.decoder.sample_size + width = self.decoder.sample_size + + decoder_latents = self.prepare_latents( + (batch_size, num_channels_latents, height, width), + text_encoder_hidden_states.dtype, + device, + generator, + None, + self.decoder_scheduler, + ) + + for i, t in enumerate(self.progress_bar(decoder_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([decoder_latents] * 2) if do_classifier_free_guidance else decoder_latents + + noise_pred = self.decoder( + sample=latent_model_input, + timestep=t, + encoder_hidden_states=text_encoder_hidden_states, + class_labels=additive_clip_time_embeddings, + attention_mask=decoder_text_mask, + ).sample + + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred_uncond, _ = noise_pred_uncond.split(latent_model_input.shape[1], dim=1) + noise_pred_text, predicted_variance = noise_pred_text.split(latent_model_input.shape[1], dim=1) + noise_pred = noise_pred_uncond + decoder_guidance_scale * (noise_pred_text - noise_pred_uncond) + noise_pred = torch.cat([noise_pred, predicted_variance], dim=1) + + if i + 1 == decoder_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = decoder_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + decoder_latents = self.decoder_scheduler.step( + noise_pred, t, decoder_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + decoder_latents = decoder_latents.clamp(-1, 1) + + image_small = decoder_latents + + # done decoder + + # super res + + self.super_res_scheduler.set_timesteps(super_res_num_inference_steps, device=device) + super_res_timesteps_tensor = self.super_res_scheduler.timesteps + + channels = self.super_res_first.in_channels // 2 + height = self.super_res_first.sample_size + width = self.super_res_first.sample_size + + super_res_latents = self.prepare_latents( + (batch_size, channels, height, width), + image_small.dtype, + device, + generator, + None, + self.super_res_scheduler, + ) + + if device.type == "mps": + # MPS does not support many interpolations + image_upscaled = F.interpolate(image_small, size=[height, width]) + else: + interpolate_antialias = {} + if "antialias" in inspect.signature(F.interpolate).parameters: + interpolate_antialias["antialias"] = True + + image_upscaled = F.interpolate( + image_small, size=[height, width], mode="bicubic", align_corners=False, **interpolate_antialias + ) + + for i, t in enumerate(self.progress_bar(super_res_timesteps_tensor)): + # no classifier free guidance + + if i == super_res_timesteps_tensor.shape[0] - 1: + unet = self.super_res_last + else: + unet = self.super_res_first + + latent_model_input = torch.cat([super_res_latents, image_upscaled], dim=1) + + noise_pred = unet( + sample=latent_model_input, + timestep=t, + ).sample + + if i + 1 == super_res_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = super_res_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + super_res_latents = self.super_res_scheduler.step( + noise_pred, t, super_res_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + image = super_res_latents + # done super res + + # post processing + + image = image * 0.5 + 0.5 + image = image.clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/examples/community/wildcard_stable_diffusion.py b/diffusers/examples/community/wildcard_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..9825c0fec5b3cb9a9ceb40e792154dbe467ae83d --- /dev/null +++ b/diffusers/examples/community/wildcard_stable_diffusion.py @@ -0,0 +1,418 @@ +import inspect +import os +import random +import re +from dataclasses import dataclass +from typing import Callable, Dict, List, Optional, Union + +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import DiffusionPipeline +from diffusers.configuration_utils import FrozenDict +from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import StableDiffusionPipelineOutput +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from diffusers.utils import deprecate, logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +global_re_wildcard = re.compile(r"__([^_]*)__") + + +def get_filename(path: str): + # this doesn't work on Windows + return os.path.basename(path).split(".txt")[0] + + +def read_wildcard_values(path: str): + with open(path, encoding="utf8") as f: + return f.read().splitlines() + + +def grab_wildcard_values(wildcard_option_dict: Dict[str, List[str]] = {}, wildcard_files: List[str] = []): + for wildcard_file in wildcard_files: + filename = get_filename(wildcard_file) + read_values = read_wildcard_values(wildcard_file) + if filename not in wildcard_option_dict: + wildcard_option_dict[filename] = [] + wildcard_option_dict[filename].extend(read_values) + return wildcard_option_dict + + +def replace_prompt_with_wildcards( + prompt: str, wildcard_option_dict: Dict[str, List[str]] = {}, wildcard_files: List[str] = [] +): + new_prompt = prompt + + # get wildcard options + wildcard_option_dict = grab_wildcard_values(wildcard_option_dict, wildcard_files) + + for m in global_re_wildcard.finditer(new_prompt): + wildcard_value = m.group() + replace_value = random.choice(wildcard_option_dict[wildcard_value.strip("__")]) + new_prompt = new_prompt.replace(wildcard_value, replace_value, 1) + + return new_prompt + + +@dataclass +class WildcardStableDiffusionOutput(StableDiffusionPipelineOutput): + prompts: List[str] + + +class WildcardStableDiffusionPipeline(DiffusionPipeline): + r""" + Example Usage: + pipe = WildcardStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + + torch_dtype=torch.float16, + ) + prompt = "__animal__ sitting on a __object__ wearing a __clothing__" + out = pipe( + prompt, + wildcard_option_dict={ + "clothing":["hat", "shirt", "scarf", "beret"] + }, + wildcard_files=["object.txt", "animal.txt"], + num_prompt_samples=1 + ) + + + Pipeline for text-to-image generation with wild cards using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: int = 512, + width: int = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[torch.Generator] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + wildcard_option_dict: Dict[str, List[str]] = {}, + wildcard_files: List[str] = [], + num_prompt_samples: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + wildcard_option_dict (Dict[str, List[str]]): + dict with key as `wildcard` and values as a list of possible replacements. For example if a prompt, "A __animal__ sitting on a chair". A wildcard_option_dict can provide possible values for "animal" like this: {"animal":["dog", "cat", "fox"]} + wildcard_files: (List[str]) + List of filenames of txt files for wildcard replacements. For example if a prompt, "A __animal__ sitting on a chair". A file can be provided ["animal.txt"] + num_prompt_samples: int + Number of times to sample wildcards for each prompt provided + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + if isinstance(prompt, str): + prompt = [ + replace_prompt_with_wildcards(prompt, wildcard_option_dict, wildcard_files) + for i in range(num_prompt_samples) + ] + batch_size = len(prompt) + elif isinstance(prompt, list): + prompt_list = [] + for p in prompt: + for i in range(num_prompt_samples): + prompt_list.append(replace_prompt_with_wildcards(p, wildcard_option_dict, wildcard_files)) + prompt = prompt_list + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + text_embeddings = self.text_encoder(text_input_ids.to(self.device))[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = text_embeddings.shape + text_embeddings = text_embeddings.repeat(1, num_images_per_prompt, 1) + text_embeddings = text_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_embeddings = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = uncond_embeddings.shape[1] + uncond_embeddings = uncond_embeddings.repeat(1, num_images_per_prompt, 1) + uncond_embeddings = uncond_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + + # get the initial random noise unless the user supplied it + + # Unlike in other pipelines, latents need to be generated in the target device + # for 1-to-1 results reproducibility with the CompVis implementation. + # However this currently doesn't work in `mps`. + latents_shape = (batch_size * num_images_per_prompt, self.unet.in_channels, height // 8, width // 8) + latents_dtype = text_embeddings.dtype + if latents is None: + if self.device.type == "mps": + # randn does not exist on mps + latents = torch.randn(latents_shape, generator=generator, device="cpu", dtype=latents_dtype).to( + self.device + ) + else: + latents = torch.randn(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # Some schedulers like PNDM have timesteps as arrays + # It's more optimized to move all timesteps to correct device beforehand + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=text_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + image = self.vae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to( + self.device + ) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(text_embeddings.dtype) + ) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return WildcardStableDiffusionOutput(images=image, nsfw_content_detected=has_nsfw_concept, prompts=prompt) diff --git a/diffusers/examples/conftest.py b/diffusers/examples/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..d2f9600313a1b687cd40bbf02784b20373b6b7a2 --- /dev/null +++ b/diffusers/examples/conftest.py @@ -0,0 +1,45 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +# tests directory-specific settings - this file is run automatically +# by pytest before any tests are run + +import sys +import warnings +from os.path import abspath, dirname, join + + +# allow having multiple repository checkouts and not needing to remember to rerun +# 'pip install -e .[dev]' when switching between checkouts and running tests. +git_repo_path = abspath(join(dirname(dirname(dirname(__file__))), "src")) +sys.path.insert(1, git_repo_path) + + +# silence FutureWarning warnings in tests since often we can't act on them until +# they become normal warnings - i.e. the tests still need to test the current functionality +warnings.simplefilter(action="ignore", category=FutureWarning) + + +def pytest_addoption(parser): + from diffusers.utils.testing_utils import pytest_addoption_shared + + pytest_addoption_shared(parser) + + +def pytest_terminal_summary(terminalreporter): + from diffusers.utils.testing_utils import pytest_terminal_summary_main + + make_reports = terminalreporter.config.getoption("--make-reports") + if make_reports: + pytest_terminal_summary_main(terminalreporter, id=make_reports) diff --git a/diffusers/examples/dreambooth/DreamBooth_Stable_Diffusion.ipynb b/diffusers/examples/dreambooth/DreamBooth_Stable_Diffusion.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..bafb5c3379c3d5f21833e9096c51eeb78110443f --- /dev/null +++ b/diffusers/examples/dreambooth/DreamBooth_Stable_Diffusion.ipynb @@ -0,0 +1,621 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XU7NuMAA2drw", + "outputId": "7eb9b063-664f-4a42-e960-728ec9608c42" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tesla T4, 15109 MiB, 15109 MiB\n" + ] + } + ], + "source": [ + "#@markdown Check type of GPU and VRAM available.\n", + "!nvidia-smi --query-gpu=name,memory.total,memory.free --format=csv,noheader" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BzM7j0ZSc_9c" + }, + "source": [ + "https://github.com/ShivamShrirao/diffusers/tree/main/examples/dreambooth" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wnTMyW41cC1E" + }, + "source": [ + "## Install Requirements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aLWXPZqjsZVV" + }, + "outputs": [], + "source": [ + "!wget -q https://github.com/ShivamShrirao/diffusers/raw/main/examples/dreambooth/train_dreambooth.py\n", + "!wget -q https://github.com/ShivamShrirao/diffusers/raw/main/scripts/convert_diffusers_to_original_stable_diffusion.py\n", + "%pip install -qq git+https://github.com/ShivamShrirao/diffusers\n", + "%pip install -q -U --pre triton\n", + "%pip install -q accelerate transformers ftfy bitsandbytes==0.35.0 gradio natsort safetensors xformers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "y4lqqWT_uxD2" + }, + "outputs": [], + "source": [ + "#@title Login to HuggingFace 🤗\n", + "\n", + "#@markdown You need to accept the model license before downloading or using the Stable Diffusion weights. Please, visit the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5), read the license and tick the checkbox if you agree. You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work.\n", + "# https://huggingface.co/settings/tokens\n", + "!mkdir -p ~/.huggingface\n", + "HUGGINGFACE_TOKEN = \"\" #@param {type:\"string\"}\n", + "!echo -n \"{HUGGINGFACE_TOKEN}\" > ~/.huggingface/token" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G0NV324ZcL9L" + }, + "source": [ + "## Settings and run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "Rxg0y5MBudmd" + }, + "outputs": [], + "source": [ + "#@markdown If model weights should be saved directly in google drive (takes around 4-5 GB).\n", + "save_to_gdrive = False #@param {type:\"boolean\"}\n", + "if save_to_gdrive:\n", + " from google.colab import drive\n", + " drive.mount('/content/drive')\n", + "\n", + "#@markdown Name/Path of the initial model.\n", + "MODEL_NAME = \"runwayml/stable-diffusion-v1-5\" #@param {type:\"string\"}\n", + "\n", + "#@markdown Enter the directory name to save model at.\n", + "\n", + "OUTPUT_DIR = \"stable_diffusion_weights/zwx\" #@param {type:\"string\"}\n", + "if save_to_gdrive:\n", + " OUTPUT_DIR = \"/content/drive/MyDrive/\" + OUTPUT_DIR\n", + "else:\n", + " OUTPUT_DIR = \"/content/\" + OUTPUT_DIR\n", + "\n", + "print(f\"[*] Weights will be saved at {OUTPUT_DIR}\")\n", + "\n", + "!mkdir -p $OUTPUT_DIR" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qn5ILIyDJIcX" + }, + "source": [ + "# Start Training\n", + "\n", + "Use the table below to choose the best flags based on your memory and speed requirements. Tested on Tesla T4 GPU.\n", + "\n", + "\n", + "| `fp16` | `train_batch_size` | `gradient_accumulation_steps` | `gradient_checkpointing` | `use_8bit_adam` | GB VRAM usage | Speed (it/s) |\n", + "| ---- | ------------------ | ----------------------------- | ----------------------- | --------------- | ---------- | ------------ |\n", + "| fp16 | 1 | 1 | TRUE | TRUE | 9.92 | 0.93 |\n", + "| no | 1 | 1 | TRUE | TRUE | 10.08 | 0.42 |\n", + "| fp16 | 2 | 1 | TRUE | TRUE | 10.4 | 0.66 |\n", + "| fp16 | 1 | 1 | FALSE | TRUE | 11.17 | 1.14 |\n", + "| no | 1 | 1 | FALSE | TRUE | 11.17 | 0.49 |\n", + "| fp16 | 1 | 2 | TRUE | TRUE | 11.56 | 1 |\n", + "| fp16 | 2 | 1 | FALSE | TRUE | 13.67 | 0.82 |\n", + "| fp16 | 1 | 2 | FALSE | TRUE | 13.7 | 0.83 |\n", + "| fp16 | 1 | 1 | TRUE | FALSE | 15.79 | 0.77 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-ioxxvHoicPs" + }, + "source": [ + "Add `--gradient_checkpointing` flag for around 9.92 GB VRAM usage.\n", + "\n", + "remove `--use_8bit_adam` flag for full precision. Requires 15.79 GB with `--gradient_checkpointing` else 17.8 GB.\n", + "\n", + "remove `--train_text_encoder` flag to reduce memory usage further, degrades output quality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5vDpCxId1aCm" + }, + "outputs": [], + "source": [ + "# You can also add multiple concepts here. Try tweaking `--max_train_steps` accordingly.\n", + "\n", + "concepts_list = [\n", + " {\n", + " \"instance_prompt\": \"photo of zwx dog\",\n", + " \"class_prompt\": \"photo of a dog\",\n", + " \"instance_data_dir\": \"/content/data/zwx\",\n", + " \"class_data_dir\": \"/content/data/dog\"\n", + " },\n", + "# {\n", + "# \"instance_prompt\": \"photo of ukj person\",\n", + "# \"class_prompt\": \"photo of a person\",\n", + "# \"instance_data_dir\": \"/content/data/ukj\",\n", + "# \"class_data_dir\": \"/content/data/person\"\n", + "# }\n", + "]\n", + "\n", + "# `class_data_dir` contains regularization images\n", + "import json\n", + "import os\n", + "for c in concepts_list:\n", + " os.makedirs(c[\"instance_data_dir\"], exist_ok=True)\n", + "\n", + "with open(\"concepts_list.json\", \"w\") as f:\n", + " json.dump(concepts_list, f, indent=4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "32gYIDDR1aCp" + }, + "outputs": [], + "source": [ + "#@markdown Upload your images by running this cell.\n", + "\n", + "#@markdown OR\n", + "\n", + "#@markdown You can use the file manager on the left panel to upload (drag and drop) to each `instance_data_dir` (it uploads faster)\n", + "\n", + "import os\n", + "from google.colab import files\n", + "import shutil\n", + "\n", + "for c in concepts_list:\n", + " print(f\"Uploading instance images for `{c['instance_prompt']}`\")\n", + " uploaded = files.upload()\n", + " for filename in uploaded.keys():\n", + " dst_path = os.path.join(c['instance_data_dir'], filename)\n", + " shutil.move(filename, dst_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jjcSXTp-u-Eg" + }, + "outputs": [], + "source": [ + "!accelerate launch train_dreambooth.py \\\n", + " --pretrained_model_name_or_path=$MODEL_NAME \\\n", + " --pretrained_vae_name_or_path=\"stabilityai/sd-vae-ft-mse\" \\\n", + " --output_dir=$OUTPUT_DIR \\\n", + " --revision=\"fp16\" \\\n", + " --with_prior_preservation --prior_loss_weight=1.0 \\\n", + " --seed=1337 \\\n", + " --resolution=512 \\\n", + " --train_batch_size=1 \\\n", + " --train_text_encoder \\\n", + " --mixed_precision=\"fp16\" \\\n", + " --use_8bit_adam \\\n", + " --gradient_accumulation_steps=1 \\\n", + " --learning_rate=1e-6 \\\n", + " --lr_scheduler=\"constant\" \\\n", + " --lr_warmup_steps=0 \\\n", + " --num_class_images=50 \\\n", + " --sample_batch_size=4 \\\n", + " --max_train_steps=800 \\\n", + " --save_interval=10000 \\\n", + " --save_sample_prompt=\"photo of zwx dog\" \\\n", + " --concepts_list=\"concepts_list.json\"\n", + "\n", + "# Reduce the `--save_interval` to lower than `--max_train_steps` to save weights from intermediate steps.\n", + "# `--save_sample_prompt` can be same as `--instance_prompt` to generate intermediate samples (saved along with weights in samples directory)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "89Az5NUxOWdy" + }, + "outputs": [], + "source": [ + "#@markdown Specify the weights directory to use (leave blank for latest)\n", + "WEIGHTS_DIR = \"\" #@param {type:\"string\"}\n", + "if WEIGHTS_DIR == \"\":\n", + " from natsort import natsorted\n", + " from glob import glob\n", + " import os\n", + " WEIGHTS_DIR = natsorted(glob(OUTPUT_DIR + os.sep + \"*\"))[-1]\n", + "print(f\"[*] WEIGHTS_DIR={WEIGHTS_DIR}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "89Az5NUxOWdy" + }, + "outputs": [], + "source": [ + "#@markdown Run to generate a grid of preview images from the last saved weights.\n", + "import os\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.image as mpimg\n", + "\n", + "weights_folder = OUTPUT_DIR\n", + "folders = sorted([f for f in os.listdir(weights_folder) if f != \"0\"], key=lambda x: int(x))\n", + "\n", + "row = len(folders)\n", + "col = len(os.listdir(os.path.join(weights_folder, folders[0], \"samples\")))\n", + "scale = 4\n", + "fig, axes = plt.subplots(row, col, figsize=(col*scale, row*scale), gridspec_kw={'hspace': 0, 'wspace': 0})\n", + "\n", + "for i, folder in enumerate(folders):\n", + " folder_path = os.path.join(weights_folder, folder)\n", + " image_folder = os.path.join(folder_path, \"samples\")\n", + " images = [f for f in os.listdir(image_folder)]\n", + " for j, image in enumerate(images):\n", + " if row == 1:\n", + " currAxes = axes[j]\n", + " else:\n", + " currAxes = axes[i, j]\n", + " if i == 0:\n", + " currAxes.set_title(f\"Image {j}\")\n", + " if j == 0:\n", + " currAxes.text(-0.1, 0.5, folder, rotation=0, va='center', ha='center', transform=currAxes.transAxes)\n", + " image_path = os.path.join(image_folder, image)\n", + " img = mpimg.imread(image_path)\n", + " currAxes.imshow(img, cmap='gray')\n", + " currAxes.axis('off')\n", + " \n", + "plt.tight_layout()\n", + "plt.savefig('grid.png', dpi=72)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5V8wgU0HN-Kq" + }, + "source": [ + "## Convert weights to ckpt to use in web UIs like AUTOMATIC1111." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "dcXzsUyG1aCy" + }, + "outputs": [], + "source": [ + "#@markdown Run conversion.\n", + "ckpt_path = WEIGHTS_DIR + \"/model.ckpt\"\n", + "\n", + "half_arg = \"\"\n", + "#@markdown Whether to convert to fp16, takes half the space (2GB).\n", + "fp16 = True #@param {type: \"boolean\"}\n", + "if fp16:\n", + " half_arg = \"--half\"\n", + "!python convert_diffusers_to_original_stable_diffusion.py --model_path $WEIGHTS_DIR --checkpoint_path $ckpt_path $half_arg\n", + "print(f\"[*] Converted ckpt saved at {ckpt_path}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ToNG4fd_dTbF" + }, + "source": [ + "## Inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gW15FjffdTID" + }, + "outputs": [], + "source": [ + "import torch\n", + "from torch import autocast\n", + "from diffusers import StableDiffusionPipeline, DDIMScheduler\n", + "from IPython.display import display\n", + "\n", + "model_path = WEIGHTS_DIR # If you want to use previously trained model saved in gdrive, replace this with the full path of model in gdrive\n", + "\n", + "pipe = StableDiffusionPipeline.from_pretrained(model_path, safety_checker=None, torch_dtype=torch.float16).to(\"cuda\")\n", + "pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)\n", + "pipe.enable_xformers_memory_efficient_attention()\n", + "g_cuda = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oIzkltjpVO_f", + "outputId": "1db9fcaa-2d0f-4966-dc4f-baac60cdb807" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#@markdown Can set random seed here for reproducibility.\n", + "g_cuda = torch.Generator(device='cuda')\n", + "seed = 52362 #@param {type:\"number\"}\n", + "g_cuda.manual_seed(seed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "referenced_widgets": [ + "21d3153693b0442bb0afe46738c9d9ae" + ] + }, + "id": "K6xoHWSsbcS3", + "outputId": "75fb2672-a0a4-4149-ef0d-42ff0c247449", + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "21d3153693b0442bb0afe46738c9d9ae", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/50 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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JSSGBkBqbfmcrdaOktQ6ql2/zvmfX6+VwDZhXqvXornvT5cNLAx9WlZiqqYp90U973uUTrpeuk4R1aHr87zWbVPOEohrNUCcp+nv/+1fh5vDuOuGrmOivsvvP97sd/22bbZskrttWecy9/0IQQS8TOd319fzy1wc5rWs63bz7npbDALcfHqdmYSAylrMbLffuek8z+NuSIlzFtUiQvvjLtTmeVq3cvf+ru+yF5sul/P5wgAfPt0ngoCoqqkTAuDEKQkhgikWx8DlcjH33EkgYsq0G0uSxaozopEWS8wtJwhhLddgZoTogG6GiZm5Ta+lLiqJzQEZTI0xENFqOHlbinqzMBdtpEMRqaX0Q19LJUJACo/2KU9ZTBWRLbxZxMIsJamlttBkC9KYNoKzEWNrttaiSAQO/ZDVFQIi2jckJHOjGEY44JtCEsAxwj2YMRyE2QGs+WHAc9c1aDsJe0MZIjWVRt/nslVmcX/j1uWckADiD8rQG740GGkrCyKXUlplISKwBUQRwMLAAkGtAjYcW3tqxR9L74AAfwMJaFi0sZlupVmQGsrHXJkpPJjZmxLYnYmb7drCWzqtDVURGlY1mDkscs7VlAGAKdQsZL6YnQqr7Xd9aLh5YOQuafGuT+NhcDfV+P715cah77vp9fzxeMWJqsXr0+vjwyNLurm/v727+27fa6nl/HqZl3cfP22lfvnyVNT24+725vY8X75+fXp++e3d3Sck+P7t++37+7/821/P5/nNOwTaXx2ACVmOuyvL/X5Pv3/5yphS7tTNIWqJm7ubnz/98I+//3PYDyz3qvP+IL5JyomBuc/LZZkvp9/++e2v//a3H/50H/7fap16yetqXVq7bri9vaX0Ok/229evf/p4H5Gevj76/VW8zNNlud6G8Xbjl6837/+84/72uD+HftP6z2l5XOtmuQQICic21STJgALcgcydmrWqjReJtUaTPryJnAOIGivRHKDLvZaNWFqUGgD6lKtpEEZgkuTWIsUeBhCYkEs1FrGtEok77Po+zPskifj6sK9L7XNKCOLQI91ausvjIvWyIXCMkhipY0rEGcMAj12vsErfudrQ6WTRcnV53LFaLyzdKJwPIDZNAJY77joROVjdKGDIiRkCzDzyMByBKREBDCmrlWHokbB2GNBioV7XUjlXRMUuDamozsDGONV6P/bc0W/fn2IU45QSX9/sfv36fZ/6pWwpiUeAeaJkHkxIQEFk4Z0kRIKwxBweCTmZjlt5V43/9aWfjYjcoawVWjWhBjoG4tVx//w4ibl3CQOFUz8MBpzG3ol2+71BvJxeS411rpQ5j9266lrK9Hr5+NMdp3R9u4+gRDIeu6HjMi/rMo/j6MbnOh2vdoZpPS/V7Hg4jkP/8vl5Vl+mTVBq8W0t1XV6uJRNx37IuXt+eX7/w+1ymtSMGHb7vtYyLYvW5bDrGWnbtrJt11d7hrBq27LUbRt3XUoZ0Kubaa1a+zGFatnW0+uTJMksJPT6+GxViVJKwgT9vq9qSqhI22wGLknCIixICIPNDLwpNYgZa7yVTIVI1QQRS9E2fSBh92CKRjKJgCBufPyUpaoys5kLJyva0pMekXMytySCgF3uG7g4iVQ1CtKqAYDRMIH4xsmxICSzAACQCARiZKZt2xrVtuFyiKExEt4oESxmBtBOdm1ZT4dQrfHWom5YBYeIfujcDVvlGEC1IlGUkrqMCHWzJudCQtUtSW74xlbDIeYI7PJQti3n1K7wEe7VScTDRFK1KkkQHACqGlIjqjcbHxJhGwVAs9A4hAeEk4g3KFjrmiG3DbCWgkRIxG8sRFyWpcGLrNSUOMCJk9WKKOo1peygLFS3KizAxAFXH6+Xp1eq6BFeKwZ40LLOiWnb1jF1VzfXav7pxw+XyzmgLiv++o9/3n16fzxcHQ7H16cpPF6fn+5//Hh7d/v89GIFPOA0nQ/XV0D5+eX07fF5HOfd1dX1zdU0f1nKqep2fX99Pp1EhBmGoavan17OmxaM+NOPf662Xh8PLH57f79sdTzsRFLqeiQ+rdtlmrhP//j9a06xH2nohnd31w/qT4/nQzde/fA+leHzL59///yvw/X1p58/XS6vL98fE6fX1/n9Dx8iAgjIZy3l68vj0OeuH8uEud+dny7Ac1QfxzyV34br20OfdgL3Xfq38e7//P3hi8L34o5SOYIZvBnZCCGGlJd5TZwAGpEGG40sTD1cWIIBHMK8MdarVUREgCxsVYUlPDKJWuOLsLkjYZsmmXliBkcKHPtBgK3akeXffrj9+tvrXlIfPux2ttmu71mBzHqL3vGQ+50ZC3JKmDNYvdl3l5eKDKSLuPfUaUI0dpGoFhpOmZNRS606IlLXZSRP3HO/91KSqtcCrhgF27MOgCRF61yWYHd0Td3QIYdqAGjRsesQsUJsLmFAIrVDJ9ykqwgaPl7drAjF4Prq8Hie6XBVsF4IN7MaXkxzkkYobuoSDEdk1So5gRb22EX0l+V+3fqHlzhpLJUOPUpe1tPh2DHQupay6TAO01JTTrZp2TRL3l3fUD/kbuBh4G5YazWg3Hcozol2u+M/f/n1Mq+16M1xJ+SfPt1KlxIzSyIySTJNiyRZNl3neTzucu5P5/nx9fXu7v769ub1+fz4cKbMVzdXZbO6VlXbLtXVEWGaL1tdxj6DGwQ+PT3vxgQYQ9c9PX8VhsOhm+dZCJMkrdtynrvEtZZ12d5/vCKPBrXdSkkph8U6zct5ur29dvfpddKtWFFJaXc17odUNrUwRqruKygKW3E3p+CUkro3+9vmykQejvFGLRNmV2USsbeITPO1KEtStRbpDQdzU1VkUQcRQWght0YlCAsnJoPWEAZwL2+ocFBVyRKtaongDZ/P0uhsEYHMDbuIZgBveQlhUm9GDojwlqtpzeEGl26VrKrKTMyoasjEibn5myCaWFEY61aYBdz7Ple1xJwkqSo6WDgLtTd91SookqTWQoQA3uJTOaVlWVKS9pMBeOuIJYYGCGp4dBFRVUmCTQzA9IacACBqqxX35lwiaj+Hxrp4w/ASugfQ2ytD69mrOjGlnNrmnInAAVv9krmhWprVsmHsVCsGRPX9VS4jwRpksDv002lxj8t53R+6JDhP2zBWZt4dutt3h+eXFwBeFy3zdpQ95w5uoZouUz2/nP7yp/+1VkcUdV82ndelP+72AnOpBvT6erq6ubnerr99fci5K1u92h//8fd/3ByPl8fTn/7258+//Wuet8evX6LY4XCYTi/v398T4p9+/nG1EuR5N/YVVB3MKOuHT91W50y6FSjOHz5+WtSXZdnKtBN0xtfLHGZ1nodjvzvs13Vd1/rt69PVzSEPuvc4vdhmpqftajeakiusU60K08t2d7e/uu/L8mXYj7t3t5Q5PG5u7n59Ov2C8QhwMixI0ctWKgAUgJbgJEQkATI3hSAjJ0IytOrcAkLIwqRVk3BAhAELcU61GmGKgE5yBcMAZnRTrNFJcgAK2I/Jil11/afjbjtXqXFX+P27+3WqCekogxKi2u44UN3KugptPSTJREy5Fxc0IwbvOEw32IiAAVUoBeMbPwhQl7UfOpEUAeHK7pIzmCJJYyywkG9TXWfO8JaCQohWYQPQqF7XlAQt2q9wACFiU2gd9dSqZjZ22SLWdaFhV7VcjeNkFTiLEgYfd9cnm9fcreYvpXw/1xSkiGoAgNQuUogiIuFk9YhpP61Xl7J7PsPzWjenJJxoWyozm8W2Fq1N6BaulZAgZzXLXb+U7XB1Mx6uFAFY6rp2476oElE39F+/fF2m6frmShivjwfJfc7MQl0nZhoQrw8vSyl9l5H43bub3e3Vr//tF1XvJN/e3ZrG4+/PQz+8+/T+68Nr1FiXUtQBUWupxbohb/N6/HRY1q1avT7uENGqpQRW7eqw67qMgNsyr9PEQrtxXNet1nI4Dl3uwGqpK4UJh1WL6v2QfUspk2nQEeIldvs9J+5SctOUUxqH/mY/C6+rqUfbu4BhuCGhQXhVJiDGMDS1rkuqAAHMBBpChIBv7FYU9BZ5xFYSDPcQSdCuWeqBYOYkDO5ExCIQ3qairdzbuFWAkFjMLAlX1UayDHe1gviWdW/W3JakwwAgcPdSjJN4WKJUa6U38jgz8bZtIqKmXc7/U9olSTyMsEFRXIQcIAgdbBgG9zCzN+mEg4cyYZP8NX2HgxMSM6/L0rAQzAwBkpOa5Zwa84uICAkgUsq1Vuak7caHtG0LIqtqzqnFWdFB1UMbuI3auti1qdOAG2NAjZCI2MFE2N4aBFAbKAJaC4HaVyuxBAZSS2F7Q/u2fpO5NckDIob6Rk773E8JZ52npc9SqnuAaQzD4GGvr+cfP33appUQPn58//p63h/2ZavT5XW8OhDDMHSc0vSy/Pb7P4ZuOE3nbjgYcASX6nkYD/d2fj733XCZ5vv3H91xOp0xgIOujlddP55fL9PDS5e7u9sb29YvX7/s9keS7tfP3y2WD+/vP/7w48vL0/z0enp+XdZynsu7++tPd+/2eO9w9nrGlA+3xx/Il8sFzEB1t/u4TZNVez1NL+dTlxt/gaepAE7XdwcgcsftMq/bOq162HckRCjny3x6ju1yskjDLlvx5fVy/9PH4fa2aH0fcw/2Rbtn4klFK5rkJXQjskQCuGlRtT9gOxjhRIJOEcDAktiqhUXzZkPjoThCQJc6LZZSIoBdtytbQTIACQBG2l+Pvul13+Nqe8mjcR4SSgwbXY25dugWnTpTDuEBAskLB2gNiZQYAAQBAI1Ql8XNMIxCzS00OziaUxhaQVcMZRoa9MjA3Dzl3re173sPUjUrW2j9A5sCjS7lXsHBAOo6S+II0nXmLiFAm9JoWTj3jiHSW60IwYQUIV0f4JlZHRI5QEBYyrKa71bBvt8MLsk/pJ47nrScqi7hCjGtKzJ1OYluo8fuctk/XvrnaQzYihd1TgkRwIJQIrAUZSZ13+3H82kRkvM03//woViYCZAAcZtt7HdXVXV6mY53h/m8zMv6818/CXLuB49g4eNhwJR0LWUrkjtJ1DtMl60fERF//fvv376/fvhwe3v7qSz+9fP3x8dLP47Tum3L6psu02YAECGShV0SJ6FtWarX3W5Az1UrE57P5+urw/Xt1dClUsr0MpPA/rAnwHVeckr9MFpRIhehMq3mDZcJ9Eb8Y2GfS7Xiai5jMjM3VcAeIRBLsWlZt6226wV4Mya0GD4KNwJCu/KqmxILEkV4S6FENBhta6MRAjYgaAKMt2gjQTQtAzIRdv1Qa22J9cbDDMSU5Y2QFq3uatumLOQBiA0nHgjBLIhvKlESioBWr2/5opZT3WrFP7yM0KiCzIjY5d6scR+NgMKDhFQNibjJl7GJiyncEJFFwmqXWM1NrcGrTGtKKcJZBCMsnCURITO4Bydya9z2CK+ScqP8m2qEMYtp+7cJQBDJbtblztQafC/MEcjNJbGpCXPVitCIeujRJKiNg4RgiIimhYib6ZCINJzaT5qCrbkT0C2IsTWVAZGQay3EqFpTSm+OnRK0GyqcKWC+lNJFIhzGHgPOp+nqehcQT8/PNzdXda3dfb67uz+dTvurnRsRQS0VILL0sKfz65KyOMTl9NzvD9p2JMDdOErKVnF+vUzn5d3Hnwg+V43nl/P9u5///e//yCLP0zJ03bJMN3cf180+f/lyd3uLKV1eXtffvu0Od4fxfb2o90C2pVGn1/Xv58+H/dgP4euSOIrpjz/+WNd5uZzLPNVl3V3JdF48MAlWncA0D4dujDB/eXj+8NOPoE8JuFidNt3U/utfP21LSdU16uvLtq7fP/zlnR2oi/XBvtwo7t8fh4XjPKXMR7WZseY8m05gJUllXId8WWILWWsNB05ctq2NUQTZiomwNPqrBxEyC2CEY1NZZSEt2vW9GP549XFezm6GSClxz8hDTkDH/XAlPamRRrD1wgeSCqqAXUos7Ko9BzoQolrNSaLdwtyYueEdHIFEMpGxeDUSCeDEQsTiJYEzI5F4+9oDESNxptw7IIXjWw+OvKycEkTougGAu2GAMIM6ZQQINGtMpMZPDVdEBFcMQ3eEt9STripJMN5AWIggZRsJb692GrhWO7LddVc8yMs8nVnr0D2fTwvwWrc9YKrWv87+yxO/FHKILBiY+h4J19nLplvR6oGE58uUcj5PS1H1gNx3ThyC+XoPTFvdamAeds8PzyT56uqw3x3X6dv9/SEnqVUN7Hi1v7o61lLXedalcO6S5Hldl3VFTMz5+8PpfJmP+/3V/cfL0+XheV7WNrfm52+n6XQBIEAauvFyObX5mUgi4cfv32/ujrtxV9cCEKqaslzdXKculbIt00pJ+kFyl3UpGDiMWZi2WlJiXXRdN5JIqaH0qoXXqmgg0u2vM1wu1at5DJl2Y4pELgmPSScHCuZcN03E7cgJgrBgSYZOgIFh5ujoCG4GyEKCb6Umi4BgFgBvjCoPjwhE0KqQhJiqas6ZGKsWTgIQXc5m5h6uCim1bW2LQwAAIEVgs7i6N7Igaa0NDdhSQI0p7f6H3rrN0AGZqKqCENMbdzoCtL4RnpnFqjcwQ0NohodbsEgttUHT2ig2Sapvee0gIlMjIHdHRGEsRdW077omgG2ARWsddEBAaeSK6rUREJERA5klzKpas5XHW4Pr7SLg5oBQ1sJCqrWtFiDQ3RKn4tpCRU38Yu7CEhgUQMSl1NxlLSYsYd7MmgHOzBEgIlVNiM0UCdvYuhWCqmo/9ny1i763y5rHrMU2BfQCEf2Q53XdjyMQvbye9vvx2+eH63d34zAaYtflsm4Q3vGwbluXu3RzUIub/eHx6Xld5uPN+8j9vGwSXGtwl7kHAq7Fcr+nUl+fTinPkunL59+/PtlPP31Ej9Pp8vNPf3l5fkBmIOj64eXl9T/+8e8dDe+u3u8GLtPTeb4s69aPw8Pjpets3+PT8/zv//jXt8fLx/e3h6EH2uZlowhimZZtyOnjT3+t9fzw7alLAA7D0E+vT4ebnY4dZn55OK3b/D9+/fX+sF+f3Dag1L1Om385T1MZJcaTlS1MldypBl5er8fx0NO2wQx+7GHzba2ygPRBhboXrIsVNxRMTdWAhEIsyEAQ1YmFkCiAABMTUJv/UQCj+tgh+/zXq+P5MqnajvkqD7UaqA9I15LQMMiAITV/Y0OPuCZJjhim7hbmEh5lAQtwNxgaUCtcw6sDhiMl0lrdILEwE4YyaOjqbsiIhLqaEBCxGiAQgdeqqC7EZu61CrTCHHqtddtSykigWkUzQHg1QjQ1ygJERFi3GgChGmHA3ALc6GqrkaDVIrkDhwEJMAAYJHVehjAXjohD7i7Ure63u+P3p4eqMbqWry/b33+FR91WlzFN1QIMggMQAi/ncrgaU+5fXy8IfHd3ZYEsMS+biGxmu8PVeLhKXfft+/P1/V0pVfr08ceP0+n85V+/Syf3H+4I4DzN45AP+8HdAtADMXdEsi51Oi9fH84//XiPRDl3H+7H3I3LVD5/flqLe9DxeFO2uqxz5vz94Wm/P1zOZ3e3Wm5urgDDaj0exv0wYIBpHXI/r1MEBURdS1m3utU8pGHIfe7O89oNnVVNzIfD3nWb5jUAun5gCnRf5o04dX0GBy3nZb6obhru1cLi+m6QXfd6WR981YAAanrUNoXWN68XFN0QompNIoQM7EgEZgAg8QaJhiBwMyZ0c0QKDHRvw5bG0mnsb62VnJnJ1JCghAMAI7U7dYt1krxx0olbcAIhQJibjckBwJyETJ0EI8LBEbDRFBpviyipOSKZmqMCUgOXEmFD2akqE4WHgVNzERHHH4AKAECShu5RD+bs4ZLYzFOXotnbNcpaUEigoT1DNyVCjWChxrRruwhAAEdt5axqkrJpbY9GJqpq3jLO/GZ8bQMdSRLhzKyqzOxhEFRrFX5D6bo3J4YRo2pTEVhKb/RXBCDnarUthRDJVNUNAi0ggLClxtVIJAICtCzrvutrl4Bgmbcu5cOh160K5emy7qE/lznfZ0pU1c6nqe/H69vrTW1dlz4NL6cTEeXchxtJYIXU9R8/fvj92/NW1iGlm9vbZVPARS3uPt6cn14RYX8Y3BZhKuX8/v6YYJuXqS6rmdu8fP/99+P1NYeQSOV5N/bLaT2Xy9d//grEueMS9fvjt4PHu5sPHhMw3n24H27vl/Xyy28P91fjzXHodsfXx+/dmPb3h+Uy//b586f31//X/+N/+/zl8zLNxBjm4CZM795/3FbDhHXdpoJp2E+vJwHuxv70MtWlzn133Mvl/LzN8eHn9xpUS2G/jJmvhBctJfJS1o144f4YNCHvuvyiMvm2Oi61cpdboLmCCXOr45GwIP6X66uX08qExb2q5i4nhgHkOsm9yG4YQStF9NWEGRE9IEHkLtVFEYPABRkptlIZCbaNBd3MTRkUIGzbGNl1w/BG40rEkSS8ohmS+aLSCYAO3WjhzBxmUTWiBBADAQATVQQzB4JQba2bqBWDwlsv2t3cS5V+gAAHDC1vOtMGuDAHB4+K6LYtoI5Cps6dAIaXogacmCJIHSxI2LQCBhMAQib2Vo4PyIHFAnJ3SMOyxvI0nf/94em/Px3HES2K0zZvORESJMG6lr4fgPK6VK0+7Mbq0PVjtaU4IMF+HNJuRIiH7w+3N/dBQow//fju++fvp6cpD8Onnz4C+LzMN1f7JHJ+fpWUqsFSajhk7qbTuqoehzyMg85FumHsdpdL+ee/vtaKZoDIyzSFu2SeL4tZmDkTUeDx5ooEwyMJd3kMBwrsu75sKzPlvrNqpkaIfd+lRMfDWLcNw3NHgmkYs5U6n6YA3O12IpAE18tcivf75OZbKcuybls11W7MhrA/dtJ3KNgde2S17Q0rwMRm3gLMbVxgHmaehKsaC3kERqQstplEhNaClAghpeRuKSVzCzMmDogmA4oISW+eFkQKg6AAcyIKc8pkb5Mioj9QZSxsqsHk7kAE4cyirolTqZUCAkLVw4NZcpZSihCrmWQBCFVroN1WnW35H4B2uGN4aKhw8oiUUq2lwYHAGoHH/Q/rOja/axsKQcviRZdSDX1DBEhqMLtWD4bwlFjVtGrKJCKlKr+NbBCRtFbANyiYYQAip+RmhFjckwiYG0RmKkUBCYAaL9rCIdDMECAAicncwaHt5iOChMLe/FTE6OiSk6slSVvZCOltm2fe+gEppbeVuymLEFF1w2MXj7K/PpZpKVuFQO5oT31RA9PTafm//Nt/OdVz36fv375d5undu3fn53O6z/fv7pa1XE6n9x8+LVor6TKfx+Phw7u7//iPz2mv73/sj7u9Oz08nqCLm7vrp++v27YdD+M2XwhVV++ylCJeY7877Hg4n0+n51dhiYThdVmm4/56rSU46VJrrR52vLp6/v4a5te7/Rq2zD7e3l/fv7vuu+n1BTnvrgfuhvPLw7Afp6Ln55NgzOvy/u6mbnZ6fbZit/W2H/tVX2/eXV1O/LCuT+fTiF0keb0UCf23D3/+ev4sFV/P6/V+/Prl4k55hLIWCai1vu+uB/AMkMy2dRq7OisNalu3u07dc+CsfAKMgLTv1vIGAWYhCojqHfI8r3+7OZZSw6y4aLFOeMdpUOgzklonyUvJgVgrUpiBmCIShWE4Ghi5uzG46UpkHIyuusyNQWJm5lWEyrpQl1LutBaMIOIIQ/MkpHVxRBZCzu7AQg2pK/1YsLiruQSGhROG1wKCAGFWEYOBEcDU4M1tqV4NCcE89X2UAsK21NQTaEQ4mLYytdaCgLq51RotSqElVKPWlFJUrNs6EBMn10oRVotTMCGWGEVsXngpp3lZ//Xt8d+/2UorOwXg6uh+Pm2Hq72aV3cmqVUpKAIDWVLayratVRLvb6+563f7nSrs9wd1W8+v13e3L98eatE0dPcfbwBim7bdOAw5b2XJKVmN87wM/XDz4U+//Pf/r5rf3tz0fd9L2nLi1D09Tv/x75/XCuPhsNvvptOUUy7rUpcqiT9+/FBrWZel6zO3ua1QziKJQt2juikEtOK3E1A7tRByn11Vy9Z3XTdw1BrmAVbqxkKH/eig59eX88uZE3ESXdday1ZWVeuHLieBLte60VTSu0DRtdZ5LU7SmjoBUYsiYZZULRiDEiAwkCEhORJBXQsBC7YRPiIEuKqkrGYtc4lE7mZqXZdbjKWoIrTTNTKLqSFC12dvBH11B6A30ymYt9Ckt4GParhXQHRwESm1JhEicghGLFtFIg+ICK1NYkcWQUiEoFWbDKtliv6nKbS1zKoWFgq3BtRUqyxEBOxAyGYuIm4RAClzOBBRrUpMYd7mM1YdhdxqzrmqVVNzZ0kAuJYtJQEH94aLQRZ+cxonbL95uAOSmjKJRxPDgJsBYVvY1qoi2LjZAO11BXUrnFJAA/RFQ9tBBBJbe7I2mjSCwxscsRWO1Y2R3x6KCFYLEANEo6nkqyFd9WU7H6/2l9MkgeHRjzkZTNOqXv7HL/9+uN4fju/6cfj65UvKnIf05fPXP/3t58NxjLDX89Ph6l1YvTvebpunnP781z//t79/oe/PP+ThuBvnLb58fUq5u7m5+ed//4+c07uPH37951mtjuOoFctaDv1IQ0KOpdRlnbGi15UTm1ZEFsw37++IwEmR6Oa6nC9zKeVy2Sp+jy9PBrVHuL85bNP5/t3x5x8/XB2vz68Pf/vTzenw/Xx6Wabtmz//+PHT8Oc//eM//uN1Oi/L2u+GlKTL3f64Ny1esT/uKvh5Xv7Pb78cd8Oeb1+Wh4dt3vXp6fGyW/D15TzuxnHD1PtwPUifDglTjeX1MXdDqhuk0DxcB5+BFhmNZaGYk5KIA3REh76vW/HVrplGLX8exk3LUk2jZKKEJAE7jw1CwoFZ0N0MLDiCtObcgXst1c3IOmgfmHUrWrsuM6EkDlVGRJZSFyBIXarLBsHNM6HLOhz3rsUqSD+ydB6A6gEBHOymvpH3zOwa4QEY7qpLqVsRSqqlXdwAqXFKMRyJAHBblmE3ABIE1moijFl0K+Be15mFIhCYCLEsM/aDVwXhMEeI0Apd1FmRiBi28yXtEbbNI9xUawV2BPGlZBRf1vU0vfzyuD5tPAyvr2W360QRA3e7cauGbgDYd1nda625S2aGgP2Qz6eFOhl3A/T9NM9hwkNHSPfv3m9l2dRT7j798Akhzq/nPqdOyCyIsrk/PJ1Ol4t8yL//x/+Z+/TucDWMu1BYp+VS8Ps/f39+WVShG8Zx3F9ezlXr5eV0fXdY1y0PHQSoVkDYj11Kubou09R1ewpPnaxLaeKQWgtiJM5hGhr9PnectK7DOJiuBFDNNDyspiTEYbUEoZbaD6kf+zIvZauuzizdCJIEiNx92O/ybrcten6dLskxMRgKs1YFQyZW8KIVmsdUjXNip4auUXNkZhRxdRIycxEGRDUNBxZyAFdlojf7HZGqIkCje+eca92IBBAcwiIYCQAIqZYKiG/J4/ZkeDu42cISS3ULs67Lro5tktlA+OrNjdL1g2ohQmupaQtK3MY+Kec3j24i8Fa+sgZEExF0AHcRIUY1a1Dldrn2tugEEGm4Oq7V2tRSrbbpDbNUrcLyh8+viR2bNz5Y0Go0KgYAknAtBaKRKprZCTyMJVlYTqL+phsJ9yQMDZMrxEDqYaq561StlbyaA6T56gOA2zQYMPwtMMrMDZUB0dKiyIGB0JBSEYHIQM4p095lP8SpJOK0iBddl2IGt7fH3GUPr16eHx6Xefrz3/784cOH0+X14w+fCOnx4XF/PP741x8evp+m8zNz9+W3zz/8+GdIowz+nyj/6/PjP/7++fjufe6H69v7p6fTOvhP//aX//7/+H91/+nPx3fvl5eHsm5DgmEYtuV0d/vx7v6//OvzL7Yr87JUYDQQyrPNmOK0zrtdb2pMfnt3d3W9ez2fex+nbfV188DCSbfKENv2+v3z09DFLjNyXF8P0i13h0NZ53//+7/3Ob378L4f+21dw2zd7O7q01aX6bwawPF6j4hIaVnmi26/rY9DTptWLXPfHYtHMH7+8nQ49gXtsOqxSt9nV4CL4noZU66m3dXtLucjgqbBkCfElcWZTE2Is1pisRypeiYT3o6UVoICwO4cToiwTn2jZlrtuq6ULbQGQCxQyuJVoRpE2ObMYlrC3N0UTNvnzSxLJsYaWDdNQ2ZEV4sATomlNw0kyp1wkkBsCbuUUgBqUcoDICAJM4RZWIS4VQVAyVl929Z1OOwiENzDIjAkZzRPXW7CGtMCBExUatWyoTuzEEEEWa2IwJx8q9tWMmQETCIa7kUhiDNYMajaj/v59Qzg6h4QVpcsu+0yO9L6+vz8y5fnr6/YuNKBZalRdb/rigcjzVthhNSpqVu1YrbfjV3qT5fL9burq/f3pVaS/PwyR1m5lnG3//7t81q2+x9/2h2OUat6MDIJM6WHx5d5ulzWqq5/+9u/rfPSdd3Vu5vU7bfJXl9Ol9ft+2kKE8bkDH2f5tNrXQti3N4fpRNmGvfDclk0yWHsUpKUaH5dE7MIMFHdNkkS7lY1wHeHHWhxcPVCKakugLBtSxLSqsK0P4wvD4/j2CM6GizzdHm9dIOUNUSQMV1UmQlFJElZq4INuffMnvIMcF4qBBKgFk0sEKihwtJsiAiYOGmpgUEApSgRQkTRIo1RgIjqHu4YkFKnupEIIQpLqcqJAZo3+K0RVUoAoHskZq1BiN4+H6U2MhowtE9SNExma9IiNpJlseIWEa1YA7kXdA8PRMCgbd1SpuqGgUjkDvCHMw8iVBUJvRhCvDG5ikoi97ZJRfAwiGgMf8BGABVOjSehWpmTubVyfpg3hGe0/xfIzKEF9dvlBkCrMrM7sLCHN/GTqaaU61bN7E1a2Xw44ESs5i00RUTm3vKgDoDqQNF+CFo15dTiocItM+rtFaH9VSGCSQJCS0VANXvrBSC6BxG5h5lKagpJB8Lq3h+G0olhPD1fbg/9ctlSSuuqTw8vXZ/u3t9ul8tlXuOy/f7r159+/uF4dTOvy7DfvTy/Pn37zsx3Hz/pwWqpRdfv37/lbmfSX9/eTKs9vOh0Kf1BQD2cP//27eOH93ef7v/jf/zyw58+HI63lV58sHBaMF5fvy/T6Xrcb7YSwsM8E5GZHg77AAzwrSzT+fVwHF6Xrd9f0SEN3fXe19Pp3HU9BsYC83zCJE+Pz0PmjvHd3fX5oqZYAcbj9ePjC0T5/fevTIgYknh+vUT8enNzSAKPL6e1TvvD0EjggL5q3Y/9ZV5H5q/Pp78crjiLon17PF+2+sGirC/3n955xHRaCCntMUNnL5d82I9jxyLBsrovjsYMnEzrIJI8nNiljoRQK2YeSLqc3EyITbWpCcAKuINac6F4WRG4+Sw2LWEgGYHJ1RtqRYu2ermrVupyNxIv3sz0IgCU+kGtgsi21dSJJCZK5uEWkplyb9XNLErlETmJI4WVqIaIpubIQAREqR/zbr+8ToAKjACYut7WRbrs1m5LW0qitTBCYJRSsOusqld3927Xo/s6n7thp6VIymbVzJfp0o27WBUxmHB6fKjTFBaceSvFlmnzWZdaOV6fnr98eVxrTSnNSyEESjk8ltWu+r6qMyITaQHJYsWYxMK2Zen7nPf7MEson3/90o97GftguSyXVf3nv/3c7wdhqEWHXc8YETQtCzm+Pq3vf77fXe2fvn2nlO5vbjDSci6//sfD07RhpCR7zunmKk3TvC5bXav0CRHGw76WkoSm0+Rau0QQ3uUUAYmx71NmJqDUi4ct82aqnCExa+DTl8cff77vU+e67nbDep6EKQGY+XxZEWDcj+u81iin5xMRLpe1S/vU59PLSas5kTtMU92WddjnZdXhluEgJctmpagFkDC7eUBDmgMEmFvOnboiArc1AJG7MxISSDhqUWR5I+YQbVr6nD1AhM1NCBGgRXeoiadJ3L2dhqZOjAFACA4tmO/tD0gplVqFUQ3cPMI5JQBQUyZSM0ZqBKxt3YgpoOEZjJlLKUCIiO3YNPMWLm2ba3dAQkYGh6YUajV2ABcmD2RCQS51CwNmzCmrtvr+H7zmwLaYRWKHCAQGDvPwIG5iGBDmrWzMTCwRjgEGSsTN9dHe/hCDkAODiDycER0CELXWnLNbaBgxuxkGEIJwMldhNjMi0qotVRIREcbtjUQbRTuISWtBlpQSEnutXe5rW5u0ORg4tPazBwRKylrtvNRu7AtJQLyeFwpi8KHval1LqdNp2h+GPOysVC/26z9+/fjjB8mZ9+nDh0/rukbVx98/H65ua9Wrm+O2GkrSpf7rP3453H1c6/L4PNXA7rjbObiWh6eHd1cfHr5P55fp9iaP+8NuP3z9/LuAOtO2nUIVMtzfHg95/+3xYT/2VaPWKsIUfn21W9ZLKM1bqUjrVA5914XhfOlTcoKXYl2m9+/e67p2OSMP53XNqad+d16W/niYX8+4zn3XidCyzF0atqU8+sv+sDv4fprXZV0U9ebdcZvKZbq8LJc+yet03sv4eF67zIf91evLcnoN5lIr511BXNbVp9OaT/X2XU67IETSmq+EUk8AAg5O5hquskUmSMzrWlg1gDbzLg8ETRDt4FbLyil3OalqWRS5tV9jK2fNPQMmpst0wdAueqLQuSAYdhndCTw81uWiWjgnQgoSlDxe3WzLJtLVbct9DlfJfSBBeEty5/1+m9ZAsLJiBCE7RygAYhOLce7SsPOykAwATDnpsnHXgQcyozDnpJs7BLFI30UtthUzI5G6bQjOnCQLQjRnplmVJJy4rlNZVgRPKem6AEbZirktp6f91bvL60tVC93qae3H3bqWy2WZVvXAcdepbhzISFWVEiyrEdO8rEPuc0fbUq06SoPmRRbe73o1fH54SAg9y8PT892ffuh4/Ondx4ANirIMIRAO5Pj4cDqdl/d3N3/9L3/rx3S5nAHp5vaGJM0vyz/+/nVbmMbduL/yWsu6fn95lo4IMA+Ss/Rjr7VyU8pJ93yadocxwsJ9XTYtBcfs1cyLCFe3+TLtjnsSIADb9PbmSkSYceh3dZtNN5e0lNr1OcwAYbrMbh4eh90wzbrb9/3QQwAC7a/2Xrf5snkxEa6qiXsnto4XwRIQLZQcTR+KiIIRzQFZawX3cCRmBmgLzTDHQGFhcxDhRuljoeYjhHA3B6SAaOMU84pApk3YSI3MbOFuSMzW5AyCjeoDAWpGwtWrWXNySC21yZXcg5HNrH0UG2eNmZrBIMAASYgi/sCue1TQLMnd3iB0wA5h7q2lZeYIaKoIouoJGMDwj4JCuBIREVm1rkulatMO/SHXRXcPDIdgeWNtuqsh0Bvy05phGQ3CnZNorW0U5hFCEBG1vGVbJaVattx3Yf8T42Pu3lh7HubugJiSaKlIHPCmEiPm9pxrm+kIaIkqQgKAoiVJMvfG2GgdPXPDRlskJBF3q+oklK5H/nj9ejpTdQw8PU/Dvt/vhuk0ufo8bdf3V7gbCHg5L68vp5QEAD/9+cduGNd5loTbfNbqcj2knqt6Yrm7vlnWSSCuro/Pl3ULOxzuyP3p8enb0zP349fHr8fjp5y7CP/P//anl8eHh2+n55fNks5Py3LqPn78tP/TD4RSPc6XE4TNcyCHCnZ9V9Sudruu68VCId8cxrqs02X5r//pU98PQ08vTy9j7l+X9TLVy/RQSv3hw/0PH64vw+vjw1fF2q4Ozth1Y4RVVeTo+5SYz+tmugy7bne8+/r12y53lZKjn07lw/u7qV4GHq2s88W9bsNejweGgpfXSmfdpu+f/nyXGXSdc2IYvc+5lwQcdYutVqZICEmIOXutVWeisNUwLIIwZUbgIFu2dVuZUDjp28Smn7eLl+IGOaeh69f5nFjCbdgNy+mMaoge5uFey8ZMboC5ByIHhtxlaMojUtdqzhapF7QSCGGBKKnfM9lczuEOialxDFsImyXlATk7CvdjEBALMptrStkpAtADzEIGRsgO5OalbCgc1YlJcrLq7m4add1yN2ipeeis1LbTysPOVHVd1TRUIXyeNrOnUlYIQNP1fAnky2V+eLqsRZEJNDJTl9JaHYkDkCnN69bngYUNwtwtYkgJAABi3I/LZZoXfX2+8DBcvn0FGnfD1dBx3TZk7HYdBILDy+PrL//8/e7m+v7uhjMzQ5a+rqfxcHRI37+ev/0yX13dDrthNZ8vSyasZQu3cMqZU5cTCwGoxdV+d7ks27zs9rum47aqbjqMfRtgbcuy/3APS5GUailX+6OQJGIjQCARWefL68vzcT+UUse+c7dlmjlBWbckkoSfpllSOt7dMMH55VUyretW5smLl1KBghj2t32Msmb+/ry2jonXt1S9qXHCFkok4ghvB6C7s6BFEKEweUWJaOrO8AggdPC+y7UqETCzmjILAKppSkIIHu7hrfwJrc/KqOpMjO1ERmpOVDd3cw8jEotw1TaUb5JoYiBmaYe+OyOpqrY7OHHTNrW7f2AQICNX1SaYxwBTRyIWdnM3RUZElJQAUQTBMZqMOBpnH8PBzUSkmjKTt9cFkojwqoAE/MeCFskjJGd3pT9cBEzk1ZspmyhExMwCWgBJG8io/XytlkaWJiJohAxEBgFoDwJkEQ9TU8eg9hBibuDPMEdiDExJzN8gNLUUlgQRGgYB6u2Tj2rWdf26bX3ut7qGWwBKl8G8IHKXbt/dLQ/Puy4Jw7KV9tI2b1sy7s7LeNj3+26327mXUubL+fX5W3d1dz/s9/M8C8OYhsvLK0vud9fP50utPBxHSt36MF+Pu8nil3/+8vH+9upw/PLlAQIt+NfPT3/508/iy8vLucv5v/6vPzzd7h++ve6zPJ9Oy/Qwb3Z7e70fj7vuyl3l4/3z0+cfPtyt2+aB3WGf0kDhl9dzN3Q3V+M2r5GIBfbH8d3Hd4nl+XX++9/r6+vp6Wm7nH69HtMPP9z+6S//dr48b/PsZtNWUk61GAeM++HyOpHA1e31etm2reyGsdvxrJuk1gFYA15vrg9FK6NZ1RX9t9+/6rsR14iQ1+d1uUTOp7BDzjjHSzqU8e4GAsAxAVZVs20DMiwEEbUSeJ1WFgM0r5G6yHkQMqtFG+HeQ1eVrgcGZta1MkqxCoho5KVuy2ZdlZxcFdxb4qHrOrAAAcmdOlPqAEgJI5DHvS1ry8trqVqUc3Y3rSV1B0MjnrQWytr0vcwUgZw6RCbKQJ2FjuNYlkU9mIklc06Xy9ztZJtcUh+AWjddi5lLl6KomSXODBKudduQkCURoHtYKRbhwQBgtZZtc1VGXuelrhsYmCkC1mVz5OWyTud5mWsgMYW7DlkMoGo1dw+eERLLy+vp7sPNtq5aTJA8YH/Ypa5zCAu4nC9aqxERdn/+X/5Tl3mZl8PttQNPr6tGmbfFiv/tP//peDgS0fPzy/Xw7rffvpaiYx6Xl5Wju/u03y5F53XeIo9ia6GA4+GQutQP/bKuXd+t85KETi+nsikLB0Bi6frOo+SUJNHQdWXbbu/u3Hxblqrr7c1dzunl+xMx5J7CPdyt1JurKwgbj4dtmkJ92I3bOiMxJ355epY+78eOHVVrl9I6TfM8nx9ewyOP3f6qS3vBjkr4pdgMsdZwMoqGIQfQFiAJRoIIAmYh3RSJrAUHAcqqhCwYYG2kQghACNDoQK2SFQ4ehsxEjECqChjC8mY8AoAIVW8GYBZsdHuk5s1u9pNG5AcWDn9jHjQtJTXToYUwRYCFEyIyt91pe3z0fV9L6XJey0ZIFoZOjsiEgVDV4I0HYim15xYC4B+EFkEELcrCf5hhICIcjVAAQbVyEgQkxlJq13XxxqT2t0IvtFgOBTbOp6fEDm7VSJg8VCshxdu0LQLB1Ii4/fjcobml2oqYmds7UCuXwR4axAABAABJREFUEVAzfCG+ecAliXu4+1Y2CPCAtvkwbfD68FbaVAcEYal1y1lKXZHYVCl1gBEEBRwR6mWirRJ4zjxQbrrzYcjgsG3l9jZHtd3tjqU7PaoxXy6Xrh8P+Wo/7Cr4MPZql9PLUp0Pd7vzq55Pp/F4P3bpn58faOj3+93Xbw8//vhhv2zPDy939x+3Zfr7f/z65x9vbm+Pz99+De2u9rIf322z/gzvf/nXr7fH8fT0qOvSjcM2T13X912SDgbuh92Y+l4NDsP+548/z/Ojbsuwvz6fFwXsr47C+dv3bxBxc3O9P+zHDJfzk2912cJsJur6PXvYejq7EASvm0qSH/7045dfPps5SgzUudn7d+//+duXJHS+lGHsl63q99e+GwriWuB63J1eTylIMNaCBlKUnr+eMlAe0wFFci2nV04p76+AZUh52ybXahDbtHGSlBgILQx0Mzc0jGKcu1BtvFqksOoRhZmppYGLtslkThk8RNiqQtVazN0RQFJS8JQ4AKwW7vYkDCj9rt+WlYhxU5Q0T/Px/q68XmQY1a1sC3dHklHxQh5l2QDBHSQlrZGHDqmzCE49gKuZbpa6zkOxEQ9zDkZiAQItGqVEg6RzqrYAguS+LptaEBPnFIFVgUClE99qa0aWsrV9W122qCosTBBOZSnbXJCxlnldFwhnDIbAQGRgoiQowABwOU37cRjGYTcMr09lHAZo6m+grs9lq8u8EFLqUjf2+/s7wK2ujsxb0dfzeVnL+bQedvuffv6Q+kwQl5dn7Lpfv33f1vqX//pXr+FbfX1a19NcShDC8TgS06a62+1TJmCKWhKTWVWtEViLYoRIqkUlJyaqiyIFUuMRu5pBRLUCiJJETd1t2A0kcDgOUTetlQWYaVtXZGSUsi3btnVDUlUIHMcBIZZlFqYwE4TDru/lelpWCgYP6QRY5Gr/CrpoASYO4pzKohTBwgGtnmrRVNYa1MpbQEwcHpI4DKWdcQHRkMhd11c1QnyDQCACtpssehgSMDEQWA2PYKJwf2P6ISICiwRGAJp7c7PXUkiIuDWf/vC6WDS3ajQv/NvN+u2I9rCUxI0ESGsFgLVuhOjmSGKqlNhcI94w/W/KJVfwcKBOuGyVRdoNOol4GAC0uVYTgwG8KebdrNlvckoR5gGqlnNSc2JkQa+OiKZNYgP+h8EgPNrsxc3MQpIQYd02EQGAAIaWcIrGOiVXN1NJDR7HANASPtG2F4hEaN5QqkgiBOgQWpXxDeRayyaS284ZCN1V3b0CERExCKhWIEJz7rp0e7C+34ds89kRxnFc1wKGZdXd2BHi5XwexmGbpuEwXn/4uK6rrvr0/VHddscdcS7qu5vrnPx52lzj7va6fn95fX6+/fQD7vuHp2VZVSS/nqfjzbXVOD+9uvnV7sOv//ht6P5U5Vgjnl6fe05mpevzf/lf/vTychm63sx1mddp3qaLYypftnHce6kkU07j4+tlHMbc6bq+nqd1K/AyFWO7u7sN0PmylOWSh67rcmz9WuvLw2up69X1Acj2x/7jTz/88ss/728OYT6vW/39ZX99w0QPn7+eXub1XKjvfvzw43m+CNVSNzPf315dLhMDK+CXp5ddh0+P867PEDJtihFe7XCwtNtNpxUYrSonAAPMg4UjgC6bMEHEepoLeTfuuQevqqq6naQfeggSKpdCwuEGEVo27nstFRmaQtG0dP2oZStbYSFOCbxqqZLFwbUoMrFRqElSDINmGUq5moJIGF9eTlfv76XPYdYY10ACqev2t8iIZq5Qq+b9niBRHqA6olug9EPELCKYAIqZKmArgSInhkBB3GoliBq2zTMQCEndiltxM1VlMAysugpxBHgYUqznCyIC4rrMocFCZkbMdV3DnAnXWpZpdjUPH7vUM05z4UTLViOiVGWhLoskVot1q6UqEO12Xaieni4aIUk8KMC64y6l/vr2JiS7hQP88t//NRnc39/+p//0n4bDbhx4ncuXhycQsqpa/ec//xhblPM6nbZ5sWny3Sh9n/eHq8v66pnali4quqvkZKUSYC3FagEQYu3HHrws8+IewtT3g5aNJSVJ82XuUpf71KW8LcvxsNu25TDuW88OkXKXuQ2f1WottWrKQkCvrxcBNw3VOgwZMba1MCcmJneAWJaCKIDCwzBBnA2wy74aI2lRZm5O6gh3VTMTodx1tVYASDk1BKeZNoSmYDO6tE0qkrkBhXoQtAOdtBozqXmopcTbVhqoEhDdPTFXVffoUoqGzEcy1ZSTh7VJSKPFebTOqhNTGAAEvIWCRN0kcYSHmgjr5k0hwJyqKkAICWGoOwVIkhpGCH880MICWMg9wjyRbLUigpozvX3sq2ti9rcCWUgQMdVaidjCLBwcRNjCmZAyR7gwNxVwREBz9rqLZHcTZhTYSnkj2TICASDUWiWxezQlZFiThmvbTxASZy5rSTl7aJOINYYwcSql5JzaH8dIZuBg1lxuAISktbKwuboDQmRKqzuTmBthrlre+ELESOQWtMve49PXs56WITPdpnHYmdYsTMTbsiJSfxjnZQlM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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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imDa7+4+fnp9uayXpZtffv91Ip73k2CQabRGxAQOLVgAgNxRzVgECIgol0ndTB2Qejgncodh6N8YoXAw7aqmDoEhjICU2NwgnIndjIlGXH+4pyxMaNGFOcw4MYEjIzgykaqOuriU2Ab17kYggRgUwuzqAZ5yCjNiBrNh+g0ISYTI49Aw11QyS+7a0zAPB7BEEgFo4xLHifMs3lu9dOtal9qr9tW5FCTGYEoGPeZJ3IARgXGUFll3HslgWUyVmBygNiXe3ixD0hcBo5T+cX9/tRqr/vyvv52/vn7/0/enx9P8cP/D3eH3X359/fJapvzjj9/95W8/C/DL50s035U9CTbCD59++vWXP5vVUH15eZGUIPCyLMfTHpinU2mLZiaD0pZeod093EUP7X0qZTlfXvX17nTXj4duAcaPh3cB8Pp6tsillNP9aWLJnK23p69Pn378lCC81izMhO4NccLxTh97yXAeEZq6jVInQuueCJn44e69W1yeLsKFaQ11mcjdsrBjQJiHilCeU6h1a5Io593h7uhqvbUAuFyvDmCmqcxaozdt3S/1leeJWThjCSNJUkSd/vyXv/72+5fTPB3mfZby+nr5+vXb2loCnucjszR1De9LTUXMofcKSIySSw53FgqHpjXMwj08mFMwATkyRh/dTDAUKELkbq2NJAjjJICBKEy8xc5YQ3dCoCAkZ87uCgKAMcJ6p51Ah1JYWCK8dSMMEQpHCw8IEspEES6C3YMETT2XjNRFJMxn3l+ur8Pu5AZO4d1VVVsbKhpMzIwouXc16918wygEtXrtbj3CXHFpzZZlaRxV47Q7pLv3cv8epym2824jbxkR38p/buMVgQJ8Y/1HQgGAu/tItfcYnCUiDrb4xhxDbDusc2K3TfQxFD837c/Q+mx3/SF5x1s/u5knFmT8/1P1JzuyZFuWILa7c0REVa27nbeviReRyQwmqpIkQHBA/gtHnPOHOOUXcEyAcw4KBFgoJioiKiJe491trFFVkXN2w8E+Yu55geeOd90aVTW13ay19lph4wIgGfJEbIjALfbkorScH3uLW25O2RQwrycAQL2nQigsiIleJUoR+9Seldkhj29fCd3MOIBf0x4hQsAdd7f52PmO8ECE4Z2bh77DFnT3gv71VCHpi3xuYztBeP2zv/pJyuejjVebjHDTcSGR6xplmwEpfDzO775+b6a1yOPt7d37d7Fdadsmcr9u/flCXTmcSz4CDeCuYGE8MRBRmulDcJXMs2QmzO4T1K0DoZoikKVZShAiWHcR6d6t9zpNxIn/hWQoNlFhRgAuJcwYEZnCQYitCwTwIPLDKa+xIZC7G4RxYQCD8FJ3SY8OHir3C2JmYQQQQSmyNbdwJHZzEUToAI4SZc7nFuHo4NvW1rWHAUsJByZBBgIGAAcHyuiKSBOhEJYqRNTapt0Qg6QIcyl1vW5B6BZSq2tMhRHBLO5qXVtzwi+fnq/Xfz/dLN//4Zv7Nzfv3n018Xz+cl6v2z/8wx9+/vGX8+NLV/zXf/3Ld999D9hlmu7v7u20mfn1fKbGRaoIbeta52maawHcmIjo/Bjnq56/XOsitdYQXpbpl58+m9m79++fL9fz0wrd5uPB1PtmoAYaVrXONwTw8edfvvvDd+FxeXkWZDfzCEYMVzUsUQcC+iszZcwlpywm4oDb0835+UWva7hGhLZuVdR1LqKqvblbO93cAjiV2J6v4F7mWQoGy5eff9mam6MaqFoRaqq/PD51tbKcqixNlciwEEAwzZ8/ffr488/aO9+ewNlafPz8U1s3DDrcnKapIpBbY0Q+LKbOABYealx4uEsGwD6lIol7D3AMcjNiKoDphQEQpYibmntXJWEAJLJwL1wQkJnVFAJEBEhUNyAHcmHurQEgRSxzGdsiiFrotddZylwIwaCDD5FNAuABRkJcxUOLEAlqD+v2sj53tzFfq4IBojFjuACFG0CEM8hMDryuDQJ74gzewVhdttY94OX8oozN/Pm83v/dt/P7b2++/v3N19/KcgRiNwt3YmQg2DMdB984bphiv5IC3+0W3IFSloIx7EJTyzKkL1kDh8YlIogH0xt7fR/z+m8OBIbAZah0kJmAwMxeNaAIOO4GkgJgNE/lfw7+408itFlU1ZyIcLfRxSyqTADou+hzPK5BxOL4V/zqNEGvpwB7ryIi2V+OHRHby/boVqMDhkMI4f5iuGcGcT5lQvDAnXXY/egG/5BXCBCektUixcGHvsgHMzI+EEbyiak6gpTy9t3D4TR/+Ordz3/98PlvP1y/fHn865+fvnz2y9WvG229gB1qISKRyRF6dw40MlTXHnWZgaDp1rszSz6vXI48nLAkz8HMBNS3LdO43IOR6lSICYGIxNTQAQPQHDEJH0w5ACOhSOvt1/dMOBK6B6XnCQszpQoi48shghCGP4gHCQGEFOJCASFpKslU5kPvLZkXFkBEVeUCZUrRtW8v69qauxOxafRujgGwRgR6XsyrCE9zBQgUKvOESNMy9d7dmm4NESWom3frbi1QmjZQDYMJa2Fu6wbIpUhXfXO46aiXx+s//0//vBznb/74nUh59/0328v1/Pz0cH8vwufnCzj9/MtPwtS9zcskcqgLrmsHs24qUsLUXTG4TKX1Hu7H04yEbp0DrClXmpby1Vfvfv75s7/10+kIBs/P185qjk/tiYh1bab67qt37z+8efrnf/v86dN0d6BFnh4/H+7v8jClr03JkLjWKdyJBBEzKRIIAcJct60VKVTj448/hKGbmetc53ADIGFRwr5dEIIITFe3zhIUI49UewDB9bLydFiv6/nS2Nbzi5eyoKACfn5+MQimzoXfff279199+z/9f/7H9XKdl1pZwOPjz7+cX56Pp2U6HJapUGC/NjDFwkis5kg8ydRsYwRraqHIHADhzsQAyFzMrG/XOk9oPAY9i3maMlQiTTlTquHqhGimRNzMUgFoHr5t4Ou2Xeel1qlIwWg2SQ0gzFjRUoogSWH2LABZ2pgIIsYFRgAxlYlTl8IiEY1ZzBt1MAd1sK1PdbHu4Gl8hiBkaqGAqhYKgQEcSL1ZOzczAymq2Ky1iO4KQm9+/827f/zHb/7xv3z1n/+7uw9fIRXrxkwGgel8mfZbGCREWdtweJ9leWTmZG4JPYffnDvDIaPS0yk2gd0ASNscj4i0tPYx6Q8Yacc5Yrdyi72iAuIeR4+qlrdKeRowXKOHNhMcnICHOmgIPQc87hDMlAN3yjoIhhsd7K4KsJu3weA8cPC6WcUJX8v6SAZDJGI3lWQqaIelYB/zk4aGQCJU9VHNYX+eAPBqMJTuEYC/Km1zFRg7AozbYkgm2GHECQ/Ai/LuzIc1B0QIcSAGgHkc5mV6X5EZCX9UxXnp+Ln13tYLb20h4jwoIyJndyiAbV1vbg8REB5lKtct1Lu6I9LWRlpsnWcPFGBmBkBwFBJO/SwEAwiJq0G4QRARWhCTd1MPGoQFMhEi9a0hAWV+C5XW1uS4tSmLIAILQuynH0wo2LYNwkRy+QBiQgJEF5ZSqAgFADFwqXnCyQQRRoIyZUC3Xl9e+rZpUzdwhXDysN6UiBDJw8CjTgIBZspFUu8skwBSrZMw67xo1+7GFOoOSGbeMSFr7uHrdaPEB13BfZoPBaQzN9cvv5yvl39+++bheHc6LvPbD+9//usPh+NSp3m7XL48PqH7er28//Z9LZUAbk6355dzESSi1Uxf1oloua2H09SvTSaSabmeY9t6PyvU63Q4kuDt3fHp85fjw93p/jbPa5tja/1wOoH7l8+P3/Xv7u/vTreny/m8rddwePzl483DnTtCoJpZ63WZPRSUMi2SmY0kf/2sq2u8efPVX/7rv/z0l58QGAJ17ZwntgGliE9FtRECAwARANRlCgdhNuuXy5Y3j+tla9qRuDclFvQSjquu59YIEXT93R9+f3tz/8Pffvrll49AEM361huxeV8Oh+WwzNPkblKkrQoApj3QANFdEyIAJsIwRUI0NyKWUgHAmoEQCQ/zcmT1ToQaxkyhHuHjBqYrBDABiaghg2WWNBGUwhiLTBUx6UbIiRhctJkQhkUYyCSEJkwiiCHaemQWijsRmka6OrOgaxDR4Wa+vFympRo0RvAEGAEN0Foea4cRkpC5E2Pbukjhwmp0WRVMmgYgXVtT8lX7zben5e2b++//+PCn//Dwd3+/3D84sYzfn3EnmyqV3exhYCGj2bxO1xERYWFZ8nI8Jxr6eybekXNAAnICD3UnHo4G+aqFB+Q7Y6cDRhcYhAHsV7PZYICZI4s+AFK6T0RaTwBCevalWRDAHuiYn+zjija7BYxgyFd4JTJXEl7JjbGwjIXklWPIWpxdDgZij8JEajnA75k4RIBGe+MYTWR/Knnd+toVc8+ifLE9HF/jAfJ1SIB/aGBp5yVwTyF+fVS+v2TMFGnfOLSd3s0DUeapHm/KcjPfPvR19cen8PZydV0bBc611FqBwFCW+UZ7LPMREcxgno+sEzGpKVFhZvcQLsyIOKyY1AwD3TwsIAKZ1Lv1RkARTkAoBTq6WY9gojoLAIEqSim1BqCh922rwibcN83FMGW4CADpSGLGJUcPiEIQRsxciYncjLkQRylI4ojsriRECIKCbszSXAk18kZt2/p1gx7ePBRNg2UCMPe812PAYBJzC8d+7cjQnbbrxiKAUYogoIYth0Nm0azXK5UybRuAWFdbddsMrEdgnQoRUcD10kjwNC1YD1e9/PLDx/X5cr0/fPj2q3ffffj040e8dpwPp6tfzueXT43pscxyvD0g+enmBsPctDSZjhOGXZ+fgigwmJBm4Hqgx0tlCUD1JrXeTPXlqYVtV+13D7fl+fq3v30m5ufPZ6NAxJfn5zdv7//4x+8+fn68v304HWZyZGRzdVdTfHl5WU6HWgtgXgemVSia23BrBFrmqV/7+culXbs104uykIWbGgKGGTKYadeGaO5GjESh7hwhhMIVI3WMfZnuH586y2TRL+162ZpMs3etPC/z4fb2zb/+279fns4TscwVrLfVD8t0OC2HqRLxul0wtnmGrZkHpFDbzKWSlEIk5sFMzNK0sTBLjXBhNzNDsN65FGvGKBkpGkbae5pic8lpJPrWEUOEtLuplsKOkTaNQns1cQchCPBwVG8bFOmAGJeWkKd2qIUs3NXCEcOZiSW9qREAgyLI3aEuNV2YeovevAIjcEotwMAduqpBADEQuOF2MZKmIEDSWr9u2ppuvcMEfHu6/4e/v/3+H97+4U8P3/3++O7r6XgMh0EZeDBjevqk7g0SCRowzaj6sAPxhOTuhDjeCJDQEADGjoiDmQMNQ0tJMXsM/3sYy8QQaOdXSBQkhkxy2OYTkZoC4ND2vWr/EYeChjPxfeelR4cYR1ivD8+HK89e5iMcgnAnjJPXJPDdggd2mx5PjdKA4gkQPZyz2QBI+kUA7G0LAYaXNCK9XlTDTqLj3urGAwGI4UI3vPr2L7EDYgNrixQz+YiaQAQcSw1EzgXJ4UM6SiCgmQmjGU613N0c0T2aF42nKhPgU7Mvf94g2n7fCJv6PC9ba6Y4Vd6uT6e7IwlKJSAEZiaCWoioIIabdifQbhHq2mxv+Zb5cISMHtM8I3OoBpIgpfkoCkISaAgYbr0xCzGycLfm4ciAgEIy5iMAQigiI7BTsGsg+X67AEggQojOTAFaypJHAAYmQliSYU7tB7rC+bmpal97W2O7NoqplALIVMXMEME3JWIzdx8mfH1riL71DWDrvdWppPFI6305zN6CEKY63d7ccilt69eXMxXS1ltvjm6ujy+GhMJ12/rtzbHjvHV73trz364k/Pbd2/dff/Pl51/oxGER7k/P68efz8tpqtPRwcjXUnmutQqad3djoeVQt23zaIyCCHXi5np5ucq09Ot2vD8eTrK1LYxwLsfTfHt7WFXZ8GlrEWE9I6PLaVlqnSaZ1/PLy9PTcnPEAO29XS/9usV84IkTCjU3NQsMDUMeWrXtciXE6AYabeuETNADJikCYCLcrAGEe2ZUWbhN85Kae4DoPcyCqBJzsG2ml3V9Wc91Ph0Ob14uHwn8OB/Pj8//9k//8vT0eJiZehDhXMvt6Xg8lFIAimOhqRbd+twpUvnn0LtJLR7IIhYIANp9osJFkphzIKkSES15SEGmvOvx3hsQuBkZQ0UppTdlKWruYbMsm75060UQsNbK0T3AB0gdod2uvi0HCcDL5aoOh2MJAVJEBpxYanEM6x6Olsppd+vgHsQyPJNVAYmQAKxUaT5C2NNpwy0A2btRYevhwe6xrQ7VW+vmqBOvbjqVhz99fff7797/r/7Duz/9491X3x4f3hAXH4aXjgCCPAjZTGuAyHCNCKeBgGDWslFbfYSDIqINandYX8a4GA9O833f8RhACx+mDnnMNU600qVnzLX5eAAgD4DDA4GGbRxg/IqkjPF9nCXnFVgKlWgEmeUQn/Qs0m/Mo3fgKXAIDvcWtJ+rDYUNIQRhLlh56j5ogHE75pFWw2AWhLt586BOHEeEcQwPORznwziwmhzxMS3+E1yjHSwbjwZw98UeO0heECPGUH/uTWLYZANiOO4csamZGxKWWh4e7gvysdSJQ1T70zN+7XE++/Xary3C2+q9BxFezus81QxOKVMJWMssZS5SKQBQiIgYGQJ6z4tcpCKI2NuGRFMtGDEvS5VKwsyCgFIEIhCZCJkpT4i366ZrI0NwUNfwKNNk4YhAXKxra4pC3rYISCVJbl7EQQyCqKbhxqWO+0VwNzIH4cJFSp5ZknIJrogRptbWddMtVzECJ2ASAvRuxsQpWXAP004iZhHhhFBKRcdZFjPjIhRYqFiYdXv6fEYMh1jXJrXUw8xc5GZ5e3cz1arat8t6vZy19dauPXogfnl6WptBYSJZ1+cvn57b1t+9f19PszX97g/f/jJ/dPj88nxeL/2Hv/1yPNVCN6UgijAzNo/dNrLOS2+OAH3TaWEMCJ/cgYTWyxULSSVr4H01o+PNFGcIo0/PF2bs27per7enw08//vC3/+XPx8Pp+fJslb79wx9PD/dqptZbb7lB+/6bjxSASIxCQkIBwUiCIBSbdRGycCBU9W3rASiF5ukE4UICEsigrc/LhFCu56uqI6Cb3dzcXlZgqdeLX1ujIvPp5mrNDA53t4H457/8eb1cDPp13ebT6eHt7Vzk7bupVNquZyp0nEDYYSoYWKS6wcvlum4QFOYQ0InIDWslc+rWUrFi3bTn9EcGMM0TMZl6oYkYiVhVW1dXCwxV9a4OgKhNO0YeulBcfV1hmgoNmgTMFCIIXDVYEJBdu6kzEbMgRp73IzEzWpg1M1WSQbB4N2RW62Yj846Jtk1Z2BQdnYSEmUpgd0hnGuLr5YokZZo6hhOereM03374UN99+O4//8fj11+//f4Ptx++mpcDVUEgdEdwQh5m/BEOSY3EEMB7hIXTa+gVvgrcAyE8jyaRmXZ0KHbCdxc9ZkdB9GEqhCN7a8AcER7A+Fp4s0/kf01u9bX07WVuJw9GJMtApoZgPtuCQyr0MosAAc3HvJhkNhImco97fCTugqQd+UEkQMhbLqDEaiAAPMPWEVMlEgLuQJgpAeYGkf6CPh7Yjp0h7sFkkCLBbCCQrC9GshNABOHDhhQGBjQCzJAop3zwSAEZAkIawu1wEObnJIRHhIA8giiMmQ6nZX1+LssyHY9vvvoAt4ft02d9fj5/eb6eL2Z2vV6FJSJ664zU+sQFLbmpuZRZgGE5TPNhqQQV5HCYMQLmsZlBLMC+LDUAWEopxdyneSKRQIwIrhMjhQMBubpwDbray+bqDMBFunZi7upuhgx1LojYops7hOV1eJkFUUANQilQagVAMwiwwChTDdoMNBqWaQJ0gpDiJDRVOT9d1bYIc3MpxYoXI9Vws1wqpUjbFAlb6wKU0CMiEaH2jQBJeKlLb72WSkStt4jYegvr18sqPZ7Pl96VCrLI6fZumqfD6XRzf8NM1rVrf3563tbG6s/PL9Z7t/jzX366uzshy1fv31m4o7//+k1vjgh/+/hZXy7aj6ZAeCM1Veo8T1QmATeNKLOYGnTPzBx3UsPU77rq6XRYSds1hLCBHY/z8/PGEqWUp89fHh5ulpu74/H4z//1v669/f4//Um31i6rbt0j1m3LTGoLpwhkQUAwAHBANDPV7l1LIXtZEdC6cZW2XnnGAFyvWy6jZarbetEOYVaLUNpdKlzP7eW5rZsKV6kLbna+vlwVu7uV6eoOZggYSI+fn376+efL+RkBjofl7fu7hzfHwh68EfPxoUwHBvftuplnETdtWmoPdHNAtXWzMs9u7sEi1T1ab8MTFFDDwAOZ1XveMA4VkAMyC6bZuBEiFPGuQWSqiAhE0SwKgXcAwgCzmCchRm9OCA6oHgDOSKpOFGpeCnoApNMxAwNgIFFABAlnRpk2c/PeDYnCYdvUDNw1JViqRsxBGJTHq+Q9kIs6rOtFCzZXPswPf//99//lf337hz89fPf7w9uv55vbMeamueRugzywdwAeUSkYEBbhedIUkXDsTpwOXCXhctoVMnv9jDEoACfviaMNDB4113hEdE1bYGBEG4mMg4OAkTyWGbm/mmDir39yf/B8wbPEDoA/IvPa8tBj8Ma4C37GY0PkPHn+VY2JhMlKwN45gCDDFSKACdKqOgAT5ko4SZINgVdCO7cnAswTZyIEiD3uEjHDA8bFWD7FgXZlaBmM/Q5i8Bjx2y4C8Guxd4jMahrHGfkTQcxMHMSEz3LR8HGGAWWaoDBUoePsvsJcrDEeawEH7nWp2jQ/ukrR1tP9MAC23v3ZHWw+1HleDtN8mJYi5O7pyCaFl+MszC0QGbeuBbGwGGMQUpUA4sOBWXxTbR2CtLmqe/i2rlVIBAjZ3COnmyR1upUQ7C29wxlRSoiwq+MwSHIPkELuBmQBF4Jiujlh4JULEXKEu8Zm0PTa+7X3pukDQBgcQhxO5A4OGTGmXYmpd5VaWAiBWmsAZOEVoGsPhG5GAaVMpRZet5fLC4QI14rT0/mZnLZrf/74F0NAgePhuJzmt2/fzzfzh9M307Rs2+Xysl3Oz8+fHz9+svOXy8/ws3Z9e//mfLksp+X7P30tP9Qe8PHTl88fX6L7JIyI9f1pOghCV2uAzhOhWCFAEDMnjDKTBG2rt02ZcL2uQrWL2dbDvGm/OU08v3388lgPh+M0PX36eL22qcx/+ed/f/PV+/51f/z4+evf/84BmMgtunbsPC8VMALc3ALDw3tvy2F6/ttPum3PXz6767o2KcWsgXmdam89vJ/eHFOB5+7TVJmpzEwoWzNTIObUg7KwY1zVuxPW6ojm4NaR4ePnz58/furWEFzQb2/ru6+Py4znl0soLqfjfMMipq0Rd2YKuOaYXGfgilLKtsIJa1PXjm3TAGeGOkUevgZCYYnhqeChSERt20otXRVJQAgdGUAdmJBSrhzOLIDuFr1blWoaIkQkW1PhmKYKBAiSVRaTJygVgjGAWZAC3MONa+newM0stq6ELFJsa9bduqv2ADSF7doBMgGeVMN7DyB3cMfr1gIIUHpoQzMhr9Mf/rt/rB++u/v+H7/7x/98ePMGhADY3cIMAwIMB5CNY3L04cq5149RqcAHCp3oieX1HREij8wUCyf6dQwlxMB8YbMOZ8JwrlmZnWXumfGLsMuZEMxetTgYkOJHAAi34UmeLSSlpgBABKZ5JUwpKk1Z6mAYIpU0QLTnnLNEQOwKINiBH2YCgHBgzAE7XTgJHAyCIL90AACTRAIwHggEAZKhtpxkCAAShjkzBYbkwTTiyDj7lbVN6hr2s4FBBmSHCw9Ti52cGGR4SoX2T4fA/fhgXM4lmY6Y1tUw3LYHLBSEhIIV4HR/2trb9fL08vzxvK3XturWWAgnEQJvBh59bRjgoV21lCJYpFYSMjCg4mbbZYstbArXFuZlLoI4HWe1Ph0mmWrvOtUJQ5pe1WM5cWgEhm4ajNhje7762q219nKN1iO8Ne8KtUYQEkFdpsyhMOsykXkq5wAZiANIpQZyRF6cIpYi7mSqAOHQSDDAHINJPFCYI7w1td4DvG2b5vsWgBkIuW8tIpdfCCeZGIC5QCB6eLju711xNzXziIrBAma9980CCMjCvfnheFvuF9X2fLliKYbY+vryeHl6Pn/66QsRv3m4OxyPD28eHt7c3t2c3j+8uX94+OGvP8gMz+d1u/71/vYWScqpvHv/Rru2vv10Xj99Pjd1U5iO0/SwLIdlOz9yMeIgQCkE4cxMgO4AwELkek4Z4vXlOh+OyuDYiXw+zqT++PmzCJJgIVm/vKzd59PR1D7/9LEQ69bKsiBg06am7A6MAIQ03BwhwrsCRHR7+vwFUF4eH9et394c3CawlSnjxI2ZpTJu+a4UYQGIbVUwmkpdsRFjczAHVVvq1FYNIgO8tpXACRGQVjsHOEF/f/9wc6xSuofdvC0393NhqhWl4raCVCaU1Im7m3aNgAjliYHKEbBd3RxbV+uhClKWTKA0gFKZXdbrBgFAQYRba4jUW5NSXYOYSykpjWu9i9QMeXQpqUnvqkAYzaQQmLFDpQJI3UwQLNCdiCo6mrr3vFFHIHLXrIPdOyI7hml3c1N3i75FihF0C6CAlleJ4B5AoBqtq3MxBHW9oisgMN1//315+9Uf/8v/9qt/+E+H+wfiadjPRF4qYuaB+W9vjngHxCPFM684zfgfEQYgWoqcwoY2PQ8CxjHwOCCIYYwDO9Ke4hSMzGDBfSHYb7eGrH9gJeFphgkJm+RaggSWR89pVTou0jGyZoZx8pQBiKjhAMEklkYGPB6dj6UChNASwIKB0DCjD04kmDixI87H6fmdMrJmUN4BIQlwA0G83j9EJKw/ViHAgPEiZy9KCAgJd03SyEiDvKiG7GNjqQHESMtZdyQesM+OmLlFhCfjERr7y516odEGk+RIZVEwTdP89qv3ACHCj3N9/mF+YYJ1hSrby6Vdt/W8gsAo/+qtqRSlRCeZWUhImmrXSy9ameZ5qiCEaNfe1HTrZarMpW0bTsCC28tm3aVKEBFeGMm76XXTy8V6B3VfbT4tCMhCgECFkAjEIKJybajkECamnVNvVwIJiIMLRpBpZxGRcfOlvYsUwKBSiBwwSiUW7M3cO6AT4jzXCFOIRWZtERpQCpITCQR1ULU0iUNVRUZPysrB3Ny8t7Ycl/QiN9WX6xUQmUutcjgdESPM1czVIGA51IqY4Zrm1lb78S+/IH/861//Vie6v394+/bDm/fvP3z99ZcvH9dt/fkvP3z+9Nf3X727tPV0e/f267corB2fPj9tF72ct8vT9VALei+1ul/BQa0HZcCfl8quoQrLsYb5p09fpulECF378fYgU315vpptAMgUUymVa5c4TYfz+VGvDdTQbb2eP/7tL9/9p/8YYdu6uim4uwf/Vp+GEObW1UxN++PLCxJfL+1wqhHGeT+5x3yYuqtjADK7KYoAoKp7ABOpKaKEJ8+A5rF17ciq21KngCBGKmXrjx8Otx8e7g5HPRxlPtSbt9PxNIVrmAH64fYGIrTpNE3MpE0vL9d13V6eryQS0UQmJvSg++moza5XjYBlns8Ttw5qBsClsJqFWZ2rpMHk1hGCCK1rd5/moqFIwcKtKTARsZkRIYtk3pj2AArtEDOuq5FEt1QKYimuHITauk+TSEk/fABiZAVFgwhVbd43jQCh6mLQ1AHnqai6dnBNBx3qYdo9mCBAwy+m59bgyMwLHO9OH76/+fb7endH0xQObjqQaMoMJkyVDiQdOwzaIMMXE64fZoiQ0PRAIBAQAc2cCGF3iRvGBlmPE9RAUjXAIJJcA4YXsg/AZ8ypvjvAvy4PAQEJ4ww9Pe/7ByF7OEYaDO04CqJDFGRE0G5EqOHpHYm7X04WfkyB124HPS4APHGU8HGblAdaqeeEMYUj5ghIlLkDERAMiAACeY0NaV8RGeQ1npMPBiHt6/LlG6mQsS87sdd6z5yA1Ks6EWbUMnLidZgPA/bgncAx2ueMD8POaKByGICAFqOLhoWjISKXQoxfffv1cphvHm4+Pzx8vj3Z5XlC8LaeP36+Pj6jefS+vbTtvJ0vDYKIJE8dTMFA2WGSqdYy10KIfdV0b3Z1NmZ0YI1w3xyZHKNMpUySol0khB66dd8aAE6Vj29vuTAhcikITiJIEa6mXTJ2qXcRQkDkcO6AQegkgIIEQQxEzoKIWmYh9VTplblGKBGVImmRAWu49vTUQ8PCTCE9+tZbuzamigzqZmYeKYXGCAeHIhKEjGJmCsaLiDAibduqXbkUQUZEIQmItq26td4UweZahKgjAuBE1IPqPJPIZb3q1n95fH56evn086e7u7uvvnm/1HmeD+u5/fg//8vPn//l/vb2/bd2fzw8vHlrfw9/+Wf/+aePP31ylCiFy3QSD7MwbYAmy5xwKwtZ67r1NNolxjytBvPLy7UsE1Fg+LIc7f19mabjzYIG81wpwN2vz1f+Fp4+f7w8PvV1c0uf/giwdB0wNWQKtxzxtG3p1NjWdphnmYr3HtbKxF1Xg5U81EwkYPw6xXptQB2xtGZdY1tbYd6ar7Gm6QQhB5GZG+ilOYIhw0R8rMvpZgZayf1wxNs383I7He8m662dzw4hFYQlFi6EJNQlDAhrKUfsq5qGcAKWAKTgejzK1qyFzQepHTfjrQ0dDIkkHm4WTBwe7hABUsXBXRsCanMmAULzEOaEjE3dtBcmqqwG62ZcEN2kYLjme6tUmAqL2OVyPR5LKcyAzFjqRCS9KThSmGt4kGtCv2RqTMXDVdU03EFdHSnDYNZN+4x6IFgOt3/45vDu/Zvf/d3y4QOWCaTsgnnHIIDg1JBkFGKGUaVdj40q5kNUCbRD9jlWxsC58+pqR2owPZACgtydmQGAkH1sGRTjuj/jYvIz6FX2PzD/vdKm8G5nXBPH5sxsSWuGXNNTjuOe0spIAjXrZ9plkiAQJdGRRgYQSbTgsBoddOqvpK+ZpRM4EgzfzpFJkIsA7iAOphceIkU3wUHDwi7/QUzzpPEMEGF3QE2mebAmkGhPzuw0sjRBw0fyJAYRwU5yj54Roz3HcIBGQODsY5BYFIZZIHl4BkbAfvWegi1EIOJa5sPpBgKAmOaiL8+xvlw+f67eg2F7OndTZ4YidaLeQR1DzdwhfKoyz3OdpApHGACWIrUwEaKEVJHChOnEDWGWK1GafXsYswjiPE9RJhKe5zLPEzBKEQ9HiiLi2ls379qboQeAA1mZKEzNu5s6KzKmqROim3emCoIOjgWkMDNzSWoKicHNPRzBtffeDDyksDcwbW4dHFhEMlW19W3rOToEgHUnwoyjyszPMJUi29Z799SFCUqRPC6Fdmm9r9vaRbAKz3MBACSaT8fWDcGXaTLTpUzX1e8Od93by/P28vK3j18+Pty9++Mf//D2q6++fDr/27//7V//+fMvn/T2vvzpj797+/6tF2ngz5+fP3683B1eDoXiRg7HA2FhCRa6XM7WrSz1eHPjdo7LZhF3NzfawoNa0+W4rJtGVyxlOVaAm+5aKl/7VSofTsfn6/n5y5Pr1y/PTz/85c/f/cc/LfOCHr2pVC0JeqRbBiBElFK6VG+KjkT5+0OhDhZL4TAl8uNpEaYImOZqW7f0G5BiqhbeugKEMIfgy6Zu1NO/H9FSNI0oJFxxQqwcywHv3x7fPPDtw+FwJwFb72aq17aWQkiFGAHSTBi40rFMJ1wQoG/dHLR55XI5t/PzGSHcwDTS5QeYxBEnnErtPdratWuSukTAtfauzNS0uQUgdDNGaW0DoECUIgCxbR0g1CLCNZwwulspKBLFYJ4Fzc+PaymgE9cJ51l6i0JY5pISowg0j5TNgxB0AKYASyGKmoYHEgFqKhGDos6TuiqEz9PyzVdf/e6Pt3/47ubth9uvv3l4924+3bjaZoZMGVmFEEGpewRwR4T0P05rjL1MAQw7xnQJGuY8w5V5IEYYDuaawGCuALxjRuaeyVlZpcIjkRZTh/S3D4zdLHok6ObwuguKBqOcFj80wgUiN8oI4syqy6l3ENPpx5xJlqOJjdytbFuACJawiVOiUpAIPYKHMzEihtugrQMiLL2Ako5GAASKgLRLSQtUySjklPRQXpRBMJGPTxp97BW6h11zGq+oGaCb41Dw5CsVO6P+6y310D+lsAlfuQQISqzpdVmjQKB03B/wVQQlTe+BjAFIPE+HWsvhdjnd316eHuH60t59+fLXf//z5fmqq/bmCm7Uu7cOqsYMiDBLKcRoYVtHpmUuUxVhlCJEKFWQEQkP8+IKBBQUDlAmqVUCsLdWChNAEXEPKiWjX+pUA0AtMk82GGxzdPfe8uqByWstpm5rj2gAZlugERAIIxcCsCIVCAAJKAgxyAnR1dfe27VtrW3Xzbq6h5ubgXu07m5OyFKmCAwGJCq1dM2pKNQsNKAZ5akCMgK4Q15h5hbbW4MQs5jmCQMpaJknJIoI7UbEgHhtLZMPeh5/BhQqrdvEs/tmANezsj/+S/+fP3z33Vfffrtu9F9f/vrTl5efnyKIze3+3Zv/9N//p3/+//7Tl48ff/jpU63+9fLWFKbDgQr0djmdji+PLxW5be10OF4ALucVEMvM6h4K63UlkQgjLM/PT7///o9/+dtftG+peT4ebp4v1+v1EgGq8eOPP14u5+Xmrrtt23a4u4kYogwiCnBAsm5IhI4iAo7nyznUqMj16Qlu35ZSJEhEtCs65sUNE3YIAlrbluc1XNBUpcy0qXtkGjqaIhoGMhUWFIpa4Vjp/bubb76/v72Du4dpeajr5q2/hHmdkJnDTMPrLFRJiLjw+emMEVSmupDU2s5dmx7vp3C9cudKHmvrBEigHCNwIhAJoyB0NYBwQgYIYTIMChqptBHAKCRjtoswtyRGzTwAt67CgGqp9yYpHqwGAhEOHpT2R4ISxK0bCatGuAGh2/AuBnOw4EIQCFy0G3MUwS7UVmsBQtTRQMrpze3tf/wPh9//7uHv/nT65uvl7v5wc8dCObShA4ML8oi1Sg0lBMHubZ/15tXEc4Dr4yNjKFF23QoAUZ657SPpKPNATOYO+4CfhdTHx+ErFj1G0n3I36sZDm+DwYmOBxGRcSkwimli8u6QmBUj5K+kO9BAsMb4nejWTmXjLq3Me4VwcLWUgiZvSpRcPcIrhJO8N2VqZQAMpRME0o4KSbjjCDJLjAxSvO/xKocaj3p0PNzrOw4SfHSIQfrur8ouzIoAzE61//VOhw+5bOJO2S0yRy5/VuHug7tGwAgmAvIwNwfEIBCasMoJmWq18wQU8vz55t277eW6fj6v59Wv4M7rqm7eeqsFcC6CkwWhQkMtMk3TQkWkgBRCChIocxFBOVVBQYauWmeBCCnVnUTYzQkBeQTmMYLMGB6+WXR1DOstVAtCcLbzPA3rwl4rmZKDgSrkvIcgNEXflBwFSaSUyoxSCoYb4XZRQEMwgMgoYN3CenfHcOjdckNVNaRCGSXP7uaVs15BnuSAAyKcjrfrdkWQaZqIYNsu2iJP4XtrQeTd6lw8dQ7CJLxeNjPbWq8yI3VXrEUOy7Ftmi52TaFM9eX5ed22zeHh7t3X33xzfom/fPr5y/Pjv/zbx8+PT9989f77r776+3/4+/8FsF/Pn79cD8sFfapFQlfTDUNI+NPPj9Nh1qLLcdnWjQmXwwGxvehVe7+ubZ6n7hbN/+mf/mudluv1oqrXy6VDBaTnp8t6vk7z4cunL+cvT1zm5ebUN00EXEVTrTEONc36uiIgM7drf3m5zIxTOcnbD9o3hmoBpv24LCLkZgjQ1lZK3S6bdQMUJqSKrfs8V31pqq5bRKCFGVggbq4Fq5tdzus3bx6Op1IPiNU6rNgNJKpMhciaafdS67xMzM6ZDms+zXVd17CGTMx4uJv7eQtg+XA7XTZtwMxtg96gRUB4tx7EIgU83CAneRZ0966qZrCr/1AwfysJ0Nx9TLGBXByYEUXEvbmjYzhQgKhDWHAVWUqdmSgCDIIDSF1JdSydaiKFhrsZA0aZK6JrM1ToZojEwjKRtt7AnBmX6e4//t03/+W//+q//Pendx/qzRELR2Du3+4e6mN4HhhLDPwHIWxwmr+VnsNQrmO81uCdLkUgjKzmDgF50enmUoZqesRpQUAa+o/6N7pJWkH8Wg8zDxJz7g0MdPR4dVAeSpkgGbd1CQ3t92IQlowwjswvBy40vh4MGjkARIZFsQO4B6WzWoyc4XFZkAuRB2Ji7/kl8hYNh5TIM2OAkqnIpBsBTuU+EXgGEQx6OoaqZ+hK06f6dRsYMBaNkp8fBL/S4/uf8VLhIC5ivx4bm5jvD9M9Mto04f94TVHIr4uUfD86CDIwWh6bITsSlnpVf1rXy9Yu67qtm6peHi/WIAybZfyo69Y69I6KRo5wWE6FCugG5GUuIgBhTETemTC6Yq2IdFgEOQijThCOHhZggSAFPQULiN4UHDjMvfdtdVPv3VRD3cBZuNbCxK11RichM0Iu4Z2ZwKyvhgi9bzJXpIbgRqiNinA4gJt3AwsKMAMzACQEMzVEPh4P1uF6bb0rC4V7GOYnYYQIlVK7mpuhEABc2xUpAs1c6zw7VDOFDD5DAgwSNg/thgWBuDXN6UVEQACDwztiMbWAuDnMIHTwcm79dHP/9PJ8fl6h/XJY7t7d37XePj9dni79aW1PK7x8uvzxd7/76ne/+/HP/7qdLy/ndVq4vKynG2nNL3qViY/3h7Y2N+gNy8QYpNrST7GvTS2kTiL0+PLi2fhKYSph0dumqoF4fnk53t8tx8PLyzPN8+37N617rtTWe3BUqcSs3aZpXv0xlejbermsl7os4OiByLWbL7Ws1/X2dLDu01SbB4Fxkd48Li2I5+Wg5nE5AwUSX7cX4UM/d2LnAGKZ66R9hVgfFrk7Le4bF6MSgVqnGYnduwiFwVKmUms4qEKw18Ld3DaNgG3dpmWBCFPN8Q4hpBIhzrNghBBp32Dtrh6h6gooGU4lQkjUezNV07y4JyAW4mRrU3MXAWlw0boRs5qXIoDu4a2bQ0iBGoWI3V0V1xZFACsH0nVVDKVKZPnLzW6AyEipYiAzQEbkAE4jADb3rfcQ3FaFKsvd3cN337797ne39++Pt/cuaIhmau7JqIow7SdIOcum2j81kjnbDuY2i9AYULPX7SToAFoiIsz8V6khgAhFOBOp78dWHoC2fwkaoAWh75ad8au50LisTWIAh2oG97IYWXlTuZmymj2TcgdnwpOhfT0Qi1d/iZRFhu8yVhBmBEiDzQGjB4wjAkRmcreRtAU4DEZTCLuv/Jk0hoApDRFCCvLIsjs2iBiODu6ZIACQzDYE+AC/8Fdt5/7ldqAphU+0B5PB6JZDjLVvTq8LQ/I7iOnBAW6veqR9sQogHJ3E9+7LRONArBRylfnA8wFYiLmUent7v/jcXlrfmnbtauBYDvVQuBZicCH29nJ5uvpU4DgTblJYhEs5QO+GSsytrawCk1QQKRxqkTSPGhAYdNx9sKa6SKXz09muG5hab9p7dE01FBG7epWFGS7XTQTzibrqpq2UOl5PAg2nwg1NSgHGdQ33cA3vqlvPH38LzxsCQgQWsEzg4SIFkLmwAQCgc2Tb9vBaODKeDMC6ugWLIEXXRogyVQgstcL4IYabphNY21ZVZcLufjwdSln6ep2Xube1lhphmxEhGHgRVrO5LozyfF7Pj32ab+5Od9/cbz+vz+u6Pn5u+rRtz+1Pf/f9m3fffP7x39yidd8uG4uZBVjEkGW7abdMTUBEx3AXZClibm1bbw/3N0c6v1yuL1dVXI4Pdze3Lz9+AvfKk66q13a4O7w8P87HU61FWyulmIG51lKZBIDCjIVLqfO0EMh8OPz84+cm3jZnLr1fESePdnNzCAeC2C4bMV63VpG4lE1fCLwIWzcEOr9cUrMByIjKIASgZtftuvnTvdQ3b26Z/XiajrcTTg0Lte7mRuxIUqcjOIXSuq2X84tATIvMy1TmEwfQ0T3SZiftc0NmiYaXSyNGYgDAZSnhQAVdUS3UwDVCyB2v16t5WLcMFEpeSc0CwZE8rHclZESsMlvrgcQi3QwDFYGAAMgMtYVMxUO37mZgFZCwa0YwRTFKyNtVOY1kWKwn3+tqrmoe4ExmDsK8TEFRCzWpnfjpsi6Pj/J8lttbJkkzTyTCcEqVTArbcSj9ISgiiDi5WUw3TeQh2ofXe6xXKHqwpvB6kjTgmzHwIoBaSnUGoD/iqgAyUTgH0eHokCKj7DWj6Gf/iX3MB4Rk7yAwKE/tYRwiDMOFfYSHcYbm9Ao9wfBZ83BhHpMzgBAHjiobAUBASKGOAMTEgJpXLozJXuQRFwH5ns2YlMAewRhIKLFfjw169hUti32o/23DGRK6nVh5BaX2PrSH0EAmJ4zXeS/mox391rBuL/WvC1wm0mVzzQac4tzdTjQxteR7IDzmUgWx3oGE1t5mwBPPL3/9Cea1PZ63lwtGdG2MUTiOh0rgiCiS2WYW1jHQu/UOuNTtuc3Hg28bCgOhAXgDmCpMjChDEUsI6Z9JGOrG2LbVhCumA80abaMI4gBGdwcDg+1qatFLGfyMuVtYqhlgD3B2U3UVrq13HM4fYN37tZEUoswczeRPxDAkGsCdOxW2ZiiFBFU1EyhrLWbBhMSsaurKTOqZioOtdYBgIZaKBOAEadoOYdv1el3zfV3m6fbmpi5Vt2Z9C+IySdoZWnQksW5u6N21u1LzgHmqL88vLfDh/lbO0w/+xd3WZr88bfAvf/mH//DNN3/43fr0ua16tk5UykQRFg7M5My9KbrNxyJS2qq1yEatltp0MzVTnZfl+ellO18Dy/19NTdmYkJrCoi9r6fl9uPHT8vtA2HUSdCBiMIUEALcrCNTIBKzTKXUerq7QUYFeLxc39zOgLWtNh8ZgA1wW7f1eT09HBB4O3cuFUBERM0A/bJeVZHpZp4PDeo8x3pdEZABBVFgvj8d724Ob98cv//Dm+XgTuYRYSRS27qdPz4v83Eqc60COkVvZi2kRCnTzW0gXK/nMguhXK+bYJQ6tba5uQi7qwgaRplpwcJX3y4GyNq7CAmXnvVwNZw4R8St+3bdIpAKMRAxI5J2dYsWHYhTbuEWGFDrBGgefj1v3shMpICYIdgcbB7mfapEDOqdiRhJW0BoBAKSdljXzsTafWutXTswU6nowIcTMqEUmWabJke4rpfr5Xl9uTnd3k4ijhEoZhoRbjYSrgiz3OecvCervEYtegI7uyvDTkKO6pJTVspBdwj6NzhHljNGjFebste/3+nQV1hjr/iwXw/k1Jz0SvqeBaR1ZpINr7EtKROiHaTKyRgHQL+fMMBe4HYoPYF3TAt9GIoYGCN1nh0YgidSvq8oY2IeHcQRidKrggaeBeaCu1O2RwA6AUWaaAwbZ8dsGbv2H/dHFvsLEJ6eefSb2zCARNNgd6jIa1/Yn91QJwGMLB9Eygiz/DGgW/oLZR/etwcY4XYwZKhYWAIigOc69+kID+9924oRdVX5UqIVImvr/VTnQnPFaRK3TkylFO8aHmEKhGHmbr5tEbXFykw8T8gZdxH9fMYuRJy+yq8NLaeSIhXdYQsPAFXdXtABEea6dGsIYN6RMl0AU9fYW0cMNJdamNHUgtC6UaVCqK3l+5mroANmWXQPi1ThMhEAlYl6S4cyQEYO4lqRWVWv6xoR0LcpZkJqzVPna+HWOzMNg0NG4uLh7prgpVtCyY4ByzLnHfhUa0AQAbFPB0JPjamZtjQa8HAuBYBmkG66ta03ezm3a2t1uTmebr9mfnx+WeOiwJ8/vfz011/g/eF4mNbrtUhRB2wODBWYC5mpMIaRmxt1wLBuIjLVaN22tYeB9yiymDVd/Xy51mmCeGYkR1T1U61IYX1rfTufX4iKuQETF6ZkwtGYARh6mBNMt/N0OBKX8/O23Jbr2pYJiERYWJJk4+VwPD+vyzype5Uy1dkdtbX1vLnCPB2cajE/vzQSECnYWpU6F0Gnw2Gelzqd5kjqCGthnsqxr+7nsLN9/Ol8WLwshsTtAv3az89a6rrcuEyltetyKmUhoOm6XrdNwWO7mqlRkUm4q8fFtVupJFIul000tDlwCJEalIoc2M3NXFisqDu0TS16Xuekb0EaUnlQRLi7m2qoMJIFWAstZna8rUhIjF1z+wUkEoCpVERMB7L1vCFxa61tzQNNezi4A9bqTDBNUpfbdx8aCd/cwXKk48GnKcC//PwjIqL68XRDNQ8MCDg5Xw8EDydBMPLMQ3wtljgUJwQjzBBxxyf28rFX2H0CzWuvPWPHhzwnJX/D0AcCzCNddcalWE7fPnwX/ttBOVEM2g3jUkVoO3Yx/J2RCGiAKGaGhFmgkV6n4uGynF88IgKciNEHY+rmwJgub7kvsBAmzxHoEAw0NPcjHA0EyfbulS1kWEh7SPxasoOJkwAgJBihOYOqfUVusgWmgWiEZ7BtZkqOl3l80DAU2jmBeMX9xyLxyhVQKkSTYNhvGhLqC0xHv3FCAOPIIiLSvcjMiamwmPnDw5s2MW7rWbuev6zt2R61FDtMvNQQ6lOlZQJzrVNl8gBmYlNGpPQPRgZw061VqQQNzF3HDR6EhIEDswgWDjP3SMbVWsMgcHAz7UrgrW8QuGonYUQohd3V1agIIVhYhCJ6rSUQtZuHo0WpwgyW/anKIJYQVXu+LdwwepCTOzCgBYWbNrOeZE8JJvfcLqN3JaZta6WUgQaGwe7Tp6ZIZO4ESsxt2/Id4B7hLkU2UwyYjgdAALToFka3t3NfMSCuLyuaE2kpdbuuFkBCdZHbh5vz5bz90q/eXi7nNeIKz8jl7nQzlfLxoxXG+Vi1m3cDZ9M4v2zMUO5mcNx9/6w7xATJkmGEMEAhVSulREBfbTpQlfl4jOACIWbtdDw+fjmXIuvlSu9vCYAZrV/BFUXcMhKLETJrAQ2cmYHxqg0nkbkc725fPj1t2ulsTEI8ny9KImu/TlMJ9lLq8/NFSr1uzQGul06AUpbTzawaFuDdp2m+aiO2ScTCgYICRbBMotYBoje0DpfLuizVm+sGbhMGfvqlTQfoqq2tFPH4+BkDTvfbcqgocLzO88IkREUQIAyIJ+0bGCIDBDJjFVSkrVmgBnZk1r45FHcjZgiKre/aVESCWqUbkFAEaDfEPJJIBtIAnAi0azjOh1qnAzMSgwMEMwkFmhM0deoZORNMgBZmYYHbpbVNt7WPiZwIMboHUy3H2/vf/V6Ot3W+Wd68NZnq3UkRtvAe8XL+wsRuery9kSJSkmseGkYGxgh/Fc7bjlm/DudjUI80RNpR6DFJw/7P+E0VysssAvRhIwpjeI4IACIkJN39BGnHd3Koxn21yFYUqfRnDhsBKMzpGxHMFAC7sye4GRInCZGHdAEQFkDIiL+i4hCQQc5AgWGpwEvaY1wvZ1eBEeVLiEFhsTtPvFbfINytTLOeAkIEM8uwfnOIyOYJO6UytpiAfT8IwLSVG0tAEFJmhOXpVop6dpnQ4Anit1V/h/UHDzw+0BEZ0n17X86SORnfaOxcBBEZ3pZLnpvnE5FENNBC5+Xm7vr0SQ6TF1DSYA/QbkYQpS7zgpxqy9yNCKRCmIMEsoRpGM3LTETgBhSUcS7gSDpNlRjD2z5GpMSVwWLblEkgQITUTTghRM8XRYgDyd0xXY7ACQCARMDMuyoSFOIi7KE8LFcDEMMchZkpzLR7GEcPVyMqvWdYHboHS2nNWt+AuHdtW09HR5FCQERcRFQNEcydhUQIMEwHwUhIAa7NCYPHr1w/LIULzjOXqWzrNYSmGuFrREem08M0TRMxref1WskMAwgYA9pynD7U8vRyFZo/P2+fnl5e4gncH+7vD19/9fj8eS6wLNM0Hay1UAzCdtWzbHf3x8DorQ/mjiJMew9Vn6aFBEly24zWr/PhLsCth5ueXy7gAAGn46FBXJ+ua9vkKse3x/PTl8vl5e6wBBhG1EnSQBECmYWFS2GqjDNBxeW0vDy+uGPbbGM/X9r9cVIFMFaAtmkRqdOBgNvFW3Nm6eqE1Hvrik/nF/fizsJsXQVJyoRACHY43S7HI3NcVu+6YcD1HI8/fUJjYvGwrbWpTl15W/X8bMRxPDxcztd2tna9mtlTXY/zomRU/e7dTe/b4XAwo3U1km4OYeGu07QgYW/ct42rsHAEWzgEmaIKWVOz7u6mxqVWFkSKIPRuFsLkCOZmamFOTMQCBB7gSE5oYFtTR3LAWincicmCMUB7JMvc1kZIbfV1Vbfome5YpUxlvj3I8VRv39pyXN5+dfzq6+X+jUvlqQZiU3MCZCSgdbvAo9d5rstMwiwcTgEWlqp4wEz43S0DEsaAHZD5DUgQ2SNwR5+zdGSyY+L6uBOlGLvGZRCXnk5xabuf32tUy8AAYKLXXpBq/aH1DIf0aosYCZP4qpHMCpuBWMOYc8y/qQpNVhgG7oGIDk5I5pZ2QZGgkeb0SW4+CPEM+UJyi7Tf3lPJduo7B3lG8HGNlRYMAgnvY9CAmTA8o8Syse4edQgA4GHjsGBQGk77M0voBhJv+2/VQkMP6mNPgsibWth753DKfn2Nsptmc3711hiQWbpaDNvs4Ytklo8AkQrPh5AZ50OUiY6H3q+pmZmWUioSe5klnyKXgh6tdWGpUzW17WrMCES1MLOEe25WSEKcpxuawY3gyIzuXiYGDynS1wYBYIYQRTA81J2dPVyjuQMKoTkohCkCmKo6ZJBY7kz7jgXMkj5NARSOCAwBhVgJQ5wc+uoWaO4AxCKIFa2Dj+OVWgshl8MpBzmAMAtiiTAiJJIAd3WuhYCEySMyLz3ARQapKMzH+wN4qLbT/VxrWZbStvV8NhGqSyVCCD/cnnpfAqirdw03bM0KzM06wNRczqtt6Nenz6te/vT1N4f67uX6OcLWtT3cLZft2jtI4WK4NSP1cK8TQoR2jcBwr6UwY2+KECwjZzmjvNfLtrofbuj27vbxy/M8zeGq2l39+dMXrHH3/k1br9vl3G82lJyFS3ioGiXtwjQdZpnlcLvUmWotYVCnuVk7P2++tbv76e5m7r0DiHZoV50P1SMef3mR6UClohNw1W7dYVvb08ulEaLD27ff/PjLjzzB7c3D3cN7jzhf1jKT9uu2Xs+fL6Z0e/cGKcxMShGQ03x0KLwaMU1LmQ+ntw/35+v1b3/76fxiz1+u6mrYH5/8cGDrjQkoLfibEWOdJmYyC2YkAhbpPVrrNASBzkIVSmsagUbEyMk9mRozm6kHqDkRSikKGgFqjoCtm7nViYSgIGIPiBBmJAEE5uKWMaMNg9zR3D3SQcGQAZmolun2Zr67P371ldy9md68u/v2+8OHD8f7N1ArlxLhZg7EHhbdmnZT23zzBiVqJRnlJWvLSEZJHDxrRw6UO+UI478CQN4TIadz3aiqMLKCx+8eZhAso0eGr/8WWcoyljZCv06xWb2Gdh5/Qyy/Dr4AMIArIMyDdkhtfaLlMSrqLoGHMS1nswoPynvdV9eK5GLdgYhEdqkThIeD4YgCTGYbcPc3Hc0yDS8iUt6Pu5wfImQQ2/46p49SCwiBmFZlY8VwAyR326+ZB/JG6TQ6LEgTE4LfjP+4ryK4T/SB6ek0CJTfqIlGF0ioZzjR5b4wdpMBAUGgI3LNFN8cWszCHIiWu4d2PS8PH9ChIbbPP0yFgdBc1RiD5spMxIWtmQQRkKORwHSQvikLkDgQkuSNEbCQMHFqVcGIyNSJWVDSqzXCpGK7NnAIh7xzRg9VDYOeoTxMyGmXBETkbs2NKIAgzBADBbs1LhQMAigkCLC1FZTcYuKDaW/9um7amrsVQEISLmTqgOAeZgoox8OxtY67RVfrHQGIObPkc7vTCBFiFqKKiK6qqlyQIlDgeFqEYTkt27o68HwoQFHEb9/d3W6zmyKFhh9uj+jYugGROYpMl/O2XuzyosdWreuhCABsW1/7ellfet/+9PX30zRjtMv1PFWo02K6rte+zNUMEGO7NgiuRcrEl6eL9q7VJwgSYQNhYkRhdrPtZYWA9bzO04wON8fjL18e3YyjbNc2CUMAWmzrFXe+zizKxBHAJBGBJHVeluNhmZaHN/e/nI4f/WdzaC4F6Olx0xlLJSKcl6pOYYpUn582LrMcb2yDHuCg69pbC6QSqDJNCBgVvlw+y1Levn13/3DqDs/X1q8XC2prb5tNeHrzzTup0rrpdd268lKfNLbmdDiZ6RbEFL+cz1Xq93/848cfPrUW18tFlc6f/fqlX8/G6Kf75XioAR0BzL1t18vWCeh4c9SGEejdjbB1i7xsD2dJMYsAEniEmas7QBHpakUYiYLAA0wDCZkpQtWMOyIHE4WQe+QdSxi2bowQ5lwqIwH0tvUy1fkk27VNgR0cy7S8eVjevJs/fHX88PXp29/dvP8w393RvGRBzHXfzAGAD6Rd1d1CwyLUNBzT9ZDzwMBjnHm9TrmAmIbOsQsrd3Z2l6Y47LFwiZYPw7JAJHDA2G/KPF4FOYhIgOYRoUgZNhc7EE6vg+9gPGn4++8s5z7UQ85qr1h6bgajRkYqRIc0390zbxL3zQCAgBxxZM8ES7q8jablZsnsUozvC3vdT0aDmWA/8spmByNAftAjArv9w0B79ueXlTjXnPAIDyKGRKB+7RW4Wy8NYjRf7uFBMb7yr19yaJFG9xjw1OBr9kYx+uH4IBqvpPuA2yIA829xZ42zB5MwBsMMJwyw1rEpG9jzi9SDcIswZvEAygq8W6omyZwxqARQp+Jmrg6UlgwUYHliFxhIaIEInv4ZgKja8+gmBwCPxAmHGsDNrRkyI4I3dQjEkKmadkCEcG0mlRjRu3XceGI3JyPVDhW4FJGaYaSX7dI2C8QqVXsnFFNiqGHezMycRRKW27buENo2AOhdU3M6oo5yUQWfamFAIWSGUnnbtFSuc2WCMstyKBBu0MoMgQDcEYCZD/eVLt2hBGhhnm9LYTYAEQEQV+DnOJk8fWytdV9pPZ8PBS89etgFen96Yvnh/en05ubg7eVy2Xwus8yAzVG28wYLE7FtDlUQgIR1axKwXhoJnG5vabXeLsG0Xjczo3xv5ZsTkQDXdQWOgryUeS7Ty5fny+MFviUidDWc8u2FRJxT0ulwXOX5MB+Oh8Obh9t/J0aD3u14WEA3d79eYp7kEtC3Bkjr83maFwnYGgHxtpmZtw6mGEERhMTrer168IzLcqrLwjI/fvry17/8y/e///adPLz/7thVP/7wy+dHI4Tz1qLZ1vWnnz/N03T7sMy1bP3ce0yz6PN1Zj+ceL55Yy8vNdBWAYjrel1/uc4H5sVLIcTa2jUMBPHmeNPWbdssPLyHdg3jMHAN00QrQEh6IHMxNFPPvRAcWTD1l2rdI4jIh5usRdjmagTuZE5TZWKTWVSdiKhQhh1ZRJ3rtMxdvXCJwE1NZFrevLn5+is5vZke3h3ff728eTvf3cphIZGU7uQIOCBdCBJmB+HZ1RKwonBwgjxgJRiyRNzr3Kg/O9w/fqsBYMC98Ru1ZkL1WXQ8EqvNngABQQiEnFQZRAazuKtnfMrusTMm1tc7sV12hDsOBYiYQhfcedDXf+QikpCCD0cjIKR9KxgYie2aURhB5Yg4PD67mxDnPsGpvhmM4ass9dcmsQNVkfAnwvCwQ6KwkDQOG/pNBCCIxNbH7J6PaNRlzKI8jtlg6K0yLSBH99cZHsZBAA5oDHaYBl83qPyLwc/sbhmvL1Y4ZHwupD/Eq/Ro3z1it8kIi8y5ZyqI6GbLzT30bluzdVvZ4+Un1evWrApt3R36NAt4QHc3CO+IQMhZRYQECVh4vHAQXCjcpIpZJ0cAYJbMSh68DYB1C3QkIE5XRWpbgxaAqE0BwPNej1GbpisPF5IiKaUKD9dACSqk2plFWMKDWQwaBAUoITKiI4KTaqiZ6VW7t1XN0SPUPNzXbROWUkrrnZBJiINUDRghnAioUCkCEBBuFtEUwbkIslMhLO5kjO6t995CrPDEFHyQbT1PNxUQnGQ+1BAqU5HCUur1ee2bw7UB+vEGv/724WNcwuj5Zb22trbiICvYx+eL9ehNv7q9aW0lcOV+mrnIwezFG9DMUgohexgGT6UySVif62TmYIGI5+fn3tlVLufrzekgJBKA5m7erlaq6dZ7K0IVWPu2oVmVYsjhYWZhkMRpOBBjt01j67q10Gmpj8+X+VAnLqsbF2ndzudeS+kdzZo7+xqAjXF+vlzOLxuRdHNTDIpmuPbePQKJUJab29Ptmy9PP//8t5+ZTp1uPj3Fv//4A9P0lx8fLy8v15ezAhYkdA2Mh4f70+fDaZ7mIwjHjZfKt131089fqkyMEcwyz+E+UYneKGJ9jonJoxeUeZJtvTC4AwCxerceTGXdelutGzigWSCKVDFQc1V1j7whD0SGjFgcvmN7TQrUcaQJ4IFM7EQ0qXtTV9PefV7EXOZJRDggkEiQ124ujCIwTXC6scPt8ubd8cO3h3cfltv7uhx/Vbm4pbyPGd3MEpkaCA4FAmcNGhG/e1V6rcEwYBh/rUVZ8JFwTxqkPQExBpc56hqNq6YYcxIO+x9Xg9+o/nHX+Kc+h2ik2GYeGewKF/8Nu5yNwAdRiwDDCtR3UhQBI7Pgx8Sb6VpBr9jJfuGbo/3uJwqIWFh2DmNnN3BHngIG4E9k5sz0SoFnKkAMcAciPMIlwf5duJpWTZQuc0MWlQV3tBZwt72PwH/TJMZMnNUeYX+5dwnW6wfkfx68wlBbucNQQe1i23S9fsV/8kUjyGO6fIb+igcNP6VAJBGe5tlubyoTI9Yij6LKdv3hzxQe5qUWB++hkMZJHmGdiBB8WmophYSYMTzcrU6SrkThQEzExdwxHInBwszwdVVJlxBG4RIKnlncTJlw6eZjwSTOWOqUczGXTA0gRgaKCNOQyqn7JEz7R7FwJumupu7ORDJP83XVZps7UpGJpqYW3pyAIepUuYi7l2na+uZqg/XBQAISdnBGDDTtjQyFSTOnOM8fuRi6gc0L01zLJFJoOc03dwdnoAA5Sj1M5u6upUqYL0udKtSC1gPu5Dx7bGFbfP/tW+3qXn9erwa+GX+5dFF6O7+dKpufEbFrfP7yZaoIASgMVCLBPyTdjEi6xoIIjn1d3S1ce9PrZUvXqGkqpRZ4PheWOhe1eHp8pgmZubcV3ULNVYPZ3ZMEY6JAatqRsC6FZpRjOb09HN8sbevs8HR9wUBGOiz16XEFanPh9bIdT8e2moPNC3UF4NKamcHuD1wAgYRqnXo3VT9fLi+txyQ4HX/85fnHnx4/vjxuW1+jA4S5BxiAEgBDffr0hB8/FfKbm/n+/v72BIc6QfTDhHe3Mk2la+eCAlSlrufny/Wln9VgJbJlIkfzSDf5ghTEAGQeNE+Lg6G69oQmOcIHvowRZmPsJVRVQgYkAAkCdUsvTAIM4nAPwt6Nha5bY3XXOBzEUytpFiDqgQTuYBFQxTXkeILlWN98OHz1/Zvf/f7u228Pdw/1dAQANXU1887MYRHojIxI4WnViqYWAMhESOEWFBG7ORkkUr1XoUHD5uwfwBiWA+uOKkMEgMOQ1uTU/Co9TDLh9aQGEJk5NSZpqzAiCF61+WOjGCCRj4L2G/Q/e9PuHQQ78D34A4REL5KEAMx4ABvpZOn9nIIbxCQDsnQnHrMraMjCUquadHjsvC3mvyOlR77jJsNXP3xsLtkkJMGXpACSK9grPkAe9Ob5IeyRqgNR3c0+4bcvCHh4AibjO+xwz/gpRfoTjyEjmZz4lbhIgGwQvPjfukZEBOYZ9GBiHPd4+bEo7WkIUsrh5gaWQ77KbT1f+irnc3v6idyvFzhMVbctzd5Z0goP6lKQKcYdOAJAqYVk+NfltyHmPDoclxJ5eOIQNnabvKg0DIuuaoRU5nlbu4Gy4Ks1h4XGYIcoAEQIGYkJCYA8EblhIIsDgjMLy3sSIELorm7OUlydiS0AiT1CpMyHhZgAELt27ZlrVLgwAYJTuKm6gSFIJRFxVwDHAGY8HGckRdAIPRyX4+3CIiA4HSsXwJkxYwBrxcLeXN2hKVpG7NHx5gAaoXKYxC/09NOLYJ9nvFFZrZ67MgqHe+DzeiaqCMIltCuzeTM4EHP0quDIhYhpOS4RUeu0tQ3D1MLVpvlovceE4Zt59+genSsC6ryU4Hh+fjk+HNx9KiWs97ZubZVjjTBXRSAzy6vY9bpR5eXuWGY+3R4Oh/oi6J0szJtR+OlwQPb1egaAOh8u19YuzQMfn1epE/O0wWphqjBN82pbzygmZC7iSOd2+fT8uXW1pufzerWrAzmYg8cAqJVBGJiBVVuBWK2/fFp/+PT59nhzs9zWKQ4L31y2h5uT+lqJl8NEl7X31jZrbaXzWgvUAh++fdebFQd0DwPHGmxt6+nQ6eEAbGF926QWc+s9ApBY5ioQaAZMZJHuNlmCCQgZZdzEuuWNVAY9autRgDDmShAahVfsxKyhXARIpJZyWg5v3snDm+P7b+5+98fTu/fz3QNP81joETGcgF8xcQsNy5ka0vHi1ZkBIIWMqQfPc9gB4AClWyf4qwdDFmyzrFU79fgKzsJYBCDrvoNnmwmAjBPEV+wmLYYyRyXHstyTcB/SLfa4wteaieCvTWqg3gGQ2Z75PQMA3QOHLdAokgnOMFGmWSamQ4BZlGjotvddyINyL/EdYtlN4l6RdhxQzai5jJiR7fCbtUF+FSghuo2WG7C7h8LAefKbDapgr8iEr0YU47YCfbhOD9B/Z0Ry3wKw5HXj1UnjV1JkMNrZ04cuC+BXwH5HisBitFDMV3b0lwT6CImYJSBQDndvdevXNy92edanZ1vP1p6te4MeZmDAhVGEOJh5SHECrKkRSqEAcANkYkIfe4gThed7yyzMCcndtdvo3YhqBkGZFwoA2jsRdnckEmHfXa0cTEBeJxnkcYnF6ahlxpjGhNG79W5miMi9aQQ7kBsGoGuYA0SYmZpJkTIVJu7mbgqM2+VKe5C9MBCRq4YbIgBFKUUY3ZHAyyxlYSnIhNNxEfb5tEhlx+DK00Ew3HpTNGTyFXtaFwIUKQB+uayAaKiTTLb17dynmWXiUmVZppd1vZuK9gwz8Gb65flxKfeM0bqjt0AkNxZh8dq4N62LpBjKdQgZAoIFRbgqlrvD87lvqpfLitegyhp6ONV27eqx3NS1bb23d1/fr9u6rpupMrpUGTkbhB7hbtM0bUVkqqf709MPH28eDk8/P708NiLsXZlw6w2QpdbetBwKYWGBsLDWgqJOjChqncp0btetuwOaGwQEy2Vt23l9vF7VXY236AZBRdCoIG/WGFHohoKEBZ0U2mZ9FDuwj+fnj5cz4PU4z2/ffPjhy7kSHImO83Q8VGsdAltnN73gtiwT/nKmUK6FwjikbWsYmYKabptCYFdDJJHq7uExzQWC1QAAwzNu0Cxd0ymIEQLM0qKSIgKIVJURLJAcEcg9tLsyknAAr6vxBB6uBFLrdHNXb+/l7s3t99+f3n11+vqr5eZephmZ3G1EJUYEOPrrlIyINHACHEddiYtYuHuKYQJ35nePNkcMsFQZUmoFI7WY4UGYv7GRVFx4wKvH5AgpwYB0B8LfzKqQZ57EHBHmjoNyzujbfPdTgIfvwejw2l1eB2AasNBe8cZsDjAePO4CGPCcZn3XIAUEBrp5ZnplzMCIbRhrjw+TosSuACB9eHCfp3MxGo4MsYsNIU9ZI4KA3EwGrUq/Eq3DNZcxXq1Xd9+HdJIdT2KE/u6AFo576UF242hr4cHDb3vHvsYStJP0+bdD3LN/vby4GJ7e8AoXwn4//apEHQx2vneGywYgMqDLVJbbu/n5AT98Y9eLeqdLwXbxbt4dHEPDQecZzYKFKo27TyIC8+7KQhHIU4XwtimEE1P68BCzK4T38JhqVTXScHMEcDXKSBBzCHAPFgqHMB8TAebSDeGGBBbm5sxEA3BCDMxUv9a6jzwr8gBg6auFYXJ6npYvED1j5yECO6EBU+vdmnqA9laZ+DADQISyIAITgpShD1wOVQSRAdnQlWaYj7Ic51KlWxehstQyi1uY2vWyci23h2lZZlNDANvcVuhNzo/Ppu14PITRl5/Ol2d4enlczeZjvV1hu65307KZh6p3a67X67VwnaX01qg7Upi5a14/EACs13Waani3DVookDiSmgeyE1gEUaklGNnybK0UuVozLWWaioSZqn95ebx7/PRhW+OyHg/3AegeQonbkjuQyHw8dLP5eHh4c/d5+Xx5VkIqU1GFy7bdHI7AFQI8TckJXXtrdjgUREYi7QlSBwKp9x5uoWC0betze77YRQEZp7QPJKjgCsgCVGUBKL1fXYQhlsN92zZrK4Ea9A7KwOprv2yPlz/XKodpOQLfT4fDNC1l6tERfCrMPKvXz5/WKhjWC0P4iiSuYZtph3Q+IxFwzFw4D6AYpdbNd+PfrDthlrlC7nn8T+jhiIRCu3kBIkmp6TCJHuhIaSJa5gNJocMxlmMcbvnu/fTu6+Xdh/n2vh5PEK5mObpDoAi5oas6IjElDD5o3VSZA3KQgY2qkdA07QUEADybwG6NYOGUZYECIuO+EcFt0JiIELgLIQc9DDnahr9qPDFSuRf77yvufAAEQBCmjT+67od1OIpkCpBe4ZNX3Gd3NMgxOou0J/E5vOcyPAUxW9G4EUtvZvfhGhTjTw7JeYv6uovA7v6W882ARpBememd0hhjZ/YEAY8geHVuCwjCMZbvbnlBlEmtsdPoe50eqJHjznz/lhUYOC7RXurz5d/dmGhwATEk/68/lZSrpKh36HQB9msF2NcSTEUS709nrBPukEQSERPL4fYm8LuXCuh6Jd1+bOv5glsHzZuyVoOEmGcJiKaNFVA4ETfCGKZtredmlXw8RoS6IYJ7ZAAyQgSqWuKGCGQebkbIPjpztneAQGSQIkQlMIgg0AfahUgBhSWPM5E4W5+Nq2MBQLcIR3cwg7FFsmhzy2QewMvzipxQFHdVAiylRISbMnHXXoSmWYQZ0ZFBCs5LYQbgAA6ZaJqZGYhBJq5SDA0ZM12EkJd6KHMtXG2LCNZVtxcPBbfia7Xu1wjrGwTLTPPN6en8xFJKhVpa4fK2HtZ13dbLXEDBDMAMuFS3jUUu5+vxcNOauQcVYSIELDJRYGwajoDUNjvc3n78dL1e22VtHs6GtVREvLRemNiocgHAqR6kTL370+PzelnLcZ9kABKCraWaQ5E6LfOyHPxmuyzT6XR8+rxq91onZ982LdSIGYC2zQghAmtdJgNVhUCLqIdZHVpTAwHC3o0iSLiFXq0BM2Ndtc9xYsJwv0BEgMCd2ayuK1xrM4BOXU6HO5Wb3q5giuGALjAHbBaXl3bZtF+A1FDO59vDPSHMBd2dIpyob53CXZtAlGmaZ2lb69cOAVJrqeIGYe4egIwBbsO6GQMgnKQIRQhYBAapuwO0rQEBFxkRSbmKY7h7MLZuhTwacK1qgVVAZLq55fkY03L65pty9+b2q2+Wt2/nuzusNZUhge5hFESJc+Y4nYg57iUEAwAyKMoidSo73jCmQMLdmDmrf2Rs1sBtKAU0Mex58tI0PzUAxwXByLSFvaaNHNDYJ+ghOxxYRWBSCK8T/eA6x6i74/x7DMGo9/hrwRx8NY1elvU6UqeULqERSfExsroCIPIuIc0JeCd9AdLUc0ziO5o1HkbA/jz35Q2JPPLhD6NQwjwBUUl30PCRQ4S/Jh68Dvew9xMKD4dgZjPjfFFfYS7f9ykclHK4R54OQzpVxJDOOgCO0LKx8qS5Gexrw+B0wSN4X5R4AJF5CLZTCrkOeLICqcHEAIicoYkAZL656f1d39ZoF9/O65fP6+cX6oYBx9OMgcjMhSNcV08A0s0QPBU3YKFdEQEJwMPUmTl7ZK7SGKhdvYd1d3AAys4PwOahTdPPlZCse0TUuQxWgVBdpXAR7t5zfQhiyDyGrSMx5KxB7IbhI2y6b7qtoQYWGI6qZoN8iyyjpbKzC4upgWOdCgC6W9I2UmSaSmurh0qtZRYkM+9lZpmo2Uo2iUuBomrAkcIMJsZABFqfVus8TzMSXD/69dm3lwZUX56By4LlsK6X89MVpSovPBu6S6V57mGEAuzIPbamtaC5X7e+zFhqAbSpTmbQrgoVrFCZOSC0GWLpm12uGy8HKfPLuW1X3Va7nhsJTpOg4XSYzOGMXdu2rmtYvJyfF10Op8N6vfZuENC3xlLi9ZckHfiIrdvx9ubHf/m3bubh0RRjjDUA2MwLQpFimxKXHqatTdOyWQNkMGOW7saltlURsZRJkVbwNUUCwQZYYCLn5vYC7XD8w//m//R//P7vP6BOv/z45Z/+f//0+Od/9/WTtpdeOuBxWk5EpNa1bx5b649MxUOQoZl+uV4Z4GJPN2XqGxemqRZ/7lvr+UsI3uVq4WdiKkDE7K0DkNSKCOjYeudSxsI8nKnIoWcshAW8ioCmqXbraN49gPLw3xBN5ikQgkA9wkK1kxCoCR6m27vl/u3y/pvDV+/nt18f7u+X+7tSpxQamruZZZ01sxSAjGJqjoT+Gp+F+3RIED70OWFj/oskvGNUJyKkocwGyCSRsR8M28hxTEoBjgO79l91KDvFOaDmNMyAgckYAAVCntcivOL7OIgVDGHOT9sjGHGcaw2j//2BDYZ4WJ7BXtdxCEYhq38iTswCmCQ2Rbp45zEY7D8deu1gQzv0iszvHO3oVubjCM0JCByZANDUEDECJD/fISjpTvNgHF0hkTRIiAn31wvy0cBY1nAfyneUBiGSrB4/QoCkLBI22xMEhmZofwavmFBazu0T/WBpRiAT/lr8x87jv64cNDgbcM0XHz2CS2GC6fZuu1y2x0c4foTlxuiLgc5FgLnOhbL3j4x6i5aLSyC6MRE4EEYACTEAJiZoDp5nYAQQquE9B3oCx1yiCMBMETlUw6GbQQAJdtNuwQVIOHPkWZiIHBwgrLlSJyJAMgtTAGdwcEPrgMhqahYO0NXMoffQ5sjEXDwUg+ZlNs9pzQHT8s9ArQhPtTADMwVEqUKEJORhhA5kJMwEUuo0FRHRZkHgGu5h6gogJGp+ft64UCFbn7eXjx15Zp6vl/74GOBG/LxtF6lTmQp2qswhOj/U7RLeNTNTiEgjnl9eSkFXYpTDNJca1s4ByCzMsK3Wey9V+lXdtXfYNkBXLuV61W2LthlJDXfXsN7Bq7pWZiFq2xbul/NlvVzLXAoRM7d1zV+JIVLYCxBLWY6n483Nmw9vf/nXv8030ze/+/DzXz4DYNdwcA8SmhkZpYeHcCHkpgqIrXdzd0dmNiCqaeDiPUOxRZALRBAAYwBEA3j33d//n/+v/5f/w//+f/cf/8OHe5n/h//xl//b//3/9f/+f/w/N3e9XFWvN6d3VW6vl+dCUmqxmLpeIyiipzKtEYN3a816LwHHZTJfWnNzy6GVgq+2AYRQVJI6Ta7BHNG6W8xlziQqQtaw3p1EENjdEEHNAcndqbBqQ2I0CkBDd1cmIAAMCiA1dfNJULtuLWguxEWOBzncLA/vTl99dXj3fnn/YToeZZqA0LpmHA2LuBsGEHNepiS2gxBmQHtA10gnj1ex/ahvuM+OA7DfKYEBX8MILok8WMpQeMId094FHQEwVJ4QCGCAI8Mcs14nBkIEEJyQeOL8e0UaNYzzK3gEpP3OK/uJWXJfKeBfuw3CK8E6zm93fImZAIBTfOs+/OYgHUr3crqzFwMF8WFgsf//XTGfeqF9aQgIFEoZSYQhETMCAjEKIIy8MEAIBxyhB8m/p+gSAYn32JrRgrMWQ4TnjP/bjeHXxcoCGZPkzD6SernULe09YN8t45WWeUVzIPadCTzzQAfItbMFERGUkG7C7Yi4/xRz7TB3LnW5u9fLV7Zd+vOzN9fPH9HMGZ0DEHsz5EgvBLdw01qxzlNeR+ziLNwHbfDmzGIdtIV1V/WRcozgBt40Anqe4RCauZBYRFgUwsyVzo4ePvKOdl8RdAMEROKkObx7795XU+fe0R0hOABTNaRpQhYYBk07ABBBrVUizM0dpPB+WjEoMkCw8DADMIaYSMxVe3dsBThRyQhwBzDTCEIGEdLYmm2B2kF7YVqeP7XHn7bLiwbD5fn8yy+X7r5d+3W9btsmy1JLLaU4wbEcwDuWpW1PhapA670vdRY+QBAEmYZ5qAFJ0W7KSEZciJjD0Q0IpRSZJmkGbQt3up7bunZgQaDWbHirdG1bW6ZqFk7e1raeNzK/f/8BCUothOBdpUj6NHE4ILkaInEtD1+9neYSD8f/P1d/1ivb1m2JQb0aY84ZEavYxSm+73733iwkyzKWQEJIYAoJEEjwwD/hHyDxwI/AT37g1Y+WX0AYC5QyCVYmdjrzGjuxM++9X3HOrlYREXPOMUbvnYc+RqydPtKp9ooVMWMWvWit9dZtUxQspTW32lxtB/dMLETgvq87i1StqspTTjld98qY3cEInciDTjF3lJQP+3YRZhZQtbzc/w//N//L/9H/6r/zb/7l33lgfH25/nr+48vTf9Xak+szAkPLdVOn65TEQFtDVMyyqLdWd9PawNXKUQ7LdMJW234tW6lbE0ZBYiRHQKsI6M2AfTowuc2HSRhVKxnVVlHETVsNoYmrKSIQo6rJlJhEDao1c22mDhBKsqiwTBsRbFsVsSTUzKdpmpY0LcfpfqFprpQ8LbScpvt38+kuLVOUnFFfxo5cIuJRbvsY5KTOLSABjpZ+xM4Be3dqtdecHQLpNfDbaGiPP6ZRkkNHZm6CkVHCD/ACOrLPOKDkzkiPLYmddzTzEKTElFzfMtZjO7r3xBBhKVbberQFdsO1+vgUjMMODgZv07UIA/MZfnDe5TYAXegCg/CIvV0DXzF3iLXA2GN4hMOB/yCEjwOMXCkhQQQzZ4rZsH6mw/FtdE8Q/kA28k6caKFg8t8Erb2pGdcsKAihkbKgkxeI37kujXoeh3jVDAHNgRhuvRv2dNovJH3H3ce1CfnXzYsDCMZMRgy8cWKy5fDw409kDfaNAS/g9fmrxySAMCiAW8qitQJ4CgCaYLuugC5TEkmmzsSmjh53M7VwwN9Uq9VNvQEKimRErrWUvcUKacm5NQNDojBviqQIpg4M1hTCkssduE8+k4FqbPWDWlpTMAdEVvVaWzXYNzNFNWBOIlRqQ3NJEnMs5mbDCTFiQZpTSrEXAMGMmDgRkdVWHFESMFESXpbs7vt1j9IXOaV5BkiX9aJNrq9tb1h338vTt1/O1xf99nJZ1b9+XV/XUtTXUkWyut1tya2kxA70eMT7aQZn5KmpEnNOE0toG7AWzUnURfeNSSlhLWbaMrAapMTquG+1VS0FqvPW6rpqqQ7IjAnQmDDlhECuTQhEUiMojLVVSUyIqqh1PII++Dv3rhJhRsRpmesyffjpw6f903K/zMcc1CjUpu57qciACRIxZ3JQcq9VbSsKyJJI2IoioDmZuxEYUGvmSikdAYySVy3zQe7vDh/f/3BY5A+fX/7pX/3Nv/fv/l//6j/6f+5fPyVfibLAlI0YzVcFNEJWd3B1NwQBKAAEQKe79z/cvbNS6/nZ9qtqQUBrRolCp5CZKYMQ5EzChIgOmoQtDF0A1dUUWjWgcCgpAAnRAZ0YUdAKEWGAIU4wwq67xTAZ1KrWKh8ygBAvnBeeTrLccTpOdw/L8X4+3OU8E1A3hI/KG7tSMxwZmmnAcNYhICBEH+70UelpyBzhzTjGB72pqh0cN3dwGjvdhxKodwzxnvCm/+zR4o1x9ug4xnTREOt4Hy9ACJY4jCAACNC1j/L6rf9wj9QVN5epYdeojnIYodvp/yspqM8uDG4WYXRAYbncmW/rGSSCYR/bHR1H7ysGpd37j7EOrCeSQKIDxopvagqI4gAQgJp1pAvCcw0DFOsAjQ/+2D1yQ+SjW/6LunvE/97tjO/d6/WxEmGkaOzCpOgMgj+ADhPFlfYQ+kDPP/1KOd4Epv2ACPscQOdcO/UwUCYiStNECHn/cNeKtta2DYmovjRvUIowoYO7psRuDYGQpFZ197DS1eboaA7otK+Fw4y6wX5Va67VW3UEggqtNUDU6mCMxKqmCghp33cEYyYmSkI3MgejGQPqmxow/NUJiaxUU0BiFi6bbtetVL9eGnCWabKGas0UmqmByZSTSClF1fdtB3QmckImQPBmyoZtrzH67sAkzEzEIAwpc5pmSSkMwKZ8aKUgUcoZgLZLW8+w7+3bp+ta4NvX9dMv5201w+lvv379ctkUqTgqWIVmrTDgt7MTkIEZ1LuX1/vD4ZTzIaFbuePsMxO4aUUnJALktTQGI1dQ1wKE5jTlObdLc0BAASdtTQ33te17a8VK07tZVBs4uqOpalVGzvN0LVWIEnHO6e7Do7V127a+YhDMVIkEwjoRkcL0Mqdm9uNf/GZ7uuCu94+Hy/MZQaKIa6bAXNVaKwZA4oBIQKBgbo523deOpjobhZBC0FXVkmSH5q2CKpX1n/7Df/wf/F/+/j/57fv/+B/+s//bv///+NNf/fP69CWDZZjmNIucsmQrW6nFvDV0pWYOzSozmnlySun0u9/89sf7959+/9ctieGyrTUzG1hmQETJNCcx1ZzodJhcVRI6ep6SVW9Vo1dxgCmzmtVWEYk5VVWWlFJu4IkEhLFUVafa3M28adtdtZmZ4TRJJwDRq1szX/I8H0/3P3xcTnd5XgDQmvYSccC2Pjg/AGRmRyTDMMgyRDBFZhpW8AMEvlGc3fonoryBhzQ+COHwVzBzcxtA861kHWV3Tw09bvRjCb0NAPSlum8wfxS4qhqiHBFSdUL0vlm5i5IAYu17iNMt+IQIhN1DrrVwHOo4diQjG6LR0LfEkgCAvn030C/3WxDD2/cBDGIhsKw+KHCD4f0mtxxM6UgJvVhnCt49AEPBrqrs744EbkZEsX3G32Zu+7Q0dt8khe87qnGUvb3pcb7rd7rFxbiON0loAHXf92aDPA9hVj9dQ2U0jtPHRJ/f4LB+hcfbQzC0/Uy7A6BIYuZ8urNaD3uxbQcCe9Lt/DVZU8U5Za+uDiLJgcrakIwxg4IZhtFm3CHkVNfiDdwRrUu9Yt2Lqbca499kDrUoIDZTbw0dkJmYWdjcyCm4DREm7kw2xa5IALTQa0Qrl9TMnUMRNR2muntrYM2bmRtoFCbuzfoMj2RRbXEpgUhyUmv7pg7NnVMiAlP2Yj4tQhjiU1SzZIwkqgpGqlDAYGtN5fWlPj3t377s3573r9/W62Yvl/Z0fflmpYET52pk3sATEYBhAYz7g2HWaX7WthZ7dMkiDAgOBxE32trKlEoDLOboIhyETxJEyOu1oRkCM1BzdqCXl2tzXLd2uRQUXrciCLIkYgbz4zxXIwfIOZdaCMGav//w/nW7Pn359P7nH+OGhjEBDkk4dojmCZmn5fDzv/Hw9Nd/XJ+el/uUJ76c1VxLq5mToyIKINSyEXDAGs2VOdeQ0bgDp1oNSMwciE3BrBqgGaI5ufi1/ot//J//O/+Hf5uOy9e/+dvnr3+TIZ9ABKfH4zuHJXn2qqVpJQN3hdZsK+1CiV0rkzBO7+8fM8Adp21KeyVMKdMhFGaJCcBkwpSQKSVBYedJkCAvEzOhi6rXZrVUWLiuag4pcauttV1SVrfrdkERZMnThJib6ZR839ZmaFBR2AwMvboJ47VVbbytW/74UBBX9wPhpRQpxbcVJiFvUVQG0t0ZxPG0h8hBvZfriDw2p0ThRzeGMVBiHLG1CwrdbonB+wwAUFhV9iISOnU84kPAKb2Sd48VY/TdUFFYrHb4ARHAeUgNh60D3sSQvYQNVVJYew58uqkykxsY3mrXPod24wUGvHTD9DugNBoF78MB7m+pIxjgHk+jv6EWZyY251JHjaLYR4AxahezNAgGxGE9527eR10HKx0RE90cwrH5hqBh/+IBFCFgN8yDLte5GXOPbxdMPcLQb9HQKfUY3U9qsMZ9KyaAUxcXQSh5R8vXrxkPS7jeE/Xr/4atITg5AfmYiIgNR4CARHx6eDCNos3yIdeTXH9xv76AuxKYGlEKTT04gGlODAgkoNVb2d0RiRjIFbQoOOSc3aDurRc44E1j0ArdyZo1Nclyy9DqjtHRuUpm9tALkYMRSc/34OrQqpmTGqhhrd4aILADaEMDrFVdwdRraSE2LbUKOAuyMwBQdRHR2kIo3LkUBQJMkoQRzEWSJHbwfdszcjouBoBqoE6SQHE9b6Xg1y+Xz1+3pxc9X+DXX69Pl+1S4bmWBql0x1pQV4SJQdBRWMBZDBzBHL5t55nms+11bnecG/PkoK4nXsChWvW9IaCL5yljAmZat1VJJbNg2q4FvSLTpuAkl/O6bUopg7mbpSlZaW3dp/uJE+/XgonDuvtyPte2l1qRJdpkIgZEU5WQV0FwnghEqpYyPz+9yEHSIe26nx4PrW2FoLkp2G5K5jmJSI6dmkjRvKG1ZjGd4EZIKaemWDAjF/IZiBKxK0Azd/fz85fLa9UVgGZId/QgKc1yNy8PL0/fqpr6rqAgaKDqrbadyRG02ErAk6QlTWT186d/6WWbyBkhTxOCpiRuzbQ5mjCI4OGQl1nmZXJvakrsSI5Ol2sLZIAOJBXLriDkoGZVHao6kCBzSgdzIGYSRqGmjTKpt2HyDA4KaIXQk1zNoTYyX2qFfWtfPx9VG4GklFLOU/LmTh6NEzGCo5magWP3QyYg6LL5Mek5Yn9nARwwVI2ObuYDNxmNc7frCSFNr4F9lNM98nSVI1kPPDGN1cX7hGS9Nh3qI/fvJqJCr4SMveYOJX/PR2BuSOE0F/uKCQBjMBxGBe59AHzwtjjipfVPjD+Nr9KXJFrvYaxjQp0aGd/Kb0E4LPDG2voop9GhH2qvuUNbO2Q2CChuHsR7IGWRbaALobq52w2Q8d5MEOKQM30/U3fLIwMYon6Aw6AOcSQZ6AEb+scNAwqIrgCQ+p663ojFOQO/UefxvwNKimOKEj18CYAIXHuXpQaAJEycjg8PBLjMab+bLxM0K9snbdu1lUJqgbyVXcEMmq1QiFmSIHHdUZumTGBmTbUpE5tWrVa31ppxSi3k0ygEVPcQ1vK2NiZ0dNtbmriBMQMLoDoJtgY5DQEzoLtv1wpIrboBI+barCmqQSleFUs1B1aFutV9qwTIU0ZXFiFEbS1N4m5oCISUWLUh8K0BdUBzUPVWKpA7+TQLMZpB3Rq5uQJjQqV9ay/n+vKyffp8/vqtfH2tl5LOu9d8/1LOK3F1rKaCIi7g4JABJncoigjHPQZMQBAOPiVbn4xQiZUEMWmrDYhRat1bKUKJiB1TA297Fcq1BqCCquQuWSbVulUzSAq2rlvKWb7TDiCGL6ZD06rNzbQ21bZvF2Vx1zTnGMDxqNwcPXYtkSRJj4+P3/bL85/+dPfu4fXL03RI20tZ7ubz50uWXNrezMPmOxELizVjEkdorSFAEmmKKYmcDquDVW3WANFjrSlMzRvKIsxJ2Pc18yKSyEhocatu9XL9WtqmvcWtWiuAWTgFEQAYIYjz47v30zQ7uNl+N6PY3EpxQWJ2B6ugTOqNwZclHY/pdDdLIrX+UO1rAeRphpREq7fmIpCmXKuVUlOa1tbUtaqWfZdk7sCcjscTEc1pkpmBTF1ZiBd2h/lx4Wm6+/j48MP75fH+9OH98eN7yTMmkUNSb+TcrHoxcOCbd4szE3bzZL+NsVrHC3q0D3wbsL8yYpph0IE3N4F4Yacwu9mZD7QIkfrvfp8FOkwARGwxGdzXKw5MC/qaMBjh+ubL4N2mauAvHRX3t8AHI2p3Swa89TAR070H+P6tx67hoJftNkgVJQt0M55RhbuHq52N02M3P+yoL+NTbzYLcIPusWdEBAhnvyjq3QVHSI2z3netAQQi4W/ODTTK9e/ApnFmHZyRw88ax1W+If0DwBm/88ZcxHNoXUTlHtrSIZ3trA3FNvQ+igZvuQPgBgTdsiHeiAWz7mgH3rtCBEdMeaFHbvuESNpqaRXA4fKtfP6MYGVvOU/luifiVsBj4Q4WRxYWrb6eC8RmadDM0Ng93LSYTRGUtbkDupoZamA0AIouicDRnchFWyFAykQkoT9FQiP0pqpOlFpTQLHmrdZSXY3MkCXVpgBQq5qCKiKSA5S95CXHjgE1w9ZYmBITAaG06jklB2ZHyROiIZA2IyBrXtfG5DKBKtS9gvmUplJ0L2sp/nIuXz9ff/31/PzSdp/UpKJ9Pl9fW2sE6rsgMnBTAzge8t+RNCF7Wo6XVlwP7z6+A9Drt88vX58IFK5PlVrLjYQm58t1m0gdnZG2vaoZJ2R23cthzm46z+SgpkCI57KvmzfL5+1qzjLPzITMKeWH+ywICRHJEyECVvC6bVxEq57uHr+9vGiz8Lki6s0QIquFhxHF9mmZpKjurd29e3h4/7o97daAybWZELuDN3Pg2to8Tw2diFAbCc7T4VIax6pCIUFJWt3ECmFbVdXxCL6BWxzGJBOJkXPzvWrjNIG0vRRkAQfA3aEQq+BD2T83r7UVIHbHLIdMM7t5K9Q2R2ARRWWCPKempkC6lZRIJkqHDOKUMS/kkJo3cBNAcsiYtLoplb2WvaoBJaacWi0irI0I0JtZKerQvAUSzUlA3NlpxnRalsfTcro7fHy8+/Du4aeP73/+6fTh3XycZTkgswGYKzjFKuzADLw7UcbWacTuhWnx1PKAnaEXp/1Jj78Vwo49rl6HDdx9oCFdJTRchoA4KkgYGhULf62okQFA3XjgPnizo+mMpRG+IU4+jHDMHag3DQjYp9gQYz2txY5CRK0KwyeNhlYltk32CneQxnAzhPAbUB4tRaCKhoQtRPAeEE+fRu4mEMMT290ZOSgWuoXGWyaBOFuIiN9l0x6n5QbNj4zaIf23yzDgeoAu4x9Yf9AgFgdhMY7VUSq/ffRgSQJW6+oL/I6QuRE10bu5986NiLwfq1sf5RhgFAwq5Q12eusnDJwC+o/dbzEPDWDhpiYsMJEIAFarJ3RJsn/5gzW8/PorFt/LblvbQa2Ym5orkuRJLusGDmqGZBwXPaHvZZpndzcDK26OTdEcwKiUGhhlGHqYNxJioKbOLERsKPvaKIGAI4EXJ2IDqLU5kDmY8rbuzcgM981rLaVYa1jVmoKaucUEtZu5tUbgIgJuZiaM2DfAmIf1GJEhalFEJMSmziZW1S9lZm4YajTkBM3xctmfn/ZvT9vz836+KsmRNG1XPV/L3ioCkgEBJ5RpWub0brn787z8pfDEj3LR8vf+td/89/9n/92HfK9b+/3/7/f/4B/8p//1P/7PdPcGryCTEjuwWmvQ3IyFFMBq4xVFUIjXXRHNdmBGAKr7DpZbkeu1lKIgKCyOxgQ585TSNPvxkHf1/Vq2fUMQBtRNM83Hw2nbK6dU92atpWXGbnIVgV+aV0JOKadpaWZq9v6HD5dv2/Mvz7U0FmpriWU1mTgRu4N6i9sJ3BF9KxehSdCbO7hNk6y1bNsWIriqWuFMHis8zaEBuIKBKia3ZlZ3t4OqOVUzN9wIHVyNVuNdW3UwNs94OC0PrGbbnhjbuqJwyuyCy2FqtTFANSUEIUzMzOjgrZZCKBMJea0tZwR1SaQJtAKLEBuQNAVYHdFZwc3ZJyQqtQmxGtRyNQMy1q0AO2uqWiubMk74ON/d33/46d3Hn+4/PC6HBRIjkzar8VQQgnkzRUAMPNzMbETImwNM3zeF0FU0Y/Ffx+qDEfY+2BoP+5CL9uc/Ym3XbTqAe/gmWv8Ei1HhoSlF7+NXN5QF/NYfIMQccgSO0GICCKGO5mP0EoFVoUOYD2EEyRvVHF/I3NBweJr5SF+9Dn4jqQfTaV3sHjPCt64gDJsD7DZC9kD1+6pFIEQj9L7/5q0o7g3HkM4TYhT1TORqY9EqgocKsWcbR+oqoK4u6l832Je4Yn3UF7yvPuuFPzoN+e1brhgh+u3MDX+gaA5wJMbAUNw8Nr9h/yi/KV7hxtd4vxI9E/etOtBhxGB9An4axDJ0t1EARJqm6eEjInHKOB0MZkun9vRSXl7Ryvq8kpEWMzMRb1urBequBsZC8yIpMxCQS2uhQsFaq1bfq3JK2gwcWzNJAkRmRsxmvhdNBpC5OXk1cGUiL0rh1emhSgRTb833rZqSG7eGrfl+rdtutRqIALA7AgE5hZkzJ3bu/ZFVJUpMqGaI3EznPDGRtobEZuAIRNyqcSZ3NCVmbOCo0M6lFvzy9frl0+t1h/OlAC7N8rrp+WWv3caFhAiRTqfH+7vHw/SR5x9au9uaPX/9uh/9v/dv/Vv/0//1X/69h3cP6n/4w09/7+//8H9S/MN/8uq6GWXMiznqvpZWj1Nu3hg4J1EAQW7uqjrnvK1bmlLdN6a87/u21r3Ebm5nYrDqitPEbiXnCVlBC8BGLBlwSQLMc57I8e74eNH6+vTy+PFHcFdVIXMk6PcWxmo1TrycDkn9/nQ6Pb4sj4em+/Fu3jZNmXRvblq1LvmgWFurXiF2GSL4tm3Ak7rmROro1ZI5hOLIGxMiJOCmWrz3xQ3IwBuiuFPT5o5NV3UFKqBGYFYv6B6q/MP8cHd6fz/fHRPY5XO57veUrNTLy1nm3Go1s23dgFCE614wk6kh2HopQDnNk2ROCd1Bt4ZiMolWhy3c6ywRAdC5bUTzNKWmlhGByBEdiAvVZkAqSJCotrq97g3K5fxUqW22v6yvW9t/snb//l2es+SMMW7HBAAgKI6dKXUAVeKotAHQw8ynV36jfBwc79vC9AjQBrF50EZM+A7+6JxvZ4RvQhpC6KNk8cBEHT2ClQ9HzogtXUSCaN8JgkLtqBYTc+4eBpyBO0eCM4zoE1VpF5kGMeAOHbyHsaYRvkMvgn3ocRL7BjHyPont7t8F51hz73STQ0JPQSPU2y3K92zyBqn1Mhy9u7pB72hA/O3l8VkGiDGqZuBhJx2YjnXKA96yFrwpiOCNAAjrKLxJc0YuuqFVDo7dLHsQLJEzPYbxoq+63SPeXZPexp3jDHqnf9HjR+NjRosAIWMFjMnYnsFiexQJM4lwzlLnzHnmdJTj4+Xwix++lZezwbNtFcWgVAesu1oD5hxtlzYHcGQGx6rkitagNC57caC2ttBrm9O+a54SINWmDi0lzHlWMygaphGmPcsRs1a32hC5FK8VrKEpqlEt2orHt0TiJMmcXGC0g0zeLdNdFRmZudYGxGAOasxhJIsI7OQKLoAaBb+BAxmSVSMhRGxFz6/b6/m61dZMDscHp7svL+VaritoAwEwhswgh+Pd4XA/z/fNZT2fXy+X8+u3s1zK0f/f/wD+J/+Dn/7bP3x8IH/4++9eLvCf/MO/8/U//2f79QvxvLvkdJBMe6sFHN1ZzRGIpRkhmKuV854YqtVInFstyAuQa21qXtwODEtiMUsIDIaOoA1Bpykj5dI2Qyz73nbb941EEMMJ1QgFCbW18FwyN2IiRBZpzdOyzHeHx3d3yzE/fX6RGTl5242ZVb2ZXep6nBcQLlqKalNNKTGBMvGUHKzpnkiqlkmWQte91oZGzqxLgwrdzJaEMmIDolan1q4E0GBnRkBuWGInOyiBcaL5cHx4fLg/5MPkG3h+dzjQWrJb3S4MuK0F3LU1I1iWhSaKFRTCxgTkLsRzFmRwdSUEIAOTidyRmMyl7lpLOx4Xgnnfba/KRMBatTlQmshBgUBygkSC7ITKnua8ry/PnylY671u784fjw+P87LkeWIRSQwBAI/ZrSj9qBu2GAB5OAuFsTOM+EKOgfX0II69140xIEdAHGY1GMCwWX8YerwKdaGBDdl9vNWYIcC3crS7s/Wo8gYpv7USzkyEbDpmmm4h7a0Bsb6HHcG6heoI0TbY3VvtGXnOvbu2jWRkBhhOlEGId9XrLV8EWDZWYxHcgKn/RgYdeL87gOObgVtchv6v2DVjLnHaY3MYEpl77Knsu9R6m3ZrtfxtaM1vPRDePh8AfMzD4SBbRm7wG+ADo353hY7bWPQcwTr0jfVuI/nEGwfZ8t3N5MOwJTCi6G5MNeiOQB+h582YZBw3R4hvWGSaU/oBOAFnzweY7+z4qc6flQQuV7us5sbmOLEwIpCpupmpuXvfG1q1FdAKWo15KqW0okAuTERsTSsBS9wdZM5FjZq25gZCDIKqjvOUAKjVpuFCp1SLasOyW9HWKjQLZ0BJWeLMmoMDCvMYACFwJZE+vWdeS0MA0jAvomZeS3MzIjBBMNvWOkGyLPWqCO5kSWTf277bXpQxHY8Pxfnlar///OXlqpdAWWFhyst0OB3vkKhWX9fX676t14vVl6UZ7PVf/Id//D/6/v5/97/9N3/zm+en9T/+x3/1z/7JP3r+9pmhbVIN4CCS8z2VS23XnMVsb61V9sRS2wYKBLS7ZqJmSEjmuJW9aqvehJnQppySgJBOE6tfW0X3Mgm6QAPNEz9v5eXl8vz05InnZU7CVpWQQUHNiCgGXUwVAUVS4uk3f/l328tr2697qw8f35XNarPt0p63MxgKYnND1bVB5gmYtJaUUlMrGIN63Aiv1x1IgAAURTLbZtYoNXBHrQ5uHgaKiq5u3vzqoEaekcFd62XMimcDuF9+OMzL6TAfpmTl+enly5Fa5fkomaomTqaeUr5eV0MixmI1Z1qOGbzOKc0HmWdelpxnjopTU7BxsJcmCZFo35UF7h/n1rDsOqEYKpG2okDQWmMRAjEEIGfGlBIILe/vZZ6nD3fz/f3y/sPheECA6+XiAPu+TdPELHmZJKcI+Cw8fLLALaQgw27yJs2Pavc2wTTC8aiax3/HW8Db1JSZAhJBZ4wjukR4djMi6QpDCg+i6AiGnBRH3RjQk/eFWEQUnr7goOruTkwcDcpgNG/IufXRKEdA5BhWHfRkTyQd8Xe8Tan28NkD9SiVuzlSqG/AHbv5aff9v2UDGGuNQ4DTWQTqdb92JWustIQ+VjZomDAOI3d36ZU8IVhMIQdXEHkiWEvSwTUP0VP/2t8BMyOXwhg66LV4fxncKIFxJc2NkW/sx+06BP52K+U9In4/Rb26f9OnYj+i+Dhzg6H0wJCi49upjlQ3GG/T3oIkTvnwyDwf6Hif332cfv6z/fXp+uVTe3k6/+GP9nquz6+4t7ZVKC1mrpnAAc25NdNaTckqEWKrTaPnUJgkVa3uQIoG2NQkkQPvu6I1ZiR2MFRCB6jV3RtF2tOYBYNatSmUXU3RnLvYF8nNmxoimpr3WRAz9ZwSEcRojwi5GTNKzqp+Oa+OQCi1NFeVicu+C2NDdBGs2FphwV0UAWslpnm5O24mz18vf/p6Pe+lADlI4pxpIcp3hyM7bte9Qr1ci3lh2EXUgVBx/eX5H/37/+f//Zf1f/6/+B+fP/s/+r//Z//f/9df7fD1BNYswd5W2XlK6XC/nws5CQEhmtq+N3Qkktp2Zm6OqGgAgAlAARkClQQkQUaYlmS+aVkpsWnJc2oOZa+AOM1Stuu+bqflnVXbt+pIHvUnjOfHnEnALUmqko/H08t1+/zH53Vfj8dF39+fv13WpV74urfKIgK4l+Z1VzPmRMKKUMF3dczORM2cWEwRE9dSwNwN1Wvbn9mRe3trCNXMCNh8I+AG1uwKwASTQQODCpptntLdw93dxEJm9fK6l6cDtseH4/0yS7V9rQ7YqloryJyY1m2dJalZymJaWTBlOd7NIgiugJCEmMkBylYIbTmkVh3RtDkCg7mi8YTmWBCOKVeDfVUHAnMW5kyYGQTTcVnenebj6fEv/uz08ePxw8fDwzvOGSgkc8SMscdKtSEgEIN2XKM3AD6IQqQ3SMFHjdj/jW/FI1HMYXoH+juUAAMuH3nBmTiUF6MMpe4Z3aeDuwdlR+n754QGHRAJ+/5vcPdmzQFubwhm3pdrjwjugzUIUnNEZx+hbHAYHcsC6CchCupey99MDrBzzjfghoi6t81N8hTrZsa5gr4mrLvue0RWx7GVCzqLQNhx/DFfHdthkFDcPOYa4/Ns7I+PNBryG+zDcTEa3gGWgdKhh9LUoBdVwesPoH8AVN/lLrwdd6TduAfwXzmxN9Aq+gjqPd4t7sNYb+kAxDROGdwc8gIIxADd3JGdnEbsj6YRncLhzYDSfJCcD/jh51q3tl0v377u5+fzT7+snz6tnz5fP31af/3KtFMpxA6m1mDfqprrpki5lerqvb5AICbVRgBVsZGDKaFUNS+KiK4+LVQbcGYgQgJrQMJq7kAOVJsBsPcNawkQXaFVUINWiiq0pu6xIwLMLefUuxKHsFZFwinPCJ5T2rZdqwJSbTsCN/ey1lIKM62qa1GZGBlFoTyvTAKIMh224i+X6+tl22qtgA088YElg9skyAi1nNu+VUSE4o0AanNHzoTTQVK5nP+r/+D/8/v/6K/VsVyuDnqCMuWsrjKdDnf3Ai7o+fAAujJDq0WbVm/gPs2ZKTsAkqyXMwI3AHNZ9yuRI9MyT/M8LTOnlKyenYyISJwE2Ki5H5IoWWbJOa/X626+/HipTbWpJIEx1WhqxGTqSBIzc4fT8bKV8+V6n6f7dw/z9CULH5ZpvVYiyCkhcambghISIjZ1dVByEaQpsQsZgnsWZMSrt6zMNNe6gTVDRkRCVqsBoAJibGSbaAmOzyBPeHo4PpwOd4dpuZ8WFvv69U/n87ejYCKeJWeira405VrVgVqz2BGVhEV4mpFA8yLTLMJO6JIZQAENCUTEVKdlYlFTdwXwBuit1LKrk4BXZKNQsjfARWpROQjPM5AZowtPp3k5HR9++un+hx/e/fbP7n74eHp4xympeZxea61XmdgF2ugQ07odLXCAPv5uISbpAJGFlUsvGRHBFcwcuLu5DfD57RUROW5eQ7HVKrLDQGLCnnNAEvSdsWXH3x0ZwbAv+IPOTfZXEFCsvAyU/I2lQAdjYnePubMIR+5jp/oNWaJbiuimOh5jawQWbmmDl7wVzx08x1H1jo4G7DamfMsK6OBjnGvIOL1PSsWpIgAiMINYexD69DA/kE5UAzqhmQ1uJEpuUo1tvaOG71emp7uxfyWSTP/uMBJ4nHu/uUN0QKv3TZEocZh2dH2Tg4ML4ZCyjhLA+50T3IqrdRr5lmBvg8+dlH8Ti412yRvaGykdHY55awroBi7CLAJuc5r49PD47kew8vrbb6+ffn398uu3v/1b+dMf/XqGy6tfL1TKft5bUawqM2/X5gbejIWYhZiAuWmTxJKkqgIAMyZOpooASLJtDRHbxNaUE0fcb62hiJrVaqpEnMyLmqvFNHLIoNgxukFCxFLLtCxRMKlbXPyUxLBbahgAJ6amak6S1uvaHC7nszrUVou2ZZmRYTlMgigs3nYgxKR79ZfL/suXl9fNmjvCnFMiFlAF8VJt17XZtXmFZkzZDcBjrlLAs3h2a3p+quAEloENqlY2LGlqDs6MUEx4dt+YpCEBca2NiJuCOwhRMwTOtRkQAZNIRmjCvByXPHE6iLYdCR7fnUh0X6/W4HBa0mm+rLbV2mpFkiTy+fMvP7Za16vpA03kZmbaPa8AkUlNRRKzUMosqVqbDo9zJnQlrofDFHLx43IopV5XKEWrNkBBZEqQWNJhljljw7JXAhTGuurd8V5yen7+oqpE1HQlWBhZHf12xwIrNDBnmDLOD4eHu/v7x7v3eVq8lNN8/OPf/Kfb9vWY6d1yfDyduLXS2nbdU8qOqbk54l4KUD3d5SkTikvG+TDlifJEkpEZ1I0ZSRDdObExmJq2CkQpMWhD52mBfTdkn1hgq4ahfUYkcCRZSN1zTjCl+e7w8OHd4w8f3v3007sffzi9f5enGWI1KYIjYJYAsUPtMwyJFQK0gF5PA3ax45BDDp+wPnXkXW+CAzHuaEWUrORD3IHdNC0idzdm6GafQ2k/0H0cuHqUw/TGS96AasRYmxwrvRBARygKAMosFt/icEJDRHbV+JQAmm88AfIbMt/Dnt8o6gHmDLS8Ty71P0M3wxsm1o/+Jnx3Jgqpu5n2hNptHoJgIDOjnpM6MI83AhyDcAXpOQQdYmWwg5r15YVmN6cJZo52pSt8+vqGyET9FGPvqEZudUfvDQncePNh2TnG8vtqX3B3MOwDxoBvvUI/7zcqAHvmh+AtYOyGiD4Ox7v0Lv8GP8XGrLiTyIm4T6YhAQGPTjQyYLRjBPPhxx/T8S69ez+9/2H++bf705fy9EWfv3Ar9O1F17J/e7XSspu6Shavhkx5TntpPWMaRE9Qa1M3AlTTlIAzNjM1ZRYgUjNTAORtK2q47w1c9q2qM8lUtlKr1uLNAqUjIHZTH7oxILRmAC5CrTUXJAZkFOJaCyHmJWlVAEGEp5fLPC9fv513tfNaXi8GpMdDI3Q3E5bomAzoumtTcvDECXgxN7BKDk25mldV90ZgnJJbbq4GxkDgrbUdvAkjwmZaEdh5Bk+O4o6u9Xz9ammiunFt2YGFJE8OyXxPxIQQ18cASDKCEnJrSokIOCdiJkCfDhm9TpJPd4fSLtdLzTnlhZlROU+vWvc9tom0WsHUVRFia9y4d0eZolXjEmBKmPj9jz8wy9PXT/fv7q3hE58BcS/VXY/L5K6IVdVqBSPI00yZFZBYCEwmtIYZJ2I/HB4+pB9Op7tPv/5y3S4GTcHQBWACUAQEMASe5e7d4w93h7vleKd7+d3f+bmWenl5ul6/2P4rwHUy+/Hx/Q+nB/Farm23VpsWq7VoM3NXN01sCk2BDzlNc5pmSeLLcUqZiQEMWTBlBodh/+kODmiH07QXXq0AuiQkplKdEwpxbYaIKZMCAKhMTHPiw5TmSZYpHed8OqBwkGqxJSPc0CBWCnNUj0MNaMyMN1UKkoNBeCDbUHxHXOzAQZ9K7ZSnA7xFv1Eq97LxBiIPXKdrYLDPq464OcjhqGIVnIZbXDDEHZ8B6HvYG0KAJ73mZRYH45hzd2hqBDEEYBiSQ+/0xY2I6JFsTNPeju/th2FR7m4W0lUaSBX2CeERCuOmtTHYhABEYSLAfRI3ZhE4tKOdFPZOr/a0xMjB7RITIUiQHtFxxBplwrFep7cwPj4ekLl/szcUr5/83mvEdRmpoCNGQQx0t7hbH9H9uIdKc5AP3X7PAMl7Uu4Xt7MOHSaDvofBvU+PvOV8j1VeYKPzoI7BdROSQZSDdSjA3a11B824J9xMWwNwlHT48DHdndLD4/nbZ7u+4v5i19fLp8+XXz41Qn8+zwkrX6m6MbIICSXOZS/NVE1zpqYOzqU2iiNhRkZkiB1InQjvC4Mk2qimjkiSplrAHVvz1jRkAu4GAMFQSWJTQyBJErI9yTJkFw7kOSdJXEtlhOq+pMnA160uxxm2Uh3VXBUva2X0xMxCBkAih8MMW9t04zxTmpUXVThfr5s5FmayuCeEEkJGyhINgJGDMTLhBFzNmluLZO1GxMAMW72+7gLgB5nIpZmxNU5ZrQBJM1S3g7BpI2Dz5o7Fdd9XIlfXfDpME59OmRlQHdlYgJ20tXQURlAwRgLz8/PLflkP8+PxeAyAxU3HvUfuLiJBXKWcWpOmBkTL3b3Wa7mcD3eHw99dJvl8ebqo7pfzZZrnutcsSRlB1QlckiEi093DHc65nl8Pp2m9Fix6WJbDgd+/f/yznz/++uHDy9P5enlZr9WMgEjBSCzfT8z58f2Hh+ORnZMcv/7yp5fnFVH/9Mu/3L79cjzQjw/v796/f7w/zc22y+X89GwpNSd330td9yoTJfbaihvOy0ESmjfmNB9zgD9MgszEIXoPEYNJomTiioCADMSYJqYGgIi1MWHVFiQaAggzZZI58zLJccnHZVqWaV5EBIBUXRK6+XB99x4yzMMkhsYgUTzab/OujHbDhUYFCeaG7t73RzlCuMIMK050dTRX6JWmeYhnEBCREUIY72MUN2JAjx+3UhAQIu713NDTD3WK0dXe9hAAuFvgzCMYAnowAqjujDgO7k1fZDo4Wx88pw0/nI4P9bSAY+MW9rjdx7si9nWYKk6CwxhR7r1LX9iC3pHMvjhrGDQAgAMTh9SKuTMlwIgIYKhNJcQ2vXELdda4FIPxhSFj/U6AivjW7IAFSt+hN/eemsbahC7T7KHa+mob7ImtT1fTrSgLvUw3c+qjfeMGwVvbAbeqf7SBMaM2LuVor3AcL9GYLOlpFMJHEKNXBYBeHiKYaeRg797fkg6nU57nh3fohXS/Pn2V95/w+Aeaf9mfv7SnZ8+v+5cXUzD2CtUMN2iEqKbIAgjqiozqNqckh5SEpiNLQhY2h1orogGyI9XqzQBZrLmbtWZ7bbWZI7aqSEyJVd0cWAiZmlomAkQ1c9eUw0/RkMCgySRIPnF296TWSksJzPnxbs6Jj8djM2+tqTYin+Y8H7IDAsm8LMfdTZ+ft2pkj/enT1+/GuwG1rxlNKKshgiCxO7ejAwzpOzeEEFdzdEdPDEAu7qkhGbNquS5aDkXceEZknjWWo85LfPcuFpVrXttzsRm2Mxra8CchNBtOSyE6qUhSCuFcBcwbRXJZJo5iSNoVWuoZsSg1hLz9XLZ9s0RtBRTQ8A+UONOjAB9JgiFkHE6LG6Py8d35z99/vLyB2Db6/bt6ZO6XJ7KlCYCIRJAUod1XVM+HE93y/F0LTXJtNedkRQNW0Evz0+/3j8+3t9Px2nK8rv1UojT3iAf8/LhtNu6rxs5H2T++uuv53J9ff7b9vlcbb2+PifAh8P9aXqYBSaiup9r25oDIbl6K0WLeph7MU55niZxqKVUkYQ0SWIH09g3wUiMLBwmEx6YlKAj1drcjRjIgJLsW1uWuRZLjnvRtRQ1R4d0mA7HyRNT4pTIrbVa9n3l5YDXlUhSxu6YH+PziABGo+KKwGcdn0diDD1dEKuxYAo6P9vj4y1+4hjU7WpuwrAZu8V2xCBy4I2T7VwBDegFbhhQANcwikMYC13cHInMlVgcwwPO314E3UXOzcIlgbhrVczN3IkJHcAwCs8ILf6d91zwrLc84eBEnVwVpBYI0nehPyCiWJN5o80p/Hni3awDZW4Abh31UlcIHCNOdI/DwebGZFsH3c0QUTrZEfKkqNgHotKx9XALGiRtNBTfldGB5PR2J4YfbtYc2HW7ff9aVF7jfvC3QwnaHW4Jp/dveHPg630Dmjt1RdWNFhmJF4NLwUF29z+LNmrMkI11PtGGuINaxxJdkRgdzMxMmxkxIjFLioJaOKV5QXBBS6fHfPd+uv9w/fib+vLt+vnz9vXb9vR8/fatbasI+m68iginalYqG+jqxHRcFhbOcxIhWQTRjV21Igkg7rsiAxADgjqogzkYUNP4XmhuoIYioe9FYnBkQjPzqknIYyld+Mr1ZeCOaCLkClorox9mSWIt4+EozRGR97221jgzEh5OMxKleSlbJfT7h6Whnbe2l7NBK7o5BHouCMiYzSDsm5Engckwi5vzAX2tfm1ltXYGbkJHQ4NY/dKSkTJ5w9xQhEDb1szFECgBomLZTVNY+2Il4qaF0ISJCdDNrZbdQCvTNSVqraLYsiwoYgCt1v1SJ04+J3TNy7Rervtey15Ka/CG/WF4phKTO3FKkhOgkaT7D+8zweu318u2fnn9+vjxuF4/fPrTy3SatmtVN0mkjmsr1VyQmaUU1W1P00Tg5+trwkwAqPr05dP59TVNU7now/0PpkAzf/jwHkQvl0+v53OrZht/rvvXT79326x8s/I0nZYf393dz/fvTqdspNt512rWFIXm47UUbb5uBcyMHAFYEicH8lLqsrgIA1gpqzggmbuw5NB0EqFMsRczhmMdCZnQkyC4RvQjkEQGVNTynMicUmKBsu8MsCSZDjOg11a29SLLMS+HWnZiZGa8Nfx9aAvcuz/BiOLgDt484AuA4ZEQURaG03uHkB2HuLFHusBq8Kbv6LGhF5Duat7xCgjRX0ePRwzyAcr0gt6hM4LIaLEzrOtQ4xnq9jM4Gg5HZCTvq7sCk+0RCwH6wQ+DNWaKifGBAUXdOyKe9eh304UGjYp0YybcPVY4xxeBwVRECetxKN4zPGjTgHeY8ZYAnXrxHaEyJm2jJ3Tv00S9a0CmkXIxFEE+KBEACiPQfmVwpNagZqzXz+6djx7Dd6PxizOug/ENnIa6SxxiN1FFpAAog0j2wcrEIzscV4MWuLUckcDwLXNENTcIIhinNs4u8o1fiDFx65sgkPsfgANAEomOEgnAumsMM3tYQxxOpzTl093x3Q/r87fDT8+Xr1/Ly+vx8gJlQ9Pt9dxqA226bljb/nqZ1b20NAkh5jkzAxOYadES27oQMOWpmaOhG9Tq5tTMq/VjK6WZYVNtXoXZCQS8NnVtTNhKnReWid0QtCF6Uyc3JEOh1ioDSnJDTYKn+6nW6oAkyYFqaZfrBoiUKC9TnnNelucvZxZS8GZaYTOC9x8/7trK/upeRZuTWkNKok4oiWAGSAiEQArsXgEyoANW92soIeLKIGUDbI67IWcmaInmCrs4EOSGjmmu+2u080JQdSt6FQLGDIiABt7QIZGL0DwnJ5NME02OUFvTXbXCcncsYLUUBDSwl9eXy7q+Y0Rm1PDTsl5zxToRxDzP58vL4eEusev5vJe61gauKPTjz+8/PL4/P62fv75eL83Bt7YF3CcM2+vlWr9B8+nuLmWak2gty2lC2tIsblU32/f91795Mch37+/39fx8/nXdXlhwO29lK2Z72+vhwCmbU354f/xv/Rv/+uXrKrvuL+u2bjtjK7Xu3tyqeTNvCo5Ahow8LTklT9mPR5yyM2Hda54RzVMiM9v3Qkwpiao5gCHWUoUTIpmptkASwFyJYoLQrdWUgJlZzTkw6jbJBNhau2Kjsl1nfQhnxDxN7jdXMQMj7Ks341E3B+wrt3SE4EC9xxOOsSAPAMCZOeJQkMYeT31fStPjSbx9QHo0YOEeJQHAgYjdb3OzMJhJfCOXQyQSu736C3oWoUGe2vdRkd5YSQRsqr0WRerSdASAvuQdR3MB4y9igjGBcAvJsT88inE3Iwz3JNAwYeu18hh7ttvcWRcTCXPMF4X/seIIu9a7qKjXe6LsgRYRxtpdcwkMHhGDr+lAfHhF96gZXy1WI94El306F4bMpudIRDfD8WH9p+5DN9QJBwtP6kj4YQHdj6dTPNBp90h5vd+4UboxzD0Sug9iHaHbscIbWzSayX6QGEAcIMJwIR98wLACxxDQdOKF3FQVEYGQus42lAwsaTkxT/l0137cj+fzfjlb20GL17qfz/t61X0vT0++b/u3b8kBmmlpxLSvV07iprpvpiRJrDVwdGRErE1VyRSaYW3eFNRgL2oA6gDBxqOjd/seBiaifEosQAnRm6txeOu7t71YgyknMyf0aSGZRARmZ0eepgWIylrl7KbmgM0KAJX9PC+IBA84XUqTvTS3OR0eD/df6x6EhCRCI0lLTpMBuiJ4A3PT6lZNm3kFBUQmywbmqODsAK01koQsTQvg7ChAs7V23TdGJyOkTJLcGzFtrbiXnKiVKzCllO+WZaK6THx3n3POaWrEZg5IoKq1oTYVmWu1YrrvBQnUWtlXB3MPAU5/hpnIkYhUQBpWIhKWd+8+WNufXy61tLKVfFxOCyZL7dr+5fX3OfEV2l4KCzWrVlAOB3Ysl4upp7SgzCyY8hGI13ohJJS8nOb3P5yef/n8+rpdnvfXp68vl6d9fZ0zed1n4buHJb1f3j8cXPfL68uyLL6vrGU7v16v214NNbmympbWjJOCgnDibK0RIrjmJCmjCOYJU4IkxEjCaKZqigpAUJv2xwgxpZRS3rfSmkbrT0JQqxOWVlQ9rKAdPTb3IoOaN2tiDVubckpzDpAPsI8v9YK2ZwGDwXNCPHQOiGgDpzXwCJzj9Q6ATOgKaDhYgmF/NrhCHMIHsxFub4FqBM0eLtzw1vj36VW8VYM4UAr4Ts0YccfHpl0zJ6YBg9MQa45p5bGry78zE0KEgYd08CpODiI7dHgCBq0beEPcdU21b+PqVe0tZ0UV64hkbz4QIZ1BC5sqBzCo1m4VcZDSsQwSu060C5bifJoaCyOhdNt9H4uKo0yOKTIER4q8ZN0Sr3+JQOJiImecjjGs3ZkMwEgYkT3UgYahtfcvNc4+uSsB+W1IzkbGiJNBgDcbjY6FDIxr9ERdAhDfFvutgGOVTS/zsCsjB+ccLrJdHNCp75GqqRce3XqWkGNxXailAKHWikQuog5pnpbHR23qVhmh7GV9PbvXcrmW5+f2+qLnM6wrlGL7tl0vtM1ad60VGanIvq2CYu7WfC+tFQAUAK+qe9FavVQFZjREag4oSeI8N9eMFF30lHIMamoNlMRJnVKvjJB0nsVU8+LpAOQGDGnKVqsqGNZpgVqglIaMasUdWdIic4O6HNO85aKIdD0dD1rvX87PagyuzFPCQ6LJfFffVauVot4UTbU5mSGDOQJ7CBwgKTqCk3krF0QrlbNwzhP5Zgrm1Z3FF0Bxt6YFjVszJDvOJwZPTFOiJOTUQDgfU54oZWNBd2TDfV1DGHNZL+emGqGKqK4bmNXWqmlGBoAgA/r6KQAS5srMOQkpuDtoNRA6Ho5HyT+///Bf/JN/zhOlBHmW9WkDUEl5Od2hweXbV902Q9tfPyMe58NRW9nK2ll7dGE+3s3H/MPnP3zLc768XKWlw+Pftet1ecD7eXn/432VzfyqBY7pwRxf/vS0ns9YraxaqlLGUppV30oF0eVwUPRMjIlyTohurcV+JzXzopwyMHlfN0dmLogs7Op5ota0qu579aHZZuRmnqepNjVXb2ZmzRyJZWID5Mw8TzJN05yn0zKdlvkwTfOcODFzykmSRGxmpBGe4QYkOCAxgVnHduFW9Xvg1x3VAUOKJa+3WD0o45u5Zvx24BOEANhrtG4uBjeA+4anhzw96mjENwWOW5f9B4BuAEBOjuGzy10hEo4St9rbAYEj0neApCPKjATd8NnNnAMI8+Ep524WO14CgnYAYCDse5+w17gR5giJyNVG4hp2Gj0nQMcwoAfBruBHZEJVw865WoBN8aOYXEFHVeMwSHaXzt0HTDcQs/A1xfBs6u7V40HpRzQEOZ1AQac+YBF3FA1h72BeRgk/HF2j3eq4GNBtFedNXzS6KBi6stEdBJtBt3bwdkTYY3vv9Sj8AuNze0uF2muUcMTG3rVA5LXh5hGXHwACUGQkZlLVyKExGSgszQ05MQkxsYh04yaY7+nwUBFd920/n2291OeX8vrczq9QNv306/XbJ0gITfbX13DJr61BNxRyc9JmqmjAwORqTu5N3ZBFwrvYuuIC1d3dCUndrKq6R+0PQMjorpI4Z0iJJcF0EhJPC8zLpE1lYvS0XlZOUpuvl+oEl3UTA5nnRJTnTCmdSzuuyVeVCYRQ2+FyvaqBgQIxaGJLZqVue9HValMxc3U3MjNlgOYQBifszowIKE2VvbHsaDxPd5NgLa3V5mCuyAnRCYDckIGmNFvdyVEEwEotJQmJZNNijeUYmgYw8FbMzUtTBNOirVRGANdt3dIjNPNt3VTNuZuC3R5E05gAiudOGlJrBugffvvjQ85U9s03Wfj0cT5fr0tDrXlr2+GUHx7v19Iu1wu03YlKWf253B0P7k23a7XKeZmmTFpOwgr1d797cPeD+MOcMqRGmTK/Ox0o1X0/q277da9lPy5HR6R8LGUFZIB2OV8cMKVplrlsDbTmsCFGCIyS07RfN1e8e0jTzOq+7w2IWdAciWlfmzlaUzZG6r7GVlWENabJAJ1AhFXdBOq6k3AsTFBCmiQtWQ6ZJ8nHhROnZZmOh2nOIkI9utxKWLr1A7c4ECOlYUtlcMsBMAb5+z/6wCcFRvSd1eVti0kwrzAkIe5AsYMFcFCEgTTEeC+4I8auua4ogZsYFdBj5A07nh3IwvD9x+6A7xAvI3CnXgfD8P8cKpruUhNRIqZiY3E6DFaVvluPOUDvMQ/sMQE3ugOHvvQ9ZtwG+D/MKrBHreEpFCcPb4IiHH3GbeMidB1R91vrYD5IH33ucv9bNMQ3bA57Gu29SWA7IxPEMBsReixv9u7jcMurMMC3IAuCJ8Huw+Jxjan7VNxkm50GiXIfRq0fGExMgQT3MKgIcHCN2E29O7gxCPH70ZH1dWPjpuwdxJDP9laGqNcFHuomZCJwYOTvdgMhAQkxMKYQAyAiOjMhADMzZ0Lg5VSPp3q9vLJUBEdoV8TTXQaF1tq+VySoO2Smql60Xos1NoemUJoicWteVVu1MDDJwpLEzRUaYSewWYQTxwVXNWKTxGZKUXsmWo55ziTi84nTRDIBC040hS0+Ur6cd3WXBKUYmZVtRyQQnxY8HJcf3lHZdL++oO55WvB+3tvj55crNFNYL00BDqqtNmsGxm7uCs3B0BtAwshTSM2VugcgiGRmXPKUwcmq17qkKS+mdd9LVT+rqghGPFgST/MBoGHb5+mEtLtbnhNSK3WvmqZEe22teivgwIdDdpOt7ZPA3eGEDoycU2611VLCwpaQtA/OGELsJScmznlCQlUzh4cf398dJ2+bXgwA8uN0ancPay37iz/tmeHjw5ESv65X1RIw9CnN1ZrVkqdpWRw2kESZkUHL9cLkiObNTkty9qdfv5j58+frtyt5svk4VzMtVi729Pw1H45JDpKOl9eX0syqAzEwMFBGmoBEkAiJaZoYWV1VJEkXEaCamcVcLWoz01gt4iLsvdohB2tqrTYHKnsFJACCuNMNjnenAIdKKzxPaZ7SMsk80TzRPKXDMR0PlDIlQcK+eyOKXCaisHkbUDtCiGqiRI2HLaaWbhNCN9A8AoI168hPuDNAD9yBa98UikwUzyp1PSF0VQsAOFqoJAOP6AgumA3SuSs6R37yG0CPQVybj7XDgIwdq8IBcXh/bXAA3byzJ7BuDTpw/A7qxOwaoAcP0rVPTKR9o3EPZbeEBN71NxHIPBbXDEQfEBDdmgJRb1bMe6Iaa2Rs7GiJLoyQvEt+nJgdVHqRHaMYY1wBh0pnICs9jIbyKTJXMCRAnX/3ENq4u/ZuroN3N4UQBCjUATkfIBAh+pvj9K3XgRvaBgPW69cGQvp72zEJby9xcHBG0rCI/s4ltF9p70mi56dBBlnfrdzVtf2Mx2tjaKjTL4jdlbtLbhG5+1eBO5E7CIupu9muzVVbK/t13wwu7g1pc/fDQSYGVawF7o6t7LCvXveXP30BZBCDTZGcTLe9VVXVTsxAAIqRQ4kUAIWgbzVnIjc1QjJzbQ3AjFwyTrPkTPMxzRl5ApkQ2fIsQlRLbbUC+bIIaIXMuntBVnVSdHAt1ZUywWmevjFtpXGG+w+nNk3K+PJayn4BvL7UF4KkBtUVTQyKIalfBWcgRxRGcBQ0Bsox6Whmy3xMiRKQuGdBWIuEBS6zgzVFZxRMOeFxYW/rYV7ydECpWWReMKfECet+3VdnYJkYFIjJESBlpMRrWyQxc5J8PCzgoHshYrfwu4WY+8eB2zJRlsl02+tOTKfHO4R337QIyeGnd9eX18O6l2b56SUvdDxJXvI8kVqDss9MwmnXlsgx5aenL6d376a0pGXhGVjaZV211A/396Xt1/PZ15pTTke5vFyc8I9/+rw8LPcyq1G5urdsxS7bJUtrtUTPnkSIeZlnQdp5a/tOyChEmZGACFotiVgNwRGJEb0WlSTSZ1YhVgTE9qHoaxs4M5uaqqdZEKQ13bfi3it0Jy+15mmSKc2HKd+fIE2wHCgvOC00LZQmlBwASpBxFCsQu1+CR5QjpOFY2OGIztINQrYzgt4Dwqgk0bt4fsz3YEz/WBTdBBByextIQySPMWk78JMImgPCQUQH7LzxKDD7K+MOHAaeYWovkTlGfhpOEoEUxMCRRlk8sC4g8Dc9SdjjQwSN3nN21hQBAdT8tkdrUKn9yxoYAQFCGJGOsjWyg4VeKkhj7ItueoYYtS8AjPhsAO5NG1FsZ0ZtDQClJ250gFjKc6Pyu4t1cJ63CNv/MbIVDcKCOpgYYFOXfwUHELcDdGEPQChqAQgpfLcjGfb1nQixRgK6dyn0EB0O7vGp3/UFQUX0fgURe4aHW3/g1m/HzhwEake3jcNBZ8TPieLd3IkYyRmiaXJCDmJc3QEdqa+RAgQMp2UkAnI3UDO1Zq3Wum2Xdb/UfSu10DLlKfHhyIzeKoCW9Vq26349t+vVyn633NXXlV/X8rrp66alAUMLez4igY5LemwVqQaMzNhMiRC01drMFMnyJMAoRIgtJQZSYk4zEVuaBFglITMAGguGGbIz1b2ZITqICACgu+71omdMtex+eT1fXteXzXPx94d5Pky/+Yu/mJ9ef/305fp6NtjUpobInNAeEa5uL1P6AZUMdgJGQMKkqOikqsjT/d39+7u745RnLZkV9suUiR0h8UWvZgBiiCCMxFDadn+cme3hYW661v1FlcCXlEgbEQkAoZG61b0BJknZHFGIRSRNCOBN15dXLwXcwGKEKLbrBX7qzELCsJdGCCLK6Rm1gC4fH/MkD4e5mD9v/6IxTKfMyeYjIcO2r60WLM3WTShJIjY1sPP1WpqdHh7nw0KGWPZ6fsVUvmhZ5iPnZVN9vZ7ViYTLTKf3793pfC7MuVVZ13MiAeZ1K4TGklpryzxrbZwBVJc5QyZOyIyYHNFaa0TQHBKKOe7NxZ2T1OquhQiJMTuygoEyx/SJO4CpypSgKCOrulaXKSMwIqtba5oPsyPKnB3JANOypMd3crqf799Ph/vpcCd5SikzIkI87zYWdEQ47lJ0Bx8rbZ1uLGlvwtHVYh5+uHMgMWMMsRIheLOOa5va2OEAMDZEEYIpuFlsD7ypRQDHWsOOTPuN3exwU5iK0ih5Y+NTlIsEBNRdpt/iu40JdbhJgqIs9kERxzL2AZn0XV29jjb0MUoWNAYQBXLQ605E6DAJgscoTIBRwEjmzkytaR9l8A5mOIDGMgBAD7OGPvYYtSwiETG4OXNvKEKD64DS62LADs8RkZCqYhfe3uKmOzgjuAet0SPprZJH7ChOb5D6ws84KYQ4BJwIAKM+H7qkkIPdNipEcu4XZJTe1NsBR+yJAm8nscOCcNspFrD4DeZxCAfa6FLG62/ar7cTAKPvQYh3AABwJummFt7hsu7uZODa78aeKl1V1dz2uldrRXdkzMucD4dlyYIkzK5a971p3S7nVrb9em7XS7tc1nzc+AXplSmbwXpeAWE+Zd0djF3RY8AiZrjH8hciVlMA1FqJTKRPP+RZchLJjq6c0a26kJNOk0hCScTgikCMAKwFUuK6m4gQOFg1IGRpzRCU83T/7u7DTvr1jMztuqlXTncP9++0ie627+eqO1JSBNdfWZigkSakAxkiSGulQXUAgEKY7g93P/3w4XGaDkRQAW3DRIsQlGqA8yRa1cyQAKGC22maPry7mzKaX+9P0+df67a260Wm6TgvRyR1h6pQdy975ZRy5utu1VpOExFfn1/Ltm27bttW9s1M4yqaKxkCIgESUoCDhHQ8HL59vVyvK+d893Cf57w+PX19fuF53rdCs/zZv/ZTfS1ffn1++nxujZd5uYrU0qaUCF1byyKqbd8vkvxuuS/VWHzbz3GTnu7eObFz/vLl1chaNaFJMF/WZ3dFEE6LujuYQSUic5+PR3OdZr5eX+c0pSlbRURIU1Ks0Dk8RWR1VEMoLjNvu8FakmBKyEIklJfJ1cyMgc1NzUgIEJGpNa0tnlBjSWYAAeZkRmFg4SwyzcvhmO/v0/EhLadpPuScmRMjg5uBcUC77l1IE+Vuu7XdvSaNKdsRDKATtRbVq7mH8rKLdKIWHnQgAEdsiVAMtwGkG1vZYwQMnckbIxD47TAHi+hJ8H2xDF1q5ACI3n0kb3hLj+8engvOTEO1CIioMYDsvY71GE6KUQYak1U93sCA6MOpk4YgFgN30r56JXQq8V+dV3d3FgKDmOrCGxcwQKkOmEGHw29RrzcrYYGMqKoBM8kgqPtRupvVPnIw4JoeqTt1gIPIcBjEeqQu7JExTnEU86MJsDFrG3kjwJwBkKGPmGu9qge4DSwQBnV+uz7BPN+aEozN4NBrfwxmH7snkHcsLU5ivwQj2cLt8Aauj66B/EC3XSXAWJoy+PEA0dSMPPS+hIyu8bh609K0Nq1qig7LNDGLpCwphUwlMulk5taW0wNY020v63l/eT0fvlynr0S8GvBlzYfZvWo3emQtZhRJBwFMRGqtZSvTkoVQTSVLTjQtyaykTHlOwpqSL4cswsSuWgCTI1LKLKhVmzdgTszCrNXKVvMEafVWcStFUZESQs1Jlll+eHgUnl6v6/Zyvjad74Sy/e7jj3fp/tPz5+dv3/Z1RaaGmyhVN/OSzAsQAO+gGY7H5f3hdDrcHd4/3j3Mc/bm+2qtNdvItykvwvlyPZOW8P1VrUgKqsKJ0I/HzPlO/fz4uHz69PttS68rvX93mucMrbhjq/v1ut2/OwCDCKpp3SoYBCmTUwa3VrTsxY8GPLZNjcn72mrK2dBrLet1BZLj4d27h3tHP397TYfjD3/+29dlPie+Oy7tdct3v7b2x/VcCfzuYf7660srTip7K07kSPvlMqHqYV4Oc8q4risAVN3Ol2/ETJSWeb6sLQCMWgs77XtTN5lnYHFQWaiVzRGBkxqAEBRtrVlOMbVkAIBc2g5mSTICm0LZlNiQMGXR1rQUPCWZ2EFbaQa6HJcYdjJ3SlK2EqNWjAhC2kxbdUNzACZzFUr5kKfTiQ5Hnpb5cJ/vHuVwEs7CmST1cjDad/fQ8PR2OwSIPSB3rSH02h4AuxEmjPmhUbJ5x5m7ztMZY2NwCDug12K9j4jfxVvkuVkhc4zidwhoRMmun8HuDKMwPD7Dl4wQwbRX3gH3YAezMLqFbo8PhkDhKmh+07tj2AgQEfKYavPOhKA5C7sbIlmnuFFD9tPtGBwRRaRTeqOn6UO8N0wdu/MSQBfjgIOpxbAxDrwLY9TY+7VA6kaZ4N5l7u6xE5jw5rYceduUiL3zGNjz7C1hj2z5XXaFQK9sBHno74UdbHd4Q+0HYz6G4yKRDJtTv4X4LicNPt0VOv5u/TLeUtftLbF7z41s0RNub4O8p+p+qA59n6XfWAIfPwdwNwIER3NgvDV3FGjU8BbCSAym1dxMzbypNQRcpoWZWQT7jdD9uAPuYhZA4oSgDRO5es2eDo2uO6SX0kzNOecJEwNv561uLS2yb9Xdyl5cHUBipxULIagZsDAKqlViYKHWdmYCcMkkgiyY52lawupHzYlSzO+ikFgMpgtOh3S5Vl9bVdjUCFUmhr050LzMP8+P5Q/b+bxta6n6vCzHdErv3r27v/v48v7p5enpfHna9rXtzRqCZcZldkrp8Yc7+fGHH5bTIgSSyey8r5+aN1Hct1crl5NoksOcs7a0PV+jEAFo7gpgZqqtbpsdBOdTJjzI9Jvtumut67oux5OLgDsyO4CTm1mphcncWkpUtmsr++H+QRJv68W0Mcd1V2RBBzBQbUxMibDVbdubtePdcZnm5Xh/PZ8B5Oc//4vzt29N9fGH99Da57/+4/HH9z87/vq3n/erLc6nPVuF61agFGMGSbu2F6jq9S//7u84Sy201ZpSSpNoc23VDXyvaOoI4IaEaUptbfEwq7XQdZP7Wq4TcTXPKZGDNu0d+7oiY54ERZZpqVZ1LVrheD9pg71siZ0TITMyALqBSc6c2Go3iozJ0XA38CGXsHDCYRIm5ERIOc/AbMA8HQATYHJnd+57I7FHAUIEAlR0Co1/VHVGSAho3RkC3Q27C/wNmoeBf4/oPUq8Xv/31b0QwfdW7N/U4EAACjdwOOLKDQry7gbt9p3JRERGHxQAQngWuY/95B0s4cAJO+KNSNiN/BAxxs2sl5cBdkUg7Sy1B5cRiPEQwsYeOiRkVTO1m7UovsXZG2Lu0COQ41gthoSmEa+AKAaYe9VNhG5xRQABwPoMBN5AChgUOSACimkwMzHx4KqGhITSkS/q4iO8ub+5dzXOzfkPAG4zeD2d37yao+LueyWhw0QOMVnWC4OuuMIAd7C3Zj0kBxRlHW8iGIRzb/vGSe7XsB8NEcaMNo0dBgFChYQoklO8vLNDDvgdj+/uxATDqkmDFKNYldmxKURQNQRvbq41sDAmEp6ZmJMIslOkIQwMqouN3d2gFWtFt7LXy7lt2+Xl+fL6en55vawrZKYlTyzobLVNx8N2uZ6/nWXi/VpVWyzSmaacJnE2csgsgKre6l6mzKvvx0XGeGFzZxaRLCxM5CySEhOBJGm7ttZaNU6SF75eKicGJjPzBpWRDC/nFRFPp+SOx5zPhFl9+/IKi6+v2/GuHZYfP7z7+O7xXQG9rNemze3ATWaeZkrHdw/zkk6T7Ov29csfLq9P56e/MfW70ztThLqD7iaGXeAGwgndHKyZMbsYaC3rasvpeH69fLy7Pz0s08KHw1ZLTYlD6YEEIikvmZEcVHVP7POcBP1Pv//9dr3OqqVsc6vknf/FIZzoLSZTbebotZXlcLznOwAEpZzmdx9+LHV1x8Pd3ePj8emXP237ThM11JZ0fyntVZcll4u+frm8XPlciyRm56Z13Urb9+X+dEKE8yWJAFqeZyvbQQTmqdRaS7Pagi1fDqmLVEia7sxCYcxrbu6GwsSc0r7vboYGUJV4nrOs+95KIXQ231es1SkBI07C0zLNB8kTEXmaJPgxcMfY04JkrggoKbW1xO3d1AmxaktJWNi8gRqCtdbMVa2hNbdm2jpEgSHSAwwtSS+rENAJ2QPQe6Nhe2iHNwgZ3jDfQbiOWOU3vDs6BkTqyEBEyhtiHNFk9BM+wjo6MJP3XQNdjjiULj4K94FIeFf1YEBGt/SAUaeCD19hHoBRvICJtd+KPdKpGZhF9EfCoT4KcVR8OSdEEr6hGRHfB8COQByIJY0JKuxmdgNOdwQExm64HS0C0fAIGu2LGYTuHxywMygEBuAWEJBj50OGwrST8zGS1Wvdt3AZCwDwuz98YwUG9xsze5H3BrMRlwwJXB2gI2gdmAkFDsbUHHmnWm5I1FvrEFL9yAH9UenZAGMtZ7SefoPHbmh+H4zurE4/RwAjUdHoQEYTgwDQdyTcOoTAnMBBtVlfTuXoSMSSaCwaREf0cUqju0PsRoxmZqpV67av63Yt17WWrZoVcJ9mPpym43z38Z1uhRzL6/Xly5NWwAkBYT7MeZq1hEMSIqFWSxOpN4DGDATYvIqamgOlaLuJUvTp0YgwAsYWmrAWFGBkQ/NN1UxVAUCL7pcGybU4Cc2Z3dtxOcpPjwKY7eV5s1X37fzq4Pu25/2OsxzfHX/z28c8H5hmVJwxnaaTgr9++fL8+eXl6cu3L39gKKLXx8cP7z/+uD5fr2VbS0lzzlM+TcdW9mnO7sRazdARvVQAKFtttdw/HNtW84mzSE6LNWmm67qmLFNOaca5TuqNGE/HqbqmZTZQb5uoteuFGizzomoOxICOFHeLmYKwe3QaKpzu7++8qZsjEpgnmRKnH3/6+fry8vL8qRicPrxzoXMpuW2Hx8Njvnv95Vxf2vZ1//Kn17/55cvzth9PdwWTefn2/JKXPC9zOMtcL/uuLcm0X3ZiYmUldwJrBgxJkgIqAhkjCjmQgFsFcwCpCpJka+qA6pCQVQ0cSaTV6m5IRELbVtKMmTHPy7QIMy6HmcgdFABbMwt5IEBrXkvrWLcbAKoqi5BrFHuOYIRVDU2nxFVraQWtgFVsGxSUCE8IIok6CdfnJ0MSb8Pwsk8BObhbaFJvAdS9u8fccIaoOwE7Ch1Axqj0vAMW/bkcGhMIutEJ2W/eDBbdet85E5ym92QxuoCIfB2f7x/iA8fQPo2FJJ0KfsOocSDGiOFyOgLnmFEYcpgYMvWx5HbErRHVoKORY9MAAKKZeq/jPfCfW3vQUZm+nGYkOkCPfitiap9ACNK7hztCsn5d+xeVfggR+26UL7xNfuFA2frpGifOx6/h21HEB/Z0AQBA4H0DZ1T//WzfVrj1Q6dB2MIY1oXofTsj37uQm3xzgHJvmNR3NcN4ufXxMxzcfz+owVj0nDLuHXDzPrcSQ+pmFrPZ3hPt4JfN4I1iBiYiljctcNzMwUO4R+Z3AHVTM411PGBNjSjN+TTdz+at7eX+3cdW9vaba9teoO77etnOryso1wWuVxfazysDiuTMqey1Fa2rEsG2FiDLMxGhJGFAEZ8SpUScISUmjjvDrbmTc6gZtLfSWtUMzWJSMB4CEmJ0b1uraGmZK4FdNgI6zsvPv3kU5OXl8nQtq/G1fFPXl/NnECmX4/k4T9OCzqp8XDKCv3x9Wl+fEqK2Zm09nnLCtCTI0Cps7NXqphWFgdiZgFyBnV0ZfV6yZ0vJCBuaMQKAuuEy51p3mBKAiXiMbucsJXGStOQkh+Vy3RHt/PzL5fn5en59/5uf2nqu27pdL+vlsizHqHbIAUQGWggAcHd3X8s25UWSvD49P387U6K7uxO61eZq6f79b+bDu7t368OHn1+evl6fv04oW9sg+XQ4OruJ6h++VqhatGn9/Os3Q/+z3/1WWJ6+vby8bmjJmgNkK14VVR2Bwb1di8xk2JxRKDu6tRrIriHWZjlnIwazUisTqUGeZpaMxIDNDSgzomltE6blkJclpYR5ziJs0NywNjUza8oihJwSAEorzRX2qjZAE2ZxJkoJmJtZkizzjJLSNJNkSZmZAlwwjS1SaNrQGUJiOaQ1poYjYnexHQA42tjp3ot6GOB7p0OdmAIi7zqcscfbhxPl6CKGNAORu14Tu5UzAiGNXY/QzLqe9EawRruBPfhqJ1GjAAaAIAI7HOQwRtR67R6fPVjSPhWGNyYDIy31DY4dwsI+uTf6lB5cgYhirKfvywSPPmOgYB2dju063q0hHUIxHyd5dFdM7A6qSsTYiXEEsxh/6Rzmre9yF+ytGMDNwsjD03VcooHL3BJX4Hp9hcrAQ24N3i2DO3SGJ/Ke2ehNbCTa7ucEQafcJsjgNobWQ3NPNDe/kZFExxTckOmaWghMY0D8+3pi3NgYPRQM1woE7IYgkdCwm5XSQBN7pxUoFb1tswFCIozNDLGNOg7ijQ6KittUTdXMHNTDHJqEU04kiUHdXcGhaQXwum9g7fzyVPZrenlqbvB8kWU+PNyTsYB4VQJCYBLWbNqqu1lVyZwSSsJ5YvY2HWQWQKrTkiiBWq17IxJJCRGsqUqLux4BhXkv6n0IlvIkacLjXXKw80vZa9vPDUGYrUjRrTjTww+Cy8yvuPvy9eVStZnW9eUZywWeU54TQnL3dcpe63Z+un84LJkTZ1tpWRKBLxOcUmncvl7PLABqhCAM88S65NfXF4M9Z0mJ798/MKq2C3q7nM9p0mU+5ZSZ8LpdmCHNk7aKTK62zNPhcDJzUz8dDhvy9vKqbdu3cynbn37/tw9//ucoeL1eluMBDXtn3ZqrEzMiTimb65wOaUq11nLdmPl4PIhw2VZwOhwfjndHYizX9enp6fGHH1s9//V/8V/uuH29frFXo2P++Bd3lk2VX172P336tjd7fbqc719++uFHU9zVt0tVAnLklI6z7LWCEkguxIS46c7IjAquhqjaPFAAFmIOLMgJFWDJU5qYBNXN3GQSmQWgzMe0nPLhOHOClCklUmuqraoCuKqGCxjhGKglWbcrDsEiCmVOimhEnCeeZ1mO8+NjenyXptPy8DAdT5QPiCIsTAyjd47GuweHUTDeHtnIDV282R9rAoztHR0A6iUnvFVnQ9nxndsa0og//kYiOCLHmim/Fe8NNB7Gap3Pjf1c/hYCR5/hN17ARywPTB1voQP6IwNhyzMK1uhyLOaCepaK9BE7ubCbUlsEbRoBzTteHykqRvrjQ2K0FvqboOkYqHNw7JsPVD1sgPvbxwRrVzqNtsmdQaLtGU3A+O7mwgwGciu9bxLJEHHCWxOEXYSEka07zhQTs31LZ4DsNMZ9scdQDbBoULVvPQtizOR2lj1y3S0Sxxe4qQJuQNO4ZXCMDX6XrDsOGbklIjl6D/wEyEKq1okAhMEw3zqa22BIlCTgMZ12e9cx5RbHR8j9Ct3mxa3zHOMuhn5dTc26+EGIiIWZickciOND2d1I2EFrbc7IhyMJiFaej3ePH3Znqi6Op+MCFUBtvZb1vJIgN/fWaF4Aq2RgcUDliSQTseUps/QOytzVmioH2N2qaqujRhFgBgAQkJw4gWSSCiiVRNiJHNtWi2sWrlanKa1Fp7vjMVEq4Iy6W0lyJUJwddP1ipzA/XBIaco23T/eL4xay9aoWGt5XuYZrJ3Ja85IOx8OCzOa171eWXxKsJ9XJXSfluPdnJOqredn1+rNrem+byyYhBXUDJqab2XK+eH9vTevRV/Or5wyOzz98onvT9M0vb6+ojm5qtd939Q0tqFi7MeWALFj5FuYxa3WddtrXY4HUG9FiejDDx9IplpLbSoTHu6JyzItH6rZtm9pni+/fPn019+uG/zudz9hSdfDKm5/+Hq23b/++mVKy+Hd/aFV9Svs1lZnxpw4MNwkvDOUVmeeDFTQN23iob7DZkDI5FS1bOsqCU/TMaUM0JB53y7COC0p8kae0nya8kTLgTmBaoWCQKDNhq4c960SOcRTDECc9q3keXJDYkEREeEpp+Mpn+5UlrQ8zMd3y+OH6e5B8gFYEFkkcXecH2O6fSDUImgQMfRBT+/TqjCgGw+bmhF4o26PkjniM4HHppcbWDASzQBrxz9HFd9LYrg91tilUm8x5IYTwKgAETA0h6MJvE0WxRjCoBMifobJKAJgjJEjOnSx/7AJcESM70JMtyq0m6F6TzIRNzBGpiO4BEfJTNSNjALvpu6GHXMVHPhEx+lx9Czm8fX7SpwA2wGHVx4axsIAChbjhnbJDSkPMUL8xV3qBGNq4b9phXorqr2TJ3Ewjthxl7dGLLJC9BCEo//qQbhPAONgeG6X9Q3c6ed+pOe4PIYBzpjfpAS9CYh9ciNSx8kPwz7EsfSYcKSZziT5d7nJ/U2A1N+QBh6FwEx9vHy0ij5wr5GHAKH71vUfgicOHQYIsQOaaUyW9ynfnp3pcDhc9w1RhLKnZcpzugdR52oJEZuX81rWAugosUYDDcBBiUGtChIJpczMzgwkKJlzRtWCFN/RomVRNe8b6DhA85wTmMMhWeW6NzBqVdDaly+XddsNVXL69nwOOTkncYPjnI6MPz3eXa/7vunzebVmBr4VA6SU8k8//zQtqV6+eSvYmi785evXu/c/iBjSZmpMcDosz7XG3dVaJW+KFWA9HYkFlgUTl2U5EM05NW0bkQGAVnUFmZIr1NLAse51nuetVS3t8nIuW1mOdD2XBtOPP9+9/9E/n6+nw6ns677u4NzqzqmbfXdqTqOrY3Aws1ZK2evp/mRuyzwTY21lOi3eEJz39aLGiWdPROiPH377u7+n7z++/3T8r7fL5dd//vTL/qttdMqnnx7uM0x/+/K8b3q+rIfHx9PdYd+LtapYGUSQUqJy3cHdTRF9yqLq7jaLgJMTu4ETIUKta2ttSiJMZmVd25RxszJNMk0kmct+mY8yH2Se0vFumWZQaGbatqi7uVaTzA5sbvtaoo1FJFdblgUQFN0wTEQkLbNMiywHmY7T8WE5PczLXU7HPC8GGL5YEIvIwW9NeVThFhX/MGePKtU7dtwl+fG4Bkgd1Go36YI3ZnHg7OhubsN/zLsdwK06D8g3cJ1A3994xC4D70U0dBnLaE4GNHCrkW+8wO3vN2wDO24Et4IVh3vPbbaB+sdHODBw7ztUEMC7l3B8uIH3sdzYL0adBpAwawiAapSYHlIi71JV6zNrvdkJcY0TIZqDmTOH3KEfJAHhWAxAFBAFAKLEfQ8YKtcI7tg94YLujIwesHgPqR7yoC6JjVgO7oCug8SICOtd3skUrqLYuSAbsfzW8XXCB27Ugg9gDQxCZkVMweT0LBqnPGbIqF/NYaga5UBMT/SNdO7GzB0TBLyBg9Fqjpm2wOMoXAI6H+I4KPKBFDkGE+CjA+i8MRkA2pC4uTl6d3BDDCdcoD7LFqveLcYIqFMjmFPSlMhayot8/On65duEsohchdu2ujZAaw1VWWszUCBnAiKUnPOEU4KUYUqcMggDJ0a2xIJkkgTQW62AnoQkJTNTjbDTzKBU3VZtKDj5+nwu6sWMxNU1pZSS1LbXUlV1OS6qME1TQpxTevjp/npZD6d527a9tCMgOk3LPE/m+9VrtVoOs2iFh7sfD8cjEsxCPPFhnliqexFmIYJW3RrY5XhkzjwllImJd1UFh/v7BSmpVURz130vzVmmhIjrdZ+nzMzuer68rJdVKDmoAxyO8+nu+Fp2Xtd3H94zYLleT6c7QiRGU0WOjrgRCRoCo7urVTWTLJJEmNOcrVYkIuDYTXa6E0czrVutpa139+/tZ8jLsiwPOT3Y9k//2T/8L3Vv98f7x9P9+8eTInw6X84v1+PxZTrku8OxvT4ZqOluBQlZ245AnBBbrJRycEdwYgBCRY0VeYQUXnbaCguDqymaKSERSmltnnBZZJ4kZ+YEzVuz5toM0BUAjUUQOVQ5y5xUrakiM/RaGAHMwDBJmqf57uQycz7M739Ix4fp+DAtR5lmYiaiELC7e5RHfWQpnpY+NQk+emLsMEsoxTuk6ujotyVRpG40EOnwbhvFZu+nfcAd8Soc5CUThUwz1rN0Yz/4DhToDcTb7BiOZ3lA+X1KK2aHEEfZ2utPG6VeEEejKHVwi4WJ3UKB+Dtjik5YQqce4C2ljX5k8NBdOwIW03A64KkBcQy0rs+FmelAvx2Dz4uGSi2CeQAUwTcCAIuoW/AfbuAYw9jg4BJfpUNuDDCWtkeRjDdErhOkHbe6nVziGMGAQKMQqWN2PqTyt6sUs9Q46mV36NvXETC6mrgNRiSGgflh1xF1YOqW9jsz0Ycb+jmKTxs9YoC8ah38cdPe4lmXKt0KeejXPW6JbrkH3tEgRKRoTkdtEIkhmoneyGLHfSjWEmCfbUYgCqM/HwN+/SSDMI/DRXPVZgguLDzPskx3p+P96bifL7Y+vJ4O5fX5ukzX5xc0Rfe6gTbARAjG5DlzmgC8gDf1liilKRGBpOTeWAhFnLqJXSw7m+ap1FZLrVtRKyBzPqQj35XyIhlxbw6NqDFrSrOqq+K2t6ZQ6tlMl9P88cPH5/PrtBVEEGxTgsQElAgIiHS7bufr+fV1mpKQeWund4fWGiPua7m7k5zxw7t73VZwZ/BmLaQmAJYmuHucRRyxlfqaEzkuKQuBqO7WfCubKBdt797fz4dlu65lq6bVdk3MiLhfN8KUFjLbzRQADncnEHp9fv74829qK3lazI0MwZ1ImMjjWkEjBcmZJ0FFYQEDlJQwTGaQiCSzm6JkTKWd62W/cJqODx/zfMrLh01PZ8gvv//F9t3FjezPf/sT/Onz58vr69O34/zz+7s7WNuFNzDUorXuhLGejE/HuZmta3PiYGsdLDGLpKCLJjmoFveEYJKlli04YLXC7MvhgGiupTXYNzdsVWtshVd1AnRgMyORLLlpM4OmjqaSWBUAg0+k2P2kBghMMiNPnGaWCTmBIzgNXD+KfO+Q8xD8jH0qvb+nt+fDw5c+3uD2lA10ZURp8/4QOfbtIoAW8vw3HpBG0UvWIQYgHDumBh/QiYcoR3tff4sbg/7EnqyGLnEE98gyXcDaD7iX87fydcAWAyaKHuWtsoUbdo0YWQq6Tr1/yDCrAEIK+ES929j384tBIdxmHnwEN+xu1j0xkLl5nxAGtRtMYnH/dx74BuGoIaEQv2lIx7juMBWlvg34hp0NP9UBpPnbWAUNGI4w2qs30K1nfPc478SBxPcFBd8lZ3c3t7A3QnD3UQFAB1oCm4nOwkfN0muE+Aj8V+Ai73mKyFRHgeKDY+l2EfjGIvfeM4qagAf4BhcBkgSJPc4SWJdVwSBPALgvlwBmsmi7LRh9jMksUwX4zuzazB0MDQGEEJFlmRwymRlXYRbGDdpyP2s5owCAOppZVW2ckWO+D9S5DWLCzRoiIQkJqzaZCLClLCwAaNOcMeYbVKd5yimtWNyxGRvBejmjeJpgqnJ5WUXwMKWGnvPsiCeeaq0MmNhM8Xxe//9U/WmTJEuSLYYdVTUz94jIrKp7e+9ZXr9lADyA2ATgJ/5/ERIghQIZkAT5lnlvprtnerlLVWZGuLuZqvKDqnnWlEgvVRkZ4eGLLkePngNXpoea3m5XYZbSHq8PBwnr/e3+9uVVKptTeA4H4teHlmLSbF1te31rVRnidti+jb2jDT22sgoJS6vH4zH0UDOj/iy3WmtZl3F0W5f726MCj/vj+ePH5boej60UWZZWr0Ik9/teRAjax94u9eZXFjG3pZRQ48AlROSjp4y5PtwU4SKqJrWw0PsdhSDaAE6VBcI2lIdXrstyHX2IrAd7eWpPv/zlr/5Pf/OL3/yFb2/3P/yw//n+cv/y/LS+PV7u37283tafXH/y8dsbN9ZuNMrLD1+I6SILSyF3MfJW+1AqBZVUeyliNtwN5q02lxpHS2TtehHR9bK06u0idbVSQIK61CSkO5tSP1RKcaHjGJfbhUnUoW7mXmqzYW48xlB3aSxCYSWMozN3GZ31KG5HP6Q3gkw9OTZzc5NAVCJOucfmrYCNPdHj5M/MSjB66MAbTrxliiyYkYXaDH2Fj0ZqSWdzSgzdODYk4hOiKuPYEXNg/i7NYB6fk9HpvUyPYJWveEejmdiJ0lzec/B4vvTkM51xPu4QfJUVKHNJogWYcckRdq2BfSU6HXcZh9xpSjTHcROIhd10xjmbCxIh5QKeAAQ5iIVn/V1Y1LJ5IWaS4FklxhUFeol3wzxT73nNPav+zN4+Mff4slNx4jyLM45TLi5PctIZXIEERuY0NWO7zoWO2Stk0P9qCIwz6XyVbM+U/M/PP2VvwXmEsebNc3UwczLR9CKacNP7J0WSNRiHUislxTiRfc49Up8AUR6RZ+qM58EpDSXcQW7GIu7GwuSspsgTGxfeyUlNQ7guNEYIcGczU/Pej/v2uG9vj+2166hruX24tmUf20YGQFmc2YUhRZYqtTKREnut5GB3kypSmNilNmIBzFRrqYXpsHQOTbngykQHF9QVt4/rj398beuiu49+LHUhSC3L3re9m/VxWZZt+54gLOXH/b6uTYq93jc4N7HtfpjRtV77th3qt09P4/FY1gXosIN5XG70eHvlcpDWftzVOomWysRUF3IbcC8Vo+/X640LxtjZrbZVlsZMXMuP3392d1d++ngrpZmpDR+mYNS1vN234su+3W/Pn6wUG8dPf/ITpfa4v334+A2AUwomwQ9yd9N0I2E3mDqJeJhqhTY4CZG7GhgsxCC7j1LKh0+ffvzxpd3aOI6f/eWvr8+X8eV+W+X1H//4H//233/+7Z/uPzx+9uF219GEH9u9Lu3yVF5f3vrRl4sUlMpV3eEUZL1mzGBT3/chAiPvh/Y+SuVaQsTXSxGnsawLmba2CNv1em0LuLi59SNIHmKKtlyiICuFglGkhjECZAaxHPsAkTtsoC7FwffHLoa1Nn35bJcLlstBxUWupYoxS/GhlNBFPkeYzEUW8X82Vg3CHmeZlqkgdtBmt25TnAMW9TsJZ/R99+KCzxBlPpGGrKWcRcwUacF7jvQQFzTRgxl1PFuBALSzjkz+u80+Ie8On/NPev/NQIvofM9kkcws+D6JzrQy9RFmx5NfX8K0iyikJN0s+HlZT0/lalWdOPsZoMjMMhbNrBJBdMJNMVyBJ411ihGd6I+7OwoXMbdgWTFzrHBEoKeZe+fZAlLaFOdGw1ch3qdORfYN+Uy9C3UHIkbzK5zN0VfiPFGDZ5LiGPhgomBfXbmsGnJ92RNSPBEzzJCcbWFEU045jVjTmt/pKwRwTp9mo+Fm6Sh/rpeffYWfZ2FCRx4DHGT+gyNkou3co0a4FrjGkoal15lw1hkSlAN3N2UCiw+YuUnlutb1uvrTTdStysG8k1vB8XiQ03Kp7srsTC6tsIDADqiZVCYKTd4QWHRVZXhpFUR9DIBqK0OdDc7gwrXw9fkKHK7FOl6+31qVbjAb5s5SGlZyVutfvnwe++ZOTx++IchxuN7vw7SUsvdOYHf7/OXHpVS6Lv3+cNXH2+vtWmrhRUj71hi3S337sY+u7iZiy7VVudXV2+ICvdzW66Xs+2OpK1cZ47i/9daaAeu1/az9dBzKsB///MO6Lh+//dClHvumrnC6XNcxOvVRK0vH5bpKoaP3WisLWdey1okNepz22O7hyqRERF4ccAgCsYjaVQAvZDDTQYbr7TqGqvrPf349zPbHQ6jWpW3lh8fb6yHl9u1PF75+wT/8/svr/rLdx/HrT7/+9NOPZibsn/dXCK5rbbLe3+5jKJFcLqV3c3MIkywMkJSdtufb6gGTMhF8vTZwEYnKQaUwCw0bDIIaCeFwBhHXsJUGTKSYYkg8sTmMCy7E2BVMzrxvQ1ZxmGHQ47iuPI7RtwP1GMfR645GTiQl3yXCTxglgUJYxmgyOGm6smKywtWNAqueRW5ki69soUDEbh4RKbQ/3QwWviOpEBeMSk4vQrZQ4UHoBsfm7Vc9x3y0LR1R8pHkE3SOsD2l7+eQeeIEuePmWT5HxIhNz6D5MZ3qUhmRmM54i6DoMNuY08QERlIZIuMfMzx9Asjh4S82wRXHOauIbyVnt4EzqM6WJnbuAkCigPXUACfmnDw7m3kJJAjTwBlZ7TJiqOF418pgnsAOgud7psXU05poDCjURWjK8uUrs8mg2VTMqz8V56Y6K1FmuRy9zvz93kbmxbM5m435UrYps6kEKE12ELoOp+1ltj2z6cjeM4cK88fvg5A8iJkXAM+bG5G3iZkTwpupwx1mmanfm6icjcUInaaHWsxyCktIMqmqqvZxjHH0+12P3RxSamnL9eMTu43HDjOBjd1aWQnq0NE7BzvVafRRGxGTuVUSSr8dN4O7ibALjTFqqyyiQxGwLtEwY+K6lP0YVXj3Y20yrs0e3cFH7wRiRjd1NTeoHuYELvfHbgbXAbjq4LKvywrVt7f7ulQXu17Wsb8V5nF03fXpUv3Y1qfL8uGmj893Gz68LrKW5bq2UtBWLIva2EulVtd1laN3YeJWH49H793Vr0+364cbg7f75rzXUno/uGCh9RjHsXcULEtjoSa4rBdhGtsw46hP8w4zJ2JnC/e4YIKSUxDkkSVoBCGoGgsbw3pXN9UB9zDvizuKzS/L5dG2Bdcv33/3H//z3/39//7/rQf9y1/8+uMvf+bCy5+//HBsrdKH28LrRfvO6vcfNxYKWQUmF3KpZW1ljEFORqY2Wlv0Uk1t9E7C7lqWdn1a9+POhKGq7hXVw/5OiRl9H1AvtSZGKETEQtXgNlzjW1nMaRkAF5i5DeVaMNzhZa0OQakstbSVqUipnpHYzMAsZjZNZRFxhBDiXQkABJx8YiWOzKcytQMCDU8Wdjr5ZfEW0JFOjvVE553nADlHaDECJXZXimDLKRGBc28A+WhPld98w5AXC7vdCU1E/T8lE1LizTitn/I/QmSTfBgskmg9LBEayk2iHBhkRc88Q587ToParyamnqHCJ4I924+cH85+J4Ouc9oyZuKc+eJMRZloZps7Z60gAglTccA1ZXycXERC4iaPOJSjLWk/EcRO7b2viFjZdsWVfD9O9zSgiI98zwPv34sYInKKKNGpGu7pk5CITS6Y5K/mWvcZj+dXtzmuYJ4ewtEdRduYXiDRy4DZXSebC5gYmecS2GzkRFjVsoQPkdLgjE/cac6QPO6s00Fhtk1gZp7gfZQENHlh5On2EAdhZmMMNXVVuDETlTK6SFtkWeHaboMpti5HkTYOt5EDDmIqRfIRittSoeqFyJ36YbEX06nXVoKOwSxmProiNnXdzVwKXa/L7gqTjQ436oeOrTO4LuJw29Xc9bDDhhFYgCLmfd83BsnCrS1mejy2dWnXdXl6XmulpSzeR6tVqNdKwnq5FOHblx9erouUaymFj2MjDLCraR+6LkWIjn1vS2ElYRATXS8GM7Vj3+Uht4/PH5bblx/97e2NNwLhcruwtNGPWviv/+Vfv2zb1o/PLztut6Nvb/c++i/M7RiHtDLG4NgIZjdXD+toEHEF1OEpjW4+XIngxGpdbbjnOAdMhZgr61AmHn2/LFeW8otf/fX9fvzxP//2H//239//8KeFqx/YBWUtfR9fXl7wuF+fn7QPU+2vo/f96IOY1qUuS+3HECEmliJObI7r89WDydC7QUGA9yKk2pmwXurlupYqXZmZVc0GCNIHpLhUdvfCtau5efhwbNsmhc0hRUYftdZQOyPAzKUULlLr2tr69OHT5fljuT6v1ytRKaXFWqkkp8NjizzKxdgNQpo9eqJEmWwBm2o1SMYkzJnI9D0qRNiGZs8dU01/r8xCqW3iyZNZGePlCAih3xDH5ieikso/xvkOufmv4aJF8Ak/UDC/4aVwyIfjXEmbs0yDgc9wQmHLmlT4eSBxPlLakqJXiDGhm2WnEuE4GUQppDSLTcqNsPRKDFkzphnvo8707K7euYgTYEmIBSc0lFhZ4gxw8+Jzeg5yd7bMOuSYm8ARrZh8FkPAe1aLy0CO9x4iu0rMrguUOEniVHHJDUmJwaTEzL7jvcCPoJbrFW7vE7s4T4wTi5+BPvGo4AzQe2rIpH52gm4WRsR5WNFVhKfO17kKufcb1x8EJ3JVsIR8NNHUrjiHQjRtKTFvZp8woUOkGDlpLqdEzjOczNRsaVkI0shZhXxUFdoYsHGwY3SGWd/Zq3VzH+5MNliYhYmIQSzMnI9C6NS1QIXN3K1UMfVSWYcplEgilDEJMWwojEghhHVtx/2A6WUtGM7tSuX6OIylk5Ji9E6PY28XrjxMB7gcx7HWEgO0dam1ChNq475tjL5e6n7fb9dSil8utRaC98vKL9jfXl+fPzyVBUd/o6GlGZP4gmW99GFl4bIsbn5/uxv49nyVUrUPN/e972q1lYutn19f+2McY3z49PHjNx9lKW/3x2HawX//D//wX/3sF/f7mzlLYQecKeXH3RNhoJS2dSL4CA0PZg4qCpzd1HSEeJwzB7umd1WLnohYqHiJOqt+kn/xr/7V9sPn/rL/w9/+f1Ze/8Vf/eon19tddYz9z999Xq+33/zrX6/rYvqHUvv9x7dFaJGyrAV+qLq6gjj0WZ6fn6jQvh8iXBbWYce+A2LaayFpS7vW2krgkN7jPhY4qzoVKswEUpAdCqBUMddaW7du5iEHAlUpIlXMba1FYWVpy+22XG71cm3L2tYrg2tt4cELJ0VCY/7Vk+MZBKM4zSfSYBRwTfgBxstsogLRbhEodImNiP0s3uLf4Sn+8lUfnpVx8svVKGtnmoksar2vfufr8nmWrXQ6MMJDQCHaguDqzEc78IRUV863ymrQOZaos73PJIQ57DwRYGa23GKYQD5NPIQiAsSfQNJmU5EkKk/k+Cw7g5TmE4LJNz37i4yv7/9ACMGl4BgJMchLimCe3zJj/D/zOogCmpnVNDhemXaI3r8MMEO+ncJq7xJ7Ma/Ih20K8+TPckRABDBb6ufRhNp9LgciM0mQXzEv5GwOwh5hApITepqQWXxO6mzEp3n0BJ4H7v5+uplcpyZj5Na0mgMTSAqAaAlOl2CHhauRO5glRswILS0LQwEQwdkYSLYzkZmbaQhXnRuHUgqzIPb9jcxJVrgNWGdTHsc+uixFeO1v42Dr1olQayWYqoIZmn3lcIVpXQsxShGmyoWKMCeKGveOuaNIIS6jd+2973Y8SDttD1W1fb8/HuMw2N2cj4HqMBEpbalceLk48/VyfWyv2/1R2kIix74LXNwv1wuzt0a6jWEHgZ8/LtdLua64PrVWeWwdMGYl1lKdCx3dWyURLkJBjhRhU2tL7b2LFFPTrmMYF5bKb49XIW7rpXx44lZff3h1dRsmT0213x93JVmenz98+IZYABap+767ayCtEbfz3qXc93bTWctQ2krMlg7uQoRSyZ3BYwyCS8l72scwghSmw4jl9vHpb/7b/56YG/OPv/3+27/8xU9/+Uuuy/14/PjdD0dXE0ZbudTrVW61bW93JlQRM1Oz4xjD3UhrqcAAc21ipurmpOu1MvMSh9MYxcc4oA4nji1zgzqY3JViJjRGNLweq5TqICrLUo7jKCUp+FJFDd1UpJVllXWhuhALlUKFSQoJ+9dVKMBBbrdzrIbzocvATpCcgn41yZsFeYAJHgD4WbT62QvkrPi9jp7cmHhus148xVfICotbVN7OIhNeTsxkbhXNfsXd4SL8XuBHSA4nEskRgI7hMJ5434wWEVyzqnRzY2MQRRxDSsLFkSdP3cFMak4gmSNMkizZo6SOUBGgi7lDYZNB7h6qQuHrEuTGAM6CBI9ZQE8puImyJDk1sI2EzgCgROlNmDJ+6pT+ZAQgVANpVtaWTFDkd/PISD7r5ZmFPAQyycMKJuLtpHa5T+SLIMw2AVSatHmbPtGYKkOeHQtm5zNT+WwnQpho6hfRWRokMXhmRE9hqUn+MCdmCWXZuetLUS7MWQIxIVzv54AohJJird3tfdT0NaXhXDCOJB9gHEWjNtNYJCWO7i9gRI6nJYYCZk4kUmgBgHbxY0dt3trBxMKqcKK6NGHSPnyMcQSvzzTxPq+VAepHJ5jVurTCXFx9mHNxKcIcEiYmXEst3n0vsu8KYHgAIL0u5RhWmI5daxOom5HU9dFficJ5kXs/4Li0WmpxDAaq8GUpIl7EtR/EWCp//HBTe60LSaPlurZbHfudWIl1uUhtVBrX1uoCgpodo+/rWq+36+Pxtm8HMT1/ejqOcRzH/fVharePT0Ll8dgA5ipF+Juff6NjvH15DPuxFNlfXq8fP3y8Pn389LG2xsKv24Oc1tuVWAKOECnuLiLm6q4BWmYJ6hajAUSHysJEBrOuIHjEgyqBSfajM5iI1bTU2lVLKZ9+/u1/ff2f1tr+9v/+//z7P/7x+9f93/63//Yn3/5iU7t//v6fvvvTeDtMXcyfr08Ly9YfcOujk9B6ab2P4+jGtvdNuBz7YeYsWoRIpNRCwjqGkTIghY6jg8M1kOEh0sAsRQeFKombsshQI3FncrNhDhIH6Ri1lGEAsQtzXcyYyyp1kdpISpyAyILEE0lHcNgpBrsRGeg9GiR0OgtE/6oUyxiVUzrXCahHt5080JyUhi/IV5Iws7ydH3Ti5SHVkMZkWZVnETb78vcZ4VR6nBH2PQu5g4kp1HhsstuJ35MbUSpeuM9qOf/kgDC/Yxrjzk0u9mnLlRRNdcp6PweiTImxz2wa6svZKMxF5Cx/swqfU4QMjxE3z0SbS75wmol70l2LG0gmnjZ8CmgyCOY29+4wJ6Xz9EXyngrP7im7GmMKN49WLvGVVAGkUPaIIj1WtEIuIiIeU0bwvOqx2j2/xNfXOHeAY3OcOQXHvwJ8KAoUz4WB2TdkARJHA84GxVL8Li7P+4nOJiPWHckZufOnamcLqbnEO+cclovdmasJ7lA3nnhlnkQCME0SCOJBGAWyB1KAhCUQZzZTM/jGXJwILGVdCNbfHL2b9kinmsgrTBOqYwaTt1p0KLciUsYxRh/EcFhZqK1LqVKo1FpMfQxl4cv1YgrUuhz6St2GAWW5XPrOXWV7DHQ6tse+v5Kb6lAwQaQswyzKRFWDqceEUHePvWkfl7VBIrCUZWVmwIwKu6sUNCmOIVJLFVlisajG/XaMQ6pguDtGH6VWMBOV7e1hw0jGMP3++x9LlQ+fPtSr1Fbh/uP3L1rl6fkZBhx+vT59eP5Yl+VDuzpofxxlbVZ5DJUSoE1KGvAsvoSYGMxC4QfqOiMWi0xWX4hQRi4nLmEPC1oaXHvfddhYnm7/5r//H/j26f/3v/4v//R3v/3b/9f//u2v/qKWirJ2jL0f+9uXYczpKailkR/D4evT5eIL/HKM3n3vj+3onZi86+VSZeXLc9FhTuMYGym7DinLsfe2Lll7gJnFjcZhaqZdpbAm8BHznhocFjOvpcFwHEqFxLlyMRIXNoIBat51GKgUCu1/iqFoPCfhnTdif57BIIshms/46LOgy8czb/iJ32Y7npE9Yz0TnRNin8LsGQAndjRxonPm6QAF0Y6nTlGUwtNK16eGT4Tyc445Ydk4PzbhkniEmWEWX2oCDwBJ3gSYk4nARaZVPaJ8DaxichojQgffR0PV5kxVmItmc7xBwqHGmU4iSazKA+B0KqYZdekrhCrnz3Qi7Zknctod/13mJ+HsveKtc5pKme19WnTGGafZiGHOGwKAmYPTcwX8RLvobBgR335ieCdSN1f8MCExn0ognlPfvNrOOeac69t5Q/oEJOdtFdf7/WbLSj33/ewsVWKYEihd3AB05lhgajsTgkY58Tq4m/Bks0Vy5jwDeeSp6UqAi8xGJz7Qsgo4z45bkJSSRBE5clqFCqRyXaQsVte6du8qtVIr9kDYhYjwEC7s5KxDs7hnmNvSFjAd2xAmd5XCEOyPoerXp4sIXIiFj/2wbqWW69O1b16X9XKjl2X77s+vo4Ody2C/u46Rz7N7KdL33l1H35sI3I7HvRTUwpeLgEY/dllL5bKsbV1KKwyj222pJVyQUJe6rI3g2/a4yNrV2R2DpfKHbz8SZLu/7S/3usjlumj3x+NhdKzrenm6BBdkvS5Pnz4e+96Pfr/fax8ifHm+/qx8+/pyF2Z1t+PxfLswYZGlPN+KFO0DF1JVZtjoTGxzpkREInJGHPLYGAnLvoQ8522WDXV0fInzFgnZGneX2o4+VFVul3/13/yXn376zW//43/6+3/37z/fX4XLL//6V8v18vbn77/7/e9efv/HP/zw50og1RtfPnxz27adGULMVLmTDFDhj+WJOCRmu+k4jkddahG2I2BdZvG1LW4MkA1jqqZRppIa1KCHmw+pUqSQu0Ng4VZkQ80BIYFwWVfUaiID6OYDRKO7Nac0EsWUXYlaLH1ZOTVXEP6NoHjWkwmSEAfMEqeFx07xJB/mTA4zniNUMegMTJ4mVBNsmg/pfPAzjIZF1+nBkmNITLPYmWPgk5V3lp2Y4EfARByJjGZJbe+1vnsW3VNf4l1DLGWR8lUOd3DutkYNGY7HOIEamDu7FJkaGgkbsLB7OhWE3kMcCEtKX2SmhEUVC5Cn7doMeUTTV8WR3YxBOJTp2JBSEIiFqfwmE+3J80IcokB50vI0sTAmNoMzjMfnCsM80OU8Ie/pi6Nezo9xp2mzFiiiMJt6fp/Ect55nXnuMc+RpdAS4cznSPyGiCaJL3LYzHs5e5hchVTIi3RmqQSbd2CUHsGV9ER+ohwEg2NeS3C1nCZSpvtJeJhnhoNMNm8LHaFEzERucImMi6xu4saPJsjdXM3NqAiNSqVKaVaag5wkFJ67qgNuVkT61s1MCrNwkVIqjdGJRVj6OPZjtFbcwFQKl0LsnQaURKUUFoGzcCmlsI/edagTe13Kvm+9+zhYRAqD0dVRBGPX2NU59t1EauMmwgRyZXBt3C7X66UJGftYF+YyyMCCy7Xdnp4YxbSLUKm8tnb7cGVxuNeFW+NWqzsu18u23ZlpDO19kAg5xtBtH1yECGOM/TjapVyWi9RCSm663/fWyvOntfeDyrLtD6/lm28+9r7zaKWwqY7eC1epMhE8zGf19BtEAKBZN+Fd7jGhXyI3dbOIthEwzLxIUe2FZLgu6zqGHaM7+sef/mS5XX/2q1//+R//8Kc//WFQ+fTxJx9//ovbx2/+fP2HH3/3u/31x6XwpbTapFzKdt/HGETDhABr13K7XIrwodr7th9wcaVRGpX1UqQQiw4zL8eu272b0pFuHFzrqiOwBKaQ8DqMBKTuagCkMc17imrjtjgLleokzqw22BT7blKYRmuQUoiIytzah7uTuSeGSQjVlcReT1dCz/AeeOm7zqVb7r+l6nDoP3OQshIOSeX9/EvKaTGlQly2GQ6G2VdgFNgsHDTJTn9gwAP/x6yXbQYWIgpyLCd2i+wk7IRu86/uZjiVz/ykFX11nBPNz+AQzhye+AaBqIioGjiF3vKz4mOz1eAszadqHhGZZn0+pouXxPfyCB5fozJnvHxPCXEjk8HMS0SaZGBN7iZx0lKBgGkybgacHuiGI5Gl+EpJoEEGbZ9adjRHNGYIoZ8433FdE+k301NzgygvzRTryHYgUbGo0M+iAOf7E090Jz8rVDtn0xlfP9/T41GIP9kQMMNS8yf7yDnNFw6KGRwenGU4IIEzznybPRDl8bmRJZ4YGT7/m7+qFMjVlIm/RvHcbGKBORwLuM01SA7iIU3ExXOwJIWFax2bjmPEybROXArA4zBiNud96+Fzve+9tsrDqDIzDTVzLGsjZ5YYWXHfxzGOfph2AqgIg2BqHtUwk5RyHAcTSwEPA1EpTGaFRBjrUmC9EK2trJfWCgrJ2Pve3/wuT8+NBe22OI3XH1/ePm+vn7+sl7oua2ky+uGkxzBzJ1JhuTwt7fLh8bi7+VDTMeqyEssiZXStlzaOsW+PPg4RMnPruF5XpiJcqNAx7LE99tcvl+uVK3cbpuowgoeiWeK/8bBx6pTkLMjfyboJZZKfVIy4YAAxsaozsbrr0DSOHs4oHEMGRqttV4xjr7X+9C9+fvv4vHy6vd7v98cDuP7ir//64/PTd998ePv+j2zbNx+fX7//4eXLW3HxvffeQdStL5Cumxkfox/j4Eogq7W0pUb9I7WZYtudBe56DB0xdaRi6Etdt30Pea9gHrMRtMdqKitokdLEWaQ1aXV5/tA+PtenJ1mXEDdW07hLzJWM1AjqImXWmBaCk5hAxYmURi7FifEisPr3XX8+sWmc1T0x0bTyzeozcOSkYJvHUxkK+0n7JoJ7FCE280AcQwzr4rPMPYW5ZrdPiYvTqangntBtwuWRQ8JfZbYBNGvObDXMCSHAEJpj6WmTmchPyMuZBEB4fQqThSUCvYNj59lzdz530ACH89yr81NYFESnumVmwbxpXd/1e+J9QJTRiZiZSsIgNHFyA/iU4stwjUnO/CqbOEJucR5uvM7MHBZNClTjtObZnj3a+/ouzr1kmnk93y0MUBFOscTvUZ8o3bg8Q7/PQJiQS16UzCBnPsodsIjVlCoO5k7ZakydpYmOWS6mTxwmtc5z5SJ2IGOyNAKxjFrD4yXv1zAIEmYOtsLiNIdRDnIIC2Uw8bhaGfwJMB82zCzot5ky4gjnF3dzJle30bupDh0+NEKSezLY2Kl3JdftsQXopEalSA3xTWhtpOruSsYeG2Wb7vfDjMbh+2597wC4MLkQxNTloLZWQxBOSc1aK0IujFp4XSsMYxzMdVnKGA8bo4+7Wl8vT08f1stalqUIyaha+GhV2tNFCtuwfX+IgNj7fmyP19vTU1lIzY8xWinruu6P3R2u/uX+drmsx6Mv63q9rW/3NxF3uLGPrsfWl6el+dLW6/3LyzjMjPuwAfryev/0C66Xyxh6+XDzoQxyNRGh8HmeWGgMVDx3Ps5xkJ/xJhKBn/dV7oEHp0sIHvsZwuj9qK2QsI2h6vV2+83f/BfdtW/7/vql3zda1vXTh6E7W+G1to+3phvVKgvKwd4NZPt+3/dAOJRrqVScjOsqld2t99G3hzur8RjBHeVaBMDopjoeodwbDzGTHkpNApAQETUTkIHAcCZqjS9Luay8FGcaYzz2rbU1mtPwtJiq8wGy52bo7MKTsR6BOO5WpuSiBNRAbqeDCELRM2/rXJqNGO0Jf4dbWBJSbPYWSFAluac2rbYQCsSBrxAhVQNsThBmPzF3FDLOnTBCBDSLsc/EXijLuSm7BibO0E/vf8w1kRZPfKoIx7c2czWVqDwoQKms9k94OAcMiKU6z0XlEwfL2y8iFIJvaeZO6rnx+s448uxcJzqRFKB4QY5pygkkBXJBc3KdIXoOAGKLwCaOkZkkdHKSwsMZo3MTwM6m5jzw9/wzFafjscLZiDksaqizkZjF+0w6OfhOsGQOQ6LzQdwJlB1DFh5ztIJJWo0ywE+0nd41N+IQMYsAOAxGxDGuVFMhsnjUiaP9hDkVgmusk4QG3PsucW6WRQ9H5nmnJuUJs21zM7Ugy0bzF8hPQloIWhURcymlM7GQVBkPcwILkeDoBxGGWREhEe2W7RuD3Gx0IhlDhaDsSy3ubEYYZEIaa2fHziw+zIaVUvo+dGjfjuMxoF5YtDsV9upHJQIPc4abQwStrSw4Ho/WuBTA+fL8XAsfjztIl4rL+rQuuD7VUonEHl9ehOtx6Bj6/M3H++fPprxe6jffPj0e9+u6SLl22++vd7jXpS5L3e/H84dPy7JsR4cTaP/xx8/a/eO3zx+X22Vd3KwtdbhujwOGL59frx/k+fLx9k215cK8Etd925d2IZZ+9MvTjYlitya8+tycSAKfPN3nRM4bI1eD44FxckuI8b2MVRiBwrO8m3Mp4+huBoVBmdiplJrVqBBRc11vLz+8vvz4xdV3U996YWpVbh9u2o/RC9RJ/bEd+3YwQc1KKbVVd1fXUslh+7FFa3j00Tc4FRa/3Jq5uAt49KO7m3Bh5phmSylSyMy0WwGokCvISFhIChXxwDZ7R2kkzG6q6gSWmAhaQiauwozQuSK2ACRzI4wcNsPiSbLMp93MaSonM5HOzDCf0wAjDE4uGQQxhd6yGkRG1ZO+O+tRTHw9QtdUJDjJo3NgMz8lwY4zrZy9SRATT1AndqEwGRs+3zNX1SaolPXyXEUyC863EaVgo4c1LGXSoayhKWphP2vtOQvJHwYihFSt8OjJ8w0NAAsSzIlf4fRHeS9qaYa3iS+VmVQS7Xwnd069+xm65wwHNPWSkgqWlTUDDvbclZgadBFEAx6ZOdo809TE6SYEhXnh39MMzdvlfM6SkernpT4vBrJtnC4xUY3HucsNYsDdmdK1FE7mCqfA8qL0mBnxn2WuOAIRIQdltvBMNKncl+tgyIvqHOcyTeqNkpw78cxIcHAdI/K5DZ0NaJ4HJjJV94C0wMIGkDATlVrHhvCb7P0wGBceOtZ1MQ2c2qx3AFK4gIiiZCM1RYctXKn245BKLOw0RIqUIoUNLDZaraYmpG7DdXh3EZJ1MQUZLhc5aGCY6mAarbXaIMJCpRZm9lbr2mqtbGo+DrlUZm+Xdn26tFWYwVzcAFNyP97ubWnb210bf/hwebq1oWNdC8vN9bv7416WJzW+fXza9rtwfX5+OnpftkW7975/96fP98fj0zfP4nS/f7l+eGqX1Ysa+5fXN3m+Lrdn+fQB7dIu14Xr9fm51FbbQsLj6JPWDckO1czg5sEBjw4tnjibnq0JM7pFe46pVk9Igx44mEUAGxr3XOR+VVUzMyu1upGrFqmXFf7tT/ujP7qut0+PY/vuTz/42J6fF0DB7taNsF7bslY1FeZSCpjM0Y/DYX0cQ3XfuyoVWUgKgWkt63pTo20fDEAVRp7jKDisVNHR1ZXY3aAHWFiY61ItBYWsFamt1tZquyzrpbVFSitSck5KzGCGMHM818I+nxzM0idhY0uyc9Rcbuo026yAThGmAilHNsdpmjIMyDexkNTNp8wt43IMb0R8loUOn2aNObBxc5pwj8+GP9I3zTzg55w1geOvyDA5KYIZSCfdB8E1ydCRWNO0pknIOSBD1fAcjcSpqu+qDDabHoL7e4t28kUy6IYiQ0Q/JEkEJ4g1B+Bp1AhyciYSPsGSkLhnpndACUCZURUxkyQmS+o+gJR1mwgRnecLPE3IKPbjYlQ+U2p2SzM00xkbMytk9IsUnucVDkwJzwQQ8XVWpkTRJgafXdlsCGfrMoGmHGbMbHzm8DjKWP+htDtOivG8def3BblbVurZ5MzENkkh+TvRDHFG8jBepjN35WmgMNCYnd8sSYjgrmoz4g8iSBEyqKUtULZi+VFSWvOjltrWp5tZN13crF0uVYqrja5jaMwM3K0fxgUiXFpVo1iZ2LdxjEHsNNS8VSoOJ6lORELmzuThE1UXudyoHE7UmKoq71DV4WJH70yjVTzdKrMcoxdxs6OKLG2pjQgGG8Su/VgutdVSCy+Xhd2O/fBd7y+Pt9ft8flRKtbronocG5WlsNDb6xtVuT1fbh+vQwfcxtiFpfcDd3el5+eblCb1/vLydtz7XR5CKK19/v7L+ry225UuTbbhja3VTz/59mUjJdR1lWWJEsDUXEjHMNVSagR0ypFwwMtqCiKeklOUdI3zokwV4hAGyA2PqJdMQc4AgzTigpqZqSoBDgULOVUWYvrw7cf1svz4xw/31+8b47vt+PM//enx5Yen50UKu+nYRil1aUsM95n5OA5DxEY7jiGlVlBRVkdZCoxEFgDChM2JSEo99iMWVsfoFOAgyFQJVJsEbWGYcVdaTeeYsS3L9Xpdr8+1rqU0FhFhMwiLmhERC6laumU4RQeeNIaQoQ/YZJLxWYI7ES9M3clZaAbJhzMyh7oBp12443TKTQyciCcLJUJJoMfxvL3bUuUPcvknS0A7w8pZ501RhPQDp1kbRyRMwmX+AudNQqe7VRohn5BHbpSSwRzOEsMMi1QnlIiMEA3oFE3D1/AApVHwKUllIJhTktM4ZUWJOcJEZj53Dz1Kzik2AUYhWE8nBBTYiasVoizZPSH4hFjiNIFOV7FEyOZZzBOUgXWOF/K9aAJVCSdNQCbEOs6Ui6BVzMKcs6ye2XhGfT/7sxlNc5QfyRBzUnFOcJC5hsjhIhI9TFKP4uw6xd0fV4v/2TGcb2VnSxL/Gu7MkYqFJRphd4T17pw1RQMXpMJZGnBuFOeEIckBoKDtRzNylh1McIsWReNGZgLESHi5HGPQcrHjqJcri4/eheUAD9ocPI7d9qFqAmYWM0gpxEagsbs5cp5sA8PdjvXS/LGzEAODNIoAKQJ4rcUWZyq4Sn/o/X70rduIkkRBWiSugEjBulRs49GPWoWLl0oEddO+P8wOQuGn0pqwIK1H3bfH3g/VvYsw3MjNur35KIdwtB7bW1va84enUmXfdrir63pZhqmRad9brd/8+lc//PDj437XobenJ1Tuh/3448tVcPnw8V/9l//Gmf/85c7X5dtPH6+fPm7dP3377dPTU61FQGZmRycRnXZGp5xMLGBQCg7mDexw5nT28K9Qh/ypuWliAtmKAlJEzYoQAO+DoYHvGZk7dVUCqxqYnr/5+PGbG/38p99+/PhU2pfPv69ytEK9czd/+/LQK2oTJteuMJdSA7FaL1cnvj6vqt5Ve3d3ProKCxFBqD903zsU3Yebt1aEGeQa69AMg5dFjOBm296X6zq6WWKQUsq6XC5VmpTqDpHYWiGZC5URsSWY7cRB3rfJ20l9vQlYM2gW7icSGljvRKLhkRw0gQuTUqLnio0fvMMZk0+BE0Se3H96L74mFowzcmWSwhlqclcpivr8AU8U2nHeBvlmyShE+iE6TRwmQ5LDhSUKAZKkoxBgDjWTIkj025QTTkT4VgHOjJxkBA01ggjyVqNzSyBCHXnyUGg2OEAI0RNO5BmxFhSvDyWoNIYMGuhZqlrq/USUzZk6pUhObF6lzfEEXPIUu8+wPy/smX3ngVOSLPOSeNROM9rmGcpoTja/zPsF9plcszlIICgzUMJNuXuV69LxVim6NN8g5JvICc5MqiYsmJhgKjlT7lL7hOjcgdxDSXxtzhwSaoqvEXk48p2GzGoCZrMHmlE+8nwUStGamWrutjn4K6/NqENsOoKWWo9xcG3cl37ssqzuLqFGQ9SE3Z0LFxI9Rj9gaqREzuY+shizdilQCDegtKW11liigZEixJVtqJltvhVhFiHD25u+bY+xu4KZy/OHyzPx6Lp1ZWLTvRXQrcF1vZRa3HXAdb1VVb9c6rqI2eFOGGwMG8OHktunb5/dAdf1sq5rGX0/9u6E6yJMT8c4YC5VlqWZ07Yd2/EAU23rsdt4vPWjl1I/fvPx8WV7+bINGWXB+rTUyiz+pz/94b/+P//P62u3y3JY64rler1cnoSKSNVhtm2ttlBNkCqulsJ8Ex/WxM1Jw/pkkhkCpw1VBZbiZhpJ5JMHAAEAAElEQVS8GiZ3MoCVNIJDliBGFmwIVx0golo5MqhabSLCaGXs+/31ZTcapUpd3Mf9bdNBrtTaoqo4TPdDbRBI7W3fDiJ++ubTuqyxuMpg7zrcx3AU2t4eY3g/NITqhCUCmroxBzJYnI0E0ooyDddCVZ1rqVIXKa2uqxRhsLAIhcIUCeU+sAd3cy41uns0TA4y17jL/ZQDmO5d2SXP3t0nj+N9iyjiLodbKqtObqgbGEnPIeCUEcvCNB/Ws9if4TpQk/egRLOejcBE8xjOH6s5pVHKbOrMRfJDA2PI5eHkkccDnsNpIo75sTswDV/1hLbchUVPS7CoTYlSJ8DnZrLHACXvogAUvoqD+S2QSjq5LebTJgE5Hj95CpOgCCeQTAyvBJ3Bp7JmnIuIgKFoE6pn077967CeGIs7zjtgDmYyZOcxThQe2WVMCI6A6OPmlMQTS+HMwzMlzGSXtRZPLWoCzdJ+NnLxasuCws1ktgUTVJop3UMGQPDOTn5PTpyt4fvJjno/6PuwGC840n6TDCZESklnjpHX7IGc5kZCQlbmXKImCcmSuQOQO9vxDaIBJg8TAZAH7YrYSSCVl7VBzcd7Gj6O8diW6wVqITveh2nXfnTrTiIixQhlWVQJMCIew9Sgmp2aw8E8VE316J2LwHk8+tvj4ezXW9tZ98PAVgqcudTi+9730cduQ5+eL1zastRaSuHiNtzNlZeFRbA0ud4WIez7duyHqZKQrLJelr4fqv1x9MLUfdy/POrK661VqUQUbqZSSwNtj8fb674swyE/vrwKU13W23Jpayul3PsuF2rXCiFjens8/ukPf/qrf/NvdW1vh3z+/PYX3/xESju6S7XtsVUqtHIMi3Rk8RjPeVwtoRJ0l7gPRUR1GBAe3vlEudv7cgBFrNHcmwwOV3AviZmFGV4UDrM+dAwVUF2akRNjuV2YfzbM+vH2sqi+cN+wvb7WIqVRW5u5F4J7HWre9XpbuJTClYA+bAxTQBWqvCzXY+vu2F53N/RDS3EWciIdgwtnHmPmwmWtVLgIg0RaK22p6/Xp46d2uZZaWIrHVI8RbB8zTcNez3leRKKTJ+tZ3QVqZpiW45xNeQIG+cwaJRQQgO0J8vo0kwQc5vrVhDXAWHOkXbeHq4bPfVU6aSwzBp1wkMOz5M6C/5QLAEXhNSEH1wwjcEvFYpxg/eww4iApibix0BDhTi0gmpldstSj1IwhBs9xphMTB21IQOG3bOYkDidzYxZzE4LnRMSlpJl1xFRmCRmYQI/UtEqJe1OnxS+cSuyBZw6BO0pqYDCmqNlcXKakeaUQCrunc9esc312ABPIfs9mGfxplvcZXxPLBs3WIvFSUCwU4itUPb7dpGzOhu69xo+jJcfpRjnTDnBWBCCi2K+f1i1IZpQn9TiZX4EpJmHonHxgXv6EgvIYfL4dUl0u6KRqiulwnXcwRbfqU8Avq5iUnJxAZ5YkcT8FMcXTG4GIAkMyQB1EolCpzVwBIx9Fr3FIqt1gFcs44EcvUkcfUkS7gAwFOmIAw6OTOelQFufdhvq+H20tpbJhcYpFa25L5XUh5wf2++O+rmKlCrvUvm392O9cGkttlQF2p7Ys62URARe0xm6DRWz0trZ1KbdbuT2vtbLrgJp1JeJSeexjNyLxx/Y49ket9XJpP/nFR7hdP1xKKft9G6ZqQyCXy7Kuy+XZXj5/IeB6XV7e7sfrC4F+/tOnHz+/XJ+vVtxB+36si11uT1++++H+05dvf/MvvdJ33/1JpBC4Dx3mx3ZQsdfPY7mshUXHUUqtpQbiz0UwNDP/xCeCSlyYIr07uap65s3QB5yrnHH1Ob28YyecQ+2pkA/1YfBQFdKuw1RBVJndcf3mA/O/aNf6+scCtDHUR9/72HsHCBK7h+5w4WrOw9yHMze1XZ0BZmJT2PC31/u+DyjXWmqtOoarmkEIEs89gUjgBGInklIhhdsil4sSg0WdNSTVss51Jpouu/OmRU65MQtU0JzMTkw7QJg5KgwtNoCm/DKRe1JfAoN7j/yBnVqwmxH+dRlQzgKN2O2do3j+lJNBPovlCRb4yY0EBVh5pgeeu5xnOZtAvydy/Q5OUx5nFgE+ubBEBFLXGclSYEZoih14WBZPZfg5PQWDPbx8DIAIhxUaR10PJK4WwdFmtwHMgTcTU5amQT0ndyDatTifAWRlNjK4h+NRopkTKknkIREgJDU+7/8Z02Pg+U7qmkTaHBRgMrPebxIg1MbhaeTiaRVwZpks8Xme3IizsztD1BrxV+fsleLs0wSG8uMm43OWJpMbmpA+n5jPOWBwQm5MzE4g57zzZqav3pPShAhOYBGPWYKdId7DQSpzqs8VjJyiEGKckHhbjoNnLxnjYeOEo8lijSwujoh2JhapDa7MF/NufedSqAiZmHV27oerdq5SjQhCKL1r16GDXIBd0QkOpxHOfSTCBaWwdetiVZyFOUsMmNhyXaB8bLZvG7EzY8AABYi5rJdaC61La4sc+y6E2ujxugHWGkshKX55Wpe1lip9UzPXox+bQqnV+vT8ATSYbkdhqNlQlUFsfWzgen1et+0wqKod/XHYuFyff/LTn26Pvjb79M03r28PQP/puz8xlz/96fOnbz+6Yn26EbhKEVr++Mc/ffvX/3KRJepIN3/78sJN1G3vu/bRLm2Y2VBm8Qm1RUkaAuGzns32n4hLScFEM5gNEJsaiwDAGEmPYGbzEkbQRkxcpJgZuRtnOV2kmmof4+hb7/vBxc1qrXJd1k/f7o9XfHlt16tt9zF033btCi7ukCaX200qS1tqaVvv296p1MoFXvXw7W3vh/bNxvBGzFRiRYdYAFf1EJJkp3UpxHwME66ltXq71eutXq/cKrXadZi5qoW0Y4IpzDSrGALcXCnn5D5Ly9hqPAUFMB+36OCzecrZ2bkylqAIBU08t/B8lpM0mRNuKUE6xeKjlscUeYyiei79Rp7Id39PBVkYpnoRQRCSzkYgy2U2FhEkikQJn5zVbwrgA5NVb+oZpTmsz9086WIUzlQ5Ko+7AG6a2HdMhWILNca5/JXCsVkOBIjVlJlNc52IhcyMAZ3OZXAXEfWRYTsP3olI3ZLbToE3gYjKmdVDbCg0n+Oczs43T2iATD6lTS2XthkTC8si3OalfK/wZ5zNVim7BPIztUzMDu9xNk6s42w7UowFZ9Nwsq/i++BMyz7nJdN8fmI4yLW6JMTNot4BR1yBczZEFGv9dDZFcevwrAUt9S1c5yEB8JCHy7k35/Gf4+dYA4t6cPZrgbYxuxlH0xmNYrY3TkAtxWywF0NvrfSucHZmQJiLlIJWbWkDBvZDlYu4OQmXpRRymBzHw6h0G66ZR6UIMTG7qu2b1lqOQ13HQqDKay3sGEPHcZTK622x3c0GyNxVCtEgYe69t0WEaH1amUhHLxUiOLa76cFCIlKLX26tLSVcfMfLUEPv/fG2C1V2HI97XXi9LtenNo4++r5tj7qQdjARrZe2Lls/Ho/tOHSoqtFlXcE8xrGU2zfffvj88srm+7GzyH/6h999/PThA5f/4jf/ujPdj96P/vLD55/95ucMqPn9fj/2bbvfq5T7y8vY+4cPz2DyMZZ1JWGEgtPJByaKfBAWKGngCriac2gaVgcYDBFXNWLTcF2JrE6kk9sHEFh9eCwfz21kEJmuBHEfx+gynN2atMvTB//pT/bidFxGP+5vj8fbPtS0Q6qQEInUtpBIYVGMWAkRbu5D3Y9jELGIE2gcA7Agnpg51GtlJ3f1cZiLUxGGtHZZr8/t9rSut9pWaa0uS6lVmGOkHBGbUwKLLGndWTLnVilCYD5iEs4n1k2jntOAWJEgKVGyY87W6USQeBJzvqrLEpn3XO2kWfdFMJnN+XzpBMspViji7c7eIvJCBAojPaW78keE0xOCkkiT+emsAucGl+P0o59xziechZg7OmzkYC/SFmfiInKKAZPLBEzCHx7zVBB5rhfDEOAzcUxnzSEMwGChCxYNSvZfuX7kdjq2mXuAHOwwLzgPP6YYZjbXzwAQc4rwEQV/w9Phc25Uz0WPCLrv0AnPFjDLYor6yzhVJeLMTvQ6Fq4A4H0xGe7TU2KOEqbNy4y2mR45xl+z+YjETEASmWZ7kGXAbBNOjNCBUIAkdnpn/Aa5O+MlnFk89rjM5q5e1OhJeHAPMk9GeSZiYjVzdx3ubnOrJI7aORFIpqDrEofrQCyVhBK3A8IihDG8k5m7lFLdBsytuKrUKq3CR1tX2Og6iJmL+FAbHneeM6RW2GGHq5oOl1LVjYBBXpip4+3tuGFp1+oKFB5dS2USKRXU2PTBTcrgtlZ1MDu8uqMIM3tdhMjdx7HfmR2tHPtDx355WlrDcqnLSmA9+qGux37cX15bWX2FD/euj5cX7UvfUa5ShC+39enjE9EA4fE4tseDSrs+Pa+L9cNe7g+WuK2GkX95/XFZr0tbHcxl4TE+Ef3ww4tK++3v/vA//E//89/97j9t++OPf/jDT//633y4ffjuz3/ej9Ef+796upRre/3ypZTqIKhLqURkXbmwx0IiUYD4qUob1yqG/rG1re7gUos71HqEGVcEoKv5LMDdqAgrgUyUo/JO72cDgYTbskqRbt5Nx3689deX/fPLfv9chK4fb0UurHZ5ud9f9n70bTvMrbZCJEc/2KsRUy0AO4kh+hyMPuJe1qFw6BhMoZzDbjYIy1JY2MxtG6gOYlNyLtwWrst6uV1vH5b1utQaVmwMUnfPnR8EE8K/KvDjkQz2UKI9UeklW3aaBudY1pOxQYHDJ5YQoTZgXkto2s+ab+K9mZ7PyVyE2xSszAknnckntzcCmTCDyBlYozTjqSofz2nEmSzPzxEdkv8bw/YJGLup0VfVJ7Ok+kEABkyx3JBsSveZ0iZikHTjhHoQR+L8Vf2cBajPnQk/WU5EAplJKO7NXLDQkWPICEFx/fK0EzSMbswLzmzokePYDc5OkoONGNxj4jDCqc5hWZgjNXwSH4uLnlX8RDzec+qEXGaOyYB+/hyYoFU2bZN46++IikcKjeF7JGUzd7cQuvK51x33B0VXkkTV2QOG/rNnz+rTayLRyUSKJtXHDExqmgWAG4P91PafTUJIXMR2aFy8oACbm2Sed6Jzt1CQdAV3pG8MA7F5EnkLTKYqORtgh/t0wQOcmNNFj9htYoJmow9TDRhx9A6X0ZUITAIS1THUhg9WRjeqviwCNwLX6ksDsbM42JF2AmzD1czVS+PLbQGTDmfRYz+YuVQWBggssq7PZl1gcDEut0u7PS3LWriSCLjWfnSAhJhFvvn225fvf4TTsXczdx/raOttra3wQqYgpuXpst+PRWwMAzEt5Srrly9fHveHGdZ2HV3f7t9LW2upymjLsn76+M2vfvH7f/rTn37/h7+9/O2nn//05cv987//d+32E2mX7bH9h//wdzT6pw+38fHb/XG/fvtTd+99W/nCIj5ifwJcBBZ0jojU5u/iVUk6yEXIuMVivcmMKJHn8Gq1UAMyIxEAUrmgEJMTm5lZkoi4sKv24ePYj/tG5s/P623V++c/97G5WkD2pTBTA9D7Ya6mWi/X2poRubo7abdZPxEXwaFSmtsYXRHDKiNG1iJBTrOu5BBmM+rHkMe+PIatA8NsPwaLq6o01VFKIRKANCrpIFtEXZlAsp16GTSnwwg0HgRYIM+WpTGp+deV2Kzrw6gYIM7ZM7ObJlpU2FQTyAnCCsf63iTUBRDPFL4rUQzmA50zRZ7IUzzOuZ+c/HBQCbJTFGtOIemD0OacriEMzLUAKiw2mehIhYkpQ+Pv+jE5DUaWyxxPrru6kbCrgSDM+p5vzjXSZDrF7cfExlk2IzcnCFHBAprxirlwoN0JtsV7MGd1AAizE5U54YoAPhfnZpPicYPPsOPuzlm4T2AdNAlxWVxn13P2RNGpIUlQOfCJeE04l8tO8Mbh5LlAl7F15ge8jxOI3i9hNhoxp86JdybvgGDiQcwqzmdSyOSTMxE6pwLzM06A8mxNJXSBAklLYW1MSC3rEkIuZ+d7MCQGHap59Dndjb7CsgsEhPJ2Q2o2QjWQRLgbwUsRUoUNgrkN18PH8H6QKrkV5uFG7iKsh9kY3n2o9fs2Oo9hOqIqwdCgHhYY/FB2P2xvlWAG09uttWXt+xDi7iMI46WRtOIQLrUsddsO982ca6nmZna0WkvlKmSo2+NtvVY4PX243j5c4eP6tAqBpfhw4nFOoJbr+ni9X5ZGQqU2sDPTGAf3ApiLrNfl6enDGLo9NjM6+mHmTx+e//zHP2s35v7h+enzy+Pl5WG0cS11wfP1qT2v/93Pf/kPv/3tD3/47vHYd7VO/H/8b//b3/x3/+P9y/3x9lII3/3xj8uyLEuTIvf7vTYBy+ijUIk6C9PzLoI9z+XyiNckiCJAsvY0mIElyn9hcesZDY0sNz0i0xMXJqLoD1gEIHOoqUgRr+u6bC8/Pl5+fBv3a3EiLwvtr8cx7PG2bW8HkUQlqsPqutSlBlZeS+3HaEsbm/d9+DACW4jYBuPZiYlLESKSEhi1u5qqQrW2Au12PFxXs023t35vgB7HQ6Qyy7JcSltKrcRFWACQCOVTwFH4swXyKbMgzSIJuTkaadXi+DknyWdLnq1/xAk1Y4S8gZgpeE4XzDBnETHT4bkCOmM4gFjPnhDqiQxQzjHzt8wNMJjwpOM4wdFDPSgKKk8IFxOOj0gVFlLvoYfIkHbzrlFB+xmvMnhOJCROiLnDlXJU5CSzD4q0EYvTZ08056GUowaa7ww6z19iaonDvVvVzkEFxS4e5asjwRR3EIMZppiUlPyph7XNV6hYnl7PBcjIQD7DcpbriKL5ZO8w3onAE+L3OSBGbt4lop9XimbmBnwWXpnNcOaV85CCBhudVTJ7ginsiF3aM7nMMxr3yDvcE5eaJGu2xINSGCDnsfBzEWxij26wlAqL+X5kbJ4bcz7bJkpPnzxmG0oUfhoODZ85mifUyafLgzsBAiiQRb6parfRhbzrof0xxu56ANqPPc4uMwvLYVm2uruT9T62vR+7DTWRak5mEClq2rvZ0HGEBz3a0lS9taoO9DFgIuxwKsTO3qDmtGspLqWMYf2+tXU5xqMPe7i6Gwstl4W4LBdh8VIrMbRbP/YoFKTKse3FhYmWpTQWrqVeCtjAdn26ccHQQ5qUpZRSMIS0+3AMGmNXs5/85JvtMcbRh/uHDx/rcrsf/eX+cmyP7Q+2frkc3/ivf/kXL1/eyGmYvx39j7///c/+6i+B9vbypYqMfixrO7bD3VWVBkKcVZiFp0Y6URYJWQScODURKLR9ErAAWMSRVV6c99Rjk9Di99DFD0pJxHDzdJllIXemKsRE42m9PLbPn++vj913O+5cXY/Rt769PY77yOecGUWkCGKs6ASiWhqXdv/xxRVwUMiEqwZiUaSGVDgRtVbNrWvnQsQet6K79n2r22Pc33YCf4bZUdYVy0q16eFwZV+ktjHIQSwSCm4gIQaYA8kHzdouwB93IfJsOjSqp4i5zJS0IHeQM3GYH4WYZaSWYfoOGrsjOdjG4JDfPktCAwSYFdtEgUIY2M7uzQLW89OBiSj9oE5qkyck4pMyk4+tGZDX1ems4SK6pTKnZfdvoRPnX63dTrGy5AL5e9ycQZlZbWCWtYkiePpvzuaTgmZr8CAd+ASfzviW/dQ8MTP/TcxmakpHB1IQsMX52+9ozRzNEyipqTZnsBYzMWbEIPMdqJqhMXsIBnCiK5HAp3nWKaRNmCtnedHmp+QV8ZlsCDR35CYWFL5D7iAUZssZzLvOOGBpNE9E7KGwF0OP2EFJaRS3zOrviQFuSsziUwIsWpPMtKd/Tagz07uP3GSUZZYyjVggRMkxd6OYX5lmXvN0TqDElMy7xSFRpLcYUPXOrm7DfYxjgw73wexGCmiw+KUUL90Kt7Xtb5v2wIJKqa108qLj6M4OZuaipg4yA8siZOCiXo/uQ4mGX1op9UakgLGwDycCs4tABGAFiMXrwqAhjH3fGWNZam18WYQLL0sFjKTYcDPtj6776PdxbD067f1tW5cWS7LDhrAv6+KwbkYCqcJFHAb2skoxqlWORWD09nh7vl3vOI69D1Ap68f1+vzpmy+P18dj/+HHL19eXv98uX748LFgEW7729vr/vp3/8e/++Znv7C+H13e3t6+/+Of749HV/325z8zl7K3QoIyp1lMboYwaM0qjOBuw4gZjMLi8fRncuBAvS1Ku9A0TDiCwzcg7iazNGdkIEO0u5FXFndqbX1+/khjXwrdP//p6Iduux1muxYTrlFOqSoKc61VzUOvUEoptT52lVI23Y9tfzweZOcMjFmo1Sq1uCkxirB11IVru4JQ12aEelkura6Eyr6QVe18OJPBtIiwiaCQHZSaIUrGub0lDGIuYkMN8wGfctmxc5NFDZIfQRSORZbRmcIcBkBQWQO80FmguZxC+QQhid584uzBoQgS9skHRAKvsb0XcSWOYOIU8TpVY0lPQEOy8+kr/X1k1Ev8OqpbYz8HEnF/TKDfCdCQhg05lwkoYWIemBPEaFCYWVVt6uGbzdUKZKcSJfMU6c+FO5zsZHfMXdH3oB8fwEKuNGuX2YskDGNmZdb7NJfg/SxkzkBM5O5K2Wola3PSed7FOiOnmGmCIZIINyb3KICwuCWTRpko/LlpnWkkS+YT6sE5mj3XufJl50d/3U0STTmgsLKZg4fw9TxJHdEB01xrzH1uzIExAZ6BP1sImwongYxE408EBk/QDO5QBIdndgIKBI8TbJQFUbCmiST22sLhlxB8AIpCycKDECLk1sWHWlfddBzad8IgWGFQLTrABcWpKwCqrY69GwBxFlIzIlnWBihLO7oNczMFsw13ENfisN7pGOjKb2/96VbNWWoFTFjbcj3etmPr7irkrRBdl+MYLliudbtvDJTWmJswlqVcLqXUwkKmY/RRiLSbq+s+7FA2Zapwh2srxWGjD9+11hbdJQu4Va7sCIkdEqagpR4vmyvZ8D+//vnp+nEYb8dObCztZz//pTETPaSMbd/3R//773+7XC/X56eny025j+Oux9u3H5+/vLz0ff+n3//uGOMvbxfrO/GlFImrnnhx3P6xzmp01oVJA3MKZW+bPnnRnY2uVATGMfojlqFzpZw4OB3MomQUiOUwSNRFpq7uIJF2WZ7sYxUnOyy4ULXc1vV47Mdjg5J5caLleuW2bscwAxdhKVwqDTi8d+1Hd9XY9CSEqy0JMzEstpHN1rUu68KFwF5XWdZ2/fi8Pj+15+v68UnWWtYqtda1gQu8ixPtxsIklZgxYg0YROwDTrCDPUmLJFxIChGF9G7inuEKEhxzIesxBWZTo2iEYwansdnFjuStWNimOghBscYk6SX6MUXuJo5ACaIDod1L7uGHlWicT2mIiGM6UptzboACDpYMwylwirN+jJJ7TgFjPMusppK0plycZeF0vZ3QNM3aGjnk5hlXYebCGRiRsqOnszFyzoT8P27+HhOJELkqgycF4T7nsJTJyafUjZkDFodUvoJAEtFK1Ci0cCgwa862A6A5mZmwt58geD4ZmUMorMfm9cgJyzyTQcI8sw8CnadzLhzfLTeNg8CUctO5bh0tBYuZns1RbNNMzJ7gSduw82NPqI9yGjEPPfoJpzMDIMH66AcCQeKQ5jdLe3g43NjJh/HJbAhOrTs7Qc1jEhN6HCPA4LlGM8XJg+wWsJW7xUIRx0VTZSYnNz3Muo8DNrQfjmGqwszSCLrYKkTKGzkE0G0vRdZ18eHHGFJF1RnUWh0GkOne3WHmOuBuYxiTc4Uz1aWVYrX6MvDYjtvTQm69hzSMHsdm3fo4DGgLO/FxHOtauXJhSGFyY0e7LMJwN1Mz7zrUDcfrQ3cdh0spDHZ3EUGuPlsUYotjXVdjkyYgMEsQn9tSYyDU1vb6ch9jjK5/+OOfmJfHASeV4sc//hMtZeuj37fL+sGKm/Lj5d6PsTyPv/mv/htffbkKybK9PWqR+9uL1EWIRh+XqzDR6HrI0UrNGwL0VZGVUNCMMaRuYZuRaGA+QMYmhAktuhNF58RwoLBq/CMzMMiZJRgMwuLs6mY2UOpyewasvnxx+/5x7yJ2rUVW4U7D1RXEBWy99+04DqPGQkOP/aFGvWvfj76PUqRISTpTrKwXNrWgzpCADHA1M4K3Wm/Pl+cPl8vT5fLpJtfmxKUyFxcMUycuUIceGOwiAJExuMTDnU8gcwj2OxPRgSFEHEU3iCjW7gEKxNzJdDbnxGGgFIsDToCIAYh5bJ74fCuiJK0zkabbewTrNKZEIsIRazN0sGNqEEUtDBaiM2fMSjN+K8Kr5x5vwBlzNSywaEwaTFagSSPPsvFEbJjYOJJBBKZgrxjg5lKEgGD3x+MQzD+WOBIPonoiyU4e3mYnFjQD1TklPqEveOJS8aDFv86A+15Nw73E5MRzLyIXsnNcmMgLO70TmRPniYBrRkTmStkKuJ+zj1A1iIYoGqVU7XBgdtkU7dh80nKNLAb1GbBzuw/kqRcRXzO58oSAFxMnybxPbInIIbP02cch4CY6n08gJnMxCMkWc5Z/cFfou9oI8hlX8nSdj2Xx9ElJhU9iJjKj4bHP5XPDLvaLzObAB4BadhJx10VKUKXkDBibu6rqcFIbh/sQsiJkYGV2Z7cedz8zvBY6qNSq286Sro9WWLsz3JlCgT3gKh1qIBtuBinCSyVWVbrfey18uVI/jJl0WKgEO41SSxEZBq7wnoQS97HerutlPbYHwWqhVpuqjmFxw7Mqg7oNU+tjMPFSmw94rLS4az/MtYjYGMyX+Rg4AcQkRZjk6F33sW37cfRtP0x9vV65GLwY+8vjgKNx3e/j9XXTfdixr+vy8fkDVXz3w+d6Xf7w+7//H/8v/8N/+PvfEpXLurLT9XJdrrfRj9GPttShRuY0QPBam4NYkuQ2o372aA7ANRs7IiYP8YwAr53Cu22WJSAmcuYgMzBDrUuRoUpzBiBcwg6CChOKqzoKZKWylstT0yF+OOkw6957P9wYZv1OQ5SlfHj+UNpqSqhMhxLQWnu++ejKRNuhgRExsWvUP4F9s41xPAaJXp8vyyLXha9ruTSppKKDlkY6dADFGAI2knPrigA2Iydxg03XERLmkJkzcxYQqQcIJiByFnJQ7FGHKl4+izxNwgmI2RjZyGc58ueEkImlGGLKkBSNiN8+ZiuQi6+YMmE+Ifest8ycmApxzggs/LGQuxlZeoaq87mmxEQRdd7hh+gtEtDGO24/0fQI9+Zz5myOqWGXUFGwSNT0NDr2KAXmmPSUygSSEsqJTOaIJaMxzYyXc3Bydw63hWyScBaaSDA7dc+KTxGTLPjfqZNgSQVqP3/PY4GRaJ4M/ZpdEzW+T3+E+cfPxBHNyUxbX/0jYomZcf6WAznB9wAConuKB4sIDnIoQsTxfXcAEzybjYBL9hpIqth5ZJ4lwdwgn71PeEDHadVOoGD8MjFgZi5u7B5mOBSU3tEZyUwFwEpwkBoA00OI2Ge+tsEeTQxSIs4cZgGX5WUYmr1YFpbdXI0MZORqZOQDZOHiZCDtpm5UxE1JKNcR3c2HQ4lMmIc4C/oxzHDsYxxdFW58HKZm4r6Z1sbqRELLUpyEYpHW4WYGDYX79XZxHRdt27arqqvbIrUxs63XpUpxG3DXY2zbQ5iul0uttTLTY9fSCykzDx1NmprCvI8dpn10qURu/dgfm6A6lMnBtcLJbDjBmUKLuLX6uG99dGJZ1tWLDydzAbNwLWU5Hv7jj2/S9nah52/WX//yZ697//Lyw//1//b/+Iu/+tV//Lvfffj48cvr/a9+81dG3i7r9QYzc+396Fe+CBFTkZKGtMFj84lm0okAObmb6VCDesorBqsHQqqTQxTCEsjRfmoEEWj6jJkrkTOTKUyttuqDh2nAlJBW2kIDPh7m1nvvOsgrEZvCCdyquQ1VU4i00XuppdaidVyX9njdCChFQm0fjtG7mbEw+WDyRlSLsIPUSNX649hcmrEYHYpWGIJjAAISIwlkREqhXMJP18t89mNcKQCBRAByRezFsoj5rNCZSNhBzAVnu08UwcWFEUN1C3iJubBZTCQZpoGSe8a60OKMncagPDCJwBFrGjQvWMQMALG26w72vHARDmgC9PFKiuW+WNHCfCJBpxtM5Cp3iJCqM1EEmJz+I6Ht1NmP2B1AteW8M10B4qgpq/NJX0IS1C1TQuROwINuMAcJlEDXHHLn24AsowzotAvOM82nljWAEh1EEJpivy/6idB8StQsfzkxs0DuA5yKRS14DvkDOmecvr5nI5wR1SfPNw93hupJGU446R3Lm9+MZrpI7Cmq8RQr4gSe8tsacTYBbs4lbv24EWwumYCFXG0+3XBVMGGG9ZjTMidvgQDYcLPY7GcDsslwtsCpzd05VkKMEIwkVYFxDPs8cD31dMqcjWhwxeEijCA/mfqIG0kBgN3GcHGIu+vwkaMcuME0jCZQoaKspdSjdxaqaxk9V1adXIR1qHuMmlCLMPF+mA0ysA03M4WZkZm2Rd7e9mUp16d1jGGszAo1IZQqTl6uTRbeH9tj2woXuBKVpTXtw4xg1vthY0gtRNxac1ez4W4Q7/0gI4i3VnQgiMutpExV1y7Wa5i6lkICD/59dwJYSm1LHbQdqn0wsD22ulxLazr47e21ykJu620B6Q8/vtCLbvv21//2L//qVz/7X/+X/3fRz7eP3/7Vb/76P/+n312vzxAisLoNs7fXt/X2FHN7Na2s8SDATSnlBGcTIG6WAvWAu6tq8EKKFNNJdwM5ws/nHFDl/0YxGOhjYMexOUygIsLEyoagnLPUda0C7g90djvGshDErRBVrs1LNaL0Bixi5q0t2+sx1I59H859jPwgdzOFuw51OBNUh7MpcbnUwszm1kd/e5QqPg474DpoFAcxCcDEYtHwCFuXiKFOjFA9C0pr0GCYSBgsgZLCYcTObIaoDR0gyZQQHRbXkCQQcycRFiYXuMMJwm5AdA1EBvJ5PSAcbwUR6xltDCRxwMh+zUESevxEI+R+3R2I+fmsKnOWGVaUlPvg7mFYf0anZEImQBOs0BOBCezIPGUAKDisExYxVczh9rwlAs6ewEwG6IAcE3iQE6NmiiGKpUDQRLoc0xpgjnZ9gjVRmsc1iKAf+6oh3wCCWQk/h3c4yx1R2riTpOiEZ2R/Pw3RhlD2PdkTzCQxXbZThiHfNV8aU+io1fHeXeAdfZmTATrT4JzNE53BHdnGRvWfsyB3c2hkovgkCi+e6NBc3YzgKcJjybqZRE/zYUzgWE+PFmUYwzlCM9yhEjMki9vHCc6lwNWHugWXjIXIzE09t3nJQ/hP+wABZhD4MNORc2O1ECDFzLVxJ2sfVMiJHAonVRtQxQCHtKzZ9J4m5jF62EgyCxXxA1KktALjvtuAjq7EVAuNYTVUaxy11XEMj8ustLv2Y5SC1nhdyvXaLisbSmsN3HX0tZUD3m04OQqv1+ZGTCzEYxxQH1s3VR3qQBGpVUw1FtbrUsmczFVjU42ut8u+35e1uZdShJnbWkrQWZbiQiylNNnu2xjHONzUx1A4LtfrUuzYVAf3vV+X9YDWUvuwtZXX+yZCTx+uRx9Uyv6mr2/7b/71X/32H/4wjl5b++XPfzHUW11KLWMMddu2bbnehGVZGzmzyLn3n0S2CfUjWH8jajjysIwhig3KzOoOEs49sgAKYl5kxsTD8z6ZAy9iZtdwn7ZoQMEitX349ptlrduXH4+X3h+m6kRShFEqc0GpYQ5Z1oWo6WGq2o/wAtIixc2rSFfre4eh94MItRQSHqOXGsBn3GuhTKemNLbtYOf9kNbAjKSQEJfCLMTsDhgxszowWbPMopP/GosOJKzmTGwgOFgKhH1Cylwk3GSirO0HOxGkELEra8qVsDtYGOECnroGcvI12UMtk+yISXOCzUMjY7GDSQqDci95+gCrKjExC0ctZ2ZTgmzisGAijTFbBjpP9lBK0gTwhJD/5DgtZzCDmVMJiD86EXvvQQIcz84J0RFJhoqMqZMWmfPP1DqNLsTm/lL+/atoaW6BVQW13vxdmi3SmE3hiijE3b3YzBVEZKpSBOSuiP3SBHumncx7oE5kKkcL7wW7J3EumxxH1PsZT8+1DE85Ec+/vX9lmm8Vukg0de4mSWhOFzjxLg8fXYfqgEMktSuYw+3IgUBuQl8b7ppzXbXYFZcI+UMRN4mOkE03tdhfZMs8QWakSgCZwxDoDYbC3c0w1MM6gRiWAwNT9VgHVqXQhAoL02E2hmU2duujVDHVKFeISW14NHJEBqVWFANigMMgArNz39oZzDL5uAQWactSiOH8GA9TKpX1sOPQ8Izf98O8uCtYSKCK4+jUPfxG9m737Xh5ebtcpMilCI2BWgoLOVDrqmMjJiowSO/dTfvQvh9961ASFnfUWstSzV0Kae9cSLxQd4EMGlA3VbB/+vjhsT1qqdu2SStgJmGRAimA67BS2+V2u79uptQ3MwOZeA/xyw3ERdreD2HRMQrR7dNNmnz3w2thXD9+vN5WHePtx3v78Pyrf/EXX768LetrWRY7+v7YlutTq7U2WS5rbcIuUS1aQHPEBBiyZsy+3j2q/sC8gyxBU2pAzTUV0Cj0bfLx09QoCJV8QpJlEr/0WT+B1MzU3VHbImSu+qbf37djv2/72+ZdK7G5EnHcGqbDtK6riMtQ2vbdjhFNwRgGh0hxG24mIqb92Hcna2sTYSK0pUZFHdbRwmy928HO6n1IXXQYg1RDfxhcSvTMzGIElmIOJnGR1EIDhjkIpRaHExcQuboVIZEEP5hzKYwlQJ6YH8RfWWqOWkVI2Dp7FOYG4kJMxBIeToYo9OeckyhoM4jdNGFiIS/mRFKCPURCMWSFZeTirL7nKpJHfWskLBH84yLGaMDOWBf/mXvFPpudhKOjXjCAY/UqhkIR8RJJkixuJ1pi0RzYfHeaPwNYhzKLvRfqMxirgRnTkiHib4T3RL5iI9kTDqHp2Jt9T3gCT009l8Khuu2EUMUDTkOwOYiIsjxaAZvIzwlaZS6ZeEyq+jG9s4aA0IGYrzgzQHY68308RKcpmLCW5xNzXj8HCQTP3iqOwFSI3c11ZNJwI3KKtXt3SWpX7nREduNSAIUZzDlIc+5szsCcgHvMY4OhYGPkzq+ZAaN3AlxHor5q3nscZ/bde5/suJANdNcR2UsV4Ra33+8e8xNhYTZyKkwlSg93Hi4GTCSRopIgJ7dhThYlhpRigKzLATdVEvismWLSZgYpXBpMqYjAKdKbD+tj7xvaUvV2GYPuj/H6sl+u11JLqUVE+/Ho2w6Y9kcqawhqLeMYj/3RH2P0ISS1FiEhRmnNTYfmPQDVMXZXAuyxPVqtNoYttdYKQVtbJHU3b61xLQozdT98uV5ul+f72GVdTOlt7Me2b9tG3I6tt0WI5bE9tn2Yo8OXy/Xb8vHtsS9rJWZQccP95e0nv/zZp29/2o+91VqICfR4e12uS2UuRcydCVIKv9tf+8kXoHOLMoMNI6VaplIY3PxE+pJ06JgrgpjeuBMBxRwSmtnIXtCjrTv6QYCrjr1v2z7U9m3ftr33TmMEFa/rUO6X9txaA8iGC4kJlyL74xjHULVS6r4dqupmYwyfArRSyMaguhQREWby1gqRixCgBNLeiXjYQTiYJCBhPUaprT8eSQ0RNgfXSopImbFVG3aHcB+AkSsXYmKIkXloogmJiBu8FBZ2FieAqdRmBioVfGic5lpIyokauRNxodRjQJA3JLEjNyepHCaY0BCCZkMPERzjsHxmY3JQqCVG/+NEnGiVRh/h5AG8hNUxJl80IjgnD5Em2o/gdiTXiMXUDFZj2Q+Wy7lInmWMoPHO7g+Ds1P+Gu7knkXt/OCJi3+NhXtqK3DWgVnjx+8Ed25OVKcWxZwzuDlzvlOxHEzH3NDcp/9X+ivMacOZNXByjowmlDMBLA/oBf8srCdx9itYH5izcpvDhTjyWUghlBscTgbF1KJIYk+culOj1SYcZnNRW2FGk5iX9h6m7ARXthP/UopdMwe7+VBmguYEGA5Xgxpx0PSHDQM8Hie4MYkO865EcI10ogBcBwzWOzlsGAlDs0WwYYBJKa6mfUjhXKjOcKBEbN6lCAo7HMmYFxJyMrCxSKQyj1VTylm059qwp8pJkYBWpdbLDQePVghO5qSvHe5FeBhqK7r3ItyPQe6cXQ7p8Mdm64X3nfbdW+N9U7qwOW3HMfZtHBuRiUirUkv1fIwIoMvlSkJuoy1FCvsIWABMUPXehw9vS7vcLjoOEnYYV7bYGuHAWJnARYS4KunYx2NsUGKuZrof49hHLddRNh1w4vt9q21pdfny9vjy+miHXjf9y7/69cefye//8EcMXZZ12/f1smJYWUXKtV3Wx2Pf9weqMNCPXXsXklKrUCwfxo3N2ZMSNFZap9RiqCBkK8wOovTdCIQ4cU5KN0maTS1C2D0fojlMBhOrDzUL6DiF+w21tnVd8fzc/BcvpK/ourlQBL4qy6Vc1mEoNWfCNsLL149jiNPowxXaR26Em4JZGAQT4nVpXFAKLdcmmRARn+tqROQDRMQQqBMLEaso4DZiBMsipftepYYWsblxESpBGDZm1qEsLEVQRG3EpBciCnBh22FF3EEixDzKBhHsQsIgduKxQVrLcTkzmBw8KMQhABYSsXFwEcSYNwalJVKO5aZ1knIZwcMRArMruQdli0WqW5SiFBZR0f6Fr3dIlqVpCii8oHDitDFiOE2Bp/JOhl0ESGMxUZxtZF58m+EweKYztiJB3YiTgROCbGgugaWOsk80hByWvrHBpLIAYd51TAM7w7uaCUiysvdhBbnHm67HibPkzpnPzjW7JMPX5XqULORBupobdA5gWj+ewBkhBPdOLC1Cuuf7ZlGbndRME57d1OxAzrlH9FLBoUBswsbGlsYAwCTYAvHZwzjKeDdTNQ1NRGdAGDCQuXfHUIf50NCBMXNSI2DoCKlWYjJV713HIHdiMXXvCnWC61DACGRjkLNZh7kbmGYNGMlVh4kg9wXJwzuUSG1IKQjOsA0dBHaqTFyI2cgNJiIOI3KatFGEdbCb2nBXkCVz0bxWOghwl8K11e0YJKVWGiWTspEJQ5hKKXwMNhmDjmMQhr8+jnFI8VJ4eVmkMoFBykRtXaHmQwlWwK01qJNxrAQu66Kwy7K687JWN9Uxjr1brBsc49DOStq1SCmXRX1049aKsHTtzBxwm/ZRWglAjLmMMV6/vB3d9328vGz314NIWKopnq4ftsfx2Ld1vfzsm19wef3y+ngZ2+/++N3Tpw8fPn28v93v+8NB7el5wK/rZd+3o49h9tgev/z06b7tzyxtWVP4Q3O/xtScQQ4Wt3RR8gB3w22bCJrzPsSvzdImS8Ug/KS5OkXJNrU5olk1MzcSDssVInbYGKHTCKnFjhFdgrsRjOBFyuVS29occhj6oQYCaakA+d73x/3h5K3JeAxX3x+7u7IwACpCsFIKYLWJu5FzrVUKqw332vsRALeRCItQYcBVXR2mUtrQh5lJrQ4SgfUBd2rFRlcHhMZ+SClu5O4swUgjLxpbJGVpXODHgJOG2ouIVGFhc3R3LhXMYAFRChY/ihRRZoNTYRDHIJqFqVQjJhIXJmHKfWOBhXdNwBUR4wuHxmUEOpesrRUQhh0EBos7iAQmAElgOMlRIQFnv4BJBSU4kWTQAhBMLlLPGBeyykISUd0dOtcQmMgiNKjhnRUJp1B2yz44kaQcphKB44YJkTs6nbIS/SIzjVXzCK3EkkNBsLkLE4FjexG5IEUuVHw2GFkmBD3GXc99LJp4/+RpzjwYcE2SgWazS0nR/6p9wTsLKcO3mU6caOofMYXKWv4OIyLnyQk6ObhR50YeJncfapOnF/crEU47BlMjtzB9d7NoHYXJ+oBpYGexGqO92xgUxMuhqRc61HrIEHoOM1Vdh6txKa6AQY8e5RXBhav2g13MBghmSo7pVBAQJWsfcJVaWaQfe2kiRUyHQQOVU1ViyFriclgYL4A0t0Bik9KZspZRN3eFmTABFOQEtyHsXomcBhmz18JeXJeaAkdwISoiZr60psNGr2D0gf3LW9mortLW5cPAo1sRLyuXWoj1CD3BPqi1cRhAx3ZYdyltDB1DWUSkxHgsCEhQ6jrGMQBISRbNcrnsx2bufYzleq1LMx9UBIx9P8ydpJRaVc0VY4zXl/u+jcfW7287WIQWliqs3z5/8+P9dXtsVvxnv/j55cPx4+vbn374vKv94le/+Jt/+5s/f/edk6EUcNmP4xe/+otu+sc//OnT9Xq5XFtbAN63XsqyPDUHmQ3kql8hx+Q4JnEjBLZjqGQBBYGy+NPJwAUMSM9Xi0qE4YhtAQ2uoFmu0ZhRkcJsZjoQk+C4JYbq1veXty/by5d923QMduvHGN2MZDjTQtxaqbUUGcNBbBPiNfP9ONwHEZGrlKkzy8TEIiU2k45jB6NU1kZlSLhZMIvzfMQtvjvYSNXZmYx0KEnWg/vxCGSbmFyVFyYW76HMQObqQWt2H3pI4RjaGYI9dBiz1BJhidYIDs5FIGJGRMKtDh0kxK1Y8Cq5GBMVIa4kBUzE4iARdkIp1YINSGHcI2kc7GCQq/skY7GwKQCCMLG4A1wQcHKsbqTtQTBWhQAn5hj8wCN35yTAg+ibVcAMdFEKMAJn5q+48XO9K2D9WQTTFDNDQjphE59tZlTmsXqYtORYqlbVUwSCGDbsHFalJUlONyzHHGdcN5RQ7AlGLYK8HCCMuibFFho6+JSV+xn9s13wnGZ4AnXvrQ5PHInS6SZnxplfPYGwCRh5nrgk9pBEjR+4CpLfRAGymqfRi46wXBIhGNxUmDjI9Q5SI3dmuGmILnJh27r1we5qZmpwM1V97ID7GK5G0feYwdxHbnhFQwQzhxGgGOPo8PDriljvhg4zBzu8liJg7WrDYjObinjwhYgYChaJvff9MHUiZ2HrgwsREwuThDORo7GJB3t62NBz7Zvgphyvgrp2smHabQxmrrXAoNqFAVeYt1K0eCfTMcxCW87CqI+F22XFaEbqYLWxP/r9sX9+ebvcSmUsutYaExiS0mz4OMY4DjhKEabSt37oWJfF3BgyxtDjqK2URq7GQ5k5ZaYRiAK3tTGIhNytLpVLrUvpoV909LqyHmO7P/ph7sTgMUBGTOX19Rj7oZD1sr/ejnVZa73tNr78+NY+PP3kerm+7cbY9PjtP/7+X/7mX/zkV7/+7W9/S01cmGtpqN/+7CcAXS5PZalSigPErGNMSR9iptF7bc1MOTVCcildh4bMQzyshiwaojwgDnml8E+Pmi8iDnwEt5gjfxBFfhEg5VlYhBgjLJuHOsiZ92GPo4+jqzqN4UMBZqm8rNf1YnWVWmtdxzjISVi06ziC3JQkxTFGwM61NCKSIoANNRtaDPu+P3+4kS7kDDUugrCbKJWIx7ELF3M6eqdUKVRXA8TUyDB01FaZCepCxQ81HzbMhHV0LhIjY3djMRpExHqouaEwXLlV0x6zcTO4upt7K2AKobbjTh4vrqFLTwDVZaFWjfaYHnAVdzYGhL0ayEkKiMgURdznqqQwZWkciAGF/UaAVmkp4hy4PIGFxJGjAQIHI4xYQKkaAmEiKhAH3FxNw+WDQQYrVMLKEp7Snjj5RLMijoSMjHzuBGFxN5021A5IDNg9Bt7k5kFqDSUMDZaqQ6MOJpxYSuVg+cdSW9A2CTACx2zfHSUq7BBZTQmg2GiYHDhTOyN/3NEBeNMkV/ocZEeGSpDeEufK0W56Ucar8P6HMsK//6NjLkoBKcJiQZCM4a3BPPiUIAIEYepspJRKH8My/aqxAzDdRk5hxhgPc1Ubuj+2WKsbrw8gAv2woRz7vepwt6HhsxkUCFN3DVonux2mzmHbHEMZh2mfY8NBXPoYBIhwBBHkJNxFxMzYgqREZqPWZJ6EBBYhJVZATEJUmasbezSY5DRM2dx8mPW8fjZMj75vpkOcyDMFBrsjgEO1TuRMaIXJnIk7HMINpN2okpEN466q5i+fX2rjpZX1Ipf28eioVcSkrVfYmwgdjy1UgFwLORcu6/UGtr4fY5CPIYX2fScFp2QqmFjV4R7i9ObGS5XKXCGFSy3Siuru7kO7Pvz1x1dXLnUx5cv6rH176Xdhvi7lTbsp7m/jOF5ZNmb69mc/WZbrrqOrrh+vaOX15UuDf3m7H//4O14qKn/7k29ru3z//ffmLpVLnPaQwy1MHBr9wXyUUiSeVYUSQXs4bCTgEI8s5sooQKrDNMB/Sb23gIocpkkBcCYdagEeOUhoGpHG3Dkso8DEbWlFHN63Dx98fzuOS+/76N1AZm6uUqy46xhuDzncR4gjMBxjH64aVPKhRoCqFmF3LVJLEZC6W20VPEpZQNz3br0zU5GytIUMSoApXNxD0doCMtfuTOxqGE5MhQXDnFl7b+tFg6furuMgZlY4heQtXNWEpbCqOiwUky2gUoqpiWsfMD+2XlodqiLV3M0GV4lpSlCATM22nVioVJKOVgkEkWAcQYCi7jBiVCEusYwGQJoQq0doYSY2Z0FkhXD2ABPY3LkUdw0QBkqYUm1uRmxRw8SMMAv4MEAzgKGp/huxcJg7CwdSbvGdT32x2IuKuao7nJwsIynNrGAKgCXV7ByuqjRHqj5jMIVqjp1US1ASi+FOIhKIPXOJqQULuXqJ9+HM7dEjvHMWkOMJclhSQecI5L2Z4fesE31GUpjoXb8hkNJIgKmincJ5kQbfaT1EzkF8BJupm3EoO7vGnHNyMxwwj8Ur91iyzB8FbGoWBTvBfYzeu48e1KZ+f9gY1rureR+kCnM9Dhs6ji7CRQQGUxUWEOtQB6kaU9gsONdiauzkPgAvrZpqihqah9uP6ZAgYhUOmXiARh9ErH20ZYG6m6sdXISFEfNkJI2ChZzBldEE8VyfuqbubEExUJDrcUA7TPXoNnZ3U3VXZwJczZQF0tjNRx9jdNVhDnPXobUKKZlzLeVQFRIqArJChdiGsRr1XR97r5XJrYiJcF0W633snRxDO8FrWaVFb4Zh6rsyO7NUKX0MHc7EVRar3WCtVGHoGNRIBHUp5s5V1uerm5sqnFzt2I9jH8dDj34fw+tyrW1Z1uux3b2Wj59uY/jLl8dQfbxty9q+vN5v3JYPF/cRzLOnjx9F5PPry6ajXZeny6d1vahhuVwe22NpCzEd297HKNqZOR9KIVOPap/d3KFdCUQiqhpAgEtUOZajOLM+dIzBXGg65gFzCjXxfp3dN7O4Www/4a5uQz0nZQ4LN0AnKbW19fn5U9F+H8d92/Wxb9t47F2JVlqOl01x1OVyuVyYxAz74zi2w9S1D1VPOTOC1MIEg0Hc2IhQpJS1MMmyFPPhxkO1CDnMhleReH770ZklQoEwj+0otekYYszgvh0ihcjRlZnHtkddrUNLEWYZx8FFKEaV6u527Hcwz69ItquNUZZi5n3fAIR2D7mRO8bBJDAjc4eV2uCDiKEW6tNSW666SZF1YSY9eqiJUhGA6P9P1Z8sWbalaYLQ9zdr7330qKqZ3Xv9Xm8iIqOyyCyoAQLCBBEmzHkBhkx5C16AJ+ABeACGDBgVCFJSBeSAqpTKiGzCI9xvZ42qnnP2XutvGPzrmAc2cDexq2qmp/vXv75WBcS6rCjRXWdiTuZkEp38QRKnEYlARFjvZYwF4dVkIyDA/LVwwMORnEQIBgdACkkKIgqCTAYi7qpOlCT93m3I88AQzjsVVGWEX5OVS1nm8+TA/FyAMOXyWYuGRzDdOwQ9yuXMdwN7+RsiU4grDXB2EidQozmy4vpmDgbNxXGKWgvBisiKnbjztjMotfD/0vfkPCUSRDErESmLkc6539R7EXdzQt5bjAsJxeQXapVOQiKM0piI092cM+vZJCqLdoRHerAQImuUJ9GMvorw3tMdmYjwfqS7Hzshwyx6T0/bd1ikOVXtzhjIFFCaO1WwCPVqQQJ7+KQvUsJj2FBV0Mzl8OF5L5cOiwTcM+EijESMwF0bKMIJiiOwojbNDKTVin5XmBDA5O6yMpSDwcIBz0xmjvS6Cc2QxPDSQXFAmFJk9OHD0yIdbtUTIPPaSqELt9BIRwohq9XRfTShgJiNjOSEJYku44jb7q8v+7YtAMBtCX8UYW1VIxJMgBCzD1dVEsq819pXVXLdM2y4R1pMJTKFBxBYWNfTlhTaZNk0yHXVrT3k8Lasn3/6HIlffn75/Hk/bvn47oOoiCjAERlsrS3bqUU2XZxYx7CXt5fv3p/O5/P5w1PbFiceNl7e3m7WBdv79++c8Hp53ffeR//uNz9I0+Pz63E7mm4Z8LAUZeVIN3czFxFUzA+lNBGWPo5Myl7XYxJhgG7X29EPaa0tUnBP5Ow8jQwziwx3j0xhLUAhSZiFmTycQKKamQwGgmyGnLslS1uXc677oQ9Bi7lya6d2lm2Th023dTk9PDw9CxbfvX+5sZN382OUACkJvBBDERERIjwlbkQWIe5NyMvNMiswU4STKEDmVun81UIePuAanolOKH9iTJYYlG7UWnjMkgCOiLT9QOQM52kCzjCn2SKAcQwSzggEoqeHt2VBAkGRsb/t7WGdQm1LSLKwjy5NIyNikJAsWg42CJN6uIcIMlOYRXhREEOYRcbRSVi0BWcUD9s0D3CTOgBIhFNhFNpYlEhrhpMQwIhkUarPDHIKvnmWo2Y6SvwETiau9Sx5Yh6Ybi66t+HeUXhkaaWQSKpMftZ7Wc2siUcpOL8yqHcYBxlZM50Auq8idNcb5Syzm0EUILh7MHPcs9GmewEad4U+6C/1PdU2f9/wa5DfTd51R7gfQZWmRNWmkYlE6U8nySwlXsivS37dmYvpJiJkCNGdU/apqCZkBCMz4771F+wUiBRhSoMnZaIydCOy29zB66I9RnQLswyzo6cZ0v3YvfcYxsTkmb0zKIcjYWaUKSIJRPcUDktpQszpEe7l1UyKYQeEmTndx3BiAkkgihgnCm1LgWd1YLHWWzxteFtaJZZsp61ECEmpTSIiI2gCjEJKkZ6oK7KRLB5uGSRApZ2U5gehwpRqGRkBVRsTNqxE7sxwH5EeVmKDEIFuTZuI2O1iQNoIZlZBVFC2cvRuIzycMono+nJV+HZakL7ImTfeObaVkll0RskSaD2t0qaOxZMTWJdWomn30Y8jRlGQuWqLTBECoS0LE1PT5dx67A1IIV7UwpOjbev2cDo9HJ9+vL38fPv1R4tM3VYmPDydtsfTuuXzh+e2Nndcbzu3NVVIcz21YN+eHx+ePuiiX17eLi+vpHo9bKV+ejg/v/uwX/d1XUlVF43wti773te2kFuOmLIugrAiIzwzQlMTycIqGpFmo4ke3ccY+7EPiw1YlpVFJ3PHEkUveY1SL3qLCFKIgBSDzMpC5OGeYbN+mIi1uQdJY1mSGniTdm5nE5W2Pchp0/O2PZzPz8/alv7af/7lz19++eXzx1+O23W4r+tKxLJwItOTUr0PFVYRIujSMlJYpAngTVcS0hKJErEokXgEM2lrIjwz/QmtqZu31jISET58fdiQSGZ3b625x+16VVFiCbNFF2SCil8hJCip5NEVikX30SNB0Z1EmFkCumwEdh+ZwffYAUqO7gWmUWSGUWNkkHF0AxukRhtnk3QFMYlYISBr69cbBEFMItRUmOECJtIFUsoIRmRKJHmgMiWUZ2J70MS6uT4aJeWC8BRUTqcIZQiQqObheYxUSxrNsyBLscM1cKjohKrC/WrULbB4ojL35lgqy3F8vSVMHBx3QGX+Pv/ZOlntQ1mnghfeI5SZVaCidwBpWt3o/i/TPZIaSLp7l6tZMucfT7MupiMCsxvgzuLev+4ugM2vRw0mUYz4ylBTJlPFrAaQBcFkWKl3iJKCpkZo2F3Zbz5GmucwCiAc7m7uvRMhjiOGjb0jLd18jDg6EDkGJVEgDiu5FRNlOCUlxyw6GFlpNPcMn8wMEi26YvY8RGhhjhlcHdNEPAsugpgYDJTcKJNJpJWuUYgrE4hlxguUvIpZSMiHsTA1hgCzhjS/iqqiNDHV9E4Q4mQR5sMjYnATCWU0AXx4XeSYk5XCgSYA9auxsC7cXM07HOm2qJi5ULZGHmIRgqRwGtyt75xv21uTp/4UtIe7q65LO+Uasfk4DhVhIREe3cYY5ibMoQSPMTqDVFv3TpHEIE7R5r2ziHX3JdvWRNtKRErELErt6Rw9H989HTf/4Q8Px0U+//zn/cjjSB7GIpE9SbZVt4flw4dvoPJ2uVyut/X56fzhUR9PzqQq59PWltO6nG9PT5+/fFla+80P3yLket3fvX8XniKkLFpKXA+sHBag6L3f3l6lLeVjqKOOdpCgrU2lITkj9st19N73frldZW3rw1pEX7qD6xIQ5j7c3KzEKmASaSX/8MqFLXUIwdPNM82RySJBuWwrItO97+f2cF7OT2BSbcvpLNvaHh/Xh5WZbrfbpx9//OXnP18vL21jfv/gPRiS7twkwoNLDM0i7fJyW7aWJE3VLDJw7Jem/PCwZGto3EQzyT2WRRmYcQDwWSUfJiKe4cOzfs5IG5P6ut2uFeqfEcxa3UphTpyiYsOY2KK89MD9EkrgmkzhQZaimQ5QUEpGAZ9EyTnu4dvTnhUwj3FQyT+EM0xWJeaERzcMk3VBH8wE5rARSKgkEal4P9AUoQCgA8SkC6v4wdyUREgUWTFiSqRU4dEIZUqhv/hWwxPgBIM9k4KquMAyyhlIwsry1QeQ4ahPPmEeGIlE1d0UskT3/bhMAFN2SvcWlztVes/AyfvMT3hlj4sU8B8eZBAmnyzg/J8azjYsAa1RXrP/LsMpHB/MFDkve1l6uInsz3/S77G3s2JmUhpUbOfUd97PBpoxCHfvcJZzrciGECLKACBMGY6wwjcowZmiMiWVRfBGeFiaxXEQKI/ho6eNugf40WMM2CCQHwfC0yzcYwxEwKMyymFO02VIHoTMwvepDCN/ERJPllrKdGCRBCHx8OQSdQgjw4OFhcTC0mciB8AswkKzazmFAtQYQA6HcjJE796TYUD1rQoYUKYmyRRVF00lH5xnshSgmeEZhmBpXOYLMidTTUTaPkQ5kzklmwxyFkGQHaSCpuSNw0OIh4cARz8yqQlvKmjqHjksMvrb8SbBFNuD4ryI4PXtogJFtNMDEWFEmHsivd64Woz70fcMa9KE22kVs85MTTmSEnS53I59jIjVkhdZziePQU68LG69PP3b46kfx9OHp7/+z+X26p+/HDbcMjJj7B5gG2PEoIiHx7Oe1ut+eOZJ9Xw+t7Yel6MfuWzb6eEsoizceDlsXK8XJiZaTkrhMfrot6skdRFlGUf/8uVlv7xVTvqqK8kAISghefJtWQAgPewYx+ivL1+OcTyfvhFuILYifN3NLSKGjWE9k5VZVVUbpiSigF66rwsQ4p41TLmxqoqKgIKR43pp27Y8bO43D7sdN/JYWfZjN+9vry9vv37u43h89xw2YEhHvx6+j4joXlmEJNzcUrnZHumjEneYiSF9BKcPju1xYeZAahIzRFWaMrEsi3VzM6BSDgMMBrfW0qO11vejhtXwsa4rE7sNERUmJo2Ad9PWKj+57jtIpBdpV6oKqKi5K8nIgYi2rIZRmS+olTdnsYk08QAAZZ0T0IOJMRwlpSdGZBwjEmjMIubBq3ITEEGFQNmH3YiacGtgCXbSxkureD6wOyNV00SXDeGAZFMQGAKiwlEL7CZKlonjJ5yAciuLSKlpCl/3mQsdTJLhSVw4Yc4xOfU0lT/kHl8RFyoGfMov56ofmQhMygn3sIqE330rdeVyn0dKYYCRUYUklKCE1s2qEB18TZTIu3y/GOp55kxtfs0gwtcbR9BX0prm7SVmjjRkho9OAKeENzSjl1CioeJt56k6Tbxl7JolozkMFhGI7kD248iwsHDrMLf98NsebhwRZnY7vA8B/DjKk4HwrLizPgqhighhQfBfNEyJsFLOAuACUWolLygvLIhFtErWgkB1F65cSC53XVnty3wIlBnBIqUxT9lvzg+PzgTdSarPcYBigEkbnRo1ysbUqGrWCNU4kR5RYTqZmSjRMyoCrOLTzY2IdNE8fBpAAiJIT1bgMCoVd7oyHXuPSBU6LcttmCRzW0jFzd04UoYdx61frrfLbT89rAOA53ZaJZ2ijz3Nu3k0VWnLKsumiszIoaoo+z8lCTVu7sbaqlXRRxzHsZs9Hrluqy6nRR9Y2S0yU5a2f7l1C2J5fHcKp6ZHZkQuUO3m6+O6PLQEX69XkpZqy9aWZbu9XMJzvY3T+yS1gLTTsazLcRyquK6NVbdtfXu72XGk4Pp6e/nyxk2E5Lgc67b1Ycd1P3Zz8w/vv+mj+80gfNuvj+/PKurjqsJEvN+uX7683W7X0/m0rWeWZbiBubT44dHHYf2otYtZ27JVPwRkNgEQklmmQ6xE38xMlZWp9RFhktP5PJ4es19Hv14v195vuiRuLCqq/O58ftSln2/H6/W43sZbH0fHiH47jltPZuHm3d0qohHuxj4NDJnRmhDlcYxl4eHjONrSeFGxiHWN1kqJ76DklXPE6CM8lBqzhHsM/0sJSISyVMpXmUjdqaK3cjraEo5ghEdrWpIhVQkLZJKitTZGz4SIhDmqVraAAsryY7NwSeYLL3C3iYwz4BlurAqOr6VaaR7iouzXPYTbsvhtJJKbJoNbcx6yrhC33mO06B1SwjAhdwinp+hCHBGOpjkgrZVDZKpkMqosfD7F5UMCBqLs/j6FlZlJFRdpJZrPQmejGua+Suh9RpxVtHTFaM/RnFNUOicwz0nu8yS6C5bvnGxhKBVoykVs3wWdoEwt9ObOVuCf3zKoyIT6U8IkFqa4uMAQzszSsCdXu3MF0dVxMevRMsrhlZRJkUpFUrugfJP5l2Zo99L5eHpRppX8jTIiuIeb9x5jxBgxeppHH37bc3Tv3ewvwh4fPiE083SvAuVq6XIAHtwEmbabqKDInchMJIOF64gupwIJT8tnHQtG5XRnVFlj0eTFTydlpRoQojQ4gUL064QsXLyafoQzshbAzGSVGgqozKtGqbhjQaXlKO8XV+oWkISo90qmh5vbsH4AwSJA6CJuHMYikgNOwRwZNosR0jOdQGtTRljxFhBAM8XCmVOaZE6lQo60PW63Q5d1WZay6pmjm+37wWauTS23beV0FlIiItlvQ1nqrRAeKpLIZW25qIgeR79djx+//GTm3418+vBuherWlvNp36+ksj0rZwI4LsdxIrThI9btedFGqt88f/P49PRyfOrjxk33vieItfXx9vnTy/rlpudHXZZJV0Rc+/7u88u7999up3XRxW+Hjb7fdmaNkcft6LeREWb+9un18nYR1XH7sV6uvR/b0zkDY3Tbh43j9nrt3fZjf/zw/vz8/vR4hoiHt8aVB+dubm6RxFBRlcZcN3siLkdSEFE6KlmzdGLayiiegSBweAag6/L4/I7JE8ZNKGU9ndu6tkUT7sdxOb7svdtx7Jfr9eV2XPpx6fu1uwWRgDwsqkaNmMfRiTg8tElmqLAqpRulnx6X7dTWJssiN+XttC5re3x6YEC1ZVBGalMIhbm7jcOQUYHswqSiqkpMY3Qtd7I7yVegtXzIk+HDXdSSd73McTukKaZtaGb+EyO8YNI51NLTEchY2mL32TIX0QpUHwbhGXzURJSjHJ1IVolxiKpnZPfgoCVo0fBIYm7i+xFNeWnUlFxscFtWgN2CpEljz5ghpkKRTlqJccwE4skDSDGZBEmk8HCfvHdkFUQkCaE60MhzFirEbFOuSsCcg8qdCiipkzTvOkCWyCjFaiKJJSLk3vpQ2mbCP0t5oKmc/4rilzVa74KfUqjOl6W4mvTqsf16NlSvFP8FSEKi3F41x+me000FN+W8l9SjpqRMFUJ4OS4oA+FEqLc8ZSKTKTNjPoF1wo9BgTC3Y6SFj3G8vGVY9J0Bv+1+O7z3GB1uiIRH9CmKBiG99Hc5oVV3ooyEDxNWXVuJOWo9matEJlEU6JmBTBNR0fJqhjA3lWTKMelB0VbXoPQZB76sWz8OFmYWJnI3FQEoCiUJsHJSQigtSKi0n0QEKfsLeVTyTzkgEvzVUUzSmgQFOO2oilMzq3WJiTOCiYPuwYvCkXAfVJ41wHyEIzLawiOTldvSenc5QpTC2SNo1AsKSxOivRvd6Nht38fDuQGL1XGYWB8e3Ozt4yUjPXLZREkakYMoeW1LREhVRcGlKYOZVZu0Rduxksjr5fJ3/8M/vL3t3/1wfPu7b99//86yreujtEbIG3WD0wacoj23423/9e2X0/lkO5n2l/2Lrksy/BjJ3Peh29J0vd7G9fZZtgNNQMTSEvn+u2+t068/fdzWVdcNiY2aWyTZl18/377cnj6836/7l49ffvzjj/3okejXq6isbTl/eAYLcRAB4bfL5Xo5ElBV5UVIw8hgsoh71MtqYe5BzKpN2yKq4YF7mfa92ZRYAIBTEynENgYihw/OoNaIWZsOjO522Nj7McZYV7HRExmDj/3ab7eXnz99+fz6+suX/dpffv7sluM2fCRE3IOIw7xSYlpbyqxERKS0LAszmginEOW66bJxWxozSMnDPOg4butpC4rovm6b7Y4gH+7Da3CoSNOlpnwxl18rUCKDU7SpW8510KM2ikZsPoSVQLKIm6tIRCRCpIVZjazyUYY7wJG+rid3r0glj/A+RNTNdGnFM5b0lksCBfLD0rgKSFg4bocsOg4DgYSz5OZumaSnlayUhANm3AStcdMkWHfSxi3SwMvCosiQpQ5XTi51eEVKQxjEFOVIAEWGinhieCShuljRmJNmgFKh+7UyBojZPaSS+WZNbGbGffDOCLqv9CqoAKuZDJF3CVBdFoSnFgh3LximCh8inF4cyFei4Su9cFf7FwWUFZoW9+hooFoU60coZ2zxOlRxd5i3G5oaqASlEpiTIpggVOUr9cAImUJ3jiBjlvhWOBmC3MftSA942u0Y15vvNz+O6HuO4beuLOg7u1vvDCaAwinr8pBzdwaxsnUHICLEyVnecS7ZYpkminCfj42AJNb5BfW8tKYk1UfKU8cqCmStVCQkJX8+ulTILddVoRrnKd1LPJqT1s/qNGBlVrYIFtFTyya0cAoF1VMwLYPFzHDl0YQ7YGYZHm7pkSU1cZtRwxmFs5n7GL3RqcgcaZSZ7EmMdTvZkTZiYbLwBWwd0c3H6N2SeLhDoLKubVNehFbKduyuzNRUVzL35fRweuy31/7QcowhzMwabjZG3eSIUpVFG4sC4T6EAMHjt2fdFmL56efPP//4eb/2YQjip988sUIWsd6jsZ7b+mGLl7fl3B6YeXNHJMXPv3zsMb55/81f//VfP314vFxvYdkPu13Tg4TYAElKUGQH4Xo7Hs4PrO16HY+yZsbPf/64rpvKgoAdYxw9VH/608//8e/+07IubqbgZdv0vI7uv/zp54fLw8O56bI02d6/fzQLXZalbRlk5pEdvKaDG5t5hbsRy7KeVFopbLIq/WZWl5dgLsKHGShFRYyGmcfQkF6WYk73YJGI6Psx+ui3m1uu25YR/dhvb5fXX15fPr7sr8dx6eMaSRA0ULZ1YZW6W4S5kK6ntabneloBX9amKkuTdGPGtrAuLEIqvK4LhZOwSGpTeHiy9ZGO3i16ZKKpqC7rtqaHNq1eTEqIincLYWZBwoaxaHjlIYKAxjojoVTDc3RraysJBkPmKOcZVExJrFpxxqN3USFI9TIs0iqLm4nDEzHjzxBJ4ESqtAxjmrcugZCT5FxShRjdWQAwcIAJzDAIOMZI6dgWmAHKy5LWddsQBRinI2VZKLLqNJi5wpWFOMsdUJE/FW5BJHN/44EEYBnFb3sCE1ihZOTskUXkvQWtOCKaCxxL5RUWBTAtZZV2Ud9Sc74cWMUSTzsY7pkNwKQQEjr/w9fxT3cO4H4y4O7Qmg6EeSTUACvL19SWZqQUTjEVSVEyF2ZQJCOFQhmUKcD093LmPSGdCpSa0vagyBg+jsOPEd1877Z33/u4XmHDjz6uF0aij2EO92KJSQgZ8CQiEfHiYwtU8mAiJhFRd0sPGxXvXE48Um0ZcfeH/yViW5pmJu757jFchDNSdNYhSWPMPsh0i4pxDkuKJOWwmGZhBss0Z3u4Lm12hEYAkgCJUBMU7r9KUFSTDBGBeJ5nROOe94I7tc+YbXxhWQJ9RDCIgZEx7x8xELE2XuU0NA6xcLglreTuSOgKH8mcwtSWFqwJ8X4kZ6mCr6+7Mm9rE9bhCLi2ZXvki3/R7eEU7G7u3ocFED5KPW3WidIRLJrpmbmsjZHciOCPz+v2sJ2fn//8j59ev3z5p//wZ0fyps8fTsdhQiJK2+Pjt1g78PmXq3+5LfywLJs5Xl8vv/z6cx+3Pvr59HBE3n7+8nrpYwfxthhk48YK+LWPtemnn7/0a5zfPZnHsftx9O1h4aXF6Mu2XT6/PV+Hef784yeWZb/BrvmH3/6V023f49bftMnteHl7lcfnh/PDedva5fV1jJfnDxGstC7btiVQq0BEeiQmlM8swiBSiQiweJTXnwAvIoon2UbFN2WVtdvQhiQMGyTSttPDhw/UZH+7tEqKIixtxZZ4BgfTE8Zt9NswCw6YhTSNzCTO6UNLYk73tiorUbAuCDsytTVujZeFt00iYlvbuipCwFRWGwicfBwGEBPLIkyctYpEhJcfhZEkyshgYVGt6U8gwFkkzGsvSkJErcZRZpEYPsuxhbK8zoilNQvLTBEOp/Rg5fRAxWbUx9KDicOjVmNm8WGZwcrFRSNTVKpaD8x+mDSJQJiBQexlM86KX2BOpT4CKqBkd25G0sJMlsWQCgdrinI0IKoyOyOgLZEqSrOfK4ocnss9EVdvcSYSFsb3rCAmMg/w7I2t+V50LheVPOM8qxQh575OQMV3IqaMcwrtCzehu6097hKh+ZeET0C5YKV7FETiLu2/I0JfVaE0A8zrx63Jg/tPVNhUeRwmE1Q8wDQkJFNypmSoQCt2qYy7KOiKkERV4zCNXkkURLBhMSzcj9uNzP16s1u3y82Pg9yjd+rDe68QtHs+Y8YwKvwqEwKVQsqoyrO4SUamWz2Fi2rSBPQzM4bVs0yMMAORLq10eAkY0JaGaufBNMAV3oKoIhoORPkimTSpGuDq0ACVfII5Km6FieYdop4zzgg0KWAtBURBUr0yQbM3o8ZEEJFHgkiY68kq2xAyhACm8CyVYTntmTiFPFwXJqEYKaCHto3d9xxhkEaZIZysEmbIYBEBknhBs3Aw972HD6STwH0jPQORbO8el/Pzu9f9YBUVriKc0TsLE/O6LiJk1iOj2yEsGd5S9bRWQBOvtCyq66Lr9vOfll/+9Osf/+6PotB/9TftRLtbpUXKw3J+9wh54O12ebmu27pQE5YxvB/j4+fXlB9lXQ+Ln3783HddV8WX2/l9w8tw8v3Yl02E9cf88vj0ZGGP755aU3yk7Xxa1/V2RIz8d//9v394evrl5y8R+vLzC6N9/vjft5XbsvDCy6YBu10vrclvf/+7ZTsdtwNBr6/Hr5++fPf77//wN7+X7aTlcSCbCTIipaCvpPivlH+idNYISmZS1hEDRMu6ClG5xlhYW8v0x/NjtrYswohtXezxHMdICwbcvOt60u1h2frN8infXi777cgIPgYI/RjH3mFpw5kpg9yNlUQECBZS5Sth25aliSpuvanSMXq7iTbV1lgoRyAjR3hmRkhK2Ukqtt3DSDiZRh+qzd1LB1je+NaWQifcnAnaWmZGphKmUUlQlxIRrYY1H96WJTxiOBNVY7aoEtVNwkXEhokIT6dOBMDM2sQtQJWNXBpuELP1DiKSOexytnyDEj5iavGZmSndEUxBPpwEWVhx82D2cbA1Gyu1RbRJrJmxbFvlFwVRkwVMUWFfKjO/GZxAUJpnExICW0rS4R6UQZJEVfJBzBREIIvKIJjYOxPNaM46DXJu+cz3Zb0g9gkQlKATlceTXIgy+ZScTknpV5+BznFeT9Z9Wt0LMvGXsV9fdO8CwJ0HQCbJXzrP6X6JoLuinzOUSTgXhlTEXiRmpxvdKeipQc1MYXIvqq17H3Yc0Y/+drHXK4bl3sftRp5+dIrgSjIzr3/TzUTb158vuqMmep0EESi83oMT3BRAmJcu222oKAtntcRmCEuYlUyJhYg4xtDWkig8kSFNRKXCv8JRhgAgmDksEBAVBhOTwzMr/DVn1ndSlI25nqZhtJAsDGXnZIG7pYhHFL0zhXBFgpUOy2fCD6Z+605338nyGBbTbk2OFCIiDjdzw5xIlZ4EeAqTV48EaqXIhXVkKtMslHFyy8vnK0BJaOfT49P5OK7jgU6nh6cP33w+fonwRFCQqiSlqnLTJhzpjupwCqWGRLhVRkfTrTXxIx4/bKDv++E//fTxj//+T7ot3//19+vjetihLA/vTrebDbOzycPjBzv8uHYPXZYz2MDt5W3H1aSdl7O9vd4cxCBp9PSbd2k7M5h0mL+9vN3e4psfvn04PX/59DmIdDldL9fL9YKk15e3b01YT/vruFz9+vlLpq+rgnl7OJ2ettN5FTol+O3NNh/r6XF0+/u//8e2Clp7fve0PT1Ra0nMossCdydmUS3EYG5k5aOt+26pDmaKS5ZSoCopGGiq6aksrTESZH1sDxx+Nd/9KK7Qrrvfjv3lcrsc+9utd7+93fbrHgE3I+Zhcdx2t+RM95g/zG7MtGiL9GVVVWYmZBzdzV2U1qUdeSzr8vAICRaWGGBVZuQAQxApTXxkejJIlma9N9X7fKgWqFSVOXIDwhIRNozumgm685ElWAx3Fnb3KkCcExrkw1mFMlk0w0XEzGrvziRmToSUe3Z4uaty5mpW61+2dQ2fRmhzjxGyCCJBOXeoQjoSAKx3XZs0zky3rqeW2ZOZU6oFEG65bpFh7gxwa4XTuBGYpSkDpS+kkq4SOLIJjXBhSU7ySMqBjHSWZn6X8wERzncwhjD1enUh+Od/XM8y170/U5irGqBSBqfUp95OmbinkOHuO6rRjakCqqFdkkzgLuq6EwFT9DmlSJlZlzj8RTVUW83MqpnTPEMIgqgwmwbiDMqURGaIVDBz+QWoCOk7+B6JJA/f9zh6v17723W8vvq107DYj4KGOAGPMGutWbi7a2uttQTCgqX8xSCARRJw7yrNzJDOQt6NiLgJRdWHAQHSQo2MmIQUmcJSqwFK7FTvLXegbqmcjIz0ElREqRnKyewICne60y9x12aV5ofuYt6yx9Ei3KpSg8HpWV5cmnxWUEyDWbpVhTPl/BlmwKRI6+OwURlHGWZmjporkWGYMrWIqmcLH9YdCGKqfHIQIp1EltMiDguK7glEOgVxSozhGcfluJ7W0+txOq2n7cEGQkSXh9Pzu3G5hnk/vLVVpOr3SJoudIpwTsp0Zh1u49p1E1BK35OjqljOufz+r37gpi8fP/3yx5/XbXvfPui6RGQ3e//9t8Mv1+vFXUfwSCpr5nJSaYvFGGbHMR6fvr1dL+NgT3q9HuuB5fRgFcSSvi2P2jbO7fOv+9urRSD8up5PvS/b2r774dvwZKGXX/+0X+yf/uHntS3f/e63T49PQvL54+vlap723Q/f9q7nD899xNvnQ3Aa/Xj9/HrcRlqmf63ojEW1tYbkryLISA9LlmqkSmYsqqXmrWLqEnMNG60pZQpPp3xE3C63T7/8/PLpl7dPn/rllochYbsR0+XT23Edx9595O1y2GHu1m9HKGdmBFsPWJBwJrW1UdA4bHgScagE03Fz4UbIcUS6UPi6clj0vasoZBYXq6pHhHtbGgHcKjtTxm0XqTi8WE6LskY4SpMxhmpDprtLU2RmorWWGZkUFrKoMI3hBTizCChF1HoXXSK8LdVDWTFzSMSyNDcjESYaYzCYatVC3Yaz0pxKi103Mpld4glAWEqGOQepRTkBWSHaQKIk4zAWMBAZsmgS4OrjoNF4XREZai3gzGHW1o1SMyLNQAQRBES58jVnZg6iCQclEnTfzSnTKZPJK3j4rtCc+DPXOMzIJLqn0WX+cyx/6oNqf6avk3uKemaZcHHvuDsG7mqXBLROvkkuZ97Plrp95NzoIyF3pvh+K0nMoFOU3gbzMC9bA1MIoREWRmMoYXY5Z8xbClNkQYHzRlMDy92t97HvPnq/XMblam9Xu1zz2smdM91cWe0Y9+WZiIhFmLiSRqhuOPGXM8yjAHaOIGFCQlVtWLjLunqvQCtkhA+rpysy0z0yRNssuCHEMG0Po+9MTMwsAmTaZGS1LV+1aqJa7ovKjxp9EDFJyQCLtSr5xz0plwAGLyKbxuIhkZQIJxGatTJTHTzP9Uo6JGKiyHLQ9Hpp3LMUFMziNqwPRBI1ZIYbEzw9Am4pIqmcka6VYwxdOAaFhUgjIaR67AweR1+0CVMq3KMftt/GvnsTNqbBRiPBK2vqQidChusi69YyBoPWdSkRIkMy3I6xro2DdRFkjnFzWGNS1W9/99g2/bPm5XK9vLxt77YPj+/dPY7cx62PPBx//uMvt92FH1dalu3xbbcMAsTHMQb2o0PVh9+uF0a+Xm6/3X7LiGM/VDTMLXU7PfUR8LEs6+Pzh8N6U+Wmb5/7uq4//dPPn369HW9xPn9/enjaBx8frffLZVyJ7HJ5ef72y7t378+PPwvF/vq2nfR/8b/6nzdZwc0zA3Azc2MVkTZrWMGIqN8GHFlaoGRttYxNAQXSZ0OkW08h4VWYeey9X/e369uvv/70+eef9pfX48vNDltEADr2vr/1/XL0m0dmdBr7AChcjpsRc3Aqrd07RtBCPUxY1mUlIIHYzYMhGGJCSU3docxuVHlbD2e67b1pU22BgKIKFhOBSFnaOA5VESmDVC3mcEslVpHSc1u4Cmtb3EZNGBGNiEoDt+rjAyaAI5ThLDK6ifKMPS9/FnHFIQhLqaFVFIBqQ8J9FJ+MEtFFpAe1lhYghHtrjZA2RgXgNxEPL7REVSOtRE1hVgH8CFdp0Z0YHhGccOckMvCaCIrIdtqyhQ2XJAQJi4pQzrLGmqvMCEA4q5tVG7GjzqAjnILojorc5/FfVJU17OeExZ0pwsRwvroHkEjkrDHOpFoyUDv1Pd4t7vQ3FewGxf0W9hXxKfUPuG6meTf+3gGIu/a//l6a+QdZQUOo6J5MlVwIa5MF0ZgBl0qqdSRmrujdWVzq/BTmCPfe7ei23/x6659f++Uat4PNq42rYG2HIYGI1hQERBJzRLq5Tid61vEwQxQiidjcCBBtt8tl3TYihAdbCAsxhecMjFVx9/RgEWb+eleYy0QxXbW8BzJCWJnIESyUlu6hzNR4/iVJxKxtKQm/x9REEbi0vlCBSArxytmAlrQIIaq3g1njzkWX06VUvpmeWeelVeV9sY4JEm6skj7Ch5DIOmM1zKKPYSPCPYOJmFUzwtwalChp0HHrIqzCR+8grVDY0vCAHBnM0pZFQXbtduvrhw/vHrdlQfT9dHqKcwchjoMA96P3pOR9DOLBCRIIEknLupYpwkcwczLW55UZFp21PX+zkfzm118/H/utX480LMs6LCRIBOby5eV4eRtvr2+NH5C6bE+szJJLO/HyFH1PGrd+fe394x//9N13x6+/vm2nFZHLumzbc7+Nz7/0/eivr3tb49Pb0cfeD6+DU2W1PdLa0dOc80hF690vFzvcKXwM/fE/vX1eb63ptjYia0q5/Jv/yf/0X/3LYCRZtYZGCLXIVCLRVlscE5iQwmFei2mGGRDuPHMYvdTfmelh/TgiY13WBPXRWeT73/3V8/Pz8Xqxyw0eMY79cnz85ZMdrqp2R5LR2Lql8bacbJgEZ6LJUomTEZ6BCLQmBLCwMlP48XqIsr5r06uSzNUaT/zwdFLmcu/KorNZJUsY3YUroMvbsmbmGIl0Va0MS2laqWfmHn3PSFFlgo2eCVKu04+ZIyAyWytr7IlkRsJiNojwHKo2nAASBtcEiEgPDyJetjXhX6uz597MBMTSFncjZRYhgogO65mubQFRBYKNPqQh0lkb3BmwW4eQZyQnLRrDW3K2YGKPrK/pRJoZ5m3JUA3htrSKPqzIz6Tg4lIzlKmSWaSU2QHPcARI5zxMuq/klSI5iaM6TjKrVRh/EfTcMfmyRLjNTl+eycjlVAISJHw/OaIuAlqTHTNo4isFDMQ8LO70LwqdrJMiCnouIrRaMtxRYaCUgliEFsYiqQHAKTyF4+udK2ZO6P3ESRCxsI2eGb7f/HbY5RrH4dcbusFGHqMSTkU0PRHJYATPUy5BUrMfAJGDlSf/XSVHysQ0uvVjl8YkaKJoU5FNgWVd3FxUI4KJeGsxorXFxeuV00VAbMNEK8KJmEAqILiZLM3NSjbq5iQebvXOywyzo63LtPN6MDFNWS6IgUaRmYJslEpOVfihkTPNdYp6kcSVnxS1LAJBQoBkWgk8hDVk5uJ5OmW0tmbAbf/qxEZKxcyGR8KZcuZPIMjX4zpUJLV1DxVgWdj98OEZHsOOyBF+9R/ab8W/0VxUHsAZGbfry7hejtvRjxsiVTFsYPS26qJQimVREm6nFt7dKwg+zU1WjgfXjUkB8YWXx3cPjvz111/2/fL66ePD+2ci7ocjM0b/9Pnl7//Dz5euOR5Op2+fnk5tWR7O6/Yg49Wur7Fs59PzI3QX/oY9rfPrPk7rSXh5uXk6R9jR7TgCMhyj25Fghrjlj//0T998+01mu7wNtwEY6d6HadNIVdlUN2JkHt1oaa21VRe5voz92j//+un8uLz/4RsokZQGUbSJmwOYAm6ASUgwS2YizDzSfdZ3ZLiVcTIBj6HQRLDSw+MDsz+c1vH8cD19vr2+xRh+rKI3d48jmHamY/Q8LqM4VRDMzD1UJMbcnd0MzK21yEyPSG+qow9mNNW2aO0dAJghIuVHBSEF0oTK5ZrwsBEDyKYChw+jpH7bZ3NiocbEEUmRNoyZlm3tx21dt4zSFDILMTMEERmeurSy71bCcWWze1R/FPkYsizFzbbKY5/S+CRVrzrYdNbmPnOVi4goaqE8U0RApiqPMVhIm/bdqNLipHbX9NFVlVAVuVxO2IWboyrbEsdgp6ROEgkZ1x0JBvNK6RHDUzVimk+Jk+4BCpxVZxizpYNECJIhSMpS8zHPAtsKoLlfCKrvF9NwPO0OBMI9PvPuEirNCxXamJXcfC/TvYM6la/JwvDQOYXnnXTywTXz7/3D8wsc85UolD5BUS24nncUKBnMZIvwptQYClelwr4xeeKkuh1RCbSDiEu1GUyUSeGwcXz+cry8jsuB3nF4uq1t8TFKW8D1jMTsygFJhpeqSHWZBFGmiIZbVI8YF81fV9QpHTOzZV0yS/3idWeswiZOJuVqdiRhvw0uWWUCCWlSCe5ZEb5E0Y2XJcNL78dEDNLWit8rPZVnpHtJGhJgJLiKkpmahBAmr51Eirv1j4Xnzh/OzCzkWRuTIBPaZgsgici6nJr1oyciDUEEySjfTCncuH7aASewCIe7KmcyGQRpGcw0hoF1YS1BOhHAlubH2InJfDw8P2wiNMaXn3+9ffk4xu3zzz+9vry0FXlY27SxPDycRBiefezb8ykioEkNjXk4BQwx+zwY6JfDRgcvbrdlXXVlSD48nVgoMvu+78O7cY/TYUZtSV6+fNnTNzNc345vfvNIbQEJgbQ99GEstC5rnCy7jZFjWJgf3fajh8P9th+jaUv26/WlH/t2Wpq2PqD8+OnjEdkRqU2te1N9OJ+1nbpfBXx7e+UKVAnvgy7Xl3fYTnlOoutl3289qlmcpS0LT1MhR1hGePi0X6qgIMaKPuR6mUY/enU2NFVVZpHWGjHFsKAM5H57u72+fvnl19dPH/t19z58t7GPYfckAGSmmw87fJhPZwi5kAiRj96WZZg5dwInQYXCx9Ia4IRkSma49wVNWqsIcW5KDaLSliUiwtOOwVKtbuT7wMhlWSQls8qOavEq4T7HcG1L9c+1thKxRXc3FiFhc6vQi4p4n1qWyMxU1QSpsDQxG6hWl5WtG5eTqEQvQWDi5Np1yX16dpAZ9yKqzERaN1FBINOlEBKGaiMwPHN6CKK1pTJz6p4ansQltQAHCMQBvx1kjm1Ld33YIDACEbVNmSBMWjQkSkNSticU48/C4cnMQRX9gwWIpJFZDogSfHslMFA9ksnfMnNGYCLImV+HtTDRzFrLTGbiOn3nmj57YpAV016Ce0JCa7jXN08OgCqB4350ZJFYpa38eixNTAiRuN8qCIl0QS7CymilYc8otRJN9RKIhBJgisivffaZiQgmeO9jP+w47HbEfkhSeKaFp/mw+vDI9ELxfdUAq6SXdd6YmYjvq0R9JMpSVoGIUlR5lA0nijWqNGbgnukKSmaxPtqyAKRNy5c7zbSHVUIqgcyttRYeYcbCoCSGuVXqUViwcGSiVKGNM+E2WDXyftP1pFXASCUIZcbXrQFTQZqTaa+7HVf4aZRgJCpQRBqI4SM5wZCmkQYHIt2tKPP5hkZmOLEIc1vU4DAQU3QT4iAYQwhjDPfhvkeCGNvW2taYy2kX++v1T7eraC4LTg/LIvn733149+FBmB8eT1tbkRRmt7fLsV/c96UxCbRpUmVUZALuxqFgiiR2BoVbfD4+ybZuj8v6vEYGCR3jSG5Qtj7O786//5d/e/CHTp9fP1NgET738bD288e3j2nw7mAMGwEew+rSHG7rpv3tjVMXfbBI1a27jSNJ3zU5nU7n/XYj0ph4mm/n9bSub28vw/32+hrxJcISqSQx/NhfiCruwo/9tdv+X/7PaL8d5oZwIlFpoloGpWQPr+quinGVjK9rF0iIq4POLcJtdBYCI4DWmjQdoxOTssrS9mG/fvr46dePH3/8sb9efR/ZIUnpROUob+JbQ2KQ5SVGIGCZYmNoUwJHmiyEDBUWIaTbcCCakCU0yc2WTcugI4sgIylAJdlzEs6MtqmSslO/DG1NlJU4RgDsltIkPSYgEclNa2YxOD2CQ4SZ2brVBlyfgomsJoir1YPL0pWRZgaktGbuQlSYkojMypFIhJMyM8MqNz6XZRnWK/UxHRWW1ZaW6azqVtdldi/OtrIBMjPKXAtC2nRjMFOMwtqri5XiMCZOcrtedVuM4eHKJLkho21NmqLqAP4CoICoNtfycyXdlTdC0ZiGp8P9zuSWNYRQJtJS99R8LhaRImLu9XPBZdyXfJY7V4xpu6sfJO9o0lepKEBVCFPK5OmHKu1STZ15mOPr4L8LkggzmZWI6lGlZzgTGkMyFMwZKkSRmV6tZnRfiP4iaMoa1FGPhIlUxI9ux+77jmHp6d2EEWOI0BTgTy1vtQtGmb+ICMoBSGXtjqh3FDEL180IxGLddG2JFBZidhttWcyNRZAV35/EDOaMKM0yhROT2+DWQOQR5TmoxHNVrZMt6uONGTGcXv8EqvMvLKRNlQITq6j3YX1gEWksSzHmGkhWZZEAVcEfz9cd0zNAtUQmszBquETK4j6I2caIJFJB2NJWkgzzljSyr6xT8hzJJcOKJE8lPuzwQdokjEKqfMiH2eimK4uuIiq8gLl3GzFGv728vJwf1g/fnP/qX/zm93/9m9O5AT3TqhhHiRdZr2+X3lMD4+ayLCIUmROOVCZKEfG0RVdiOo6enXQ5lXV0fdhYm4fvhx39OOy6PX7DC3/49t01cbmdPn9c98uXfiTIX97eLreXlSmG2GFJESIgTTNzgwBk/fKGpEXkcmS4b+va9HS9/ZrdHHvv+347ZDsptgSt65mb9MgARXp4N7PjOIjSdQ108xsBPWLRNuxojf+b/8f/63b8yx/+8Bu3bCxEnBYzFNqtEp+oZABZop6ZGsIFQ6bXn4ioNpnJu0ggRYRAFBDRpW1NNpXWsCRMEmM4E1MKM3FTFtlOjz583OJtfR3DL5erj3FEeAxzCAZGaFMVjBEgbNuijVhIlCLjOHaImsNpBKsujRLDUKUW62lllfQK70Flgi7S+rVTQGQKoAGeJYyewpJEdcP2cAqItigzfOS6LmZGHtRIWNyiqrdQOanEUk3aYFYhR2lnIBxl0I/ai6f4h3VeDYb3cKeKXKzFVQiEcIp7j6O7J4iFzUKIRFuEc1U9etQVBHBU1yTT8CimPiKq7paEM9THDobEaGtbtwddmjYlVUQR2jwXdZKa3kFVMQNkCpEyRWKpl7JglTIsFPdamv24i1TvUp661tQszpzaoOn39QqXnrN7RipRFfMANGMY6liaYXD1a3a4x13cian7LF3NV3DofqMqmIju5rNkkAoWxcKsDJmiK68cZczDqwZlNUPSRIIqSUkBt8I7bFiEaaYkWKW+nYndLbP4KckMd6vrzf0ISynWNCE6Lz/aJDyo7GbDiDitZOLpbgwyt3qYTFQx/fOJ5jKyJxHCvG2rh6/b1o+jLS3DGUSi9+QNiBCLug/fjZhENdx5glRljChPQmRQzpg9kIIaoMHLksoiiAkRFVVDlTFYjvAaG8RAgCvCvkLERFjCLSGSBwEirBkGioh0t3oliTkskVjbko4MJrh5jmFITYpufrndhoe0djqvT+8euW1mcXQ79n697X0/LMKtM+Ph++cP3zz//q9++/0P7663L/3YPTsx/DAw6QrE0Rrd3vYmUOUm7FWtw8mcIsSLsqysRQHK0fcg2raTNCrh7bF3cxeRPMIDTTcb0J7N6KE9NL6+7l9SBqJlSouAPEiSwS2iLadFhZpe+wtRT8q+78v6znzfaMWgq/0y4jiOWx+XJL7l/kjvFpzOD8+6MlfE0n65jiuCirSI6KP3xMEIknSMw7wJRtivP//y5eN3OZwDTRdkhrksnFUGYFYe+XnV9vLpFRdfhN5UJTIzgSNyFgRVa54DKZTCqQ/np2N9HefbSA05LuN6u/V+2TnIAixCIvAU1acPT9ZH25qPcbvt++UgeOUka+Oj921txAQEcWPGuq0MB4WKsBbWHOAkSo9AedeJtDWHj8MFzKqwHOYgkiYFW1TaKydFBoPDMyNFmg9jbYUXEKEtCyoXmsAqxBJRrnvPmERCZkaFTFHV2vAorJ8KGOfMEL1nyQvCR9vWMTzCZa3EiIQgraDXWkInkVkppD6SVZjZzUFStlAEiS6JKBE5EzxcmGvtzSyZX/bhx35laaft4eH58eH5cXt8aKdVRJkpAvOzO0MEKgcKhSojkwmZqcJuIZlK6YgK14wJHVVnAKZQcmYclWR/dlFnVG0kTUE5V3bQDO7JLECaptCnUP2oU5Qys4zLdyPwfUkvT1cdnXQnfkH3+M96AUthOR0FIAQjVFiZm9QLUtBzCVWZyhl5dybU6xf3BLqCa4SltfV0Pn9RykhPI09OjnAiTpDo4sPnKVIPWIhV0spyLTHfH1M2G5XHiUSlpBUtM+e1Erl3z8y2tvCZZVW1dhlBRMERGRTJKn3fiaXHjUQKi7HRuS0ZQYR6F7o7gXURESKWHH7PB5WC8KbXfWmZyaokEcpZPZEorpkynaTk0oRp6KubWNQnApTV6VPPZTiIaNqJADDS6ioZMYb5mIGjXthGEgQgYR71nQzVxYN6770fbW3b4yrSQM0tLtf988vb23VPpzAQ+HQ+hTArIXHcbtfXt8uZQXm9XXQRRjB5Qvbb5ehHeOjSjptZ77wtQtzNZak4Q12aJNHSxMxYaZVlmGeOCL71G62Nm4Lp7fVt08dx0J/++I8fP+d/+vcvn1/ay/VxBLC03pE5MCJjjLSMTM7DejsaE5jpGBcV16Zm4XQJ32U7e9yOuOzj0h7PK70/Lse5t998+4dHafvh4/JqSsfbl+vxYnkQi6VlGkOBJFjgEGhjDZAo27CPH6+363FcdmIhwCNFJcyiYuHMiZPnC5Yl2Cq1dxLS3d2lTfN1hNcXeIWxgUQlzaL79fXy5dOXy+V227t3JycPcs9+WL/22+1gbSICwrKqqpDy+tiIV92X03m4RZof+xEePHVmpCLKHAizsaySSE/PTO+D27KKEHGtzKyNREZYyQqpNMVu6qQsbp4jRVRUMfPeJcz57nf5Ch17d120wAQCh3XeVncXbWkOru0QbiYqlNB1jXQWsWNI4QACoJR7ShTJCAsiYpZ5B2WtxIP0IEYkOBKAqOQcJhGZgSrmzcjUtsT08URQRjghpbXyKyOi2NdMiEiPQQSTgG6PH7798Pvfvfvuu4dvvpG2skhWAPMsQLknnmZghnfWKRSZQSRIZ4ARijRkEMBSkz9xx83/mfYzY/49NURniM6E1zF386ze6ZwhlXMII4sArn7DDALVDeCOAlXg6FfUCpO2nadxwMknUs6M+9VhFt0lmKBJAtDUqgcT3autsvCvioqbuFIFWdcTASIm1fbw9Gh9nN+9e/v11xj7A68LLaMf4T6Po0kfowpFmRQ+Jf9UGBFSWNydCy73FBGiZG1E1dYLmiq02jjgZiJS+vxJGpfRYTZQTrGNsEwWelkCFiMq5UNEC6LnpIL/KnAi3GGQRdOdmtxf9UwHhBORwiTEm4RAlDydike/s1gFK9WBPrmAwNR4ZGKyMgQvOC2zOg0ifJiPQeFKwgy3JJLIEGSC7fDwnonwtBE1aNa2nL9/SugwOvb94y8f90u/7Xa99SQ9Pz3IytJkOzXJB1lYlkyil5eLjb6dmUhoaY2JaAi493H9si+nBSnnx8e3twuJhTZpmghpDYyI5AxSSCVVNCWFNnUPy2y0SmsPiwqvby8jIJdb/Nt/+8c//WN/uZ1oXdaHc67f4dWObkiPONyGp7GKysrLooBHb+10etzWbdO2+dFfv1xux4XJjY6//Ve/+9/97/+3/+IPf/tv/tu/+6//q383XuLt0/XLr7++HbfEpfuXxAGK9Lznrfv8CAHuFhQiKxgQ2d/26+vluI3okZHmTkzuYdbpfm+uct0qR2QiBECwMXzG3xKBWlNzygiLGH0AvG4tzMaxv13ePv366y9/+vn1l48vHz8eX24SkR1IJTkDHOF+5JE9M44bnx4WCERlfVjWRc9rS0s7xhvTOHpZFIePCuFpixIoPFgZSBZldRGOcE8WbpUrJ62xCCJhkQ7mBFgN7HU+EjOLcm1pbl4JccyC6iGpu3tkWEAoE/ChyzKO0Za1Rm1msjIlmJdi5twMmB250qTkS8yJssg5CKlNC7uIdEqKDHixjUmgpmrmqg1cqVlZnW4iHJ7Cmom8S2lAAkYFqRUlhBKUE+ywXNglRxqrru/fv/vtb999/5uHbz48vnsn2wnMyKS4x31M1u2rsmYO6zn/iq9IZKYQmEHmDJgTsdSNP/E13L+6qmpQfwXRCwaq45iSgek2uSNGX38BLER5d8OVptZD6Q6fAQhEYdnlOSogiO6EOr4+jNmBMpNsMpOLzahYnHLrSe0XFbTvVBvjJKq5us6KUEUmM/M0xiSpnt4/ff83f2tmP/0P/+56vaI5AsJoKiAah2WJWZiY1C1YGAArjzFEvkYNTdKahHRRG9UC4FSJ0FyKLKF7GKuIBBDuJEIMZikvGImUJUIXLW6qDp4IL6VmSfAL32KVcKNkaoSEsGACXPn1ZSdhaZKAF19xbrQwrezw+0mLmDZ5ur/wEz+bp3S92EF3VUmBfSUM9XJEhA+KKM9LIfMZCIsMpkg364fNG1hi2RYNWh8eAXn9fHv58vL6Zd/fdmBZdZNt04etStkqrNvTObjxiZM+/fL6o12D9x9++833+kGkMZGbs+r779758H7bPXI7bbfjWAnUwJHmFokRlulLSltbhcgvpwdtyrp4pCWQ8MSwWNo5l6cP39Kp/ULifZBwKjY/SOls3oHBxIs+87L85m++/8Nfv//P/sf/+cePP20r/ct/9YfffP/t+fHp8+e3/8v/+f/6//5//te5D+cRcfubf/G3/+v/zf/yfXvA/vp//7/9V3//9//p11+/3OCMAG6CAEWSJRFBEHedAIhpK/s4AWOMSFOhl5eXH//xx+N2nIa59enNSRBBVbjCGiOEBJSFBKf7XI2Jwq2tKyOJxAdThpsz2U5RC+Ho++12ef306eNPv37800/jdRdASJkXGiuMhdbMXJX24zquBovg4AaLdd22ENq20+nxaXncvNu+7z68HzcgkzLJ27J6DBDPBHjA3P16JT61VYOSKtxSGELEmdXiIkCEjxThGhKRQUyly6wKMGJibmHGojY6a6u1tFUhNlKXmc9YLR0ETqCi0X1ERChz9VhNZUf9Rqj0PLW/TmqdkkXgztKAFNWs4HKi0nBMGWOCRZgJnG5e5wERBUJmaZS7OUtNkhDWQdbJxoj2sG0P79794bfP33///P23y8NjO53As92wHB0MzsjS3ZdfNu5pf0zs4cSMWZoTDDBRY3RKm4KuqgQvJEj+4uAFcA9lmEsqqlA+MImgsvRiojhF1eZXYX9mXUUyhRCROo+PyWHOd3ex9iBkBEnlUU4kaA5uQuXaUb0aHlzu2xkOB0YyMLnZipgjnhNwNiwTVekllxQ1wwPKEb6cTh/+8IOuy7KsP/3d333+6Sc12dbNQcoKrnim+i5infba8GxtcRvCYjZq+CYgIhYmpRSGTMARyaRABVQxN3WPiBAVlnkxQUZBde7JBNmU6l2IjDACSGTsR9uWOntg0ZaVykZI089CBCZibREGZhIGc0W98qouSY2jcSo5omS9dT8twv5+2BNzggCrZqQZAGLuGQFPZFJQYxVZIiKpJ7PniDF8mPUY3TPBkAyJ3t2QHiR8Pp+JJFLCcb31y+vt9dPtejU4r+3c2mlt66UfAEV4Joabh3sfsuowe3o+e/RARAxKCo8I54btYVWS8IiBQFxur/12tEWYuLVGFGY9COGjNfZIGi7KwpIWxnnalJMoOZLcPTws/PG0Ml+xtI+fPl+v7zY/iHokj/3NAxmpQst6Xk/bu/fP/6P/4l//zb/8w3/2r//q3TcP58fVx/j40+f/z3/zb/74x384rsdJlt2Hgf7j3//D//H/8H/6h3/4+R/+7u//9B//TFDABS3hgQEkZu1fTlE0CrAvvUgISETCD4GCaHRbthNAPszM4C6iIiSlZslq6gsAykJMNsJiEGulvwEJzQAc4enDRoUjU6c+2wlpO20P54fraX14OI9cfD+s23G72nFFciYtJMmqrOBE8LAOx/C47cdpWzzTtG/baV1X3SRs3G60X3dpDKY+ugiJ8ARvE5kRbhnL0busyq3VGt5UA07MCLN0Yio5hjKFgSnDvYrQw5KVqSwvxCJS7ggwuRmBCrAq842sS1K4p4dTViw+iYgKmzllamvhY+xOX2WgwkGposWqlNqHJUmlOLcwczNtjYQNKSAbQeXh59q5Ct2SiChfhvBCXJQtgzDi8PSb3Zydn9bn777/8IcfTs/v3v3uN8vpLOtGqiCwtDJ6IYNU8ZV6nUD9RJgBnpdIovqMe1gEsWiaSxnH0xF+j4shAtyDZ570nfeNsihMg1jt1uGFNs1EoAm4424li7o83NcYJAvrxBbmpfSOPE/YDqoVO1uzlghlCqF7520RmqUICFGoMGMGUSYjIlhkKibr0YuA7peI4hO4kkudRZLSWgoodXn+3benx4fnb7/9+T/8+5c//Wm8vLoNp8bU2ioi7JZhLq2l37Oky/Frpq0hk5vAI8NF1ccAsWeoCoPCPciFRZuWmIgYMsM55gMjZhYO8zK/TKOARyBZJd0BLEureyYnlRtFCW1ZIr3OS9JaG2PGd2SywKryhzIXdnKqEjUmEsE9KOyuvsqsysy56MdkBjLcrD6fCEvrBGd4WA/3NHMzG8PHCIuwDIvwrBg7AhNlW5uKamtMen3rl8vt8rrfLnu/Dk1NJkhri5YYNCOH+XAnpmwkyyJEGfnly+v2sGiT3/3mr37z7XcPZzG76qYsbO4QSNOHduaXvCAzvTVhltaUnbK00OXMyFRdIjG6wXA7hrZlOz+1ReHVKRI29tP59Pj+HfGSKcfed/uUKWZHjz3QGqHvX15f6OXL3/3D3/2367qFZJBZ7KP32+V27Bfv2dCMtgQa1v/w3/37f/vf/X8d1EAOBoLBG1qAAilgAgUCqMY+wf08JpAQWKKPnclJ2n695ehtKtKSCSRSASxJGZ6RZh6UKTxTYALuEcITZFVdkJThXm2OvbcmjVr1UUgkZZ630+//xV/F7rF7nPL68vr555d99ONyQ7KNsW3Lsm6cWJZ14caDoRje88jb2DN8NEmCKJ3ODxEtJHUVyru7MDyC26IgrKuy5LKeksKt21h4cVCScKIA2JRF5Uny2nGkLhVaKwiXEuoLcTlMA7Vdjb3r0hLh42s/pYuyqMLCI4iyrZvZICIlFaU6D8BcxUrh2dYlKStCuDZMdyOwtiVhVQYsLBEZNoigTUEUqFZUJ06WnBBBqYQEJEmZRGhrSxgF2R3DHWHy0NbHdw8f3r37/Q+PH96fv/1G2tYet5iNXZERJJJIZkYW58DhRvfwhcRsA56CnsivAFMdt8NLBsZR8b1IACxaeV9f4SMGW+V6F65T4pYK1yYq81oJOSfEVGTCHUuYQEJxviBk6tebRd2tUBFjdcG4hz0go9iGvBO5dCeBC6emjKlvw13uWbchEMqNRiUx+ouQiICcz8jXZKIEUpUziQLSdPnmeTmf3v/wzaeffvr4x3/48qc/H19excystUxhlUWQYCUbvrTF00v/AKkmrRBhB6qmomYa5nnJiVBdZpJtqVqLuEfhjMWk0LKsSZNqr6RvrddXlIjynuNId20DCQe5melUK69IH8NIOTOExN09LBJVk0eiIsRNKq/1q6425ujnymPw+f4wVDhgZmlcS4rnDDuOYd326zgO22/HfkuzNCewqiBlhNU5KargFFEV7Xsf/bZf+vW2396697DuFC4nYY3InuHrSnZEMq3bZhncRAh9HJeXSzKR0nl9OD0+n9aHh23phtNpQQY4quCXYpT3vh+GpiJSln1wqrQ+elg8PKwBKg+szz7QQxcVDWEVhR/H3l/Wh2/ev3/+5rtvL9ceHse45owuNIJHInIw8sv19nq935+rLAFWN3GFMPywHiABBSBYEsnQNgneKBkjQA5jcCD4jo5S+WYglagYFeBI/Ha9WHYiPsY+DvPuQQGrjy7F/AwlUambMiOds6qaMpKYmJWTATYv+9YkyHwMyCQM0vLLx08f/+mX119ffY/Rcxjp6eFRt/PyPOw4bm+gGHENn4JICCMgtIaZZX+LXbjKiTIyl6Wtp5VOzfbu7v04SAUURBAmZAqzqLRtYyFpqotWSg8tKqpuzkyZljdAOXoyAA54lDQzLEXYI1S18vrLWlxaFBCVKoxAzOzkJW/0Mep+T0RudS9vxXwKUxnNiv8UzD4pIs1IbmojSoAamVRCbyILz0CRT/DQxolaO9z60KWJskeQZiDcLDVH70YJye356ZsPz+dvPrz7/vuH5/fbN++0LbSsCbrvrEQAi3DlTlatwFRdVpBfTENPRr3WBZ6M3pHVFZxfRbFhXlHWMdnc8jlM1leZ71z6FJ7P4J27CPKrSPSu6ynLNaEgjbuQc44ZQnpqwc41k91nlGhNOipXWkQiRaYU6F679pXVSOC+0BTXWyH090TpCI+g6kkvcWlElkVsXgiQzMxEEZQFgtx7iM1cTuu782+39++++e0PX375+e2nn95+/vXtp1/3t0tDU94qfJgblQidGRByNyau1bJSDMtlDUJpeXlCWR5f71OFrXCFMkUSFR8wYrAqFexvwUxCQkKUiKov1xkNpKp1qH9VIxTpB0QxFuE5xohMOQlJ0kKyKpSDAuBkIGLK/RFC6e7lEC8KnhAeRqCJ6CPTB5GlW8bIMLPhVoLySM+IYOImK0NUmDFGNyRsWB3OkTF2P/bebxYjG4s0OX/7BOFu7k7mro0zwau2lYfbInr0Psz2fiPCqu28bK21fhyffv00bFsf+Muvb5nWNt3OizIreCQAbm1lVit6MJwVEd7WhZhJVIihDubGGBYZ0fe9rEeRqY1JcDpvv/39b377u8uf//EfesTS6LBbBIBkqAOOFPCCJWAJMNhhUmwchKanog6MxsTC6Z4EFwCQgDEo4AKSkvCB758qq5y3f/ZXRZZSg1UVPNbj0q1HgszN/GClEiOQY0oqKw7iayskSESUVYjN3cIJbsMqznZhXpeGyjwFbm/X/Xos6yPo87atBx/rg6an94BqLtGEkHH0Nwu/HnsfpmiZuZ0emCQ9RRRjkMj1y21ZWeXIsO3h1FZd1mZ9gAaQbj5y53Wa/txMg2lb57IN6KJtbaWCx4jslE1gGT08k8MpOT1YaToZkOXBDw8QwQOADZNFdGn1tFg3VuGpPscM0BRGZFtWTyeVuTEDomJjwGae4twngAiTpuHuZqzlWEQCqjrMI5yotvMizIiZlkU8LaGG4WOEwGFt29Z37969e3769sPjN+8fvnn38PRueXpCChplpFWnWwaTrMuSdbnOwmWDCJ7BJR+vAoXavZFV6+buZiOR7h4ZZh4z4SUn/ptRguBiUxBEyCphnHoeJq6Nv4iQmIYqJsa9JICy6qbojvcU4pI1izDTgEK/ovq4Hx/ziwuiQQGfdIdrJvNQ/638GgziLEsbZfgMp76buygnrBX3w6Owplp065zHFEETQeYxVS4JJWEhkDzrw+Pp8bsP/a//sL+8fPrxp8vPP7/+8uvx+bW/DA1htEWXtjVYiLBQc/OiWEp1O6nsSqEAmARcwCtYpLKQWdQ9EhXjzOB0HwQhJAnDQlhAiHto67y4eLCoLu2e2gZSKOvoh0jtU6QiNgYzk0hwBkNWoY2piSNnSByBWYqrYc4Ij0y3UQdCRhCSksMGEGEd4TkMcITZ2GN0O27wKukm0dkEyKkUnAGCpJtbjGFIiLgw18WIObeThEo6iayR6aUgjUAQkZDQuqxkdL3cMiLRT6uu27K1pszXS399eX15Wj788Pz8vIE7SeSbL6/r48OJAv3w0YOJg8HKfhzcOB0U8Iy28lRaICvLlRbqI93C4mjbw7ou7Hx9O8bl5a9+98Mf/tD//B/3//QPn7od5jsBiRbwBDMY2BwDcEFNICFw3JUSIKIcQJSIbfhBADCybq4IwBkJNMLA9E4CyQSZLvn56Yj4el1IkhQGvvnmm3fPz7K2Cu8lSreRGVWMIpUPwhCSMTzcg2JZlypNJOI+OjyEOYWtOE3Pgi/cfYz4/Onlj//xj7/8+Mvrzx/3zzt5+uEx3Lv5MMzIoEeR9vA0KOK43jIyMIYbB2VCQZQiSuNqb/1tOy/Wx8Pjtqzr9rASHgHvx8GUHn59te2xYaExHH2wqvchTWqoMTOzZjqvDEMEkVN2h4OZoEIsbibCeif5mBlC5Qltq1RqUJjL0uqZdfOoGEu3RFalhLuDSrkYRJOe1La6jdJn1vuz2JnMZBZqSCA9mCj6YOWlTWF3RS1o0zEOyxzWs6WZxSLt3B4/vD+/fz6/e/fw7t329Lw9P/PSZF0jESQgQgSYOf5C7Ne+PMWXtetXM2QmAT6R27rN424BtIzq7PCIGGZJHAX0SyMk3AEnmVAieLKBDs8qASletU7Y+EvDFxGZO88W95wDFiUYRM4cIWTkNCQI64RkKjW6FKaRxR4HfQ3grsQGmne3nP60uiIgg+BCmKnnVEFlxMxBSUjOjCQljqxm6qmAEKFSOdJXUVABQ3WfIdwtY8RMPrCcTstp3Z7Pj999c3v7/e3zy9uvv7799PH26Ut/u/brW9+htDQ2SiYlYSZW92DRiAwPmQ0sWUwskKwaCCZxi0hjFUwXRrSmkaEk9dLeZZlBKBNu3WIL5OREVGxWRuFFUlaROwbISaSLRFoiLEK0cWuk3NYWFCByZL0IGeFEE99HIBFmEe5mtRq5Dz+6W6d0CksbYxzH7YrCzhLLujZZwjwsosfo1m8+jj52i8yyj4XDwE1b05Yr0lM2JoiNESPSBxD1efE0Cg4Hg0Qjw066PT2dT9uWgcvr5fOXL7e3i//wrItwxm/+8GFpSHK3/uWXTzng5n23cNvOqxSsIJJwUYFk7zdxIjQWNhuRJNpY2J1gZCPNuxu9fLrp0o7gt88fre+lHg4EIVEKZCTBgUEwwtxqHAFYIgiSEKKBrBHvkU5wpuqjqK/5C9ia8zd3JmZmgtcmwYGYlbMQQmb4gvbdb747PTZWsgjh6N0YFNPhT9RIGcwCwM0TwayNJMHDevlahFmEhycojr6/XV6bLtu2jcMub8fterxebp8+fb58er388iaGp4fzSky6gpfhPUo3qHI+rUL00LbMeH17S8/ROyDuMAnZsZ02ZEi2sNhvBxJuvS0LMW1CaT7GaE2qsZ3AMcDKow9uymK+eFG6gUymYKKlYYCszr1gEsKUvVJShb6FRbWyEBMJiYhVVmgEEiGk6+oR86pBKNyroA0qjK+4U0Kkc5sHJxHA5HPX5HQnrpQwowxd1cKKziZCrd6d0iNIhZ/W07vTcn5Yn5/O7989PD2f3r1ftpOeT8ikpl4h9SC3vaIzhJouXOq7iGChyIgS8iYSpBNkD/fJkJaazev08UwP81HD0syJeDLYBLeREWAOWEJIV7sbwSrbB3MAVy5Z4dSVrFfv+LoW4P/v14yfmBrDu9645EOp96/KeptnXToSVZB5n133asp5BEy7WJSE373y3Yrunv/nGOFEmNJ8oJZclMSIKVE3FM6c97F5DyhOxCqchzI9g4hZFi1LdmtbPoz16fz04cOH3//2eL3sl9e3j5/2T5/fPn7eP33ejx0JQVOsSkihJK8LB0pQXw2RTKxatX0RXplr80YpUoV5qosdh2ijRCVhVZ1AVnxnwdnlpAtKBANSCbZR8GiSEBIBp8aOSEUfhlWyMRTcJKUuUVn+48iK76obVCIRaWnd3L33jOAMG93HYX1nArkd12tmhFllyopqJvvwNIzutvvYx9j77fVaDAK3lZncXDgBRQYziCQykcHsaWPddKUlWTKjuGRlqgRe93g8n9bTGp7Xy/Hxl1/7MZZlyRA7jFlE2vlx040vL1+yhu7NzO31y9VjIO38/IDM1lqQZWa4JUDd19MWCRvWR6iuxI1FxxHDwx2Pz+9dzr/+6cXttm5te356++VzgoHOhfPDAQ4YKqMQ/lUwLXWtBUXY7LCAlVa6QgRnMSclVYxBsXigIoG/ZqpUACJjKX97ZXoSPKKvoueHh/PTOzCub5fWQARZlgzzCBUNzxSmkgCCmFVFwtNz+HALFzAozcyOXlHGHz99flhPNnzc+uXl5XQ6/faH7/fXS/90FeUc1o9rQIS0kTRqg7IJZ7IA3jtnCPPT+SEtjyXgMXbLyAwcvYsly76cRbVl0t2zCYiocFuEKN2NRGy4EIsD4HS4h5mRSKZ5OGVCOcMgFOTVF5AWd7cpIZIFZWdDOFFJ73nKSZhrjsCQFMxSXcoTVymZI8gtmJmEKr0/KcNSmoLSLWYedWRYrz4ZEJIi0rsbGMN6jUnaWFbdHk+np/N6Pm/PT+v5vL171mVr57O2DaJ10BdHT9LgJsyqS308ySOJS5ABqkbzKtOaa3hmRrq7+XCACJJAIMI8ED68PuFIeDgqkrWm74x6p7qhBmWmT/I6MuBT+VlYSYHpdRpwxYtMA10xTVQc7F8QlhrgTHzP38lkIi3PQGVJ8P34qH+EmcLj/s4nTrhHUcxFUNRqzJQVZAlEVuVh3XscorPWkojDAxSilXk9JUBJjqwuH04pkxsRIPPokhlsGolGTGTmTEKsusiynrbM84fR9/39b/f97eV4fbm+fL59+nL7/GV/ufTXWx/XMFqWTagVAlPnGZLCrahbaS0T5kNESepTTxMIcxddQBmJ4SXunDLbiGRJZmLliEREabvDjXj6sUkIgKdzErEMN5DziXPllIQiKHJmXRT5kqUWR2bGyHQKMjvcRwxDpNsRxzFG97EnInqP0Ys7qr4kJk6P9LRh/XaEmR0RI6yHd0dSeAKWhEDIstTyYmbCEk6eiYRuKtIiiYUjKH2QpirMIps9Pp1P59N+5MefPr19eZXk8+n8zXfvPMO69eN4+fx52Z6VmVq2Eyvr+fykwk30druOPo7rfn48MwsQHp0AH4NSVWNb9Gpslt19ezidzg/N8uXLKznfXq/BePnl888//vHzZyP6fZIEBqM7XLARMtExJRRTLgcAKYFB0ClshmVRUFOI8LV2IzF7p5CQ0uDl3atS60MdJwGjQpBwaFQlz/5Xf/2vv/3N+4i9327dD0raHk7a5Dhmu28TZaJwr0RCZqroQHZiIgWNMcbot9sVhNa0qX737bd2GGXaMGLKDBF59/RMP+BRz19+/Lhfr/t+qQ2CQNxkW9vpvEnk+bQOM0ds64pk3Q2DrI2MaK3ZOFTYrPvut3DCaT01gN1tWZuNY1hvDAurAJskJzVe2uhjOZ/uQ4VoqayKcoZ6BuTwvDlVYkFmkWTFjiYwH7Iiwz1Awu5OJCAIM4jcjEptUYcGi41RWn6iGUxNZWJCIgaYRInYM4kkCDG8JyXSh0USWEhW3R6elvP59HTeHs/b03l7elzPZ102fdiIlRcNK88uR7rIUqHckanSlqXVAzH3+6KdQHDF+pYPDYlMZvFwwIb12+Vq5qe2LttD1P3drXjgADzcrdxiJDUbK4KLy84VEZZuVWwa4XcHLE0N2n2hwdTtMwCHIwrvibtwH5mZXtJC5q/fOv1uAKC1chf9Uvh+XTNmvA8Vslarc8w46IwJf0cSkYIIjslERKKGegXZJYFB5GFJLISMqBifSSkApbFDJToRExMC05NQjIgDRGlRj87dpES7wgBD2nZecDo/vH/nYx/7vl/e7Ha7vr7evrxcP36+fn7xY/TrLQ4nJwS3dVFZWBkJKfGWMKkmYOkAE5WRePbYZoRISwIUPm+ENLebQifDqg0GpVSoJwzJJVoYbhHgEZxBjsa0AQucnLk2IJ/sC6UXVuWR8HHc3M3tSHMm4iQ79n69jHGEjUx3G2kDgRyemfAQbUTUr8cY7nsffSCIktJpWVYmAdJLrU8AKNw9kF6ANipRAwHSFOYxejc7jiMDw3sTPp8f27rcrv3zx9eXL6/k9PB4fvfhXWt0jHHcrj/99Att364vpJ3aIu6jOMDT48Ltqb3x7XKpAEficomL8OLcEBGe3bKtaj6Tc2yYLOt2Or+9XpW1nU6Pj+Po48v+ysu3TZfhC9CBCBhNfVbw3SmDxL0IXLmifIGEAGBwzhquWvml5nzJK+b7Ej6vqdNeWSEQwbCAB6Cg03JeJFY+EeHh/PD++29l0zNWdxu9u5lHqDKIItOsI7EsFR8iNSA9DHDiHKO/vr7c9l0omy5JOJ0fzg9nH2GHHderdTeL58en02/X2/rwoNuXT19eP3/uYx/7MUZfcs20ZOcA5GyZoFSSfhyNpZ1kPzyT1nW5co/09DwOEzMm8mO4uzYa/Xh8Pi9bSzvgCM5Mx/AQ50br+Qwua2yp5IkaEUUy+QAYQpwcY4+mtW5qIMystlcWKY6NhZAzNQiUzGzhMarMtepPg4WJnJVjZjW6KtfsiXRunJkZdowOS4eXjhWNwLmct8fH5+3h8fT0uD6eH9496/qwPZ65Le20oSrXJ5qUmUSq6U7MHEkEEcmZ4DxRxkiAKKJw2hoNJW8vwTdlppmZOyHDPMxKMz/Kal8mmbBKIohMZjIHUcZE7INZIkMIhHQEwYE6ESp5bgI4Od1UX+1VxZxPwjengmZO+PtUn1KGzKyzIDNqWdTqULkjRNNJMWH4TI9k5kRGJlcpQWRmSjUxgBgpBK1jIJPu7Xez+4UoBEKckZ5efCpV8CjS3aX8Gom4S+pyYuZMCaGqRgQouQ4JYhIGUmhmOgNJqiBhJhJt2/n8/lsb+4cyyNz2/XY93i77y+v+cu2Xt/3tGscYdoPDe7S2ELGyapE8WSUVmZ42QktIBvUYAkkIBNVDkDNduXK1KYmghAxpGulTw0AwH5gKxAwEVvCmvDZqTE2kaX6N6mZkRiV1UHjYyLB+e2NKeAmgM3rnyoklIabGbIk+doSP3ShADe7px7DuOQJebVMs0ij4dHowGxmZCHOTpYVHUuQsDwIiPU1E+m2X1iLCbOgi7sas58dnluVyOT7+/Pn1y0VVt4fT8/tHbUTCNGC7BeL65bqf100a0iF0HD0atrYoVA7mTQHMVJVwELEuKojwcXi3Q9u6bguRMGe4x3Fry3J6ekhqp9PTD3H6q3/x5T/84993s4fH72wMi4PIci4QSUHI4NJrElfeCMFzrvNTj0H3cjzM6x7lPRQFACCJwUAiGTOqtoaAgAS6YGNtwvT0dFp4HPby4Xe//dv/8r94/P7dy6fP7dzG2In5dFqWpRXezcQUGY7RR1SKSZC59dHrjKpyt9O2qMhxHC9fLuuytO00jtuw8fOff7x8fjvebnEMu3X03J7PJLQ9PR7H5fL69vbyOcz2Y3c7eviXy4UY54fTujXhBmIjXx9PgQGJpuwjMIicFfj/cfVnTbJ0yZEgpmp23CMiM+/yLVUooIHGTM/0NDmkDBehCF/4/1/IZ4q0CIc9DfY0qr76trtkRoT7MTM+qHncIksAFKru/TIj3M+xRU1VLd8ClyW/7vPkTx8uX7+8Pr17KrexnsnQfhIzQ8Xc5jojbJ9jMVZmDbcUDj6KS+23MKMtgFP8afUHtQcXz4z1fN73olUBWlibEeZwJ0t0QYAto1UJSdP4MOfcG17PWXGk7EE/+fn8PJ7W5em8Pl9OL8+n56fLu3fr6XJ6fmfLMs5rFctIWJnma0F5WhCR012WAGWLoaKatpIazMkqIVk+RtuxVWplrJB+k46g4MbMQtb58pQJVM45I/bodkjoSh0kz0q01F/u8Ubtv5ysJAcqUENF+QE1C3gpF17TnM88KlJGVEuJBK2L3VjJY41MpwUgoyqqdV4HMx9q0zLayZPHihn7Ww7pYTbNTCBggMnatigzNVQj+lXa08CmmUahYLK4gWseQOJYb8/Dta4pO1Uo+OgVB+LPpoZ/lRxOgrSIJAiYDcsZRfj6spywvryHvFdu9/v9tl9v++12/fq636/Xr1+3L1+vX972t7e57zN2g7MMZcNWg9NhbvAOCM6VwKzJhJvNDHefGQRIt+EHh0eImsEqqcnTbu4cDAs7OS6Dq6fT3ehm7oo6hXQzaPl77RrmZBuRTu6ByJjBTAeNo9yqZmZFcOHYIrtXy4ptN2jZaQ93lmXR9pXC1GoL85FHwwtJyQEabbHcImKezucZJO3pct72PYNP52dweft8+/Lp7frpttry7uP7p9NpXT3mbhz7/T6WxZhzy7ev16f3z5RYVExjMy51frmcXs7bbUu327Zn3As5Vl+WMU6n06g5JyppGG5mYu9VIjgWA/ftej5f/vmf/+E//sdf/vzr9fzsl+VPf/35fp9XAiGju3alaigTUPUWBY391TWnljMWgP4joP9U/3iyJ2AOBFAFM1sM6wmr12JcfT1Vvn798nluv797t/7Tf/jn88fLts/1soyFxLqsw0z2OUPZLkUJmgW6G6+37X67ZsayrjXzen29PK2Xp6d9i89fP4/VC/H586fr6+svv/71p//65y+/fnn79GZR189vPu3y9DwGnt6d371fv//uw5cv795ev7x9er3fb7HNuW2snHOON3t6em/up2WYBx3L2TnGtXbMrEJxGLjf59xzmQNW62W5327uZmf6uoKs2CMqy+Ye2zYXXyKSA1WYe/phZVCrYVTN9FWbfEmHJNA2XOuvt303ZxUyEmYiALepg3mhsdBk0DJmpKxTUZGzSDL95H7y0/Pl/Py0Xp6Wy/n09HR6fvLTeX26+Loul2dfFltXArSR1ULRBtiB3lWMQpWbY9jD6qYiNAqFEWX8G9+FIaQ6hBNqXtSSqhAurJZuD0AsmpxRgdznbBidImoymMw2/q8+pllC82tqcorcK0Zh9JKcrtAKBhHbjF6ZxerlMFWyDv3WJxyDXRzKMzSf8+Dum40e+UbaQcqsh2CrCjKb023QijWipmwtJGvEMFiVHXRPSCUZDySJumEuy2pwzhQ8RVbN8GE008ObFZR/Px7ZyEsLKPlAtQIg2umBNHOVb5VZoWUzvcsNDnOzdV0vS6aTEXO7XefcY9u269t2u2/Xt/12vX/9ev/ytr1d719vc95yL+NA5MITi2NZ3E3b10RgYkHlQ84ylqTXpeKdTQ2Tcw7oksnVoK2O1bB6AiYTUNMsPgqIqt6so1DOKlTEnNcr97Cq7fVKsCIgZwGJZDJZGpWAEh8WAJg1DwrrYvSKNBuHMb23Fs1COycFwT3AQPehzi8T+7aB9vz+43m5fP58/fTr6/1tGzZePrwj6vnlGYz79W277devb8vJ15enAdveonZ7evf8dv8cM3Ogtrf1dFouy3ATQHvf7nO7uYN2cq99n7bQhs2t4raFpa1tJix+DLDNqOXl8u/++x//d3/+59f/+38alU8f/jtj/PmX/3Xu96pgTWAU7oUTHiipdnO0D1fD+k2n6Omx0EhJLmTcJIHd0PDAYcu4JGUlGFvdqqquI+stcXNs//iP/+0//N3Lh++fX6+flsXpQ87qlD4/J7Lu9+12u4I0+ppL7FvknDGlirjfb2P45elC4/X2dn66nE6n56eLm79+/fr8/PynP/1p/3L//Pbbl1++3l+37XVels+n5/Xp/VjWAeTz5en56Xl/t93f7r9//X37cnv7+uaT+y1v+63AeFrxtIyLuY1xWXyseMq6T2xltO2+MXh/vWdGbvuTXdJrryBPYx3LeqmKisq99tsGmDBDH4uMVTKRmes6aqQqSVXNY0hgWaRF7L7II4wkU3g49xTQy4gZwgMiY2aO1VDmp2U9nfy8rJfz6eXp9HRZXs7L0+n0fFkvz2M9+3Iay2rrAhrdUWKadHgIeahFqLg8VqGwg0zD0V2Sko+QXkajE9l2LOLyVyUMGQU2i//gCjMyIA5TJWA1Z0avUzHz1KpnlNFmBMlAKCXV8RvFbqpEsqpow2fsZSvM/yYeWpWMjzJL3BlGHRbZnYNYPCL3Y6r7NzmgqTzGihpyUDLrBYXVqSjN7aj7H94QnbF4rLV0KaqqaGXy2eEBRMn4wpglH9AeKwjPUTTXejKGElp2c16Bh8Vpy5eJ5m6CEMh47GYEEYdqDkBUsWQvgAbarH8CLaQWf7ITiUox6zNn7DHntt2u2/V6f3u7v12vX77eX9/ur9e83bf7vuXdymvCx0DmYquaDYAcTM9lmPqVnBMFW52AdEAz5sygwU6jhl4Ml3V5qC3MJCaIkvoXZbSi9frSyNju8XbL+zbMtre32KNTOxn7BrY19pAuUiWd1kwZssqGGy0gzkC2iEQkhG3PrJppHIRl5LosO23fZ2Qge7vSy8ePHMvX317fXu+5zdPphOJpHWP4GHz7csu93r5eHUT68+nly+df5+5ffvlyvpxRS8x9WYaBlXXfthg+YxbSiPW0jiFHpmJUzG58nLa93W2mjzGW81isloU+7veq7f58Wf7Dv//jf/lPv/7Lf/nXyN3dfnj/d29fv9z2z1GszOLZFH0l2YWAR4gdpKukiS6RiSC8MOtgfxqM9KpKxMDidtGCOQNvuRXeEnAEapl4XYHvLh/+8IePP37/3ux2ff1s795lGTJv21zGGO61I7f9tm/D3IeT2Pf7TnphGQuYEXNdfFmXyLp9/brv+/PzeV3X1TzmXKqut/uHDy/X7z/+9X/56f5le3u7LbZcr/evb59//51P78/zfns6nz9++PG0Xp4v6/PlZX93f/305frl65V7zIiaX79O1PpsT/dlW5zrsuwV6+WMJRgi1kTMQNS85fW3Nzsvy4nrutJRqLGsPdLba3K6z8C9lhxVMA6izFDpi3FUbmkKT7rsDnNyHTF3H37fNyD3jJkzlVQrbBCOcVnPz+fz5eX8fLk8Pz29vF8vT6fz2ZfT6eky1pOdThyD6+Cy0BwwowPM6JlqB4OWVZKZdA7XipUCKrSWtUXZ6Imy5pHs2OTdOvaywirknChGJQ8T+4YiGrSYlfWAaOhN6avOKyQZ2gGDBCojhnvXX0i3UfWYCmOoKY80i0KyqaRlpGTGEg4zYVogLye3Ooa8LLb9VJGkIUPMnweH30ofFzVEQRIoy2MjmKipw70VBD3UFPs0KYZQhWW5wSR1QZRRfFOQh37mYKVqC7D0XZWguXlnVzVcUdUjj546A1CtD9IUJkU4SPkOFkhZe3cN3YRwV27ICIhDChRh2nyh4ZJpOS5zTpiPy7LY5fLhvX54RsQe2+263bbt+jbvt+162663/Xab91vue8wZe06tCIkaY4naNC/WViMuPZ8BswYI43nUYmn0ZYE73atnyOp5H0o78YYzM/Z9i+1ec8b1tr++1rbdbltGzH1WpJnZMMKKNYavYzEyo2bO2meC5YiZRlP5AzJqygwqUFG5bTf6qvO57eFmy7LORGaYnzK3iByn5Xx5ejm9e329//7z1/t1u1wuTy8v19t1Hb6c1yrc3+6vX64ojtPTPm9v1/synj7/+svpdDpdLqenURzbPVDxNFYbnlnLuu73+7qeaZjbbvRhY5wGHXPuaPEEY98zcx1P53F5nduc+/V+W1evspcn+2/++Ye//Nfff/rzf0w7+fhQ8DHeee0zg9UeLFVIzKOfn8Rk+z24tgip9y7EIU3RaNNCjBUsgGXuZO01CzNxKhjgKlIG1g+nD//tv//T//h//j98+OP72++/rk92WbmeRgG2IeZ+zz2uMyOXdTlfzjQzYN/n/X6twlLL03ld15VORO4zIhOsuc/tdv/tfv/85dPX3z59+fnz/eusO9f1ad9+2m6bn+356fLp7brdtq32i5//8vtvr1/2l8v7Mez56Xx+On384bsP3318e/369nrd9m3Ofb9vX+7X09dlcXv3w7tlrDl3d6+q02VU1GpPFXtV1D0ARPCON75cxuopy4rVck43GFartAzOrbQ4pJBzGhJrCaX3BWlayBE1KxBR6dMwSCOHXc4v55en9fnpdLmsT+fl6bw8ndbLydfTcj6bjdPLs9FsLBWwZQHI4Sm+ixmKcJ8ztH3EtCPxsPLsnYq+lHx4RJRAjWWhe3W8UUcvF5h6SPe1lhdHZyDwQx6Omgi05AuHHzul683jzxuHIUH6vm+0XmSvz2BiooslSmZOSnGq6p3lqpArtE3UMLRUs00aZNXHo2eRPzwPFk/JFELDQnTfKwWxmQCS5r21NrCaB92xu6oyR3MT0eoDPYWHoVwkUWMYM2gHDyraT1QJg8SxJL2v9AGR95NlFUyrmdPoVbInQFrLGVQvo5s24vjSB8ejDvwc8pOWmkb/Y2yHaRrNLGYKovGGvEJUM9mkmXA1czrdl/VsT+/edz6Pqox92+bct+0299t2v91vt+36FnPObddmx9i2nFGVe0x3olgBeYLSjKsqf++MjMPBVKtaCtrekBUai0XEfr3N+x5vdytaZNz3uk9kYmbNqFERczmdT8saETWzfGRUZs1dAuh0Hxiib7EyDaZjkxFzmw+u65xltmRi30K6vpqT5ax0Oz29++H109fff/6Czbz8tJ6Y4cbTZY0Zr5/n9W2bkd9/+N7o109vr1++vrx7Gn769Osb7dfv//ju9H6NPc6XpWbOLENk1b7dho28JqvKvEyChEJhbjssfeFYlm2bc16/fLlNeJidT6esOa9fLZd//NOPf/2nt5//+vP1/nUkgXWs7xELPCunJYCciCgvKcKbB300i/ib/9vgpK7yNNjACi7kYlb3eZXV/YapDVNEOewFzx/W9b/59//m//p/+9/8D//Tv/PnjLhjry+/T3+7uHOclqzKCPFD1vOy3e7oqdW8b/d97i/2tE839wzEjHm/xz4J3l7vEfuMGGNZz0/pX7+8fvnrf/n16/X69IcPdvb5dv06r+VwX2fVa8by8uH1Nm/bF4LP19vL9fT+5en5tP744/f53bbfJhCv19evnz/N2/1tZub8+P3HKk9CVqy+LmUzQ0ZBuN7fVhtzi5i7OXLHcj5VFWjF3Ocelauj4pB3FeERIxBzxw0DPnysayF9dV98PZ2X83p+fjpdzuv5crpcTpen09PTcnpeLmf3laeVblxHVdGHjAdyziI5DDYqe8CGVG1YBNX75r6zSPOMEGrTbl3Zw1CzgXbYQq8RxLEE8Sh9jxoZkeL+eVaouZccAWRlRYabV6GXCWpvh5S4mXTLtgOqmADCh2VEVfukOXxiZwPsJQxGppzMQhImIDsAZOykcwxTcZIVvS8e8iUWAJWdAxQRD7jkgBk6gh+nv53iCHMOEYlgKC2Wkhy3XYcaAlOoMjoP0ErzEUT44GAbbLfLQr8Y1VZIKJt1bOmBsFO4GFGCosqrDXdZGbPZGcc6arRkjEcbQTNjg1XHfICyFNLGgRYSaxIis9kC6IfTebOckvRQ1dmjHlQAwwHTSApOLn46nU6GlxbmRkTMbYua877t22273/b7bW5b7Nu23ee+z/sttsippeQTqAxYLpwcdmKit7hFVUgWVyb39CxUIIOo3Ld5v8f1Htd9e92k+T75UjZm7I8s4sS8b4E9Jvb7Pveo4uJeAIflrCyxV1Fw8zFzd3MOQ5nTymK7h3HQK2LPKHPuM5bl9OH9h+22/eXPP2PLk7bIvl396fz08rTdNhS3647C+eW9n8a+zX1Lu23jw8vy9LJtt+3r7e0yfLHTu9Xds+77fs85gcyZM+7zngyYx6Uuc78nywbHsMvLc2SM4VXX23YluKfZch7Laf/6FvVUE0/n03/7z3//889f/+N//pc537R3WnxmgDtgHDL+ziSQxnWPcqTbkrDIrZG0ToSPGUAVyuSCVTcpaxNB2MAFcMdiwIuff/zhhz/+4bv/4//lf/vv/3f/eHr2P//1J2D38ymjzs/303mc8qzTuZ7O794/AbnvW6G2bfexvn//Xqc1ZrxdX2fW9fOX2/UNkZeny3l5sipEfXnb3r7e3E/ny/uXd/N2y73gOFfl7Xa7z51m9GUS7m6rVWzzvr1et9frp21/eVvt+fL08nJ+eXdZn8YP8+m+f3z7+vXrp8+3+/Xrb1/MeD4/cZKLnc8XDr/PnSgWzmO1UaenkaVzbK3emgYzbqrJ5lgGqY29DsvlsjhPTz++e3r3fH66+Pm8Pp2X07Jezsu6+rqu58tYzmNZxno2c3OvJGkSBtAskXTLI0/bsgDImTl3jVKVgyupyUMP7m0oOhNqBI6EXl1vwozt7yS9fokR3xyYYzGiQlyvJKtEZYTYxI9pweHvkAXKiYgE3XyvSbEsCpXRFVZkPmaoIKTwzATptKisnBTK1l1FUxWg2Df3ohedYxWSgtSUEWZWLEsK18VRy5Di5KAyteD22BVTpPW4snox45BBXSUkcepBgnaod5NBovwgTWQb8BUQRiCjmNZ/DUY+RrLm7M4gwUElRQ0GgMqcaAO76oX2wo8O5mq3N01uIS1JY5k8XLtbkVVm8/k08VCetmNskc2JejA+NBDQQtHDsk3NWqNXhmMlTgGMlFebsaRRSIA21sXGSuNTZaWsS+a+FXPftjnndrvG3Pb7Pfd9bvvc9m2fEXtW0LS8EAzK7lNrNkIxBxkzcs7cZ2y7MJ+IMlpULba4L7O2oSRKIin3mtj2CnCWpc0JmDNt3nOsbmQg7tvuRokTtLN13xM2HlT5/XaLzNj3sQza8u7jx7GOv/7LX7UQ5fnpcr/eZs518cqq4G3btm2zsYK8vm33/XZ6HmO0ymnu24z99e3KUeP8YS4VEfs+18WM7utp3nfUnlvOmNt2o2WxLr729lCz/bYRtoy1kjCjOSYGh7nvrPWMv/uH5//p9u8m8z//y0+3bZN4wnEKZLBWf18ZVTNzspDAgtPJbdi659wyAxEYx9nQ+Q7CL3g5ry+v2+uOeyImICNhoy84XWw5nf1y8mH7xz9e7JJfPv9qL+9fLucdNoF1cXM7LQt1igCrWowVTJZG2too4FzcyYr7fn97fbvf75aY2/7lPvclzfzrl9fffv3tdr2hLLcsjprYtj1guZ73230v7Nu+nHxdz0CNMUhzNYKZX97umSPm1/v2mu8/vvjp+emyPC2XD8v7Hz98+f33n//68/o0Lu98+Hi7vl6xVyBYxl4JdX53mmP31dKLq/sL1/NpuZxPl9N6Xtfz5fLytK6n5bQu61hPJ9LWsQxbT+eLL+vpcgHG+nzWsl/xNNwHzSuqfOxRssxkFmzI7YdJlLgo0kbJg8bckBGSqhTh5lwWBSVrIu8D6O5JgKxsWA/K5MG5UdA7gBqlCBzkTDRbS7H6YOQZs22YBdTgwHpKvJps+XG1BWe12FUThRSFkYzoqlTDWgAKyir6e33mQcrMyoo93SsXaaMU7qwnrZCVGr/9x6LUu12AlxiSZd3ahBwJBaAlwGMnMMCc/XCk3zjGIqVoqCU5daiFjXAzY8oGjo0CatIsLozpk7n1HgSX10KVtRmcMrWmtUFQpkHdrMCkEVdCygyas9KdnYPFJHhszdGnEJDeGVEfy45n01QmdLY3yAitTBuF6BZapSva6xiIqoK0yEpEOQPa9Ky9kghzJwxc3Dj8DMP53A4Zhcx9ZkxUzfuemXPfIuaMPSLmvmfOmBExI2dVabQARO173ve87/O6v/3+Na5XbNMSvhiYZXOYwZdEorLmrKyaVXtVwt2RuZyGu5k7e6V4sriMJfaqSjMyKjMRlTmtLJE5k8WMmaiMfLlcnMvP/+WX+6fbMszA7Xp9/3LZmffbFnNm4n6dy2U9n9fX6/Z6vW/bbaz4+N0Pl3fnYVyffOb9drtuK+MeWMewSw76gsxpp+X5vPoy71/eVq1ji52OdVlNkaJi37cqzzKzAXknLMt6umx339cay3h6Plld9qrTOP/P//P/etvv8OF2ZmxEWUYhKhOIRFmra9fhT5b3iNzrTUQgOyQCAA3LYs85lx3YtJNKSCSwgu/O4+zr9e3r9PEP/9v/8D/+X//7+9vX3+6/nDd/+vju5fJ832dFRMbtejs/nS8vpy+fvvz65fPqnig3RtW6XpC45dc8n9fTQvK0jHDP4aexbIUvv3/99Pt1v8ft7fb165fYZ8yMWbe314j7cN6/3rdtui2JweDqT562ns7vLyeP2u9j3+YWdj6fwua9sL/m4Ovc76cnE2iKE/xpef7jMzl3f7vnjDPs7KfTuo6xnJfTeT0/v7z//t3l/bv16byu6+n56fJ0EXqzLOt6vvgY6+m8LIv5GD5suCCvZTnRLaPMHclCOVV866Ra7mm2APQ2GSbNsgpGFdRmlEjYbPRWgUy5frpJfkZ3Bxp2ZrutarKrwFsKk6YqNKsq5IIp9ckBRYtNI7UfYKVVFuaekUbOqXhlqOZ0kKzsCR4ptgtn7NkMyhIGkyW3mAHU3KPZBwUU3KzSDYYsLyuWi3dsJbPiI6sZq8wCCDW4IAqmrwYeg1INrwpi2/AwlzSA7FViUn51jiQPAyUgMb6tjlGBLBZjtyL1aKMEp3Qopw3SGWxgv5S4MkQEyLZ/lbqhimyzHf+bbSc9rCBbxWCSlnUjI+F7osG5Hh6zJB1MkDnZ9FgSJUO3A+yqqtCWGGo7PAhrmQ/bXLMSxTxSB6yiKDNmOqows9p4lbaM5iwXi5GA1GLZe83agqOH6YQIB3Tr1VIGPLffd8leuyrmLC0KiDnnPTLndt/v96i5vV2389v99LKc3hHr9vY577f9+nZ7/bzasp5HZeVEZs0txjAUZ1jZoodhw5PlS3t1oSdYPiM59GTBItMQM/ech6bGaSgiuKzny+l5/3K7fX57Pi8cfnu9oujLsMHbPvf7fr3t59PT4uv1dZeXpy/0xU6nMZzu9HWZQXOQfP3yBuTLu5fTeNrnbVnPGq6dny65TRbXsey7+TJAzD0UlpfT07bnvOec5cONy74lB/aYrELc7m/383r+7/6775eBQv3P/+lfXu/31OQlnCwgDBZAwgjb5pyxX9ax2hgcplVfgPyfAyhkwYZd3Be7nwfMOU+rDWRVrMBac8Dff/f8T//+H/7h3/0AD57mb1++8Gc8vX1+9+PH9eV8e71lYTjja9y2e8R+u15//fIlshYbz++fx7tlXMZ+u0fGsGcfa1bOiFHc97mME7H99K//5cuvX7bb/e32lrPW05ozb6+3p9PL8/sx7HMlXn///Ie/+3D7cn16en+/zz/+6Y+c8/b1eostat9qt6U+ff10udi8b9vL5eN4su315fy07TvM+L3/+P7Dxz++v7ycl9O4vHv3/PxyefeeZU/vnk+ny7o+LetpOZ18LMt68jHMXSffaArZohNWlrtnprHk+VwJG1alrTgRc7LKxhhjEISjKnMLHy53PJP3cWaTRVBMkbgK2SI9g3FYzQ6VXeVnFcoOrkqD3TJJ7jK7551KAzimiRlSGqMhD1GGqvYt9MeKPz5cGElEqIiHklNGVajUfSBHaHojE8k60ObCcE/xNrOgxbRC2bS8RXCUC4UUuxSZRTezMQwTNWtHempkfNA4FUQpI4VqH50g5EDccL+Sa6QQDvY/xW4/CnJqBUvhux3dOm8cM2WwNz9qrosqMIBw05Kkykit6RXabz3OhtsQ5HV0SaJVqUgX2Jbd7GjiUYcOO3v9sX6OvrNSk/ICvb3+NQvqHuTxk4+Jdieb3nrW7ynb/oigjKIbboK2FigqyL76mGh4f2wDYdWbNpkqWGQ/g8ykw3xQilqVBDAr0kdVJdNo/sAZD9ZYVtCQM+e2V0Xs8pPf7tfr/Xrdt6/72+t+fb1+/TL3m3Hevl63t7fb6+vbpy9uC2LOrW7XG8ExbLvtfl7Wk0VFAfMu2Gv4GL13omKfIr3UjGnmcEZEEeYDWe8/fn9aLn/5y78OcF3H9W1j1Pp0Hr7uMZm+36/MciL2Obfttt0zg17n0/njD+8rtvvtdawLmIvZ3OfXL79x/8Dg6d0JZnOvsXLPPbcbVyz0ZazjtOxzn/u8x83CTqczbSzLuYD9bU+stp63/Z7bNXZs+47c375G1nL+8MPf/dPHa076/Nd//eXz5/uce+G0s7LCOLyevI9EzJqV+x7bzEmYRF5OYwWwOxCYW22WMSzIOi3LsjL3fbtG2OD788sfvvv7f/y780uZ188//3Wv69/903fPT89j9fl2u1+vOWdUfvj+w2mx23b//PnL6bS+//Bxu15py+V03u/3mOHmZN3eXpGvWYmIdfB+m9t9f/30e+7b66fffv6vP//Lf/6ve8zlcrLyPXFZTziZLcvr29v9uo1tbNebff20Lqftc+IetufT8/puvTy9+9P7H1+2uv/Dv/3jcn7+t//uT/fb9bsf3i3nxd0uz++fnl+e3727PJ9hHMu6ntdlrI0oNPWCrOOaosdfWe3TkjlFe0Rl9d0VZ5AVJcsj0uFw83EY/ZN06SVj+jK0/EM6JqiuYl/iqgLSzJCEUO1mnMu0sRRYaHUQ4NHDPdN2W3tEdn1wkoQ3MqRtM9kLtQVgCzBRwcpUjZg2vklgGwASNNR1H7T9EGbtadrTYA1rufiIRFWI2y0M2tya6lfhXLuGjRCMZWT2LEtxfYdmJDXNB8R774VRYBvnf5tlCVsBkQjlQIN+WjUmg2+geRUGWx93DMHRcE1Ue9QdYHzD9CSQIfd/EwSTIrPjMN0Xoooju4Kgj6XfMQ5L/qNpIFPj22rLZdlCPGYAx/ljovR3viFgecxVWnRuDzRMXkOoPJa5VlXB+I0PK+Aoj6wCPM48layo1X2p6XVVoqyMRiMTh/Cr/yXOgETdMobr+6MPGiVg6jin+pCUeYoMpccKGwv+f2ycKuY0ImPLfY+cc04j5r7P7XZ/vSYq9v36+fPty9f719f99et+ve77rWqPuW3bfZ9ht0kA5XPm7e16f70ac859TmTw9e0+lmUZ6+22kT5nfPz++xz20y9/3ZkDte3ztJ5yOflpiYz77ZZzu5xOlbk6zZZ9v2kKyBkv7860fHl5+uX2CTN9rKfT077tbzG3rzcDC/H08SW2W3iuw8bwbd/JCksrM+c2C+YRebvdh5svHONlWd/SrGKelvH29hZzImpOLj6+3O779fM16uU7/A//m39493L+X/6Xn376+fM9IjjLPCuMJ0avJ3KOLThI0g0ZtRno5pGzaiZ4Qs18PXG1fAMm0jIYe66+/t2ffvzTP/39n/7xD5MzcMU6EtuHj++Gnd6u27Ivl8tIhjsLuH65botHzJW+YizLuIyLDYdh3rfMeY/r2HC/moEs7teNhNH3+z3q9vLx6fTun7/7xx++/3c/3ra3dR3MuW/zh4/fff/DH3/404+fP93efbxsb9v377/78Ycf3z2/0JaFfLosL++flouNy7I+LRh2Og+OlV4x0xwxs0j3VaXInrOQIbjmiOg1A2gPYGGeFaVRKt36+KZAVNfkLzNYpW3vmd0wix5I9ELUYVZs+g3hVWlFHP+s6D3H3LTqIHo+KngI76h2FG6r+78NUOJQUFc+Ce3bklWDFapyqhbMnvwhhSDJsjdbLJYx3d2csTfkgA7SUIkc2YoqEGXISGlsmlDTfYAVECW1gBYXwE21cfnwms2eF10FJfAnWfK5kW9C0lhzz3KsKwpGz6bEICH17hFSFOCLJZhduExH2qKcBfQsD5c2spSEUVDu1rAxo79iATS3wwklmVA+QQUywZRrfGUQsn/rgEsQRVXekiqYWfZiWqmQH1ajMnhI8yM8V8Ka1KsJAE2Od72DRhmvmwlF92yyf/+HbwTYfiWV4KH0Ya9ngoTL3XIC3eIVjhnMt/IAELqD1kJHOV2LDhRZaFYpKW9pQiWkUojkt7BvPfepEN5Ad6tkHv9cIc1HVuaM4QM2qsLX0/rOZF/T3j7DDiqyVWQ1PXTO64ZRc7vev37Zttu+b/t9y5i5z+12vX3++vrpU8ZW237f9tvr7fX1fr1ec9Z+3a7XGXs+f/fy9vvbTz//PAw2az0tL+9fQHv7+jX2fH1988XfvXuZiNfr21jvmmGMYd/96f333z9fVjr2D++ffv5ff/n4/XfrMOx8ebnMuX35+XdbPviwy/Oynk6+ENxoNq93bTLOLUg+vbtExJxz5qzczO6L2ayZ952D5wVL2Ne5vX260598nOa+Ibmcarlc7PSDraiRf/l5iy1sjAxZrpqbG9dl8cqaEVXm9MAAymhmVuEDC+F7zVMtjuHAqLqMs52WH/7ujx8+vHtaLz/96y9b3v74Tz/89S+/3t6+/Jv/5seI+vDuUr7fv+wkfMGPf/zx/fcfMvPLr1/fvr4WNzw9+bo6MOe+368V+/369vzy/PLdd+s6MDMLudd6Po0P3/3v/w//0zIWulQI87T4+WVUXGOPd89PL+/f+2lB+rgscd0znT7gpI3YdhsNdsjkKYEA4g4aaza7xIdlZcxNthcynUXsginpbGJizzSr15IIUKgU9Gpuipi6Rg7CnLSSPc8yADDZILC1IzeztzNlJl34icjlKpdUwhfzqNFUVoeWahZ7mVo15/uoytWxtDmYWBX+CAUgjpq6ej0KdT0bYxBHnbJyUSNSggt6i21fNSXFR/fQCjACdFAsu1KYigiaZYgBkKXPhkYfzHoAWTKKlyd2FZlT5hyVZuYFo00UkdYz40L2KmN0HAaLecwDEv1/IlTsF1tX3GNVVcKKh4q0Q+sSK8uM0JymvuFT9Ui+VQq/BpDpBTf1VyEYRomapKTMoLkmB5nL4ugesDLTjFm90qwnBMcSAjSBtw249MFl0Vw98+5eMGXGfVjO9WuO6HawKTtS52avIS0U4TYEN3WDcSxgK8I1KFCAhhC9Dt/NL+oJjNiXdvzzsv9hOzQI8Wo7u79x3hjHYi8BX+5mzMqeYkWAZsPkMugwM89Mp3GsKTcUGN3FdEYYaYkymI0B0E5GYnmnVU3I2Pe5dV9lMPd5kxvSHWZAzm3Oud+37X67z84Q97cvXyry7bcvr1++IK5vn75s+309j+1+f/309f7lbblicR9W17c9rvs+N198sXG5nL7/47ONydMsAI7zh+etpudMg/sCM0P89uuvy8vKcVrH2U6+3dPdsPC2X7frPfa4PF+sQA47LVmsCNRr7TydR/v7mO0Rc8b9evWT++kM91PB0myhv9j6Dx9OZi+nz//689uX1znLhLuy99FaZLRKhXSO9qOONPjAUrDEXAkCG+KM5Xk5X04vf//j98l5//r59LT8m3/8A9dxWvn3f/rBxnw+P58ul3nfvnx6Pb+c/uFPf8xZ/+k//uvb69fYr24crD/84YcFl+++++H5+eNyWmjwwvn5+fz09O7Du9v1lvd9Xmewnt+///B3352fno0jEQhVPwQyEVl70TIrkpmYdA5XJZvbPSK8UFWGzEpmZZWHZcGHRU0hLBVhtERUhMPcXUs8nBDnzYo6VJkV9/SFPjwjMtJ8uFlE1ZyS1AwfmSGRFJoLTppXIyIFyDG3C6F6GGybGH841sd2P88HPCGT6Y7zyULktIMe0519zz+r6TMish9Nu+KHorSZzZyCXs045y6ZpLY/HLOCw/emel+UEAwcMFOJ4VFF0sBkzX13d6eXpWCKg0xUbhYh7T0JsBCV7tr6F6gay2hrae2RCsHVARpBGcMxa9AKNSOyJjmyWLNHtc2RKZiG2E3FaYDDcEAVwLfRrh6t0ltw0B45oYEPtGlGqxTExCEluc5OA2RVGg775g6V/WiafIJiwd2hhK/Z/fDKMpq7ZTsZcXBkHZmnMmlGw996wwFoHwy4Wx7GTQq4PFTc1cOLQkELRbuBDKGTwpfieMVpdPn5gXD3qOgE04fYImdlGgfETMt0amVwWvJoTVnx+PHNdD3Skk4/vxVQjQOaejfn6Ipm8IHiJTMLoI0xUmtCzQGLubFIX2CDQMaODBhtGfs+U7prt7lPmvrRJTWszsgsf/owLh+emuZ8SOt6ehSio0ZF7rNQuU/kHvu2z23Gfd/2fbvv1+vcttr2iH17u+37tt+uEZm3DRnbbbtfb+taGbG+nd/9/Y/329y+vG23a+V2e93KcP7wtG93jufiPiNmBKsicHl6WZfT3GPO+3a/jXUdw+es+75lhvmwGpUs2P41I3m+LD/84f3bFRkbsmqaeXK7+3b7ePbxd++/vzz9mx/nTz99/v3rvu3z9Xodbtt+W+q81YyaVjAkjAH4LECLICzBO7aKQa2b5vO+nd7e5vX//dt1e3s+rfT6y19+K4/zy3Lbr0/v8N2P7/f723a/PT2f/vR3f/TaFsPz5fRv/+nvv//h4/P7dz/88O7yck7w8u55PZ9suDu1OYd0de7zuqXcZGbkvn/99PPpdPKxggPpe2VvmIJVlAFB5Iyq0pvMQmWYJYNSa1rnuFLFVDGHcAGzqmSFTiTL5P5bQMwA4HS4a0KGLD95FWoWC4sPgLmnGWGjp7CRqvweMzaNt3QN5IBvlLWU+v/qShrtO6DxqLBcOkuYCdPgKc5Ex98eCfYoCwcBo8cD8sKK6g/TGqasygx3j4yuMgtH2Y9Cb90WCq9EONwruzY/xq15LMNCoheMyl5ljOXAnlogpkWzJFVVm9u+3ymHEbr4pnNORYkUyuBmyRlBp/UewaCY6ElzZ2XF1IpGc9eImKXVxCgA3vmwEZ8oEsFvJtJZKX6tIBA9oSLGgZopMwv4wAPzag4olbJljzTVwTlhimZVEgq0P5GKYzkJsQpJjIfds/Y7mmuzKHw4yYhdRKDOIIlqZmb78R8jb7CQSLR1aHc5EKZWAJK0QwzXRUAPj1shoDWTkCsOLCu6v9EMHY8JGKoqVMvwQX8avfUSstrobgM074SlIZV+VHVbpK5VWq8ekFRVCMISn1g/MJGWqAytobcMYUM6shh2AlihLFLmC2zotgimy0gy4YgZVcmxtgeTLxkzRKBWN5gmrUYDaHUo/wCttPPToJ2t5tk5M5Blbog2YRI/Ws2fAgdhc4/r6w1jj/ttv28cy9e//F7B6+9f8v62f32LffNR9/1tPHvoL200Wq0W5uUWdd2L+9wWlKfPOfctspDbXHEpYtvCaoxxfnu73nd+etuQe3G5Ts6KCu7X7bSctptV2fvTev7DD//4o+2x//W3T9fb/O3z232/RlyzAliiKmMStmFP6f+wAxPgZl/erafLunz4cDk9nXPi+d1lwdN3P77Q8P7988fvX8zn88f1h79/t8/NOP/hn//08fuPP3z/4/sP73xdxuI+luW8FsBFMEZqga3cKYyjDwlYwHg+VWElo2Lu97j9FHMWNvqJtmQZml5G8fEkmmq1aCEyzIG0rkKkFGKHIRZcUiYmyApSnebwykRSAy2nFkDKWEHoB0uwDmAYXeElS0bOVZrbdd2jJruA0tI9Tdu0IUPh2PpGUJQVyBi4mp0iSEBb1POQIj1w2j6qEdnbELNoLqo8enmRqkVWVYWKuex2XupfkpU0j0wN3hSQZCsvthG6uVDdVQVmTLQJo0T71o5TnYIIiH5aGodIYwSZ3mfaQT5tIXFRrHcKoBdikFPWNQQStc3dxLIvrZGYRYdnRfjoIhuPf0nhpTTZvwqNQhw4Rz5iVY88UQeUNyjYRwGuMezup3oLgX5hNwn9m8yGoYo53Aky42+wcz0VSuUrEKTbAi0/QEl+BfaqBzevb3AOzfwgfha0IcBIIGZokFAodwcNccx8VHQIR+x6X7/i6ECF9+msdsjTpJoyjVHbm3tIljZ8qVZ4Y3EHTMus+zGqFlB7DkPLzupYj6THmMhq2hZ6YYMoCtWzfrW2CWoSoMKk3IdqKPElKhtNUvlhbuaM2Es3AX3vZK3Rl4bmrkIPYFPmICFdJjLhIF0fqVhIuA1zZioqzoP77EhocReCqF6XUzxyrQSWTBjHwMu5zAuGyDDUH/6hcgbLGDFvO1Fzu2/bfZu3Gdu2326v15phybndau5VE8zYJnLGNjPm/b5v217IOfdtm3u+OqzMeVqHA/e53zaVYw7eb/vnz1fw7ffPc+CM+/njy4fz+cNYT0+X229fbqN+32bc9xuMCMOEu18u59qxLMvldP7h/dP7dy+XBf/+v/+3T6fxw5/effz+48t379bLOL9bx9mWD6f13XksXF4u9OIgh3CqAIDTgmY4UDVXgjCriEIgMyrMaXaCJoczEGG+0NpRPCpBN1/Gy98JvAWbXE9j0+m+TaoELVjlPpyg9divCGvpU6KAaSjjaNzCaKZGX1FJqLfVYXBG7TiqFJ3b3VDAjEB5730F0ZD34T2gO5j6WAVD0YzI0GjWxuj2XDTx6l6h/SrtocG0qkqZqJSFiDFKIdr1gGICCrACnmWdA0U0wrUT2PSRBDbIAlAT7Kyyg0+UkWZshoe4jpoedmLgPicpWwRBNwfCgcOSp/qhqZY195yh4aWCg1ZXubmm0SAzM0uD6KBZSv4ZUYfpkAHrcNCPTIgqYiSQ7iQCHE00EhYGQqvAGxfpjWX2yMjdC2rMQPEOO0JbjeoHecxEtaNKkD8aKwC/OUTTqJRdZBWrw+nhJoQyoqwpY007egiyQI28aSGmkJ5Ib86KgDkzzUzzKMc3pj8770s+1tu6ijLd7ZXFwih1fAnQLfYQyoee3+ryFGCH3RMlOqDM6dhqNf2hJk9VbLqq2uvWW8CHP3q9Y5b+kLJBnpwdfJslpv8pGDMnbXT2opB6q+yzypLZt8BEfftew4kU1azZ2JVVM8AcNhpnsgMaE/E4i+5uvfx5uIs1TRK2FopeoFc2b9VsaIEfqoZ5n5XFDoxUMYLd9bsoB4eUD9kcj4iM3Yyg2zA7D57LyLN77Du05MAZ244qd499n7fbWAeRMcvd9+s+t1mGfdu5cJ97ZM3bvqxjv22ZsVxOsdl6PsdepFXE/e0+b3l9e9u+5pw1v/L79x/cLmnL519ff/v986efPr9797yaf3j/5NPWMZ6fXy5PT8bydT2/f3p6OS9PNs7ks5ftfLJ01rCq4kBV0ZkAJssBjhLoaoJbS2KTDN1FlXPBNDoiwyg9jrTzgLkvziHI0WOGuSRKofNkMBgzM9WkJlF1bJ+VryQrkTXFI5KpdUZm7aO9AqRTgflgtViz9qqisUx7Fq1rU3lmS9LfUdxdob36JjuOolWwtbm15F4FiFYkmSG6imKv9m2yDb7Bo31PMpMHBVAZMytZpDd1nYSZi4mUkmvxW9CISJq1HghHb8Gm0amYFFev0AuaHhcc7HhVGZlp7lWQUTWg4JY0yEixDR6ywR9zz4gC7Kj8dU/FWooMwlEB2JyRXeQ9HqTG0ak2xckiJgCUG/c9NQEG///2Ojb7NiLNCu7iHAVCd1F2TA1yHBFJVJzqMHsAAEJuxFitGoBYMUcX1jycbt+Opq7dNOU9NJlODHejV+7Sm5Flph6va+2qMjS3P+a0xY4xPDKzInxZVP+CfnB0ivSM4nA+lL2A+xAa6N7gD4BiyttD34Rm5taxXsyiCIXIyCApQwlNBMQzQouBQRXveXB2UreaMpITZSirLK13WUmz7taZk8yMjqpH0kNmY+tGFHWy1TOb+HNsF9PmSSjct96Cmfn4meK62mFXUlFwA2U4RVQZvEvC6uvJ6i7fTKOU0CakNkrprwAWkWq8vPJhlO89EUC799EOUnLPT9jvlxQLjKlJx5AEkjBgUdc3I3Q+QlYAAjLoBJbLqSrN6Zen8fTO3WmuqfW5mDOaDa2XakQvPjKaswttWb0VcfxpVcyyxXIHZ4Du66gtYMx7Am7rymGZ4cNgXlGoqEpYVvO6KmtHVmEH2qCViYNwJ1ij2cxxYBUk5anYUOLRbUbuKie7Mc48oOyjStOaPLesacYMRfKCOWBV3ZdHJBuxLjOAVsmssB7nFZ1ITky30EI0aayaGsNyclabBBxQUnXtJ+hSrjd85IDKSpvRzmiaNitSNxz7DWmA3q0RXR+woYWua9HgxMN/ubprfows9bEU+zRBTaAqwN6tnZmy6XYa3LRtxR/5pP3XDtSLx7Ip4fUZGkZqKVCRograAW0frTQb5+Aj32gaaEBlMVkm6+nqkQFJt7anx2ze6TIsnRm7Kn2Vdx2YyGqOjmVlyURUJg0hDnpPTKusqozmw7NoxJwz3Suzwh7dvnhWprXQYjuBYCpWq4IXMTJ7bZSGGWVmqBqd0xodPtqB/v87AdAsU6tWYcbI2s084cTSyGBSOxjQlKwqrQoAJd2GQh8rUyueC1UV6kAjGpo3UiYVRDeI4mbx8S8c/FGK7NVe3kd5SroLsxJPqCt4pYTMyAaj+pWbNm6iSts+jaje0GtEJIqZIcIWQVGPdL4rE7PjAZ0KQTpg7LV2NG8ASDB9ZqDSOnihIqolDfLk7GNe/YuaqmxmrdDWNC0TrnfHmo2SqY8QJfS4R17Hk6iKygx26aHJtbSGgLWSUM7kjacp/wux1Uct/dQqmHv/91WUXi/pHL3YIDMyYMbeySjlcVZEwSJVPKbRmA1q7VMblDxmUf35DDa6YEVYNTiMLkg396Ef3NGHPcGKWYJ8GRYoWNI2KxOswScv5sRUn7QXcitEiAoGDf1SUKx2pJEAvW2sKov0rJSVDd3angWqqZ3phwizo0nLgpIiKiSJSsyilTBoZXgVHS2o0TILugp2K21SKFKrm7Uaz7rU07Li2FFEjMxt8SJdrghVrJqi5SCRTaNsBzQrGC0QEBtbDihOzc76zPdwt1Tqxpx4cOKrOZrU7m9RNlDOnpNRi5oaRk5oOtp/HyIfUlt2Bf7WUYmWCY3UemxSHMJGZ2hDlYjuYoZUCAVYKaLo+xItZFJDXfkoR/WLbMgpvdSpuI2QD0oWqbEWfQzCCkFAR7fBhqP60VhbuymhAhows4gUj2fOqfCcDdjqqR5bYKVXEq3IHP35ZNcseMVpC+iFMUu4iIroAspKt09EAIFhaEEwGtpic5bscE1TswK2hK1Gg+xUZX3QOasBxp4ZVDZMrwmTj6zaszT4XEyAfFhllVRdeKByps4BJTZ2bwMC4XYAUulmSl3JsqJ5Y1A6dsOHFANGB4XFP2hZ3TccgUqKBDne1RheBYbUW1by2RCUJHNzNK5CNkylXqe0OeSgzWoyZGZqiikWf485AKJPNkB32N+8X0GU1pkURZq7LzVT/h2qDWVyom5P3WU3STxGKbq/mptrYIteiHagcd1qmbOSRzlVJqWgksLRGZcaMTNFAsL1tOl6kCrQqrci2uOnAYfWT4+8HufmaAuOWk8/mXRotFU1Bc2Jfjt8NL+iiovnTI7G94Cy4bUVDqwNIiijKmdWwh2RcssDaeZRoHvs02i62gXOPSpioZIi59xrTzMzLgW4JoIA9s1aaK5ioao0zarkYSRIpx3D0TKSHHq2CAZZw7xgopMRBU3FwKg4LiPBerTQwmsqCsN6YFWpi6UrpU1U7XmoCoztOyZwumYRkU1r0UlGxl4ZMMeD+pHB/selwCr2UArNbtFy7pZPHmfowPf0Gx9yUJ1V8zFjHiCtqj2WhE5Vlb2fibBv093We0KWMLp/XTQXqjBns7eJVhyoxKbUTEeS109w6/zU1wY8ityjfgUbBe1ID6sqc0HA2kbeQwKgqdgaPJAH38fMvDKFy6vT1Jk/wKW+Fywtpm/cJnu6AO2w1PlVN3EYq8Ux+EsYbbBCx8HquFIlea+RHA+VUZkhSXOBAQ+OvnWETZaUbpUoc7f2rlBRrImeRUXvW0FVloFmHHp0NNiRZh/jpW5YCyUZLmDuWTXMs/YpCwuD02X/0FUHqDqlYRSDGs3DvEHBsNVnLP32XonlQAMR7Dr1gf2JteI9KUpryZvcsBWx+chSrYmTDIRG815r8GADPfQKOkzGCtCYUyMa5VmNiVr6gSo7NumA0BcDKIZMCR1v50Dg+NVS5Jjpbem4oOcox9fXDeTRBher7LiV2XG20LMuVdlHjRNiWGeFdH3VafFo7HHEVfPKTLmZWiMUrJJTYaVwltaRdAIrsVXtwSpAFAwioBwprdR3dyOuWyXGRSUpbR+LI2fAMYwZpXjegy9312GITFbFFXMxd9NUHFmEI2Fwmnx+OEbOSboNAzyByljcIENRckZapqxPBQBmbmQwBpjKbpGF3B0JehZAr6jCbHv4flkAyECi6YmU9UkkzSKiMH2sTEsGiIqJY7NEIljqrIh0Sa5Q5TrS0rLMySMVUWqallYJcWv1EamfBOkBCiztP+ZBWhfikqmBbRE9/a0q5LEVpJWQ7PY+tUqVx4UULbJjcvEwUexTbTaUFnJO12RI5U5VZURGk5p0cqTsSRyQ98hmCAK02MOHo8q8/TW7gOFxxCsF4SrMyUTyWKXSXEKdSXOvSFVr7ASeQO+4riy6nBt6Up1VXf5kmegY2bewS0wtRnSPuZtR/bigpOataALSXJPMzhDqz6IvSLPR1X43VKnf7uaRKfilOtTRzNS3aIBWBNzNrMrcR9nY06IIG7aYlD/ybhna+62F7+xI4n298sEcsRIZV+Yc7DRGaQNjgNR63WqEqgEWazpjZRULHAMok2FdpcGyChH3ghHrcF119gK1znzmDasogyqYqlbXGIfWZqQuflPreysizS0i6F4ZJi48m8diPKRvoLuTbe8wZ0gBqG9zVDc47pI+g6vt0FjVlEl1+FNtWtOHOyofkc7GyDlVlHVVryWaBV3OJpwqYovZaQUcFCEQzH4DGinrZ0RbDB3ATDZ3TTPBxvQ1odE2YujBynNUTyGzhLQ8ZtHfMkp3ONB02LTkAO0B3rcCtEGVa9RIsDp1S6hRlRV10IdJETwakEMWmNFNpBaD0ERNF5UERbMhVX4P4rvx6hQCGWbVrHHqoJZdJqtiqwSdDqYCWhpqtHqhKgJIo1kZEw7cx1h0a2lWuZsNHyumgYVBRBHTfOHR5VZaxWZjMQBImmdZsUBkA/jtFKUWCXDWtLEUPYtuHpC1sZrUHicWUnYp7RUVBSuzQQRJbY4UumCLdKeFw/BKD9hci6ha5FJgSfZNQD9WDHrtY5VuH/2uM8OKcrJUIulGo1IlHY4M04mcfXXU7PRvpPAz3coJo7MxAxxCJJVbIJnsTygW5oOSL3hUV6WQM9XHqHoGqojFB6oEotPcj2GATrC8HI4SXVfPZgYgXnweyYsQK0/z4eNHoOSuaaFteu0+1PBGya06AwVzomTPEGYukZZ+b48IMzM18kLPSjLk4U9/cJJIWFbNeAxdNCM4GAK6/20+lJBQDjAnuBSctCijj4QHONOj3ODmSx2eaTNlooyYD76lHjOrqkmrR/rRqy2NOEwfm5kY7Ama7pJAPaWQbOhcf0MhkpL2QS7PmbhXoiomqmywhg5iTc1XIMmuNixT8HEnTGh9c4E0/7amoCV8jw4UR+JNLXQ+EP1veawZ+I2zU11f1xUap3RS0lmXgpvDVOKRhgzQKiL13miqfYuiRLHImvsxR0jznkA2bMP+Fq2xcCsEfVFGAah80tgbFEMesFu3W8pglb021KgGp4V97T2nP2j6gERArtcqM4OsZKAOyqh6gAph99VNA6uyzP0Y9isxafKhONHFGHpYpo4OTTrSo9QhUBfWg3SDyL6qWPtqEdrLR4hvbkN2TZ021WWTlpK1c8marCbMgqQLQM6qyiiTEIkHbUXDXlGzAwjtqA63pa1xTUQ1uLM4gtOMBi8WxyKk/0B1A+sCuu52adeqPYZhBLQPVgdkVBbdikCIf8NkuVnR2bIqRpZ1y81OYvpRvZjZrIouv4kjUaep9Sn1wJrfSPhayFINn2YOBKKsKS4gs9pKwQqzty05ECAEQDfGa8YqIvWYD0UWmk2HLq5lFdmF3IF6I7JM5JgoOg+k2A4uHkp3raWLDwYK0PQVewAzpfxkbsYZwWRGHY1K04vbwEfgsZgUYkyitZVW5BglvEG1v9y2FEBSbBSQFEkxNdtA6fCLcF5RdAFJYaR2wmYEj1XsBBHZo3lBAyop9VSVR4UuHVO0yuoBBoBEat7V5SQrsjSQppmVSNmF0qTEbYGvAYsZ++QOBLzsZL5yrBDTLFP2JjgiW7NBG9/AUbpC4xL8TSJIIUhgUXWZvk/165YJUFWZN2aJbJdQaxTJ0EhACqSbUdr4OcjVbfUaYyVLfjdQoarCoRKauh1OPypI64jQGsj68MiJKg1RjyJDLfIUicl9tHU4GFUE3TxTBv0oCEQxHvN3seEVslNT4KM3MHMYyYPq06ousI3kNJMzNFxkB2m6ErCEGqPMyAqjoDoFXLEMNMIrM+aRh1MjMkj+VYV6cI2MllqyTZLZaFnLsR/9JJi6BR0jYLKI6lWoqt+tFTBlcNV93cC29S2tW3gxaiG2K6qASXdCiKTOiWXbqlQVMlRElFkHr8bGUhDqQNPchD9qbTSUnEWD0aCZDrZZIZOMuZmTY2GCTvGUjtry+LIRiSRhYzCyilbFgUwrGIJyYaoiyqKqYhqMfkLZsgwAkGJfiZIrRXyjkaNAeF8eRdVDXsnMZInQrQNRcNn4yeSgFhu9diMAMiqPdyT6Ugp2oFMMtJL/E4lsSBoA/MgRocgtDnCPQ6yPg4NosMeMZGQQ6Sr0c5Y69yo6y9DR03qXUYP6qhcLtKMT4PG9eUhrxdgqNB7E7IoqIS8dNRICUsqyLWl6/MtuNlSs8EGo6z3erCwOvdfsrV48jH+7mk80c6Sb8MaLu+47ClroFjfnA7RjqwpHlw40W4bmiIhCZo4xVDc24Uo9oHvMePxQF+yjC+fI0G+J7gKbTgL0IhhEptGioiqLFdUrTKL9K9sWAwC8V3J0qWtG2rL4jIpk2Uh6RN0D96iyFWOlr4BXUdS99l1uMLrHTAr9h6arC07FzYMx1HKxDlBmlTk6KzW78cA96pslqRQo7OahQ8AeCXBxz0oj5p6VZci9kOBK0Mni4CBD/pnUZ66jHyGrSlXSgRoJsqjjU5INbHcQlKmScNBk7zHI7P6OpGmNDAphPH6shswHZN8R30ybZeohHG66keKrigRFEVWFRyPXMbS6oWrpiyxDymgOMboS5v4oRg7QvM+WFGqCXnggTcrJ1dWgun40Cq8p2YPp1gmxqsrce/7WGN5xJSoL7bSOR6nf5H1Xwq+IYxYj3LvoVjELqAiNRo6KoSoLImCoKzhw1/6XHpw41q2MaeK42jiyhR/qu5OafJQuYZVgkKHvDFFResQmbhwKhQgD3EZJm1YQSkaaurdCVImWD63Z8QJtPEp4MKrSdC4wKqtqaqCjh6Pu7IGNzGJTU1igzazc07O606EnmZYOJEelWHyk+m8rYSLVPV2CXsJ3Dopzaxgf00MC8GpqLdSDHB4vetfVSqpSg8bIEMGNJGnJwwYsqzE2w8H8SjZfTldNv+6IxjpQaOdanT0FF5Q2G4NsZoTZYf3TGCQfzW3BJLCqA4HUxwHbYw6mgtcywoCjNk8iH4s1juZLJ7FhierE3jZZwMGMUthHi2nMeNRL3Y1au35YWXFi8QFayQFMFX0X0dXnORtMi30eKaonouJ0VECsWWT/atX30fNkVeMWIey6b4dM3GRyE1mkm5YUG0GfdCxWwdvEXthmTYw04zj5siastKnYhBjYgzgAbb444os7G6wpHvny+LNjBoQOhIHM0SV2HymKAVIyizMA6T4ywkBlem/mpD/gmonC8AKj7B5zjzgH94ULcXIuthBFRn1DHtPKso6RwNFm4wEghKQMZdBfVKYG+1CWS6l02MaJCKN32RsrTUh7HR6ppJi/2QASexCaVag5ZV7v1A4ymluvUutj0ZMTovXDVQ369ZM8gm9WOk0sj2qHBmgy3FhmJtwFjyEze6VZb1s7RACORoOVvpPOmcFj8QDwqNl7/wbRZ0I8XZQiRl8hEZ80hO9Ir6d3fNJql9VeBF2oimiopLNLpz40sItmFmvPdVf7IlUcQw/1w9V0wPbb0ptKZIR45UexIZJB76vLmhWQ6t/MaTKhYgXKtT9vMVhGaB5e2fFmuDayNjlqcWrNnPVQcdLSDU4rN4QK7ebngF6NnqrwZ6/MAAtB7yNE1xRCFjJgxdAWVIY0pwf8VmohM0GDHbUbO64WxDZGO4PoMqEtbwGRgvvQdHOsuyKNj7l47hqVzSqau+JWvy9zwT0dmI931P+FtFc6EFUyiqGU51rVVxLQJF31AbrIAt1HZfBQEtgYHVMO5y0q7HWdjBZEF2Ce0V5FLND9OIl0LT0/qA7qVws4WFT67tnVsh5uVTY7sS8pDznREUhQSPgAhMhnZtFHxhQHQ++kJBiihaxNmpeZnFHHl8ypbN3MgAP6bBLjI/iYWZvAmGWlN/+Ubio+pLElAB8WAdpYxpiwmZjFOfM+cZ81i1HGsYxxTiDhqeJCBX7268Tf9G1qdY5iRTXMA71EAwmgaOhKUwo2vRP4W0cl7KIXOdZDwhIN6EI/PYUdpFF0bTRqHomZjMzrlgvxtNiTcbAcGLYUYFaEQw6zZMYBfIJuS1VmxbeUILJqJelahqDJWDUhBxllMHOBn0ZY725WW21NWzwCn0jVCmWC0Y5nmH1u0+3Y9KlhQ5to82ijqtjlgxl600IP6LoW4MNdhEc30Qp7NN+0KlQvd+rWFSytG+huwh7lodLGYwimBiRmL9Q8aGYtzaIa5DZwB9ke0oJ2ewCtQ2H8dtvAB2OBbozgMGQSVqHtl7QWCqXRm31SWWloSqxk5NKjJQjzg0cYPTlvZr2RVWN4l28gXWWSsO4Aj87Jjm097VFowj+NpZ26aD4f3b0iG0vUcydtGMiMYCXTc27DyziqvzthQoyPkQ8elA1Vml2UoMnwBhS8zEZFULY5RAFVAVIsi27CWxqmIROs/x6YKKvG1dlJ/1vLeaBdVWnVZ6ADpK5xBigws8vb7uzkR9IlcJ8pZEKGN1SKPnpRHSOxFCszyxf9M1nICDUf43EyD74afKDmcVzJEgk1j1N9aE2PNr5I6/ANEqSzKhUtus3tjqdZqzyk/hAdTSRyQGBphejycpXQtYD70o9IIJWxZj6qXrVUbn6I84lq7mg3CMpwuuaIR/KgRPQoSGGQijmWVZBMt6sv6NYkdD11oah6RVF05pTwo9KIkQIZxwJaDe6JLAvyFnmdGcGEFxlwWxfQki0JTrQiusWgOlQHCtGULT7updxjCtmt1wMk6DhGsuNvDXe2KYKEQgCPriYLTpPg7bDar6qik1lxXDYjMmtmsOhjQTqIwIyY25zbHKtzHb46V3IYgXBhFKkS/dALV6h1BWqMIbF1FeyxmZV1lLcHgkkeH7tnbDIbosQImrCTRlnTtcPEozFV/i9UZCw2qlS2uzqE7PsFTfIkVa8qWe0RGj11T5UPkU0jRKzIzsk9mSBdomXQDaj2fT3M43ocx27OOpA+UCFVyCUogC7xkhhER0tYOPgBPBK/usTuZY5ir+O9Z6sh9SBIEN9Y30YWhzMObngD+seo6SAkHz2mteCoL3N2QAdgViF+ph06zz6j5gNHjUsV6bKrTNXOOVGLP9gginiHnRHrMYpGQ1p6BqRp5NS9D0zSM/bEgoUAKir7YTBRXaMrBjUecyDgCo8NppMUvHm4pBSQ0l50l9r1RxfjYCcQFgoBuKYwytOPEk5mIfRH2hT8f4hu5cMOHS85kCvZNGJS1GZB9iSX6o6puYz1TWl0UOdDdZJk6lXURE+k+UfOEnhBpX/RnaOrzeoO0vtw6Saw8cHSXMCIFuX0aSxWVi3LWjLWleTKNSntdFaRcEPP+oYK6hmtOEOjHzQf5paZ6oZnRg8nCq0D0LOqIKxtlKq7Z9HT0MGg73hGHjuljOZmIytIRhMx1NqJjw4cuCqMTAMF98/Mqgq6oRiogtMIOIdnufuI8qLt4BZ1z7zNGTPuIbLL8LG4e2arHoWEkdK7tlxOJhx11HQaoghcnhWmN9K0KPQd1AsSmEKo62VlZo7GRNRp6bcYMrLp6qhv+HgBhqq0MoFZ/YCjgdtO7qryeErYHvPrLJ85tjoPe7qMk9PpaIqHPAW1s/Ab2wRHZsFxuXBwEfTMs46aCSlzPhKSK2c0EzRn2rBmO7iCsyg9MkgpVNJqqrmjNxzUBqiuJZAqmI3extoa2uKgOH3DTcUgrk5LMqGzx9V2SLbSGjgeRX9Shc5xzRpK1HxAubbDqrZpVW8x1l/t2f5RJn4ro9TiVEniIC5jpRB1iHib2SVBL19zveKOxX3T1D2ouOHRKxxdezUFsyk9dYhriApASz3qGE51+DLC5Xqu+BtHAVsFNJtrPLooRftKeW135VqlS575qN0ie8wOOxCtvuiSdLSNOVT7JxJZgWRm0lKrqZT/jrIffUkaA+x3bILq2BInS33HZguhyIS5F6tkNDXzaMcUpquhDb3qZIGMb54IMkioSvT02HCY7JIaV+pToQ7QV4mRonVVNitRkZY8alQ2MQQ0eiEEQh3MHbkQws19eMxoOnBUIX24nELqgar3Owa0tPYo5FWroQVACl46FDQ9H6PDK7sbIYUx04YTBVcnWd1PinjSp7y3wz7GWxzuQhQT7iOrhOHgGKYd5bliVKasd4fnDJMDcKF/rFiRZhKsCj7XdL4kTdFOq76S6AqywRXVRVVG6QeLrJ70MEKY2Vo2ih5giNBZ2GZtkdc5d0GYWGBeRl+XKsJdIsys1NFysxYckIUyY2hA27StA9ZDFyCEhCPVcEcj6FVCLAQDHr3lUNDXL5ih+quisirZM4xWgcWhChPQnD3qO56FG6IKrKyJBDCGt4K8cs5tr9oq3LEgz6uvjtVtHTS6D2bI/C6EP0VkVZoPd2uiOj0y5NfWPIJGu3iE4lmAweYMYUq5RyHN/CiOp5hd4vbo5ZM0OA7hVkE847DGEa0YyWTJ9zrtgbFWk146oEfSTAJ1mlosII70G9lWKZ3mVANaF9D+AIQ6atesKsiUjQeZFTKwSJUgKl/LZV91VMnNwcCB/3StrXGQZF9HMab2CCoAox4zOhQPtm7/4w2Io295t1hdS+rSSb7QR1CVZsJsdAyvlIi4m9KHtC6qyC4A1H6KygiiYD60vCErEdFz/j7zJN1Xr9msFtdkZc5imZA2gJozWUNpJOXsiIIcY2knAvQymmQNqN7pik5EbWRmTTCWMaS190CAblTpweyG5rAYR6py/8bVExhNWby1cZ41wCgoqaFa4eGdF3SBCWSGWKqHQK/Ek7ExjpOFPhsA1COwaw59gqw4gIMSsZGVbsPUpSQeeE51Nyr+mzwH0BGdViabnYY8dBRyxsGtA/xRhPeTFLWj2W5qbzIlzu5IJVxSxGbxmNDcHpIyC9AJqaz2+UnMikNzcqgWegCe0nWCvceqKqsmzHNCs2Ki911Sg/8GEdXNWlXTkvotTJFscJjdMyMiogB3z6q5RwGENbVyDNB2sGxE2T3rHnWbc9vnFiULRnChu/sgEVryI9tYOZvSVPfOKINFBmgJ6EhR6Eept4G02aqQNJrV9pS/nf005fLBji2Y2dC1t4cVZVUB/nBrUsNe0kc3ssZ2ZmZaFwTfTliWGSLL/aiOaFXkWGchQnKZWGeeF7ssOJefxrA9h402LFZ1iex9PWDFN5m1uiEBIzw0Zk6vLq3Qax9UVCaKNiO0lgEVvXOH1m/Veh5bCdJhwIxjWlIl3ipN9wwFzTfrQew58Fm660wbh4ydoKB8BJIuzfXSzCqkRwvxXCOiKoHlANZBPUTl8YM8JtRe1Y02z6mjyEgb7dbZxiwCeLsalBRbV1Pz2mTnQasCdDIqDjii2RR/g5R1odlfXJFM3P8eLNWBQGr2rLQm6xNrnmkjXfnw2Kpis2ba2Ag9hW5apgI9BKCJ8oyj8zN3mQSYH4RRs0LC1RtDnYg9+iEVQGRDZEZLLWWzjDyCMIhMBAGx3KHmmscHNojWLNuqovQZD9G3q6oudveo9w4cSGJHzIbP+lvzOB8off1uyLuXadF1R18NoCT/ioQKOjekvHWZPaHXvOqQ+LIPoM4k8kBRgG7ZevtuHcdU3whGVsgGFUeDqX60a4Aj+x+rJBrYPL6smsf2C+4evkGezm9Q6YDqarcBaLLIUFvWkCPptBbugm7y9dKsPmeiABMmqLEBO3y5iCOP4gWycWxczszd5gyazTnNPOY8BP6HUsEtM92Mw0JrLLXnS12JDaSpFzVfNDPYs2BLgDtwD26JbcYe2AJZzFwwxtDCD+E5tJlFWlTo3hVNVf/B9GR/8+PA8DhPqlr0pof7DJUI1vIl9B1V6CzdNUKH0MwyYjzuB/BYGpldAogs0JkQR5MnVLkBDy32wjfCriy11eJDBp5OHtjcqIo5mZnzjrf7/nTiOuo8fB3UmknX6mRmM44bM1EkGm6OQkSaD3OiMmYKB0AV3SVWjVnH/aqCFxgRVL1bByEdx10GfLjwe6rBT0mECLCHmSWUtoB8TMj7cXT4aPVOXxQ5qkfaGFIvo+czHYvMyOqXhFIA5APiFCSkS9h1yUE9zBmklVMNneC/mCFuXEO3ovZ3g54HjC1FeJNYx7JkhLhRlandICQRhQfsoIpc+Uaxoz9uL1YjmDPYHTQBESo6SSiBQWkLCSIqZNYk71W62dFP6JSjTVNZrYojfGG7MwEa1gXKqubkWNCgXUr13bAYj7qHyYJCulLOMT4zH2gadidTHsC/Ij8OgRjMBpr4KusuOZ4/Zg94pD19SBNLpQ4XoI492VbDxwHAMfyUSQIP+MaGGq7u64+CjEBZemRkVAvsm93QqSUhO3QD21qr2IhXdoLs16k7pSYeBbQboHgNchKGNlhBRWVmB+M+XoWjkGyES4kV1ip9YLhnZGSYiwJf+gyt5qAVesbnZGq34CCjpTEGNi4sUobwemGvwv8IkjPTVBh13ZHDB9gGSzDSHjeujofcIzsbI4GKefRILCs/DQ0DWGXDclru21gs9kkX/bQLGk1MjMy0CBQsgIJN5Fa1z5rFe+Q25wQjATpseAuLHAdsAcSsKtRsqDF1kUPmz2qU5XxzjLLFQKp56JYE9lhbyHWReERrCPPpYRcOlrIYPEVydJFPOL1d3fORfCsO1k1HpPwmjdJvOUb/qm6hU6ixUaHMB4F9FpnmwwyZxrEUsEVURcyymeeF616rcXUu7mcais6hssF9sNc6C4alWVv0KDRlHaah6AEUq7RU7FE240GALe1vLjOvXjTaAVecCeMCF0hQiv40iQXgR6eqYk2LPRtrSGQmBw3kcNGT2+FPyIzwc5DoEb2ubva/W0WI6H2INkBztXhojEfYqFepybDjFfes0tyaV6MObyZML0JqdTuAIRKlI8asZlgnu/22ftU6KArrDSIZeyipqyh172GwLtBZtkhskqIdvtGFEq1I7JyeKMkyTm11HY+HHVLTzCqqh6WhyhWkNwOZVYiqkPsM26WSPQFE83Ag0KmawI7qMlhNSzevGnlUp90DS80yYmaXx20NVHRV/QV0yy+d97d/FCqvNTJTvGPDGsJp/qYHb0SOupyo7n5cfgHdQTzoQAfjovsPHC/J2J7YKniKZX1Uo/nyduCBqlr+dsxwZDAY+nuYpibf2qa2pSX0frMQ9S3TUiVjO/GJuna4mfShR9FdtWa5j0ypAD2r4GSiWV7M3vQx08eIKCWn/x+ie7ISbj6zvUVJaPitCg9VdFQIRrQe4+jdD+sZblVZZQaHuTur3JvfMPfpZgWLPWCnIrWgBmOJZAAxM4GAFTgDRZuoOVPbp/ewvXKGSKKjOPqBujcBe5ac19uHyFgF1bWZPZvWcemTwA7iiQbKFHIPWT5wnLuuZtn9X49ZDg46qsy0wQK6JWaWe4xuiFD9V7/dQTssTBv0II+MerRvOHqlRsb7hlmfafkFseCsNFRF9OMnzdcFNSYLWXMvr/TKZfh58DS4GBfH4u7SS+grlqF6HpKCmPWeezcFCC3XFIaoPSgeeygBqtIDa7iMahvGJyxD0r7mRmnPqMAXg8lFjW1fmlWpRlJyBHE/g5rX9mOprtmJqCJsDIm1MYPN0WRp5pTVbDcV6fICoktqWNWJpKIVm3IhJkDjQ3HT4aIKgDkV9ECUeJDmuu1kDy3EOHJa60qq63ohPUJaUt4W1oHj0esYhbIrEIk4XciCWzOV25kJVZmH2bLZ0oBBQW26pOF9MwUCaKfV4Tai7h2JnG1pb2bI7IaUlqoNTRoBAGozGneRsXLCvjWnquNNz6eLdkpz1QX34TYpBs7xWCOL1oyp0ujSD0O3KmTA2s3xMH+1xsA0fNMUq4F03Xp25IWwS3u01d1b5zEr1ifsTrVljLIlrwj0pJQwMnkMMZNjqQwe8CxApCYC2qpFSSX5DWJqpAXmSJm0C5RS4669wmIloapsSNFWDTFInNX5jEqzgu/bOEA0BA2cslApyvUQhklE7FbDYOhFvupB3ZyRM2TMoGwXBVgh3XrEqD3AQt+pOW0kjBnfJpQV05eFxjlnZfgYdKdbxFRjaj7mnGZDVJwqp9csFNeMmJgZ3IuZ2Kv2GQmbwB4ZGVk2Z825wzxVjcPNlm+siKgHa63HuzigWPFonTiqB3QYFXkEQikDECLdKNmBd3UBgR6LNOKorkgwFbrxA3slSTx+RQHAQwdABZSmLWSiskVlx25C1EHYODzJukTpVIB6MCP1xUTKyOMUyhSTGcUiA6iqplX0wk3e97pn+p7LwkGeV56MgzVId3PaKHd3oASzDvMDlCwUzK3m9GWpBvuApMB9Mb/bl7+0bq9R2lLXgBZKkWTVzERlzjIzX0xCxeqvbEdDBPDYv1MFswIyApltIlSFQYjSJ3TC2Hm6GnYgzN0rSYtOjpIQH+SRqsexoFIWisWy9tEjMumDDzAgoKoQVYhSGVZHhQy62gb1jYpoj/jS91e/WmH8QYgXyH5kuTouF6hZQmZMygr7gICQx+KqR+gvoKBFBVqkKVA+u1ovwouwKvNROSXmeMjba0bDXCIYuujvOMJTD+QFarSQDw1bdcIXQeL4LHxcH3VNZW3nKvcNApoh6x+MghsTcKZs5ioNOBxu9cOrt3UV1ADl0aYp3j8eQtMp1HW1OT6qeQN5dN39GKuXZLBNHSIIwodet5otXbHG6TQr1ivvWWK/zNYeNyJPHp7n7AqKJa5OA6iK4w1uVlXN5IN9asyezItSZaWc3iCETpZujBy1ZGKg1rZQcrWc6GEOtNAxD6eR1mXKH14kbz0S02ZfK1Qd3rTmBggQLi5+aEeS/WTdiIpcx0ixrcKJWnypTKSbndd1ZBFjgbEq58xrxH3nnkzYts9ZiFlzRiamtMiwLLHZHWNhl2MQrzgKull5fHTFVhKaa/JAVzKi6qAg66gntLkvVUSnrlEXF80qYPXCTmGWTBq1LqQvciOZagFRhcjoOqa0DwDaByBp3mO80+Ei65FCjvojjjfb9Ut/kuPfDn1sViLtsGlVZ14pQjCBMrPMjFk14KT5YFUZMmMDmXXfy5CvM53mGctiw231cRnrcA6UDxvCLLO8/RepnRYPf0uIOsoHQ6LGGHQiLCNUR45lABXb7O4esnAoBzLRoHlgHM6RVaY5Ic0zInOaD7MhuiiyaH5YPTT5mIZCqt7KbC452Lt6MxGZWtUq9Eo5v5k/Gi3Tm3OriqCyAjm7hWwBQXe6rWPQEu6DRdrecxlaXMI2lQAebIKOlB2rm94jiLlfc/UIqgcrbI4JDnRWlV9BU2mj0YZXiNtT0hbiQCd5HPVscV+puofB6MmoTKNH9jpcHBWqsmbFYwenHbU7HzFWPYg1M8ZbgSYIpUlEUNxSzY86WPnZ9T0R/cdeh/pffqvdRXX+gNVh0NFotfF4jk3IQJVkH/qCeVyW7ibYYaHzWUQdpBiANBO02EtO+gU9KoHUHLNHhbJ31fycrm2MUiAK0qH2YgCJTHMotTZswCYLuPZtFJzaRCN8lX7sqTwSabPA24tYiaSPsBpZHElEeItsHo6apnjY5NEs1UKVtgnpkZRpxRBqohDp64pkxMxKVJr1BKvYVbXBEuhLj24xKyiCV7kbTLRXXxxJckQVl6UMGTXpc8eXW359vX99fdtyOz2dwwzF7R57TH2vaCSUkQXSfZgbIgvl7pUVFSrv5WT8bYLSG8GqxC41SiPdfoPHDkjxk9CIcv0NziP0D0e45bdcqFHm0UJQThhGksNHta0UegYizZCGAUYWxlHEAWCv7qwjf/RV/4YDKDDU38yn2HmGYgqjx/AS2/XZUvRgHYXGQWvt4DsbgzeS5nSvSndG5h5ZFcjgnATcePL7YlzchtvT+XJyX2krObgsy6ikGxLhivcEYnJxY1alVtQR4PByU+ujL2HDG3frWlPp2TinsoLbEFDRHOBqayfSdYXpGqJ6jWrQNqkoVYCmc9WTCaacgogMobEEMNvV6AiYTYjDYQ+pgGCZWVGkbBwORK6RZRIt2BGjvP6GzldHoEyBS0epiGo4F41N17HQBzj2GKBxv46cveCvN90oKDcSoOun1CKdin6Qik9VhexytTJT7CSdXi387Nb22NSho1ylqX7TzFnVMqvujKiUV5DRj5buqp0yNkqt96T3zKP415Fl6WA0JI7edIJkUX5V/f1U34u+WfobOv6u6y1VMY7LCZSVH4f/8dsoWL77ux7CMVXvgg/OXakRayQIJaOxbNRbH7RUI9uqUAABAABJREFU3ivHsRMzpLEoEN73W9h9DwBJWEXCe8wL+U5nQnbadRTprSdTx8tjPTGI3lCvkKYQgE54ihmmkqjwcBFH7M2nekCWh5wVPgwlB2agYMvImQCy0o1lQzJL2eApWlkUWF7guvbbK6dbRcG8AnKJsLEgcB5LAXSbgIZnUXbf8/Yar9f75y/3Xz7d//rX3/7yl9//+tNP9/vv//w//ON/+x/+3enypNFehfX9NQ73zHStEUbv1drmPqqaO2dE+XFlhIgYi1mZyDqodBKQHYYE1EgZvSiK0Qm/pzxVVdm7fFTsHalFC9yqUkYUTaMWtaMMoATMiAgN4bPnOh3PhuDPBw2AR4AQveeI3DpUBxNAe2esGUaKX12HCB96gJY97SuQebhwAnBwZmj/j2vQgaZry1Mz1TcOK7S2fmbOqn0WrWzPyvTXe25ziTr7+v58ef/8fDldloVjLD6cgyiYBoYAIBgtm23OBBM4VFjDUV2kHEshSGPlmhEmzWowYg4jbCDCjDMKLNow4cVV38QqR2zWwkWzVlnqKRqsChXTx6j2rEMj24K+3atlUihd0W6vdF0NxphhZsfaBhwBhRD8ehxASFvnfTNVNGcExHXkMbJhh12ZZHRUVYDNR9bXvOHbNjE5rx2sGdNwSaREHuwLqjwMoNAtoJINiCZNR7cUmREhkt8hw1K9Hz7Ggdp09GmzmEr0miT9eR7gQOII9t2dHp1NQ3/fxr2aYrQkwr2A420CFZB6/dCeS77uPURtvr5e9PEBG8yvIzlb23CKUcNuqpqUIBPkY0adof8oBCyOHks/rerIHJpU5MOQUk86A+5HGq/m1De3O8UO1RcDxRSSN0g+klBVH7oD3+9HKhzlEEzpTyE5QmceKaiP4baZSdjLR8eQLDRZuTKQtJWViJzuxtUI9j41mYZnGFEVqmvMBqK68EgYh4x16ESAy4o00sXAxTCAGEBTAr0Ke/E24/Xz/fPb/fdP109frr/89vqv//rp558//favv/7y02+ff/9yu32a9+tyXv75f/y3/+H/9P1yfofFYr/Tx1iFFBUOExKoFCtGVvujJGbIPLBl8Qg8+LhZsdi65aYq2kqyqq58qgosP0xhq+QBcZgvHEEgu0hVjiAaHWRvi67KKvGgYoaREQXmA5FV79VjZt3grNGT0eOk6cEeoCv1vt2YWdq4bmT0trpu1njc87+Fq1UqqswqUfQkXCB4ULIe1+8B4GjL3WMzfQHuBtJt0FDEMFniVGTet/3Tp69ffv39959+zVu+nJ7/7rs/Pp3PHz68+/Dx/cfv3z8/n85nXxc3g/uq35SZLYMbzJysRIWZSzNoxEGRVgyEa96VBsLHUpVGqzEyw0eHqERqTNPsksVgoq5rway1VoZahGkoFeryipHrV1IIgGyOiMYuhdnJVE6rKYoH5uBsuDDQK+YVXAyChtH+9UcKbA64KlixPg7AB6SVV89v2LwpHUKFg+O1akHrUTWrMke3E6WxsFRpzB5XmMkF8VGwJg69d1lGqV3UQbc2fuC3c1SNY3aBaXJgFHOzilZRNP2EDutVQFkBIQZzpYwmC4WHvZg5KitV1B7/1Debg6MDM1BoruumssqAfBj2Pfz/G21R3MzUE5BvUrVUXXCQI/Lxm/AYjiRQ2SIjDxmm4SBiFSwzDFYH9cbINqQMFo+EkQdySKJPHVDtqqdhcoGVQXeZ9EkuVO1fAaTMuYX+y8fY6G1+RSAzUITLDjofHZr4PCUlKUkOVPZexT04qsUHRqPDDUAxzJgxzR3UDhgvYQkJjuV49UI43Nwri2OYDdCVNjEEeZnYbplMIGfdtrx+3r++bp+/XH/59PZff/n0rz//+l//659/+/nLT//l56+/v95e3+6v4e65gWWny8noT++/+/jHD3/8u3/z8vF7rL7F3pCL9EMxH8Ms68UcD0hb3noY8qlUBgXAcjL0GOa9Ndg6DCkDGY3QVLghD/yv8dI6+F/SlR4cazGWDjbswTWzYxHwgcyQDWjjuLBlhGkGIIq+fdvOWtKVCOttw1KU5NR1dKv9+dmB/qi8WE04Q8JNt146oIos95EVVYli6jNq5nBACJKYkTCzx8q0AyVoi0lJA/csHFhU0u3yUqd5xdd/+f/8Lz/9p5/ibcs9l7F+/4fv//5P//CHv/vxT3/48Ycf33/87uP7j8/ffbw8v1uWdbinOAWOpRCsqlDKUocbBIsykahu6R5DHEEZ1ax/86OLq/RlWNOsLWPOWTbMT54RNbtKbiiPGXs3H+idi0RpiajLwCW7QVZsUvwDD5AQnUGzYtLbw96Gybgks6j61MlCRGZM8+HLyEgfa2Zo8GOGlENW9X8EWe17Y1DI6BmS/U3l3mkSndgOHmeX2r3HQ8wCmPa/H3Kt1Krno7cwoI7OU9x/c7YFigmPKoH6VK9VksiVIt2BJgNSn2otgvdwWPrODrOtPzYepKYDV+MxkUV/GxxL8lIgju5Xwo4v6z3rqzjUmAQPnBdi1qKyzaBhgOQN1W3wYfFDSol2/FYaVWEGwuSsl5XdohF6ltFwVyLNaGNUZMZUYPExUuzxVtv2YhXN/dTrs40PgNLa6B74Z8GHk8iZDTw7S22KarjjFVu7G+KbA4qqEkdVVqaP5SAa0Ba90BR6xWVUPAqZ4mKkowBa0YxePsA0X6wcvqJRyWPjXrPtmDLRnHF7nV/e9i9fb5++Xn/9/e0vv/z+00+//uXPP//0l99++vPPP//1z59+/Xy/7pnTaauNYefL6fm0Xt6tz8tp5ZMBY6w+TnF5Zy8f373/w/ccXlH7HoakgpJoJB0pEdr74TbnPDptWd313xhuogjt0utED2dIO8wi4W5dQKqhfYA9RacfRRfZCupyF6AHZEWke8sjD3BPdNICHnBiGaFN4honuNnR2rVWSbuNDmg/U9xzajTnPDoDHh+OrZNteepRyItcIAJHV22oat1lalJUfVaOS+Jda1u7yHeMe1xoO8ZQiUrYaERCTL/MSthyOY2np9PH7y8fX1/+YJ/+/Ot+u/7859/+8//rz/+P+f8054eP7z58/PB3//Rvv//h4w/f//jx48v3P7z78OH03Y8v7z48Pb87XS62OC7n4SZJdskGhcjK6K8uzoRulagJxdYQZ1ZhSMCm5rq988pWFqrmBEmvnOHDpY/DMRQ3EhaZwTIOanOakioP+XG14EuQf0DhSROk0Y+ug9EY1UtKVG3LpqpEu9U9tTHyIGlDg8IoybgUtfsAghIcdVRCyaqsM7YiXNPnUSDakZVd24qrruAO69NiAw5m275XgE6ysemUAqLa+ZtjVER/KiFS4vYrUogNqRqQ3c+qidSQqeVvjfUYtTLMvCrr4cKvN9jQmbWEtI9n0ZHRw3Bq1NuEJFgNpOzNBYh5HcmcFoBVZr+7nhO17JDFKhM4xD727MK2y0DUYE0ADPXcveIUNNfHNjl0Zbl8faMONKoNwmwdNYtGWS1kpQ2HtuDSKhIs6aurKmfZEHYMSTbRZP/MlGWvGrsD58eBSYvfomFQBp02ILIy3Vi7kDUjOEZVosKlqY4wW9glKcxXYJgtNCe9yoxihoihPDRtqkJF3rZ8vc2vX++fPl3/+uvt51++/vXn3/71L3/96afffvr5p99+/e3Xn375/Ovned/2266uciy2jNPL5cPpcjmdL+uyggvNl+HUgudZp9MJjiLHui7r+nR5WtcTyGWRLTwaF8MhkDwGZhHzqIdRSSRiTjNWIiyry9Yiab7MmJ492FAcPzqFAsqdEG8+FbXjOIzCTGRf0NyORJmRxuhUjcz0wyEJKjNB5QkdRYiEwkfRUyQqMLQ/AUd0Zw8hS78vI0xKGXxDqwQQ6dKIkWpNsugCsSvXB87TPLmoQ3rDXrordKxwIE6l/7K+8dlUnthjyExWJEwgfeasMrd1+Oli5wvG0/J8fq6Xyxb7dt+3/fV+//1f/vqf/+XPWTZoy3p6fn53ef/0459+eP/Du4/fff/x49P3H56++/Hdh48f3r97+fju9O5lvZzG6aRpU9kAjt0R6LK/mizY4MhERjGoqNYT2S5rGx6BuRY9EzE3Ej60M4+odB9F1JwqELLrUxrNVs995hbmtMVqCtKpEuW8qHYQBFLEgq5GQIOysQiLqgphUjDohRNUcukOQ50t7XAgZXcmORsUymPC0VOHHk8iDpn/UUH78Gr36VKC0RmlEKpsWFP3Rum8MugyvW7YggdeI6NvVfeC03voUE3BUygm0EVUylb2cG9tWj5wYJSRoWK1j2nH9zaSUNmFmShETrMhd1HI6y0C0ZtVmltrLXWt9n9WeTVU2anRafC2ioLGffStEbWrAS+xbjS/FoKjZZ1KvbIrpZy6SM+CsHlI/1BlWmkQRYqZovpEhMQeY9BFhkBEoO8gUb39yojIzJI1paMNv7S/YRg9KyLDzH30EjjSmvmaxyrdXKopmN1oVSq4JzmQSZ4JmK6ADVCQzigYwH3a3HC7xuuX/fOX25evb58+3X779PrXX7/85S+//eXXX3/566+//PLzbz/98vb65fXzp/v9jVWZMZax2GnY+u78wc/Lsp5gNtYVpI+1qnyMfc6xsCCsua2hZ8WwMU7LPuP87ul0WtxRlRzynE9kL+9THBSdsWQbk0l3oeuKDhFloGyCrEtykL3FhGBkDvdqrl5mr1xkocwwq1Bws0S5WWQPV5DMnCqQhZRkBgwZRZNcqVSwt0iwOqLWYxhzzL36LMIqa3TdAZJlSg1GasdhHt5+6nGPdu/4f/mAqERTk4Tj6C7VLHRVqcmIGvbHE0EzvEV7IKt7DqC6GBT37JhLZ1WZorK0tXT30/Np+bqM57UW86dTVY65YjXbYo3ct/vTixXmftvmnPfrfr1+vf+Xn/7T/+v/YwNjWczx/LReXk4fvv/48vzy8f3H9x+fP3x49913Lx8/vLx///zxw9P7d6fvf3h+93LyBe4AUiNil3d/LdLOHGzyIFJUzMqgVhkLoosySh4cUv+A8lQoghxecx66sCbJ5V4gfQyUCmDPzIowXwDGLiesYyqnraTKIlE9vRG3lUMLtwnLBicPYIujGwuBzloZ0X9Jr8w77spz0UhD7rNJn2JlHPpKtg2ORuhiFuhEFg/U4RhhPQYjdbQF7QeDnAbOGe6j04gwnCwOQ4bKGe1cq4xjqGotW0gkAqXBe5aGPor/jb1IzaA+wEno3unSZBTdaab90ZlZDkSySm56ciMVZUzlSldqVQfCq4PrABQWSyMi+jE47SWjWtpbkcalwU8a3DABojk3h09XNclSlhIJGIcLn0ZZZdWg9hVXlrmnRhar1Qwhpxp+FFAybrOhdtOHVYBE5CyUG+idGKXu82UBUDHBGlJEzuj0j8IxIUGFY0E66RpUaFhMN62uM7GS4GwMp/adb7f965fX336//f7r2y+/vf3y168///L115+//PLT199/+/rpt8+///772+vb29vb3LY9NsHtZPnixvVpXM6XE83HenZbiOHDEzC3yEznnHMIKYnpA8WgyLwZttDdhgnV29fL6endeT2N9pR5rE+vqjj4TkfhXv26DzpXAk19ZAsYM2lFmvYm2aELkdiRZiFsDYA2TgNyVu4co4VLPZEBCXePyIqaDKOKxWiLNrHPeFRi1nSUR+WjyjwiSUZpkUuBHGaMULV2EBl6dNdoUWUOd30+RTj5vrB7ZVEoDrCKeIR14GBeH3oQgK0rORrKHl0CJDLLy1IB4hgBNs+hU1kxC4aY4eaFjEobWJ4WO7lfVgwPFIajnDDMHO7Ioq300wrEk2yL5owtY+77/X7dP3/6pXLTwrXVncNP5+V8OZ3Pp5d3371/fv/u/Xd/+OOP3//h+3cfTs/v15fn8XRZXp4vH96///Dy9O7ldDmdlhN9kCxDAhopCOnQ1ANHU9MldA+i2+RX/653WECLuUGSCVlimBWSJEeWPCZhlqgKc5LHDu4qarJnhUgea07H6jTEvscWfh5wwx7qyuggB50VgWSVl7PNEqstfwqsKlsWVFWkVOoyARBiwK729dq0vAE4IEK2OY9cFZAyX4ps+DFLBMv+CoYiohLaotyojldE400qtI/mmZJfgnQT4Kanbu501F7sLgE0aAarcRS7FQak4YgqNJRtw1IgjwlAQhZiT8c3CJVWmfoylGAtj+Td442esNsRF6r/ezFFxD7KNB8cIzudSnNbZgutMkK9Js1QdgjuYEZbRoWGZEWA7lwttklvmqAGBk33BOmwsscLkja8HqBt52fre0gi/ejCjWUyiC1xIY69UMKzq4zmjZWhQKcTrbfICWwR21b3r3O/xts1vny+f/4y//rzp3/986c///zbn//881//+vMvP//2+vvr2+t1u21zmzFTkznzoZzq7oQv67LiNOQPMMxUiNF8ePX4rMSJz0gV1zCO4UCYq8GikTmjEIT5CMiTykDHONl6WZbLqYi57ck9Mgbt0F6l0YycenB02aBVMwi6IdfBVKURkfJ11PFsskMdJM6EmUWESOUNSVp3tmhMtaDxHnCIfxtcSckmeHS/3UyS1WPS6lJb/4vMb8iNngbQC5IMxwRYnOlK2rCI0PE3d/Eocg9FZV2hv6l4NBpgHBoQIdaCr44UpqiI+kb+xreJMuDSmgK9OFOglGJI/wANyeh0wbLDbGa62bIuNlhOrp5bynR+j3C3SmTknjAHxwDK0mg8nZ7WmDPitD1XRVbcbxur9v12TXz5/RNRsf0ry8ZYaMPXZT2fyvLlw3q5jA/fff/dh+//8MMfv3//4eP79x+/e/r48d279+vz0/L8frk8rc/Pp/W8rIsbyofxUPhqwikuFKuq918lqsoVXhIEhXhX9MwZD/8gSayrUDa8ZtKlXyVYYzGQVdOHxM6bG8y8co+YtBonsyELvarc2cwzP8RSBRTLu6KQUlTGgQUEhVw7R2R79fjivVslBBtWSfej3vlY+V0H/IIqI3Of5iKnVsMmWTKOrymRbbFVzAQKEc2VZHMnKgu9tlOtdlsRoZfEOgkG3D0PeN1b/GF9K6rvbEb1Qp0SeXGpSH3tno3NQEHOBQrQwkjNrXKyiZ5qXIM+el9HZKpxM4c9Og4oOisSu3vKl5TA0gCdWJVyfhbbhcNyophEjcW0IFAETx+CwHYtSyaSJju/qeiJYu+1tp4dszSVUtkxCCBpMJaBdmiSW8UgdKInEGpLG0JsI0IjMxlVOWtGzcjrbX+9br98+vLXX3/7818//fLrbz//9PnTL69ffrl++u3tt5/f3l5v19f79fWtkLFv7o4qd6eZYZz9jJU2nEW6F5ohBLg+QRcVBvr/l6p/3Xb0yJUEQTPAndwRkrKquvv932weYFZPT52TqVuQDtj8MDhDk2tV1pEyYm/yu8ABg12iq7o7gnXsYd+516jbFI/nQ07qJep11mNzwGRmZu7Mdc0/qvIr9iO/fv2+vi3dxXuSAZY0jpw5KkAR5yLD4woyE+AdDqb+eqT2KmWCn8ZpzayrmrzxMPEPqqrw7HiHZsBuJNNjeWFjvo8Bdel2+D3zynDHHO0en23DT9R9PMhaC+CcUbfdLomBRl8VU7/POyMpIagq6LN3mxURMSxyNsbVpkfuK2lC7K2ENIvo883m9CLIanmlNKk7l8s0YET46816uex6SjxyPZ/76/vej8SSorkYazVrMdXNBSoeyfM6p860LQenSgV1rMe3CFWdiCegr/6t0f0lSHWqu4PsPu/36/Xv/5x6/9f/8+4+Ef8vKFZExv56/vL49suvv/7rl19+fT6//Y//81+//uu3//P/+r/+x7++/et/PH79vn/7H99/+/Xbb78+f/vt8f3b8/u359e3tR+5FmLN3hfj4OMHCdKIPPB5pO756UNCKjSEB+pz+HtdbpKJRW1Tq7wTcNAkKqjKyBkaaXy2KChJ2fV+Nre+7egyH7e7bZa3/SM3VQ117q3VfWqlEYajUjjyB4BUhuzWksMcwKrqqpUba+kUc64BEZGpsOtAeKnoQdNeeFw2agDH8R7RsmUTBdqgiTlkpNMMxp5hxct9XMmL0W2X8vQMxIgV/SpTvuZhdX/jdVdOBOk5b1Tnc1Po92FELJiY4w2qBVDDH0l2dfeZKfmqEro7Y0fkqTMl3X3RSA8qSDD1EW67IcAa9X4J9KomceSXiJYAKNQLopNGKaLNcTB1KueE87ZwzGDiM3Frzkfr4eP97vfr/Pjx/vPP159//vj9j79+//3v33//87//+/ff//7x7//++7//88d//efP//6vf//793//7//9n//8/sfv//7P6++/Xn/+IIOd1GIFEH2w9s5ciHzupx7fYi01hoMx5uQIh1JkTC8NDhuY4RWFJcFef5wuhZKmWav1RodiVHSZAQiPyMxMMiYqDCFRdIpD9HrkL//r18e33F+bOYZdfRfgrk49PFoHiTi9nHK7a4pb62flgjkNyggfQjGGCwaxusVYnM7LIHLP9n5uhwyoeOSGgLVWlUMdLLyertDznDdKaX4NOZGgV7o7pKPWipgsTGH5r+e8Y62jiOway4gAzxx+ZgzQqQJjsEdExJ2dG/KIFNWTtgyrQrqRHJbQAGU+GzHnhzDvZ5PDePEwQ0FDR7ld5N1yWyWBYDz2fjy+9tcTQSX4CJR7uem4QPSoUodIu9fq6oL9vt/nWGdCSbm8nPJCvveyqhrrFGDSRfndOe9z3q8+5+93/ef3//v/+b//P+f1Oj/O/r4jcz2+dj7X4n7Et9++ff/+7evb41//47fv3779H//r//jXr7/+r//526+/PX77H7/8+uvj2y/fnnt/fXt8/7a/fT0ej/3c8XjGY+d+OjUAHIdq3X1mD0/R6ipPowNCttQ2kB3SAEC26kSk6hCcWMoqrqWrv3damuVx7oG6lJnI7NNYBkTQpvZWgWm3ZKmDy6magHkgwgX7gF5rI9SnGRuXqJrW7DTI5VYj17Nx5HiGiSMfsW0mYUuqajAylx/4a6E6mwUGwdBb5n2OlnD6lYaUSVO2dEQy9xoCqHF5AIrY7NeMv11FgblshUS639fKjXSNRK4n1M6uNtdzLSrQdawBVIuItZ609st4i8yKiD4IPO4wFECixVZgWwhFcLiSoBqJMGND44chY+vTH9laws9tcKbsoQrciWo2bazSq/rHH+ePv15//fX+46+///rx+vOPH3/+9fcfv//5+59//Off//nv3//+r//6z3//f//97//+3//57+nf//z9r/f7vP4+Qrzf3R3nwLehmms/Mlbkvx6IlYv28doMDpLp0KfcrFIkVYgVqkII11yhbwJVkN5dX1jAbfSQBG7GHy+yLzJjW7CCSLfLgBvUU0yR3M8lNRmnDonn2rHy8f2Za6Vlofau/il2GxQFF6t0goaL1PT7xtz7yuczVIWrPbQ1DO4mzN3cfHH40oEczvI9hQdI1eV7nHM+/wM/d5G8P9Lk6WH1jHpAV1RlX/HRJWBlsHrN7u4OV7Yidj6aCUIWHUB223f4pYuqjYoGlnIt7pJ3LJ4Th4V2k+N0L19Ll104pztsAzOo6eBI1deVqN3TYur+0FSwI//+cdB8PPZ+7sfXOn+XXs3IXAmhiXo3I7reIDKjThk1eL+OLdFP9VqX1VkhGzmAsXKFI37ZJSAaQoCRKxco8r32l9BAP779YlrF8LdUkN7vP96v/v3393//d556A+o6BB7rUYVkNN778djfHl/ff3l8e359ffv65dtvv/3rl++//c9//euXX5/fv3/99q/vv33/+u3716+/fv36y7fvv319/2V/fz6ez/18rsczn19rpReixeEEXSjpyhcuc8smA8Jk/zRHyWvipvE8DSMUsL5bnjhjVjaQyFUf/zhvbluySIkN3HRy0w9Qn6ZIYXxGjrqDdUDMeYuwECDXzTr2axBdn5AWCs21ZgLgGmCFYcebMbgHHU6CoE3/HO9pFFvYM4BmtxDiQFde3gy82ciheiF4zmHBcLNRT9VBbjeRgyRpSQGFF76S/am2m1aMIpQSmIFqRFIg13hGT42O4e9SI8i6mzTgA7birvbmrcXlRXmnVK2/3+f11vulHz9ef/15/v7r9dffP/788/X33+8//3z9/p+/fv/9z3///vt//fuP//793//179///d///Z///Pdff/7x5+9/nne//v5Rr/P68aPqXaekVHtliAD345mxAcMyj1h7r4jYwxhWCLFyq90Td2RKAAPjsCFBTLNS/ViNMMP2HmNrAcRa7me6e7TZrpFTYXPKkLPzBuxG2QVSQ5KD1N25VgDAMUPFMuPgOt1r4/HtkZuZeDzWt+/fIljngO1ICFfvoVMPiunFkQnfMUO6wY4MybYHJtbQfFr3QhqbsjsL+JtWCz/FevSmmOoP8f8jCjPh13SYbhUNac7q14+O32KCok2H7lpn9gRX6miJDxauE8zQ1OZdYZDGdn0YQHJ6DgqzNbrXw58sxglgxL8WdX64njMNjPfMz8fZPawn39bHFnjGPUNc+DkpwEiCOMzA9zm5Yol759fX4/n1/As/OGsRDgn7kfV+gYqVXYUe366NXd2UFnej9ZY52n1EBnLsuX0DZtabUIus02Mpemez9Vh28zx1IpLEiqj3IUNdDLxfZz3W+/XqU3vver0B1XnXq15//fj9//u3rRBsMaTqtXeuiFzB3GutzG/fv+3H89tvX/u5fvv1l6/n12//87fvvzx++x+//vLt67d//fLrL89ffvn266/ffn1++/b1+Hqux95fvzy/vq39lesJAWtzrVzpPnkaDKkjWypF2GHb7cqn5fRo7rZLn0UXhAxPH4yU4Rqz2fLeYQCYBLYp1BdgUBKSsn9G8RKABv6H/AEJIa3Suio1DcpolAISYV2oLSpabSHl7NoMa3pSQUgmgM54aj3z/KTbZePymUNei0diBIDz1JILXP6jERgUtzkv64p5b3mXYRzS6AixWmIGSOZgAvAzfoE8k0DK5JNZ9513vd/vH696vevv1+vH6/XXnz9+/P3+86/z55+vP//++/d///Hnn+9///uvP//68b///fvvv//1x39+/+Pff/39519///HnX3/8cU71W+dddhtsM6r9GgcyHDu6yB18PvhbPLJTjJWRjPCExExTe8MveFDFXOs6GGZXR6QlebRnJ0RHrLyPlyNhveS7yTA1BTnCRwmTL5AcOYWb/rx3R5+zEuq6Ghc05WTPYKyV9hYExECma5AYGYvenq7NiAALUUw+f/329f3x/LZjuWtvtoIsXNHDXWfOYdNT/G4NFISyzPuuP92lezj9wLgGdy7Z++duzN+u5LjNixRhVmQcFw5f88AFWmjukJdSE+ljFtK1M8AIiW9hnE4iyFatwZE41Z92m/eWUoMNut/q7klwut+Q4Z6SgAy0tS711BDKJznaoLaBsznGfVpNV3lnnjFLEtyWDWW8+66BYalwYLJdMSuttR7Px/PruVYWy981Vxpohr0AJTXWc6ls4oOqMwjV5cuCWhE99CzBLFQDX60du86pqhmKLOdbMQsxe835ye46atO9q7Qi135AWPmMB0mtfKgLX9+lY6jE78z4RdbxQyai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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAEAAElEQVR4nEz9Wa9tWXMlho1o5j75NWSRrCqVVJIs2QIEGH40ZAlwAz/JzY/3mw1YtgEDglFSifya7O49e0bE8MOIdZJJgkhm3jxn77VmEzG6sP/2v/5vhu4w8wRBdoSTgGG6M5McADAD3MzY18NB6B+SDLfhuPkMAZqZAQBh2L83M5JGwAC6x1R5+MwYEBkAOITR3E3/of7HbKbdDAYQ7tFTYWbhHOpDRRiAIdwAgz4MzDJcH9BADwNp7jOd6WYEYIC5A9BvBAFMRJBmRjNy6OER0dVmZgb9xJmJCM54GED9LHfnDMw4Ew6SkWHuU0PA3dyNbMLcjKS76xEBxmGEz9DdwDHXUw2QZt5deWJIkJmn7vucnCmP7B5zR4+7wcBpjzDz6XEHOHB3t2lmBkGDc8YzQbq+I3zYGUGSAMgM7xl3f16vmRtIj+hud3f3mXHft+rmBMxA0tw5DHf9MDcfEmBkcEjQzECaHjppZqYHzXYPYEgzczOQDcDczb1rzLQ0aOFfnwmGZ3EZMO56jUbCDaQei3czIqcrwshGJEe//evPd0b0DMwNAG3Y4WbmMzAzQI+HMHO3GTpsCHPD0NxIM5BwOKfa3GCOoXk8LxRm1j3AGMzDhmNwrT5idtNAa37MnTMRNj1wB4Chu3ePuz2PCdqn2nQzA+rTBkEzRvitjnDODGkwYjCzK8ecM/qNWsWAzYy7d1eEE0NYmHWPWWh1TbdHDGmAmcPRVe4BNwPnWeBan0A8S5GETU2Em3l1RcZ0mQdJA8iBmZl3j4EAAR/SDSQjtJAMYLhPD2Gm100CRgIYMwzH4/Qt9wAHBkDnjM4keviw3BywGQBjHgC6J09Mj5nN7GM1I2jaBg6Y76sBqZWvHWGGnonM6TJ3Trn7jMGoT6YThYS5sdvTZ2iAHrWZDyfTp7WT9EkZ6dN6LOCAgLuD1BF0MuqWRWhlegSnAZ+efXc68ZoGNwPYHtGj1ThmDhrwOzcckKB+NDg2gxlwoIXSdwAD9sylOYnhjJ56s6oB01bXmaOt7u7YRY/m9NVrME6bO2Bmbu7mZgYL03E8JAiSoE0PawC4h3nMTHjo99jzV731BxywiOymmxtBapM7CDTD3YCMAMGim4PGnr1dhpmuP+NuBptipDtserRcHAYgwsIdgIfDoBPfCAxJ5okI3Sw+NVp04eZhwDgQpv0GvX400/cnububGfdX6N0Tc9LBCYcDMx0enOHQSDeg2904HW4Rzm4j3X26XV/S4EaSc8uNEc5qXZbTYzZuhm5wHHQHbIxtNgZyZg9Wo2EcNJLscKDbMJwmZ7rAMf0EA8HpMgzQAA3kNNlge2g3NTAzBXZPgRMGsEFy2mzMBtNmBIddhnanYWDjRuPXv239KE79s9+ifwtw2MMpNwJtxu5LDKbBAduszQmUG4k2DOa6E9ZuBEafpPua0Y0wgg0QUzAaxoxmAwznDocsAyPgRk6ZYfoNEOipCwxsDONBMxgJlvkAbaBhIuEBToEN7tc3G7fhNKeINhtPeAxQxHS/OeUOojHtzkgjy2zIHg7ImeFMhpsNu8GZvv/87ZgPMNMFNDhAuxMYkOgiB0ZYm9GD7gTaMDNFDjgOgI3pvlcvpetON7unWx+DnAhEgpieitCRpqfhXQXOdE2XnrnZeEyGbvHnN/bdu8HorrXRnDan2biKsSGm3Q0Y6gWh9HXMRkc9hjNDECyA7AuMY9jFuphyvVwMpiNhGOOYwxz7ItiGMUemmbVeK6c4o7pqpjnXMDCS7RhzAjP1Nh9tETPAaAbYGGgA2IYmZqbMCAwwYAPgtGOAXRuYripzuI/bGNjvT3YDvf+VPjNmz1TTSxzDUEuCjWj0zw4yLFUHuFlEAIgIGDg0IDNAhHtGuMGArnaP8HDzcHMPg4WHmUVYV6kWniGa5/XSH84MdzPAzDJj6x1gerpHdT97HB6R7m4ON2QmiK690J6y0WGmujLSCdgQ1Afz/XURupxVFg15MgxmwHm9DIiIzDQzDD1iegAjmJZmyHD1HJFhjpMxQw67x93dDUDfdvPMjNB3j+kGoZNcfyhP6pqcGpibubtHBsy6OjIJmIeHA5xuc+saM/fYN9KteuS3F6Cvae6ZEeFqlbqHzXAPj3A85eFew2CfkwAz83UOyan2iPQIcwLu4eEku0ZthId7hJmNqkXAMzxd/4RmZuGhv3w7PTczuK4icEA3bbfxjMiY6ukhoU6C2PVW1eYekZnJYd+CmZnpA3q4u28juq/XZ8iBm5t5niRmhoR5uoURhBncYM8T2UoszCPC3G0XldkMuxpmFq7fQKCbKjRD/6+qchoJPRZzm5oZ6iOZwd1VGdm+HlczB5pnZKQb2JwhiTypp6A/M0QXZwAzvVeSVWMesHB3y4DZVlQ0lViRSejzD9yHnGfJURVJupuRqFtb1XrA3Fy7w2aG6mUizD3SOcMmgIg0C70XM4d53VbDGyqXwkBYeKQWTGqFR6R5mMc5sX2b+4yeNmDomZn2THWCEWGR5p7ngKx3k5jhcMKNzW6aW2aSrCpCUIFqSDfsujWzzKPiTAUAaCR07pDT3SS1vCMSKuwyF+Egz+vYs7O2NVNB6a5FEhlak9svk90EEZlm5hG69Twy8pCo29CZ56ZTTYeMuuR6X1MJDJumCmIdqQTvu/YIMIvjBPt2ROiEMzcY6rY2ubmbmtbw6e79vdj9TXaPlpN7wAwedYdCNWhUtdfdOvcBurps2IA1M9PNqWoQ4QFgpgk9hjag7t3u1cxgZvTtunQCjLnXLTefnu72MPWD6lncTDsdpmqgqUbMFyExINULAzMDsrvd9fdGMNLBAUgOOVXlbqyOdDg4rG7hWurnR/fgbTPLcDMnwWHNe6umQU8J/+lWnQWDV5X+/TkvDqlWSD346NerYbUZzjB0bpvp+XQ3AIdpk7OJgdoFdycBGqf1CgBoqevo1PM852DA4bTAlIkIdVIkZrqrItPd2exq9T1D1u2eapabmSEjq3q4+IMBsVgS5jkcw8PUkWs79TgMpMPYoz7LPepeM58aENMNg1HrzzPTzaYas98oPPpOeKjlIiczORAkaNrWkV3T3YJ55raZGb3frT3ksHOOwVSmuHl6hPvcDgt77hgM9BDMTM9NX0Q9p8HCPMwd7uYsCpEIQ0YYzc3MjT0Z6WYZi/rpUnRDRrCpkzzSuyo9MYgMduuUdbP0mG4HzscLNA7rtsFVH+sICIuAWguC1PPB4HnOCHM3Y+97AWkwncSvPOGun9zdIFOlAcEZDMPDYSdfJAyOWWgjPY2W7g7TYjYgMqHXYSa4jDOqptwDxMyoee8qzOK+RmDmlScjX/kyOId7SwyMJkTUAV0X4QGYoC3MOHy6DXZOgmb0jATpbuFhi/9YuIe52tzQnpwJXbm7jM0FDrtzYBBkh4g0Qgeqm3WVXs3U6D7salUVGE731sGjdw1QKIJNjwtsuA0iI0lMjT2V6Yk0ws3C4pUZHhzdRoJAMXsCwc3cHGBE6F6f0nowh4e5m7OhK9lg4a6Xq1LDYY5FUR3OJgiHGSzD3XyKFEgFhkcI9pk5kWEWHu77rMzTdWwD9PDprYk83c1ekSS0CDwE2lLnly4tM6iudPM8oZ+gKn56qPdgOK+j0qyHugZmeqszQ3frzo/wc47qWDNTRQPg8/3mDBaCExzleRJgZN53mTtA3Y0ZMdVwo1HltJtxGBnkxAO4mGMrUIM73KO71UFwRo+yb+l+ep0QL3IizXW1w83NxW2gpz5eH+HxOifTv2C+cM9zuicy08PA6reQX3Ngj1iG3puZe4rwqHp7mM4CdyMHIJxw6PNzGhxzfXj3sG61wATwOudEvt+f7p6Z55y6DQ4x7lb1jnQIU+PM9MxkquI2Nwfn3k8zaKkYJ9K7bp6MdHOEB8mue06aISKm5ynNbGrImakeYcroaXejDaGbc7quKikVXOR4WmSAjMjwEJiQr8zj6i22rQ7rrkgPdXl6LNqZU1WfxIDwMG4nPrMPyojpLvEc3E+79xDAez+5pwhmegRzcUAMx4VYuk3XFy8mMP2+33miuwF2VeSuCjVGIz7BkSo3TJXOPHck3NHd5vDQw251b+4emQ7r7pmbGabyZZoYAZfUZeWGERw5NdVdOhM9baarq6bcLFTc6V1gSFa3q18CPRxuhH4gzHXKqLF7KnqoJrkZoa/G6UiL9Oo7HHUUETFdEREZQrfzlR5OLvKjamkw5pbp4SbGToza+/Ozu4WygowwktTJMUOQmIh9d+b7MQB2NWc83B3hgYekEijAaRVwou4ytbP6nJyhShIjw9UVW75yqgSEEqIxOFO7x0xXpgvUGl0iFGIBCwOQGSpq9VhyP8aYo7tAzHSGCxGKMDNOFxzDyQyQ5phuczfz7t9qXBh1CMNoRhVv8AVLXOhnlf5Mi/QY9pahBOhqPzlcHoicEXYmtHNGjIwgCS6BqQMD5MxDAJgbYG7dI4pvejjPDQHArKdFzWX4yXy9Xh7uHnw+hLv3zK2ijqVuAPuyOZk6Bt3N3cEhBw+91uekmy0QZOL0sPDLjEcsRGGxVSrIpn6Wu3PoFiCPzjb4x8fLLTg456g2b/K8smdKVbwtpnU/i8Q0cwG0rOnu5ZIiQg3HOUf9P2kZJz0fgEUbLOoWzGbJUjj84/WD4EGtPtWJUw/3N2PmhqCg+GmQ56TYJHIIVNU5H3peMHy8jsFOptpDVRP6y8zDQ+ioOGoOPs4H4AHTOwVxXq/uJq27l4cRRUToKnoQK3u9jruHeebRptnD4mHsu657hEVmYpvqbT4iw9yIicjX64MEB3CDcXpcxQdsalqMCGx6i1UzO6+PsATInsx0DyFiu/e7I5Kw6RY7ParRYBhmHINXjc4pN4sMh0+NW6jp1j09BGf0vTDMSAB5DrnULIHuBqzqehibXaON5fGgK/u97d4yj+0pB0vZmVUVHk1ExFng4+lZh/P5vlNTNQTypJpL4ajkGOjix2H6KcLAe0ZyCzeL7Za4yDzhEXWL0DcbUbKkzdDDM8IQ4e4ZMFsoFTaE9kVXzUxXRcSMcQj33haW5rb15bN+OOieIc29Z2DM9HNOZqR7hHf3NEmYVtTW5jo3THy0NAjqFcxd2KloK1FPYrlJaHUZrLtJdLX+rRsjTrjBFmMUNnLOMTPAZ8GQiYg4qdZTlNsMDA8u7RFu3dNX5wBt0XWragIEF7AJaRykRjGY1e3uWZ3L6M5bYJYEOR8fh6MuJMwSw76tJ9PvyyEbM1yodggi4wDm5tLUuPk2he6uW11twoOr/IbnLtDJgVGVvorDMBcUIwyXM60PzhZAdDJVdT6VpajC33QLPd1TZjZTIGE0uB751ICsdwl6g7hjW3BH9572A7d2hocJkrIFoMfNpsclBHGf6eFYOmzbEdg+I11WHj4Y0W23ywy3qqfMUFW3SuKo4ajWnubDhM/JCPPImGF337pVVx20CmDVGl1l5jP9yEok1QiDaG1IsOGOrhvuYjF1y3pYaK2FmSO2gJoIj5PD3lcpZUKuwINokRwzq5xROU9MnC0kdYCrYlINouu2b2VkD81QXQZGhmorgaqZoVLXw9VJzIy4l0VvwurWPFWbGJ2MCB1MxMlXuJc2t1H1prkJM5zp0eZRW2s6p2lE+ML6tIUHzSwyJFozqLwywDxXJqGFmnnU2Lk/BJb7XtIr5aGu23PSPdSoAEZMZJKTcRZfNuQJ2/PYI/NZEGPxaJZs1TGvc9wMJvB3tGXYY6Kb2a9z3C0zIt3NF4JIJ3kyp0oAoHkA7G6CKoY488o094gICxVED4LvJAwh5uS8Xu4WkRiBSMsDDGe6q4tG/dOZAnDOAdm3AJ48xqcIMK8qnXTztI+mBhS7KvYLcntcc4ATD5EWmTpzBPqpKdFaUi2Yke5wR/UUmxg1+h7b/s+oydAlut/CSNVYwIJyewmldHExGHfPXK5OcJ+kd+G6Erynqm+Iv5E6i7+VzJmiMr2r1T6qsfMIVadaaeS83xdknHTDTBs4HBjynBRyBdMajwytbS0Mg7khPfbUqja3DNXfo2ICy2wiwiUBEsYReTiT5+hhCggx2K6WpXNMHC10Gkz3In8GVfdfLYyK1W37ZqaawJ7mnIWioP7LYXid43BVedrV3aUVkOdMz9bW2mOEkFl3f71eGaE/5u4fP3wMoY0NUNDNDPXhn64dD4Is0csIJuK2OVN9I3xmztkOw+F9S+ygvl6ke2xnJKwmY8nq7hI7o9PNgFtlwMmzLVtmVUmgJvHBiYwMgfAZJ8I93EDxyWr2hV2o+ZruabV+2CaH/DgvwDyi+lrAfZERk0x1tm7rKV0i5HA6RO4sZI0MN4O5T0+eMAHXnHurqjhTtwSm6ZwFnuav+6lEEBHDzhPg1qFmlhn6pVJDSgbXKt6wq6qn8wRg9b4hkVMTovWmpaVTx5Ane/p1TtessBiYLvOtDSPjq2afmRTjHN4zIM7HK/YlrX7x/X4/ULDf94VBBZtYonOyurYvHM40XHeekROZ7/enmU9zqufpl1Vh6kFHBtH6f21B84F7V6sLEtnIaTOH0Q15jliwW28P72kYM3M3ugFm93apXjXvnhHfTXBgjuYM+4tHFXgVgr6MESmBoLCproIElbY6XcSzUjg0I9vCYeqHytxh1LoVoYQVdGzT5xmOGGovCx+WKGDMYaRUjLtq/KnDzKQpmG4DZnSMqKZ86nQyTwhO6BY8EhGeEZyp6p7JDCE+thTXl24VAklW5gTVT6enVEyLAjRfoaHKhfDYmvJLbmmC3QxmfT/jpC4tIb0RSentSPeYbosQg22OUNVMUijIzLCl5hDs8zpnL/KnZZvWkT7TDeH3Ll1kixs3INOrRkpfC4sT0lEIg5GE0AAPq/oNyVFtRI7aenWZ6nH1wHUfEBxOhNni/ONCsUZ4hbuoD72/ZS0AMd360doYkoTqAeRJEYY9M+T7fcM9UryNZeru0jHN5eIAXUrkAHar7q26Vc+nVPPRs/ywOzJimhHp5iT7zq2rQ6OnRHKqlHvIohRbWNXu/nodXc66NkRd9gzg4qK7yoUiw7oYtqzIu94AIo/R3Pzz8xOEue9pOHR3iV91+LnHx8cHJJAnLcK+GGB1ZGaerk7Z4N091ftwgHffJd3WfSF2jXWvLl6y00PSkIznIJCOr25XSV/kkQZ4xL3XQHMH8Xq9IvxRcMXcVvNuRGYsWhLORzdNEo8S/uPjh+newoe47/sFHbiH2A4zW4hDQvUIYP0WZL/Oh9HmoSjNUVUAoRsLptpBjJ+aOc5khkEst7lZt8TOBK3ulZ46YiFB38XDvUIckqAAnJ6qyojlnIQnAff9uaVcU3UUV4iMvleiIID7s/bVDMiMF0CJeM283qVDKjzCg5x6F+BPYzHqTjz80QzDLO77Ouyc1/RkRNVV6RURUklM1VIGbmKg3V3SuXuvpP009nR3dxek+ZgVZUSmOgmpv3MfRes0JNmqS2x9AO9bVZ8CRmZK+G1zZua8XlS7E0IqyjMtTIeOmU/1PJRjnmS325KLIN+fn1X1nNXiM9QiYKbzpE6bxZJNzZNH+InMiPf37xJQZB5gqbt1v0wbt1GmXDXumYeUAtu7yqUbXViHZogT2w3LVmLwOCBPZsSpYt2abn2v10lBJLrzTUKGaQ7Vt4krr3vNAI7wWcqE5KjpWe2BgdO6d4W+PsYgW2MD3u+SrK67XHjazAzqfffZPA6YWHcRzDhNQbIqQCKOmWX4kN1X9y44fa+vLJD5cUjY/+W//j8M0c1ImJlO9qVAzSkg08y22dGvl75+r4fFryid3F4MZtBZYIZI57R7DPdkeXBJNSNSgzLWUkGYpC/bYWAREoPhKUCEPu2vk6zK3cKi5mYEMR/niHIQf2UG9Wg95S5twOTZxl8/ygi4raYBRtbXh7RHciBECcA+7hUWyKrTKxAM4zRHAEVOd4RPz17hBiGLz4OyrlIfq2LcBRYQHtF1MxOYV+atCs/q6+HsklywaySErfuZubejyk/JUuOcvleUP4h7b76OUB88HqDnJhbK7LIUye4nAEmEwUyHWURU91OHwW1dEJlPt0Lmya5uSbDUs7ml+60KWUAAAs9ZLAfWmPl0eYQAKz8+XStzOgfkfd+IMIngwureEHmgZc/xiK6Wlky9rB6IYHZ9Kq1+Ke3kqICuKPDkuXUBuFnPgpnCVFT99MJHsp5R+LitkgJfgikODeMey1uswHfLC13Gey+4GZcf+vz8fs6RzCPyqBPSyYbdmLTfyk510tbdslKCOtwmT5Lr4hwwI++99kAi01Lt+PM34tKz7gXpmV0l1uTeq85YraS7bdnM591/CW5EhnbbQjTQDm2ViVr85hwOEGbNMTAiqxtDqRsEEqweEdZ18xwLk2bGjGau+kmIcXgSI2W2Llc++PLW9m7shpltQ6DNbl+oy5BretX/Goay+I1bPBfb3hvqVFYldbLe5WH22D/rfSPDzT7v+5yzxb4+Esc8CLxOdk9XmyEyR/w2CUptZgtUqKPac2LR731fBv1kNehmPrL11Vs70fPoPQKG4Mt/ePcnZpe37rAZepiMgaB5D3cr2uKZOo5JdNcWPEIGHEIAZmiPsBFytAxJu+/6YjO6WsW2sOxpcOUJJEWDoLvEdEGW1Yjqjoizsp+vqnmGlIZXmqIB3c1ChZGFubjyYZuhu7WBxWdGpJvpMtMNpO/o4Y9ZzLpFce1RLuy+S9otWYtUDauIHyEA+4p6AK6gxLYQ0YYJc3AkB3qcvTCTss0iPdwFLKgwB5Bx9Bk9VgcyUwQ+32+A7/sJQ933F9hibgLxMo60lythHVYXHF1XwF1VV93zeoFL9kiBboD80oLX9prXYW4cjj9nDmR26sqTuvl0+3bJ9NT33hmpZhtmmWGwzHS3E1FVDyVOYjKdYmBpBnRNzzyqLXgYuzwebWvXdJ3XKzIzfDD3vuOkZDDDqbq5neUM597bM8IOq0olanfLwr3f0YzDLzbVYO/3W9VJeopmsAdFxXD32HOgq07yBaUkt1AJJhMWb3WzhZd6BIdSTIo0Nke3FunXKRzTsrCh6s19FyKKBP5T7qovKbPAH4FRXS1gp2uXbfeC+8I0uNKXhYf0BGYmMrlk3jo77l35mdgXyY1HRPmsa6xZEh5gSMx9v4fT3T0tXKVnwh3kVEGNb9jr9RpSSjPYyNcixEogm2gkUIyLLqoO1SLuMGbKojRajvKhqDVTNwkT7qGSdEm+PCfCM1y4EzAARbmtLgNU6WBuAPyY4gPu+/0gnJwuYoa89x0nFHygf56ZBswgI7X+3+83yB/OD4vPuVfVdEdmN7t7yO5aBsWc1FK0oZQpps5G5+291xwzvPcpC4Ae2fpKqlpCRDcXARxrlq/VhieOinkXGpYpYNn+r//N/7HFdmMpf0rcKYXAViuMjGEvUsxxD8zg0YBOK7BBUKyUQqYNpkY+Iqrq4/Witm7m2ruHX8iSihd7ug2zxRUJ/bHf+AZ3WzW9gEV9K5Glrl5nwv31emk5ahGfV840e+TIkE5a3QeAPbLdZ3E0CYyw5itD15wTAuNES7jDQoeIqZ6SsIHG2KfxKLSUYTAj4kHrdR+UOTDuOdOrKpKXwny61pBiPl0EPj5O3QKZx7vJGY/ktDo2EiqmInOmLRwDUWdrKYBZCEpuEJGB3964St19mFoDbBmT9i1kZnf5Y50/H+d+viUlmB4LmDl7ImM4UxPp3aqYHEvzw1yi6e3tzA0z+3AIGSHMzD0IVU9iCynP3ejQMYTH6GP33ljTY8sA6KWEuQ7lDQ2QQlQCCXvejmQS68Z+9pv2zIpnBt1C/1fu6eHsRZaUjqDK1LDwJAcWId9DnHwkm8teEVAxbnACmXGvOFX1f97d7LF4cBmIjdy/35YYmBoXrLTKAn/kEj677PdYEdAqJKRrXxB0WMgqeiWh3trAPDkdEWovSLDH3Kc7T+4npO7yh7wxE2eone4bsmIPjxjdMz2PBJUG6y5tane7n2+ZCs2dU93z1PJjq7TVGW95JDEo9WcwuGd3x0pXfarNTIezejVx+x5R70vivE73PNhAiE8yQEz0tFJPrm0QBCDnk4s0Nsj9gOfAVEu0Ghp1nKPvKPAZUgx7THeEqkDraVc9bzCiusEFRyxkse844bCqEa+QEbfKEWZbs3q6AXXbI4cdcYat266qdDsq1kLair4dLsUmdke4ea07zm0lYbaibzcxFrQlwefL9ETZTKHKQgyYuNAI0yZZgMEwvUaq1+tF45DuQeL9vqqqnpLTMlI+iOpWmb10njsMqnYliwZXPLPAsZQIHHdXv5DnuMf7lpoYYbX3fe1xFWxnI4rv0QsBbP2Qbp2GJ14iXZ+LDYC5RGm6sobmW/CSdLNBR4Q6Co/VEMjORsJ8WevMEPYiRFX9/kyBo0wkos2wbvoeOPLkMpPkrVETY6DKHHLyBB6Rj7s995uRtDDChBNzpEF8spkAPAful6EswiEtpmBZya1XsbjS3vv+jPRwm+nHIdww1L32OEtCduKHROXIES0Rzjq/1EoJxOtqffCtWBdjNHfvma6SCxoQMdbP/YTuzvNyeFXDQENVfX6+CdDkJIdWVagYh9bx2jjcV06+hKFRilgzI8Zcik9GngXZ0gl2t7g1S4uUvaDNwk/KRUn3+35Xt3TDXzXBzLinlNUldAWQF12LP84BkJlm6C7A5B/umbp9bxMQYyl9BB50SRi6wUhTZVC3el2ETiLSiRFKtg1rLbdJQCiluyzolblLTky4HL/dtZAgWyWC0BU8iucHOKUqZ93E62QB2JTGITPX2zjwiIyQjLCqASyxHyk5/HJ7EVfqmgitG/PobgPvvVj+UJ3qwKy6ZUqQAlUb4v3+FArvEVoDZr5Kc9AcRLuFRIxYOsK7Zojq6qnq2RrXF5W6n28RXe5rrtUpO837eQlFhMVwYN4CxJ56oWZ80wHgsXqg9R6TKgrdVAOZuVe1QqkWG/oCPLvcYjgi6nT921otxwgPl6Q8dktCPxNyoLk7IYZcvWa7zgWlnqlr3L9gbp4hNt+VsWMArfurZBvdAU9aQH7ZN3TFyAe4FfwmVVFPQUWoFtNe1CFPhst4QnB6zJ09M7SQsAQ6vvLktDD4ichXJmDyIqm21Um30FZzpOr1AJaNgSHTSbt9hTtJOEhOSM25aUMAWXXtt7g3GjFV4SZv3tqlh24CUSXWhOBfDB0hdcTGC8Dc80v6vbK5MCXl7PbWIxL+Ku/MgyHa8sYh0lJP0szf399ar4Jophq2Zg7fmoyy0S9QN9tUz0Lz7e7ytWvvweac4xGlLkHBIb8tkcdo3TXTPa0vZ4ZIaZKGM6+z7vjMM6siTV1a7oiMMJtm3RZg1dMA1LBK9jJyW7jJZzCguddDbr9eLxAOyxMegaEIFRWPYms9HLCqnhasrPeFns7Mrl4Y1uCRK5AbGBykAk4e+TzNEBYzNFiv0m6fjax17hZpkFAV7B7TZ3vyuRQu4hGzgr8CIQJcJ5UqBqWzEOr3QU6va5dY9JkgCa9bSsLYMxHoma4h5iFFl0ERlCSPhY4vgWZySdhTAq84QXjICCWEnhJNP7xtv+ny9tPTt3UYCd+QkRugK1lLPSuR5+z3jZDvTORBeHa3R0o2LZ9gZsCDXXlCh1Z4dJWItMxUqkCEGpdT1eJpIuL1Olqx3aOAFVeJ1mOh4MKZnr4Vj+3O3Kbq9fpwINPP65BkjyzY56Tw/J728FYDYowT6pkkSQKxBm3SImbXAPDE8IlzXeqTRiW7DYSBw+KBzXuxRtIz1GnZl8xqrU1KAtP65a2rGJbIIzBceL4vpWyQPEN0KGcyzwPmrsDDHnx5ux7174qP6v30wPOx1h+w6gtxtl/WsN+QGXAWZzCd49vkGp6e7xEMYHT9alE+9L2OnlHXX9UKY2j2Iz6Z6rZN1FDCYqSqBrPIsDC1GsPVpIpKu3VlMvgtf4IjNknL85wUchWeGTFdshEserMVgJnTwwakDQ2rP6QOLgLorr3MMBlBDlb6QmnqRbh5KAtuAAtPUFyxfsYq8CQPX5iupVUZwMg5J6ebWKOvuRsG6/peqf4SAE528+mcXyfDTc9f0oWFTGGUAMM2QkA1IKvNQ+JOleev9csINNtoVSkHqhqYyPj8/DTlPkEmnf2+lBzAHMYMt3DZFzy8u/AoHVVamFlXGe2cV7XMFrMu2YD5dnIz6xyiQd5N9QBmXl3q8L7wcY8weGirGGbqySAFSA9/vy+5FhXtWWWBCYoaNaASAfr6GNzX6BvhPaU+WJiSpwv3f1I+Hv3YTOR59nXATLEE5xxz9/DXOQtGkDqDSILyphHyK4UP2g2Ra0axR/Ep8QIendP2t1BZEA9/yC96Sa9a78i+/lP9TNumjhzocHfPPDMd6R8fCU73dQ/ZqQhaRNX1dE5/rWjuJQSCPbqHKGhIHEtPAQOYEe52zukpTz95MvPRU0TdMvP7+akD6pzXs0UoXIUgXBeDu0fVe8iVlQvtMZAjNku2LjObWZd13QuzIR7k0KRMI4CR7WYzHGF0R3eJ8q33nWkJouCgDacx7HnqPDczv3WHLRpf8m7pqERKy3zlK5W0ngKsRk9pXSbuD3Lia+9cV4SqUrcn1MmWhzQzj8SKJsWxDMgueWsfJ3C3Tg299a6KvQNsHicPycwUw1M1X7ikABaRjGIZpoctjhRmG5chY4o/9ZoZRI9z/au2R6At/AfjEfwN/fM9DAVDzTSHVWX+2Nth0/06p6rM3J440jUNeZiZWFCqVzILF4dJPR/1ib2hq54Rw0ftvlmVGEwuRv/lAOJzJxlBVbJVTSwL+iWuyJP5OiroqlookBxtVXchHJPdlK6iYKhMJBjhNqwlxn1/+T4Nwj1GCBeAB6UxUyaatrdNk4bXx0v3JcWui5AfKc/27oHkCuGcwcg3gPCYZw8D0MPnc9/YVt+VqfTqISC2v7cM0ylAYFNQZppkb4dkXBBJW/dGBhzVb3dT2yHwrd+r9mntDcUgu3e3yoItIVW1ADNDcxX7NK5fdFqpZACmCm4tT7KcAVJDdauUk0ToQfyCg7r9Bb7uJWKeeWSYERwBmb8Wi990dQIeQTbGSBWDOiZ4b+nn9MOy6mhQQ+AZVRdcvl6lyaPk2Zde1WYBUvaUSNUWj6/JY3pe54DqsCVCEtVEUsqcdf4bltkh9OLMnZYuk6abtgCbE7518fvzPSvVs26xWs0he1VppPY3vipbMRmZ0bel/Kuu5RAVn74RM6Ndqvf1+uGlz1j37emtLKPI3uwG42DWrui5Mul9RKKgR8m3K1pwbEsP2LIOfLB1g1JzkK/z+XmlMqUBg22f9M7WuAMY+o5xFyTMOHvcSavjJocj770jBdegOcTyxoor6VU3SLekR1qUzW+zNEIomK+GEyQdUguQo5sWqubmvu+9NTOiMvRoZPRX4IZgVPdQGobaUtsHILmaRfqwV/omP3S62SYW4QHi9ywHSJ0RcU6C6Olbm5HYI9xtvXZVTYHUMBFsADBoUnYB9Y+PX8Ef7jtywR9ZyBkRXfe8TncpMEYFQkaqxpWSItKfA0d6656ROYu37mx2HmRuWHvpIt1uwL3vdefbaicj4gsiUPt2VsQ5914ZRpQ5Ufdtbs2JiN58mJVOZLrYHrdlqvPkIm8Ze8ead7WqfvXWOn/THRzHavkFH4XcRlKwzRAoIe+9x67aAsFYz543YAx4nZcZIt0Ca/fTIVEy8fPRnmC6M5IDB1qhxQpkz1C1a0/4tm+J8Ijtw00twsbfcx2wvmn4wuUF5+c5dUurYvHQJ3w0IglO1SPNslAgT/hDS4b/s2i8k+dkTM/H+djwMi1Wo9hRLT6V9hFnJTF7r2C6+GBjs35KZGR3ySkZESZj8m/GIhDwjKWrJEPEgMpISEEW8oWrqZJ5atYMH/oD4pxnKJGSrk+ug1cqD42peDBVFRBKfxKOCOoc2C5TWe4G4TBSWt97l52S2VOCxh7SKEWU0V1ADU6EDiUCuVkPSw9gxiPkkskMsPW+RO/LB3DOSydU5OnqtVD1mLRa0y4sPsUZaMlI7UeJtSiLPjhT9rRKW25qg0wL5pNFTo118zlkzLrr1r3v29PhnplPaukI/bZNdeR5pXaZXqj+8/DYJw/0tNzi03Pf5fEkLuu4i4UxIOmBziWMu1e1BKMzMh4ayQiPVP6HzORadJ6RYd7VywJCNAnIcUqK80DJEnU9dwtUK1e1fM5sCiufniWjp6Bwr5nBGGCGzH3H3S2VnsuWM+giCJf+RLGuj6T0ARFgQOnP2aZM506PeWhY4YhUngkjlTy1jvLMPeJ1yFa1hde9mVmtZKB2d/bS0YCrlOvHJNJfkMK0m52TM8Nh3Q6BM+Hn5PveJTx6PdyPfhccmvvomh1+5EdXhzm/MgU3KIj5xB1LDefmHz/8ANqGQG2ZjJPHlxbgl/738/0++YRab57BQEaRwcnUNjivI6BmyPf7E9rn9hvWLzmDkEG1xlIWysuz1imZmW0dniQyIk9uaGGE7q2vgKAHoyapuSdKIIbI6u4bmdXtYflKA8/r1PsK+aXKWRFCtsDZYtxc74jwOrVx0nc+SJgKC850npet7o1sbBds5gqYNPd02sYacjTPxLiaNyUPzzrsNA4FK4QT2yIIkQPP7NKFFFq3x1Mv9Qivl45QIiJAXciS5GbT6mkEpI25xK7BpolvmOctaXNi+HQqemL2zClyU4790Bxbke2RRzA9ImOmXVGdJupeM2qCVNqjU6j9Inu6ekP4j28gpZH08LMKZleGo3vMcxuJriCp001hPRph5BE6jg3CRjWRagtMIdkSs5mDw3ydr9waQGvcvngLHcF9KywAztI2D9hK0jDTUK/7nDIqWCFc1o3KXF0YFw6XNr1uGeS3dEV05OuAfJ2TGee8AKgwl3am7ltP+5HJ7K1R79JhyEHL3b6Qmhb2EJYZQrBhm1kkTYR5TM/UqIeG2UyrKTGzPEkgbIeILIvzVYyaSZxNEDMSKc7MCJvMdIJxtEahQlsbOE9uhapRJE1uXvFa2rh23dC5NT1G8w3WJ+XkJLrGHvu1r9Bq2xJudP4qVVxHhAhkwLDwsXqck/v3Rh9FomMfBEk1jtr4OgUEL/oTmyP9QPpm5JpvCSPuzgDbelZaUDdY3SsiRArRmY4IGX965n3f5+MFruRU+tFcDhMA2KWkDqnuIrzuFTVEWT9AC6++XzXIvdc8NExHxa0is7FaTcJIJ0jV0eck3Lqvuz1nNzz8SU1suGhkyCjonh6pjOHPz7cq6D38sXotiX/cXESLBWYU7aPpBSPHQE+/7yc5CtJytwUS4ewFxGU9k/UYxK17dxKcrnF7vc6stmyhOfnVVGLrsdtGR2yS79auymEmh00SND4QwUOBhaKl3u9n9fuioraUBpUzIW2oIGnJSNyj68o1I9p+32fXypxaCsJUhru7dZeqDS1vhw9XDNOzQg7FuZjhZKqub2WrYFMOFVA8DYzVvTClOVJLUmusH0J1qg2QSmf9HKZfN+rput6urFzD6+OleJluTvP1esHlJIJqC1uhoH9Jb0011mrEsAM6AOkgYMBgxy4tZSWd2ybR6mf3bUV2S4XBR6KujerhK2KG/7Ohdei7petUzdAj+kq8Cw+jzewhqLtEDu0x9542C4vQ2aUy+ZE68fkG67Cve/VapWQTrSKkBVCZMd3tHmTbIxIzoN/3wS8Uvz0yz4M0Y2Qqtkhi5eF07wADCeLE5mf4Fxbv7qpvVM+oH/XFkeLWrb5SnQCrOYw1tNu6aw3do4AmGExOaVq1epQQbzRP/AFn4gQ4mFE1Our4wJEkRVffIvUDnQXdI25HIAcXwBmC4qbFCkbGzGBWYgxCQdW6D/TasFDhU/NgVsFlK1MV0XGrCOjUqFbvSYiNNEqypoclUPIrKZePSwIbhBQjBYhpKoBJEid+WP21eGn3p6uSFpOblVZXnA+Fw6je354JBCG3Udf9ql9UoXUNBhHnfd+SVM8j+9uL9mmBpYniUM6ymYaZ2C4PPPPemJHmdFXk3ebmkbfepvxiYBlIt+rq95XAUThD11uzATLi9ToCCoaY9XAKx7RqJfS6wALpoLVxjSbY193PeelqMj1EKXefSz0jhXuMIo7TZpblPq8zo5pGl8p6ysP90ekDZsqexGZb+lSRY/vN51lAq1AQ17dyFG4cGKdfrw/b6UDmZi1QyKx77vsCEghqkZgqUupkMZ5XCt7sKS7TmORE7g1rNBDdtDV5u5woglw4XzKbxVsNMPPbg3lOCsJgxMr2zfF6pZqAujs6Qg3ZCKM7IdhE7Byes8pzCT7A6645A1A0kILWoVkxpp5YRYaKzUdDqt7dtklyzAZhSX7qT3zlsmexcMr2eZIZiSE1qgF0T7nBz+tICKB7NDOU0vyFRJlZvUsemt0/ml0KyKjMYd0aJXGeBeckSFUFYwZZZ9jdVWq5NHhKqACNHD4GKNvQOkOVxC9575VS2eGv8/JNZhVDsMJ8lRHSFHX11GTkEunpfNCqL22GssWUIN3D6c04UPq/WvnuljunqqUt3PclXdDoV2xlzBmDCasXmTO9yRNmXm8d2j5PzP66Z4C+FZGYOY/XatVf5i6yheochUp3R6a2Q4RAJZx1nFv3bFJ/hPYWHgElNfcApqtGzeE/o2sJHdBDjsbo0N2rSofAfV8hDCfSxKmKKFdaziw7LzBaB9ut2lPVXPNgNriahMN8jeYqi+67pKvRdlltE/h4RBX5gnOOdOaiKLtbOx/Gnk69c/cp1m1t0NXdbSaB/NYxpASyXfdkKthfoNYT4ipIDXKj9NDDNBVAYwPMHinXZlRATU9zHDh56n27NX91IbWTaYbpDouTL6NSLyUjs1EtLJcmYG4SwXEmMt08ZNlXndCa32vDxwn1mCpsIS9VLgGDb34cn+ByQI1O6g7FOUfn4eNmJCRuEwxlNhy4CbWbaTOIKsR2yt6k5luaR3Wpa6yqnjmxmWuKF2lxC9B0UrzfVx0UV+zgYdLFOKel5hTTqNJyhmZeNaJDbXvTHRsHmPKrVf0oRkmkic6/ODmluadcHeHTQAtB2ovHBFuPXKGq8NSX66rYA8s0xAbivbDjvbxnhN50zcJi7rI+UetppYHbTtkq/RB5xAUIpdDO7Sqsw4s01bliF9yWP9gW08xqx02KzrLINBeAHpzpy9q0fZOKj0+17uaGNQ10NUSYm5py3/SRFX2kZAERCUGa52PJxfCuyp30hHpfc4/MrvIdCySrh8BMEDbcabIk44SBU8ugGFxyBqWfqiDu7tvXv6YcdgOcKioVTsViuBb8rbuAoXrSapCeTnTd2uhyR7p3970XD9WjbjVSW1AV2IpcbJ0Wc04Ksv/8/L7X9oMlEnrPpBw6YvhnZiOTLSw0JZhdcLtdUMR6+kbtUhOTTeW5cgV2tLSiKiTlkeV9nl9s5uTMdLWioqn9IZfNXshhT6ml+l80DASNqtUnuervSCVSuXxkO9+Pt2qRXnh3PxNUHq4MdLN8AuwE37v7NLtlSXueFCl/2cnc/mOzdWMwtpwf1ITKtShniphbswCgGW8PD7HHvW4T6db56K6UFMgNWhlTMCRidHW5/TboZlj3rc1JjEqH6YKx56qQMSgTzYUJ6Oo2B9zv/bTQtBDdDQPy9pDMEwLEtsZRYpo5e8ywydVDFR3i9n1Hw6vXDn6lLZGSS+3FrZLWNePiUZzviJKBWT9ywG3vJOI14zBPmgXBPHlOarHhKTxJnAiYRNyxGi6ITlN+RoLQVQRDREgxFVJvKbV449vMnsRyialVhoBSGakgiJ4RAVv3kpjmaJayGWh5zgM+LIdL0tV3f2VKY6dMkJgq0yU5I793RtYtRcXrViPIRVkxt6QlXUr/7GypbobGjCpckxiTQGMhGRWkohZKSCN3qJyWSmyqtG0yDHkycucCD9k7ZIn0R/qcISGpGahaZaa3QnLLc+JL+4cRfDTV4TatWSWatTBwAKPX111PZhAAe3++CSymurAEVB6t6mmQmeYKniJpKkMX+lAiRa+MV7goniFl5EhEK4uP+w5R2jOmWfcqmHqeQJfqkomVnAjfGBX1jqDYWBoEIEMzbgkV7yQ9pThqEGJATqavf028YIR7ve88jakkDPoE5k7lrT32K+GxfUsqa45CZSQW79/98LsHP/CH8VL/vXUGNL7ioQxttVMzkKkrZPxoxZEO83VWnENujKXUOl+Ek60QYiOg+cxz0DYL3/xxFYzLZlAhupiHWDa5H7c0Jmbhmt+YonRCYpvhzkb3/qIf3QfMR5kV4SX2wMwEBdhzKawYdP+r336FTM4GJT+rhgLgSrBwh+v2XkToMUHJKdOq0T7v+9blg4nosFskRzU6sfrfmXvfMnnLOm8PPkvMycO1NbDqrVvknLOPWu2zwQzhnueY64Zb9MkUNAicCF1mbvv0xCt0NzV3YegS1AuCtQW+ia9co/YtFyjpW9XdM27NarYU/ZPZJylD9/aqGnCmI76qxW66mRQ+qkG22eSqcvXQ3EMWzXzlMz9uJT09s1fq1hmwHTuRj/wMWuhYtGYAK8nkDQ9Ab9tNin7Q8eNeXbbQwVRdzmPnVkleJd2gSv5bd2bi/NaxCWgWJM1Bc6rvsxh6Qfo1P60qaWbi8TAq3hPqD6Yx9MhVDMzIZeppFiYQlVQGsgMMN4cJviMZEXGEHSpGLcVkbmdN+3y/hYK+Pl66rcfEjy3zod9qvh5opQj0vWEBxDSxJ2jUve5Kce3QqL7VivB8vIbtbtVKdTU3n5HEgwA8/XnG5BrgbRORjKs8nnEPzQ7S5iLGNPZ90N00uiGOiZPwNTOqzHrU7lyuywxVV+tttWcRvoym/JcEoM5D0/l8w1Ek0m8D3FKiyZ01bwBGVJaOl6qKzL4XSi9/EmQ3fYw2z0xQjU/3HbhIqSDInTFlT+2gNRK5yLAMrdCgMcXcSlHyEP54ZCY7D0ByUoVVmN97Sbw/32au5JhzXnBNr2ydfiab9OycgP3FnDFPo8RG9sCa4xFKSQQIt1tvFQtfsI90dTAbYrPRdVgKIjAZ2Te3q0sLHtTwNsJg03M83VxiW9vB3DVsztx7BbpLV5OxMNuD9ag+0s0+ZOu6AeCpEX1KrFt4mrvO8MB8uk34mF3a3XXSzeylnVuSAWT6Uvt7B0g2YxtZ4R6vfKUHe0dDzILBMwtAwzfFyN1sBtWlqtbCqsfcNYXDjTD0lJFdApQlDeaQXVey0ViKCiTOiSVjSAg2fSbS+RPKpG+BJ4RW0011bgoH03aycMFfokZ8NY6xqajY4aD4sjQ4H6DMZlF96nQxs6md0MLfughI5yD1gSTPgA2n7lvFstpqM1TLSrPJo1PzpaImOzyqrnk0dVVIcJxbMvuSfmrI5Ad229x2PgrqPCkdar2vDk21rRSCL6szofg5GN1MWh0jlEYZLu93+25SbDLa9JAewVrbge4z3czTkxkRhvky5EtfsaJM95h+3B6KWOiV2BHzOi/pTU2avYhmR9gS6UIJMBhMd3iGuQIB1WZxwzhpsHNeyvgjCY67/RZ5yC+KTj0f3HQ3JAxHzbHLB4fptchMtUS6Mja6m7wsos11vmTmydDVtSUj10AKo/jVbS4lTfSdSjaY932Ta0Z5ul6aXH06JbtnGi6ZzaPCctvi/ZmpHUez+fqcgFlrCPr0xkvPTLM2cspokye6ysLmyd+OjMXcvkoejPQ1go4VdB+ZIzvkNKFxCCY94DyHomcqXcrcS9ClOAMzkTEqlHX+yP9xq1uDqcO7K/OY2+vjtdloO6h1Iy/PSaGQVB+2OImZPp+espTbM8ssk0z1B7GC/YWitgXzJyOMW44/R6Sg/8cdE91lgAKhzpG4YkdWZq6E0WxTEXRwnzwRqdZ7yQNCuXcigmzpSYV0OwF3DXlXfSPfCr6cBlU7IYE7eUNnt4X77E1gPSUb0Tkvaa3A5bHXu2grOayuZvfMsd+5KU6SM3IzyK4su6NaKnmFZmYwO0VHdf/MwDA9sUExO1wMAt9JA9ITz+LSHxDsqw2QJ92savbtEj0TeRzo2sGJHirNhPMI4YRtBJCdSM2kxOZv4QsSxcZfb7+o5IY1r4YUx66vQ2CmPj7OTMcKMyQq3+O7SkPp7BmBo45LZyinOjzPeaUnyZD7jFRgFjH+laFF9K2nhpVHY0etzXJU6Bb+2Y8ByqTvnFalIIHjszLWcTpxDmT/Ad/vd9163HI+pGs1PkMFSOqXhLsgSt9xrIL7ZRJe1wX36oXUMjPF1rTFTbgzmXd6Hv5M8SQwd236BeVk4dEoFXbXXbibmtzrpkGDHkas6XFvX860okxnx87QzDXppbvUzOqRSgdqvginrUVjqQNI2j+73iTcOEdEGrq1g6BqnRzfDBdMl1p9OCKj+yqnVPtrvvxKpuAWkeQELE9qrZjZNDF8vV5SpcttMzORUXVFDld9ahap7afT1Bppm7aZYM+MsvlKkKCFg5OvHdB23zcyuag4+/aorhKu59tb1L1qLvn4swBpIzUBqTfDZla/JFHbqNxM5WLo/QbZUmcYKDpE1Zcq2mXd5fCQJcXksLF6Xylu1D7MkKAsvVLxhrtb1PRX0q0wwG0ohJVJeSaR6j8XovnDZak37xJ1liudlJJJp22IraWmkooqySeDNHZEn72rImUIlKYCNOPIFEGqLTKXofSR7kmFZnhW3G7cGdUNZnJusRd0YldnHkk41MBEhIsZNgA7m0m1oGbOnJfA/8w4NQpvagke2IRh34TSXB/14ZvfyHmdDzPzUKT4altHWitMKAAgj+gRicNaCLsGkoSes+ncMPfzOveK411rq7un571v9+gqfDkPoExXJRFOpEfEbM71iM1Y3DYCynIh9sc6PVwDdty9b5m5eDO5rrT/BZ6FBmBRtbXZeoIU2NySkNZDOBMmPFB3jD8Te2A4mYsMrAJNWxSKc9EYLLj1veppuluiCA+tsTb4VDtWWQizzCSo/0oxVU/HLCZ1Rvnp62u1lWTUlUA5z9l+RaYeeJ4TQglqBEGyOjVHXG3ipudrhOxspHaTO1okVMQo9WWqtSqkYDbz7YQlnWH3DlTRQy7MkKzP93S777RFXVgyuwguEN8REffW5qYoBWwoBxwe8nbBXdtzxBZJ10TcHV623mZTExIGyAioEJ57e2U/JFQ9dmtTuCmFSXEAxIzqku2Bu7mpvVirWC+gGu6rd5jJOKrtlBlqsDjpm/W7gN2eJ/pSO0Ge8pl0VUSePAAyzmBZRmhU9Q4rFf5zxN+Qswt+FJA+kirBrNmZYY7z0ryBjiMWTngszCQ7bJjzcQi6ed3aZeeWccJFMHCwtIfgKF+7zLIOfJQ/MNy3DJ7g2DyJttNjZvk6i8fKHwMzBbXmEeSlkHFNs5EuzlQyyRcKF4kJs5m2//N/9b97CMDRCLotg3VgPnq7ldZgF43BxcFw56JAiQjSMcjXru+mQ9CPu2pV0XMuAiCUELJifPcnK2EeXZfJeaS3mPFINdRQT0eEQsAM8FiVBTfkneTAFYWhXlYx1wDmvD56yqCxR/olmhBNrdqqfp2sqnCLON1XlkJbGtYE/6XwXFik+0otleYYfa9nRNgMJWcCNbsYe4XRIgMub61mRhp2HF0Y2NW6d1dWNoz06TIYMRjAN7j4CdMDRG/CzeWtzX0Fsv/s6nvwKzMJjeXwH0F5nIUUFbGZ8SxK09sRPUDSRIerIF3aLa7uALfNBOYKq8wADWHGalTCzHNH5NS9MvdH5rAzJNqbk6em2NRYzl6BjiKCXL0FZI7fCBUIhZMEKdLrtj1ylIWA3TDw9K5NPNZaUQejPcZR9mo/gIxzhZSm/YZ/pmHsaZk8TDHrgPB/ewSOQyxbNGMRjzABwPiT1TOjpWUPSLapZCvPxPNfgP9MtOrdNTpXHOE7oBgSUxIcdLen45n5owM7VLet22jrgy8Hn3Zfvg4f47DuNF26GsmnCc9QqqBKwPD7fv/w8Yf3+1f1zf30NEZERlXniRn0leYYghBD0qPZI0h012oqeqg0gVnkmgPOwANddkLmrR5Z0721xTbie7UDuSCwri0AihLYOZQCe79uRQVczgwxZo4hJHpWIJK5GYRgn9fHdLnHrBotZjQYWZLFTZHpW5s5upSv6W8knVBhYQ+4p0u3u7e7ItN9KH+10shBMjKna12fTzWmbS3F9HQrKVLZtLLCz5NXD2rx1xIHMn08aKMZEAqHFDEd7uHSDOw9smH9+0JESpxzZBOVuk66kWWXTpijb2uo7DyHxr5pw7rvBk81pIGOIXwQCpARVLJ28IF94SRqCSV/2hgApVNJp6GuzMPW3mzL7A9a3CncVKkN+fp4vV7HI8TSXME4KhPBW/XoBBbO8Wck2dnMd957F3oCNGdj3bM6Ud1mZYRGMF7JjVTZyApBQ7mad56j5EwMSIynL16u4yxoXyZkxXQo1Qt28qjuo6Gn4eKCVuNophdnmUFMhn18vBzLiemcVPO+85IAzfI0p4Q06w4AnskbanS6h3LB9G+lLsj5ch3PkN15Mj3w6As5k0q5CKNs7r0ZnAog4abMN4znpMPOSTNE+pqXRw2kuflq7rEloxaqyK4v0FJAXO74Lax2ndzrisiTKxCYp4zRCQRd4evD/KLB9M9FJ86MJDiCL7pGt644nJk2B6fdxXIzVPOGalRsfKyZryxtYKCvgZhfBtKBTv+MzFeEkhCeASJzNTFmUQ6VPW4GTEQqG0ibeaUWZuGJZ0QdBnNb6g6Feoa47EiphCNS3w5UVgFJnter5i0ZmIzNKuQ9veod4WqAIt1NNyLddUhpbNSMqgqu+MLC3dG3JDhWH0czYLBzkAizyBSsKlGZBpn19KoW+Fy3fHBgWwcbdgR66/Psve4QzAjAFHJsZkBYzM7zcoM/VJmicF1Tdzi87yIxnLpXhzWVZHN3LpMonK2vnylyAkr6LpbLpsSSvaSFD7uuRArL9PZsOOMyHrpaeuruMIaZJqR6ooC4B/AxdvsTUK/59NshqkKisuiewpgLdeK+78JEWLOuXDzmXlX+DDvVTtPlTw4hXN6VAetmT+TJrKYNO0Pm8cuoTYG5325hgnz24UJlGLF/Zthh2rkT0QVxmmYj+Wa5cLVCkCbVXdIrBZRSjTOH7/d7JyuT9/12c7dA2Pu+faFi08X+m7JBgyaq3YPTi5iTTyqGaZO3xLXEiSMyA2bkdadbvaIP6g8Hf3P898c+HH/44eOAx/3kSUn7aUrEVOxJHA0AkqvOYl+T6Ya/fWE4J0Hlc8zHx4catVDZFQvEaRFqhpQSbGALlYiHV6KIqj8BV/Jac4ZARnLYCwzGTHfVXE32U7i3KdrXoeaQmnqoQSLq1YT4+QI40Cw57mw8DDtfp7rM4FSifWKvFrEgGt/oSmez8P4qCzaty9z9/fmJTUaEUkzqlnzXmpD3OKVlRHjHie5ptuhNbY6n+vEqkdb2ENHUkaFHWvWog8hzJMjjsseEedgqu9Dd7/d3YBVo9lgazYxg39J4YcyEy6QqwHVHVsQmAxs3wsB20I4tMGJ74gnqIWxT1sXbcTe+uI0dU0PNZdPPiK+KWBvTarq7nhlhHa8ERjYnrCgLwutlC5jZ7M/qGqDqynCKR5FJzbtfvMRUxLjHLpnNO7II1+R6RaTovMqTntsCUhFP5DmnpyOz2Zx53yvQXxe9qJKZ2UHcZGbEiZmeat3WMJNKR7ehi92mxMRmxgibWp8/yWEv47rnsEW4R6onFpKybIQ92A0m3Pu9Om+PHM55HcN6sO5VrrXClBq0yBSewdnAsq0aqVkmiEg9/aq+t3RDqDHVb5Vd42kiIYmt/Z/+N/97iD7lbOWxNbXCJTfe6EEMae7mplkPa/eZ1TJyXelCWhek40xoBstwOCeiOfZMvlXRoVlj6k9Ef/tO2nLsj9rrrlvzo8cj2A2Dss5NbjSQmBPp4TOamuvqgu1xa30NUBWWp1F7jt+mdwktjlVYNgEjX09DoN5CviHp98856poBoOf18brvT8+MdNmhdXTqOlQqLNEZOMHf/+7jFWmc+vXOnZ65n+XxCiUTnGC4pcMh2YN65OEAU0Pfwb/kbIz7lBwD+hIdEVMd5/WAelSNqWrxZDTxFPV98gxGd9g82vmnwrWHquH0nEyYdd+TH7c+UxowQNSl2ZJO0BjeHvPAlBp/qZLD3D1XyW10RYlsYJ2LeK8uqdRn5jx3g9hXdTxDiSVM+3+PDOwwKYfLR1OLDPhCyCKsZQIwU5BfT4WltFQqI17ndLOnXueHqhvhXZewyDPV7kazqRYU2T2GyddrquBe75tHycDanDY7XEX6J0nX+ZA9qvLiSbH9Ksagrl8srGQOe/L5ilbticSB7leKCN6dZfIWYTi9l6EgIvhQuoynDHzMq1vVK0rMLMJXp6cHPhUeMN9kTGjH7mUB6M4mm4o9lBZ8w3F7FijuWbfaDEG9AJ2cRoM0HQNJBHVkjrpDwj3MoI8Ui1qrBreZqS0dpN3pc86974E5TLteB5IJu9R8gmG4w9dbru8vGFBbSCvZLZ6+9+vx2jzODNtwZnRdteCCOtREeviWSACo6Um5GCmU12SENDkCiqn09MjsWzCEZ1WpclA6BZ7xfFzy8miul0ah6A5zmZ1m2ZbMAHfsnbC0ODnvTtdnpTpoLUclrjT8K9JI6d62CKWwLDwFkIHgfV93Y2lO5L5dtSeipDzMEeSYgcMeKhigB8ROMJctYLE61/TU9RZgZ2fnXjMLpQ/lsCfhAClbdnUZgViJGKQ6XF5Aoa+W56gyMPepitdRwxIRd95qTYTpw/zz3ljNbu9AF6WLqA5yF0/sYbeuyDQ2yXZAeOzJ7Cl4O/i7H/Ll+Bd//D17YuLnv/78/S/fv/16v31/v3+9cT4cVhya5e9eFvjDv/zDx+/TfkgMLZ13EGYOpT5DwzFAAtJ4qeXJVCW1hwohAuaBwSyq57xO3TKaqCe2Mq3MNg5oIhRUslrp8MATqbjdW7iZve99vV679s3qXtdJwXFD3evh/S7PNMPJ7J7VI0kTqZR8qYxsyKWw+FXle9x7dZ1gy56ZmfDQNsZGwRLAeb027XXpaHh41Uh5mxGliFmiSbLz4wCjOn6Guum+f74z023tEe/3+5wEvG8phTEi8BDabsaxFeVNqxiCg2zA+fioLWCl3dRTI/WhrqtnpAGWfyG62tx3IqNqiIypVikOICOobFG3yLifbx36Ul0DsC9z/k5e8md0K3BHUfjmO4Jccz81ew42ZrDwlqkbeC5aDYG4ixhHGLj6IkzmqSoAnk4u+qyug6zz8epbomqmNyjJYHSa2TmvHd63MP3O/IFZmMM0E2l7EXOTMUHmQayqzTJzpvJ1+pZlDiYjmgsWLSkrPUbJNFPnvATL+M5BAcWcY8IcYdXtEr8+PbHB4Kh3qUXwsKrmsOeZhVUy0ONkVu3QZE1f6drxSkCYhnRG9Gb4mJhOdfFmLuqieyJIQ2bc91tCzenpacAyV1H6VTTABC0OYQtjmqdKwqdvnmUFmmSOuuaaCFOovQU090I3icQG2nUIURno7nj4E/3QSM1Xc36hrrEeggVw9gduEQKzGZ447/6ui0mNPwDuVbUXj6b2SGP3iIUmnom+4nN8B5/qsHPDeHhPrY942jLCrRpuds7rERr5THPG88gm5W59S6OgwuzzviMVfuW6sf2RetmOdUVY1C1ICzw0kD0I51Tm4fIh3ixaBef3r9fvI5327Z9+tfbPX+4vf/3+y8/vKvv+aT/9XOpaeub1+9/hp+r6PP/Dt9/98fzdv/nb+OD5w4k00k5E4Wvy586MpfNJtVsBInTczM5CUsXRM+HAQPMuIIPFjAGL5g3h1k/Q+bDMPDyUcV9S/ZpV39d5vatC5nsx7A/gHmYcNUw+a/HbljbzNdMAP14f73sfEnLM0ZckI1yqPhjCj9K35dd/37eWODZDUCm/8PWy+VVwk0lHskDwnquDO6Xlqqk9UwOwR9ngxzB5omrxuxXAkxmHYFepWL635pnlbbqPVMFE2BORL929ue1U4Yj95ySn4xz9+e6SHMCfrbSkejrk/EyJhRRH6IJjNqa4JyKVE6nll3l2ni2HWHP4o0w1caYCdxbaXTBB5ZRGFmnnmRIWYUKEKHwvMsKNMGovhintYIUJqq9b00xvRsIwIv+eAT7W9JCyBQhXutSwYToVbVvJlZaclsYG1mh7qG9BM8oqsVWyql700eZ053RPP1M8l5TeStyUYrLjiwVOqP7YgWjYaQDuprVND5B5TleDpizR1qw0kEA+9L6MaSqy+MCyPaOaZpoyKjQYmYS5Izx6erg1St+SrQtuGfm+1+jzFGcKF6ir4OsiQdMgW05rPo9No5XDH2FqXXoyX91XPb1GamM6bYXnwn44M6pYpV/UbB13p6EluTOnWnJCETqCRITp/9aE6r6N0C7SEqwuxVENerozfTARKd+HmFizZ52UpnoJ2htpQtQ9bOICCMMO1OZSOjA2GhxxDaEBuWZmvJ8VJ7Z7gKax08OVU6DLvzDHEzOKm8o0xN5DbuZQ9px4JS0z62577j9Xw20TMEVgchivQzZivPoPP/zwx9eHV//y47dvP75//tO3+/b3nb/+5dc//t3fXsbP7/j85c6MO/7FR97b9ztxv+ef3n/+p8/f/zF++Jv823/9+/OHPB9mgv4dNiuBTU/C7uf7RLplTfsSM/GAcPSIE4knCleT2nQ3iEd1M/pIZKV7Ueh8VZtTGrvVCTHU5ktykJEzI5IjT3Zdj4wMwWgwYYkr1jal2cynR1a3wWc6njjlnpUwTXd8pEnlpFlvsVKculVDWLtbb3llX2qNZWs4bGQmZ+eLqX6e1dTM6/X6/P6Z5+iEEzfgkSs34GS+ONMzRizQCM1IgQZz65rRQa8WRMmpQvw5UzWbhGxyz+2kT8ntXQi34T4Wv9d56RCrW+bZt/TVMV4sE3wUjxh6JQY2Vakuk5K66FxaXhgWV05vRTrP/Ma9Z/Qd3pZqKc6TqeVB1ynRGbkg8haoYx5T3bXR2dDAKArI2UlqOl5VOAOMSFkQDIijzpIKAVIryWX+NRmUBqu+X3eqR8jV5i7t+AxpDkcupKNmf55BuNxaWEON3JZ+lKhGJhKlrkXmTEfivhESehi66WKY2dJ6uUdVKV1abGVo6Agh7GsI1sx0nOTMvSU1Tfecs8qrTG9RtblSdA+/1eDk69X3cugZEv+o5c1M0N7v626wiTzTdfJosjG4ybhmNk03a3ZE2rRo4V2cF12XG6kpBjRa1L8ah2UFbWEyCWCeWVoDct+KwivIL/nN2irHZPfQ+g75wpQXBp8ehEUke8bWZ640oc2Q4o6WxY6vkvthC22P7HrbyhjWMm6AGeS1q2Zm9FRGdkm9yxBwYcxQhgy6BgQCRusaOwFlhS6qZQSUNiEMVj+hOEdzR4kHLmNkTI2ZDKnycy7lILkRNWVps336w+z3Hz98+Auf+PFP3//8T99+/Mv3X3+pwfnpp+89uFFl9mv5L5/9/ryvxPf68Q9//Nufv5WP+Xv++su3P/x6zp8///SPP3388fzr/+jv//C3P8QPSYccXpSnlfBMjl2WgN9zTvdknrrvPRFmzNwzwJlqzxBlGSc4FJYKhwTi5+ROFDpRtyisJKK7wqPZDkPAxpTTSxmwp0yzwodb7Wa+328TxAl6WD3y7Rou6LlNK3Sab0jDLc+I3OND1YVSQELYqxSHEDFAkBahKAiD54kdn7QTGqxnjP6lBZRbVadsRL4/3ymcAWYW3aUL358kBomOdL5Ot6vo2yHAJJE7tESdR2QqLVFbLKCdSdpOQwttmVQ8tRIdVAAJY5SFflaRJTXzdGdmr2YDT3OwBGG6UwF/ccbQVeeE5+u+36Zwm0zlkmcegAingRvzp7iFnS3u4SiQ23/LeDgzcdx3wqBKvYTaCGOelPBBnOlXDj7czB27+Gy6jRahJmlJAhd7gAHHPF3+Ytfla8340v9M0yJnii75nCi9BZCrZGkyDs/HqSr2o9dalYqiWzdQ2uDVcz5e9/0p6k7vv+vmed266UHhfWzzHTvo5lBjB8z0OR+aLSgRTDi6OTPn4yg2K0yaRifx2CAejt58jY2SbJLnHFVsCAFpYoDZUzBmrOokMm7dZe1DMDWmLoXDDz3AoaULd330bmpuLSmWFaZymP1MKpntgLhmPK1m7+o8AcW0RnRv1ea2U/GwojfCEWZ0jSCnI0ZEFiE93sNKWlUpMXUpVh0cVCIxQSqLH7aDcvLEzAwRqY6p3azfFceJ1TPIWyAZfj9a3Vy9nHaSd42HzVyO0TT+1xR6ka9z32/9/owzvY5WbngM0TuWk0N/JKFCnEF74gpcEq7fvfIPGT9M9s/3xx8///yn7//0528//VJ3ePt+trP57cfv5vkmfv2sd+GS4/z8688G+/759sFJ80//5Vu9vpn/+f3rn/7xj3+b/+o//rvf//3vXz8kzhNqgfUZ+NjJc+8dG7hVvfdKlZqG/GJ4dFp5eL1v7Ng/c/dmO7RPZOfpJ2lLkZnjEWjG61X9PmuyHU+7t8ws80HnuwFTNLF5QnOhq1953INgrAFnJ0I7TMaFR00AA+recJ9hhEvA40aYkyoXTFqmlUlMZYZ00z1jzjipVvXe4kx+5OIVAAaf7/fuN2zEEyCxr8lvQdq9pW5JAgexF90NeSQx0uSRuFXYAZMeGfdetzVFzOwoAqlrvtjX4VgDplqy3d08NicVAJAn+nae17sKO9p7XJWsxCoynHMs4v2+EQrIbA/JqXXgAh4RFhnKvxMFSE4c5dY1SEMvumqQCFUee7LdoonwAzAzpHu2DdrnQyHbUx06h+lnAUkQoLnkXqNPSNoQFum2YwAEznE2EhWtsUoYPqIuHTE+ZCsUIE/2Z8GdbAunnDd9ZRRfnYyDGCkm4pUeNvNoBZ8U4/v5aW4WZh59K9zhqeEQBKY68ihyT12oIBMZtsMCMl9wS+hHwbj6KDOp70PRzRu0a4+3eg3u4GiQ/czM3qbTIhIjou7VN9LwYOvpHg/LyNruijMzYLpPaZD1RtJqnvG9na8UGmuwxDMEzAIaGbMsE5fuQHMeeAc2Oxhg4xBsnTKwe29mbI3imGZgdaz7ygwnc8i55Zlu1n05REDof82ck7ID1H3rD3dVnqOtpT2m8icEOulcCOGSrs+p+uWRQogMozQ5Q8RAJilptdTEqM/IyCGH9YqsWwZTPvODk1JWdZLnZFXliamBwgu74bDwqXq9Xl1iApuccL7w+rDz/cfPn/7p57/89f2Pf/7+5x/fnzDLvMVLu+/L7o/fe9MaQPqdqe/vc87JvErCIF7tFq/vxfr8/tNf6vd/7r/+6Z/+1X/4/T/4t//i9//w+vh4lYbpuuLn2GjT4CPplKFcnetq23dsgykymhyRFurD6n0tTMGszhuRpP2m3SUjz3THOdWXg+IAK//4Grajw3QlFrZarRGLAklJh6ZMc4UXO4heyhHmPrMZTYY1kfQzuGaAE/auCdthqt0jL2Ds7e4c65nEjnhdc4DozZXchadNuQR52pNxUlYGIQYCvJQhcE4uJWjKnkxy1uYqGaYx3W1tOL667CdeCT2mGQmmS6scGfnk26xgzIcbG7HYNL3eZebv91vtlBp3zjOcQ1nuy/LNeb10fZ7Xed+3Aa/Xq+pa+ExFfAjI9vBWQ7NYRHS9I1L4ADZskR6xMUQuZ77Rpt/NpIVhzN1mE1Za0LzYQXdRUg5lfSvRJFPHsAZXyY0Ey4FNTfdM8/PbJ4tdXe8KDwLh7pk//P4jTkQ61nTJzOSOT4Q5bDwzu8oImALNNvFQUhA43VOTWJT+Im8IKTWUP2q3rT65ZCQA6Ollnqrrkd31ZY3Sn+/SrBFFr+P1er0/PzOiqg2WmZ+fnzZlYUoYlTu+ah3I4bHd0m+CY9S886SwPhkIPKLeBUI98escTTDUX5HP3aZAuqGq+65aXp27wfXSU2MUBQHVtL6SL6a5sjUxgcJk9VOq+pzQvyTHKILRZNCHpsdLIUAAbOkXRQQF0DPhUknP7AgFU844232nns7M6/XSwx0Ma+P0BN7JA69bs2cUJxBh3BZb0ul+VHQkOj1I3M96fRxSarEd5aqeRiA4zIl22/loMxVrhd2yrm/lOZDghG1uOytQZF0vHmcOd5zIY/b+y68//vvPv/z5/eP39/f3FOJdrNvx+qFIWtLxvYaGSxAUfnrf7z+eE+e8f32bnV/vO8d/eH20RTf5jb9+/3x/mx//8Zf/+L/4h7/7D/8YfzRzi+NTZc+QqYaEm3AJAyIAnhNdGyIvDsqeUDBhPDoJsepDwfqn7nV3rkZ7AL/3xo4Vw3ZpmozKNvdwq2dADccI6sxVKx6RfUsD6SR9QVjfNpdqxch5nY9b79rSiRx4uuJGQN4uzckSyWlPwp2mt+uTSHj7fr8jo96VGc12i8cTy3XK7MA7hsZsAdXtYxYOs6mycA2EkH0sT5CboyBtVWS+Pz/zJLvhhi9f+sqHddYOzATgQjSgvpTiLmbG3oCdPPd9PYKDyK8xs+PhU+URYla3AtN3FggWoVpVVBqU6T3Ur9OmNDcbWBg5DqcZfQcLC6LJk/Wuh1CBUEQV++pa3JTb7CNae8Y32APpITBuNrvJPFH3Cn83Dy7ukXodxrqf9f7sX3/6dn/9/Py1Pn+5n9+rP6ff1Xdzt/KkHfvd33787l/87o//8m9++JvfvV4f8TqY7XrP63RfC+8puE8VzFWJPvW1m/nm2UN4ziJ7de95nfu+fCY77UYeAq3WR5G9zc30hsm8TUjaTGp+AUkLT/d76/1+m9stBaDFvSU/HQhGg0oceStk1BHVDV3HM4CFp3wPUx0ZQwTXMXfOEQUrLnI4rDGDr3ja5bGYoUaWanK1DnQFF6qccvd82v+n5REmLq0u4HCzWSxySwzutEzBtV/RUd2mmVZSsQICsyRX8Aeb4857Iqc1Gl7CLCX2afFpG/MhdTkUaCNlugElk6oIH1uvL4AwF5o03XlCevMRjxRGelVH5OucVaropmU7jkSHAEENmRl4boSTWbg3a09GIM+5VeFOlBl62nxHz1OS+cGgMx097jHv+/nz+y8/ff+nf/x2X9ZmRX6+G+ewcW+/3w1r9c8yIcq7RrPv7/crjp+83RHnzT7Z9CiJmIkfv/H75/31+7//z9/vv/9P/y5/n7caxCtsxi085rmKbUW7kQdskT8cNlrkLWBDbmaRhsRm2lpY5963rmqSElOG2wzMHd0W8SVk7OmIlOsRZl2d55D/zIdhcFkfw9/vinC49e0wnFdKVCfIfOa7bAsg3QEHB8OGOTCv87vv728OMfTGmdf5qC7OQElUGfW+uoGGtHCCXeMHSwOrlZmJ86r7CdpYKzh2b/QARiwLZqRZl//lSW5Ba6l31ckD0CN1/Qg012PxzOlCuP2zdCn3ADYRKMLXwr0EkgN4nVPTQqUFd5zXeXz84hUAIGwv7+nxCN0cM8M0Phr0DL/VSnjO1+m+kYetfCsaLEIdrWnSnNL5q8odIxW4+EYzmFzobWZVncsOMjJpxqnIY+7V/Xpld2mPd/XrI0SvTOPbT5+//vWXz59//fWvn99+vr/85fN+u9+/DYt1ZwoAWqpZt48fzvf7mS8/f4iPP8S//E/+1b/8D//hj3//h3Ms0jkyZnu3UvYmIuoqjWPxRj9nOB45s+9r4TVWntw+yEz1cZzTt2AmxcGM7/HtSlWLqgLU3tgYQe9WFqqrttBBKhxb1Zi7d5Wwd3PLyPt+50kOTTeHO3d4Tgh5VwVDYmY0FUx7UYHqmgo5HE3lyxO3GgP+5nmgrTzQ1J1kHk2SsLPpgPbf/lf/W0m5yVb1mhlqelQCg79lHvgjjLPNxRYyLtZibGkhrObUfNEuEBjATsSQtMESDXD3TImxdCOpRHX2BuzoRCa5ljo5yMLimSyqEXWcUUdiGxYvGlY3zDO4Uf/HLNwijyggDWg+j1XNHAprfVIROGwXOlYNw3npP4R6cGFIMMZCZ2LORw54oBP8oWjf5/Pn+fGn/umn/unzfja+3f7TX783vGE17BpirmR24XCvagm9HXjlx2r+6UBlxux4WgTwN7/78L4/5PzN38Uf/9XHv/nP/uEP//KH80M0y3bUDNyQGX1vRgr9t3B1A1UdbhZK9jFJv7ClCD0cbqyKeNFGilE8Fo2ZeZ1Xs42oW0rrNRhCWkAHW4E/04wwuffPRz5hVVJza8HO6+OljyTIaGY07SB38MQ28ZL6fWEI6t4y895KT9UKnImzA7zU8cyg6hpkbme4D6nJE+bW3XlOV4WHDBbqBKfr2QoA6O51JUbcHA4zZVwumbeacYq3NJaUP5q0MwrMcIlhdmgU0JipzNet9/n4ECLcVeEJM4VlreZ9WLfO6wzbBpIqATznJWdDPCOCtyyD1b2uy96CLD7xcKr8LLzrgrqwJQMDwNBEWPGC8K1v5AhZyZPDyB6PJwC8R8IBkzhSiUwuNLgJCw+Dg7yf7+8//vrrj99/+tOv33/8/tOfv91v8/nJ+kQ33u85kdOKocT7e79++OipyxmS3fnyPPz4m9fv/u78m//Zv/mX//Yf/vB3P2QaMA6C5RGiBqZI9j4Tw4y0NyGSSUSombnSD0MoXKia5HBqYBaZskewN35Ygq6tgTbn2N2ip43M1+kmuy3dYFXvuaSDOzxj7MszDgi4kpD0kWK6BsKY20yf1wHQNUNqFNr788IsFROLfUpUAr/EETr8VRBoGqDxocciLKTWpdkMApRviI+bipGhBE1RQzo+xEtI8OwRUy0/zhMSyefyZMsD4dL4gjQjPLL7Zp5hE5imKovX63RpZoI/LNFOso/8zaWlAoUbGa0iHUIAVBhqv/W0nBkzzNwAMsOEe93OV5AzmLAYkl2AtpllpjqBRwzzG1O989Z8cy8239i9q14fr02v7M5cW6mbYKgAp+/79crTrB8/59eh/y7D4gDf7v3en/ea2bumJB/zIJ3IHgwYM+GZfrghBWUehEdG3e4azJi5lIYD/uGH393v3376K3/59u3nv/4P/8F/8jf/0X/x96/fHzvKRKJ8eRoINxiH/7MUPzkqlATAjOwayWf3xoU1TOMWdLlSiSAeGVFd4MQ5ifTNU2tF9b7f7wdHNnd/IlB4bxkgk51ipsLj/a4dkn5LYE6Ee0QVxxD6iDt1UtFgq9NRqdOqzZ8chQZQm6Oiuwo74yS0yAWk9G1bW0lguH7j7pPHbOd9yryqE5w9FtF93cL2cFEFDZMDSMN4RzorEwlJZTS2BvdA88rcrG69Pn4YtFvA4Z46iVSMC6jFrDFVbYTGCC8JPOvzqi5zmGdXxaoDbbNghHKSmJvnjNUjhIfB6v2ptvWRL+25VNXiJNWaZPqQEQ60wZVWv/cN9yjkqkY0glExnkuTRrxomOb7l/dP//jXn//xp+8//vr50/z6c31+9ref3vAcepyPW7d98cPS4NnXx8/fPzUK1N0zzjQ+b3379v7rn79/+5m//vz5b//n/+Yf/vXf+DFHjxxEjrkLzPZ80VbIPMSEx+zUIKyUewcUyuiQyhOW/qWrFWIohmd6aA13m2Xo14UTUHKcAH1RaJnZTXdk5uU1g8CELk1Vc73WkE/nsaDbyrdC1A45EWndBO77HeGmHu58UEOjSQ+/75v5Gnae09McZuS6HR+kFzsXYuguiYr6Qpejxk+wn6FloBnZtDQZu6tLXTZk9nu0ob7zS0W29IbSELYxT0628CnFYE21xsI8hT+AL1GpD0cqsLrX0x9GWnS0/Wa9UZCpme+JMG6AW1W/Xkf9S3eHp+CNfKVuEbeIyFuVAo7cVC4pinnN2Q7t4vsud08BgkYR/Ga+g0whd4jbmG5KXdph1jNORsSxcFohv3/eb+/PHn77rJl5T3+7dWkDdNHdjb76kzW7zclTfQ1wICK7h45bBXKad+qVrx/yhbk182u9P0564H67f/7+/unP//Su/tf/8R//5l/80T5cYw4xBK1m3CxPajCvYUd8eJo2r4ZV6xVb+DQb4xqT6Up2zPu+melmt8rcPHKqPPJWxQZRmBKZILf92VhaD6giBilYX3V0Te3KeZIy95iePq9z79uecWPdJZtbRNSt8/FiS7gigZu8ovNFISink+aRMW9xj3ISod7v8/qoeze2j5yqfJ2Ak6X7RffBLs6mBtPN+H7S1pys1B0g3naGXX1eL6mGNaBDoqlwS8thy22nCRn6+U5qOK16Xp39cY6eRoSzOdXinEGYx7AwX4FXYhJcVLWZm1uQTVb1xw8f931Fomtuosqm8MiTVVct+9eQnPlyNjTp0J+cbiOhMVkGRiyrBCpaURN4zJCZda/SXlFkz89//fbTn3/+8d/95Zd/+vWXv7zvJxzZjWovHthp2q/fi+PVfbuMuDUR4YG32eetjx/O95ofwqXkJs0r/vqP96ef/933b5/8X/5nf/8f/EFDQLoa4Bh8m0FJiEb3Ohu9xlQqXwtONYUe5p6agKK+4XVOlTiAx/gima/b1cxnjjJAsTL8gmRLQ3O/73d4yGpq68scjWEx+mPNkSTfqgilvFXZk2sjHzKNCpc9eTYeLk7XNYPGk+lg1Hybz+/fU5lCbJhhFFuCLWg1spfrKWNNdovlfWaKAnxmlppBVPL+VjPxtKvXth3iPNOR0fq/1fZ1KLumupsmqIGs6pCNxRRRG9NXo+Hr3fpsQkg3HJEQKcsezHrTjxKYJdFb4Yf1jAMfr9M9nrEyBiPbVgcmuwzQ1RmpsU2EVY2H5WuH5iiglbCvEVTVncLapgF8vD6q3mamCcMwmkNo0gynx4+7wWgokvj2U33/a/36bT7f82779XN+/uyfv9UVnNOTx0k76YRXy5tF3Sa0sYF7ZK6mse6NyE8UBjX9yXdwgv6+YyfCHeZD+/Hnz//n/+Mf//yXX//tv/38N//ZP7x+ZzXTNee4XHG3W36e963zSrQg1yEZQaQrb0MPgZyadnN3r6phn4+P6X7XejiGU90Be1o0mEezxc9neN3K3Pk/3HzdZJcDIw+9IUNRChsMA2B64pXTg7H8eIGsuniEOuqp6yop1qdbMXBhvh7B8LnPnFEZNU5KFponu+q8Ph4U++Y5vNfM2YOdq94PZuiq2UH3zHq/1zgiSM7cnqCezNhJAK9Xd2VkcWSRA3BS4nGr6hMpln260wNwUbY6JkypJrCR8XgoADoiFnmEa1IjHhGnS9c3mJnXx0tz2Lefht37PucoVXe6z+vF1pxYf98yo0olkk9sJMzgMJyc2f1CkC3Z2JrwpS6HcvQi4Ox7Tx5yXueFwXy/3/7y7Zc///KP/+7Hn/7060//+Hk/+e3n+/rhd6q8vt97JfJC/PzLZ3qyaU43DoKwgL27BvjllzdgOI5bS0/66+e/fPcP/L/vPzbiv8z/xd//qxcC1I6WBCATBBuKCxSTAeEZj5XjvmslgiAg7wIEwN5qkf9KuViZpox+bnC/VSL/q+90L0gNWJiaDA0IqGoP3wSkHtewnZ7VyKyAwltU0Ek8KcEaLV51M6KrGDoQDcYx0xwO3djnKA5o4CYpP5rCSAfja40zjBzjI2rTzNOf8eVYJGALEO54mmlpLofm9n7flPY/w02WWohCiHDsIKQxRyBmNrBinRekksG7b2yE7KxjwHBSidOmYFWsXUESbH0q657MeN93mHv6yajaXGI5A6taaL8o/q/JR1JnY8YjwqOnzitlRqfZ9BRhhvCQZnYkKlNojGa7KeaMz6gvc6NrD7i5ramPGmnNHvN4efSv9cufP3/58f3L9/vtzc9mMb4N3mNtNpfdBOdkAhMKEiEUPZbmAHqKQE+d8OnyKSWwDIzDd11v5Edo4Bsueyo8PV4//uVbf37/+X/8hWP/5j/929ffnMbdUEcy5KBRsJ/5YIzwWDGPGvwh3IKbN4uIlFO3u8e7lQsGKH4146hBFjrcUyAjTvfbJUwTpp+heTKyoYLm7tMj9YGYCU6f8+oePHv0nFfd0oKJ3wY+OxXbcHe0aYS3Ih0AEP2+wp3stzGHsCeZHdTkmQlPNw317iGnpUdakjOfoccW65/0EPSPiNNTcXJ6NAxDWguFqTrWXz21VAcyZ0ivjKSxqyPTTSbEMfj7852v1FpdZZFjSgJTF9asrBhdujIHGW3D/7gGFSk1N1kMGLR7CiBVCoVuTequMxr86XK2IfYIRWZ9DVaTFlDuhIjsfrt7T5nBLVcMQ7gfQ8yd97331++//umXv/wPf/31p/nT//Trr9/7+y+DSPv43Weh+8KsWkLS+P755tj39/eMuN/v7//w4eG3+/NddxjmNPoQNYEAGK8TJ21iGu8f+//73/2Pf/Mv/u6H3/3rH/4ITT/vGfLJ0F7UO9gdkXBBCBtwqW6+H3Ov9NCv10vxy+7Rw7oF8672kC3FqkpeGUnItGBUiCgj3WxTkLu3u+2uL62/S1UIlfY+lDDUAe8qj+SM0CR3C83yNRuOjd+WySbgeH+XJbglI2ZPxGtmiAlPA0sTFJ5pw6qAYageTfyUt9un6eZjMzUZPsPcBCHPrQf9VossnTvJIEhQXjAJPEC6mZqaVWc23dYDs1Kf344XrqHDQGP1SFiGd3nG9pa2aIBsHFS34eGmaBJV2noNHTrNiNv39UoOMYjQXCpuyDCh+FxtS30FLWrOyHspYo1mchiQwEA/HOSwgwtGdZU6BgRf+ZKKVMacGTLs/Tm/fOO//8u3v/70vd3hmf5xyz/nrQ1zwh1ODO+0+/l4oee80oBIM2SA1WCXZabjug34igSdGzg8t+/HeQ3vr5+XkqYxAh918fPP+L//3/5/v/7863/6X/7rv/37391+u0Vkvt9vxagReL/fspJ3M9LMjU336J54RQtDJYSH7gRXLFBkjwKUqxY2naCeLp343jjgWnCrkQobx4wBOyws0pVKxCHWPkLPlWC935+R67kbpwwtJPKE8kJ04Xc3bX0hOsskpsCs+ZmapODQpBHDZOj8dRk13TyOWA26PUKghw9SYe6y4XSbb9iOua8YVOswDiDgnTadmaMxbbqEBh6u6tsgnQQI1PtG5I6cIz0CCqE0WSjWW27r2lNos/fQYAPYrBZudDPIZsb+eP3QfdPzXZ9uFu6Cq/V0FCrmEUqBVYrXMrlSIj0Ui9TxPZMezYJ7czQ+Cjs/HSDOsV/+9Mv3H7//+pdf3z9+/vin799/7e/f7U9/nW93buHAAna/v4UT/u73P/Twp5+/EXbf7xMG4+uHc16vd9X9vK3TFhzOx+awhkWY+62CRxXenPef6v/z3/0Pf/8Pv/8Pfvc3sSKyCQ8NS7Hn9GuOcWM59JyF8t3PK510nJxqNVhqxbXq4pUKbtsXa+LbydnMc8kZu0qzVs23NK9uD6/bke4WWB2eXLS5H3JaHhI34ecuVU+4nXOqVay0R8wwj5O87xKDnJkjoWq38p3uXfUwjCXXYYRU0ZJK9ozTXANrb6cptyt33KvSiByUbtp1V6T7E28gTaTKJNtFCSycJv7HXTm3ihKqUoyRngg3YLJXduJKfQmp+LuKsFt3ew4FKpGQuBirRxrNpVLJaaguTRiWBCLMu3WNgdQgb7i5WPuTQU5rK2oP6rBz8+O22SlYWm/zjRcbW52WG3s8aDAn6PbEDJPLPMOG09Xf+/vP9f3XvoU3pljFn3rH+sDHlS0a4ZefAxpHs3kMxpnI+Mhz3wXXrNc0omoi7cMSbuy699JoxpPnPRoZbwPA4Mzw05/f/7//rz/95cdf/1f/6//8j3+b/spbJf/9vZ/ndSzSRdnDydYZNKQSljxM3JQoiuqSHcTDq8o0a0SAQF3PHEICrXgJqZNsVMy/C9ixCE1fmhqFz3fxyf+yqVbmgsE+v39mpofrjH59fGhWn+oJNe/Kju8ZH3CYGu8zkyfrXoN8i2aEOYbjApkAmqs4CHeipY+C7GA7Dcq6exYcl1bSNRXMlydTOgLkJxqbsOAT465VVF0WPjMozchlpDT6OwjaFP2mfnJGDaXskP2YOeTo9vCMIJxgRs5uKJVImqER5DVPoM3gY61kgv1TUNrjTD+OMcZC+ZoAml23oVDxxMN8R57uMUCjvhw2RgLhsVpeN9bcX3799tef//Lvfvz+U/3608X4t09+/5x//++//VL8XozIH3730V0a7vL7P7z8xLefv39+1tR48Ic//CHTDfZ57zSG7P58ffzeTAhXs2+8fsArPt/vcP/552/MoPnnxf/073/87//7f/zd3+Tf/sPvADuRPb0hV8bXK79/vt1TgZe9TafYe3u9zsxwbDa9Z0cry9f18XEk/cj0e8s9qt+Wx9hP2C5JsNfHPs81P03jJibsdSnR/ZKmpighBd7Fye6ZkaZ2VXY9E25X4CSZkdIcaBRBRs709CgyFQvkmLSo99br47QGWj9sga5CGjiwAH7j9OVp3tMLZnZeR2Vv/P+Z+rMmSZIrSxO8G7Msqmrm7hEIZGYtVN3zNP//L/RrD1HXVNd0VQHIwhKbr2ammwjzXebhsnoWZYIoADjczVVFmO9yznfku+sHKANshmQge+UID+vpo0FiRgKHsG6EWfhnFp3TYP6E6bBrxyCho3uCBwMp/27ARMKcf08eciI0dxswZ8gWlQhZCCOx5t8BQZjmoDGT+T5iy1bdctQ8ckIQwcNTHGOaCXi5Y8JHjnNAOBFCoJumVTXDkMf8miidPhkYDYRcuBRu16bduVIpIqUEoFrsXc21q3Z1Gzt5I8Iy1YxhIQQhnGuhiEmYBbnkSluZsUwUYB4aoMKY/IxUr2ZmwLyWwWmIaN6ZGYNv1/j9H9f/8//4b59//dp2YykekFL9FOka5JzDIuVVlu4hywIqh5ujgiaOgAjPxY+U9BkHM0kRhBChiCDI1CSnrPohSi25YU5SrqnbADVj5FcAUUQAgktKftBUay3wCPBLB6l7BISICLMUzlxycws3IipVACB9AGrqEWqpC7QHKBvTBQoAkrruAUEcw9v00wNCaz1nKcKckJKAUM1E9bzW8i0md3dToCxQ2NwjHAmZMSlYYd/LCPgOoM3DJx4/DA1jKAMAD7LYSJuqpWYdR8Q5gkv/dvJ5AFIai/Ggs+AIEMeHvywpICnEsseOMYadGDEcCKDWCZKmDijMBMBDgxGqHQmRpbujkKP3rmM4gTLXWa/b118+f/zLx9/+66ff/vz26e+3+zle3vrLzb9e7K25EUmRdx+OZZYgAMblNM+HqbWOUqQURJzXWarM88SFrZmwSCnztAqVSaZaajhIXYIx5QPNHAsDclOf5rW3+Pkfv7+83NSCeARmlVI8IszVUvGZdCAbZleCxz4805VH9lmpBRDNgwjcIxnXWc7n2IJITDXnzBHhZqppYh5kaZZBRk/dGkQAgGoHRIQQYWYeKi1IBwOkW4KyABz3MwBA730Mf5D8ATzOCZJ2zV5hNMfqhJicx2zLTC0P8DSZJ5z/YVUhbeoBj6irnPyG55uehykJpVYk1a8pDUQcD+sY0Q4FYYZiZ8JfmkqIBtz13zrEUeww5zA3L8I0pOWYAALV9XG8ZvjJeN3SFexueUykRgMJAhwZbNxzkHegj9BUVFdkzKxOHFOd1P2M0HaAMbmmLMvGsCCRAf5wEpCajd4aB0DGw4lwqjVnpm4upSIgZWtjlhRCqiRFMmikSsEEjgMIEzOXqQiP+VUVzp9TmAiDwcM7EdRKQN77vrdGgvNaEKEIQOgy1VokvG37zdyc1EKnRebCBIQYBnY6rrVU1HL53P+f/8/vv/zly/WlCRcpUwD1pOABZTUpzGmETrk3MyZ0JEXH6eEy6zl/B8ynEzizFbN+d88CPGsZRMrM9AhLHKawUMqrgXjE0xshBWBOkZgZaZxo8HB7pCVF1YQlHNwtP/9xqgrl6aYJd8wkeA8azhQASI4IBEYAtN4cfG+bPbal+X9mBjg8xqUWYUECDxuBg2YBYRlcM7KIIWfCTEKUraR+T6TJ+Rci/VtKcULTADhT0jCxa4l91wxKy0qNUgOHrK17RBbg4d56z79yby3nuek1Q8IUfKppOh/BXUrpbWckzy4H06Y3enFiRMYkeAWAWR9PfO5QRlDrd91dEjiCmEm4TrVyKc7tfP/y999+/W+//PL//fvHv3z9/PPty2+3662fN711vG7xctf71pniwx8Oyypmqqr5GBPRNNepllJJJloO07RUKuRm62Ey75NwERbEuUghrKXUuUJArojuzSwQkIV5EnGNb7++vXy66K45GIjIhhgSjipSxugcMQVmeaxHBLFkHlFEHvepjI+cfeWvwtwpRvKJKetCgEhlAQvXUpKRng5Bf1jTTQ2JIaBIJYgINLPIKbE7YGZe5pwKcoFFj2XMWNQiiRTtmkMpd7NH6TxM12P+EB6w9900t1kOSK6mXb/PXcy8p44OgRiZ8FGJ543hwZn5jZjFNpdHhjWAuaezJk9HyPLAI3+IVDlld5OL2UjLNFL+ZPawUOPI701ROUhqHGG4eSOAElcLMX7uGMPWLFrzN837083NsjaF72qQGH/5RMKlhCsXMjjypBBrraMBBBQWAEwIXnfLp8Qj0rCXdwEkocXTFIZ5ZBOTumnuyB7fmweaxb4pkqh5GCxLFWHyYKK1rsJFmMFBiIUFIAhBCKZpyvQDhKiFy8SE4K7LWue5kkCmpUvhWtnD2r6Z93Wda5Vuum1bKSUMMKBOsq4VEc30ddtE6jJNBebLF/3Lf/n1X//L369fb2AhwgQUQKYqIkjYrWOGMzhk4gIOwrs/XAHIxMJFpHry+tNtxI9Aqxw+Z0nyYLDkZc8iaQvIr3aqIxKAJR0UKSql/PpyYqGtIyI+EDQ5Ucleh4j7rhHBpWjT1GtlDeHp7stVUwACsFBL3DENhauQZD9Kj1FMdrHglm77RCJnXFFO5ISFAEREWyfk3ASq9kyRc42BlCUmzmEpZFMLjKqaZd0jxQlTnuQRpk5EzJLjw8fTFA9D2QjVya1LKqQSTcySCdx5oCNR3rWCiCwU6bdASi68mT7QS+bu2vsYOakHZMJSJiLkUJMytBccgIkKq1vmsLp2BppqQdPbl7cvf/7083/+9fc/f7t+tstXfb3Zrdm1w934bbNvb5spPr8/vXs+rWslwt47YCzH+Xhc18NchK03EqxTlUKHdUKI49Mh4UIRUVimqTxyQFADeJLWtbsmpIgJBRHcoEPb4O3bfd+dWFLVYl3pEVmcm15zRUxdFo6P10FVMyDaR6LUv8l58hNOjRkSknDbGyIiDw2I9pEG7DHwLe4ZRpn/nzkQllpEc6N01rtJSRPp92qAMOMnEczc1AHRI3mvHuHEyERDkYbD1+8Wpv7ACsEjyA8RQMrQW6ZhWFWRkIXd8kc1QHBwAgAWRiJLPniyLgARIFdVbpFAhrwVh2F4rHmdkJg4ApglNTYwxJwD3UUPBkE6h5lTLIz508PYygLCyN8uZWRkx2PW5OZZHJoZRh76SdkVQJD8+GjsrJIVwTRE3DgMHAAAXTVJ3BGupu6mZvmKutv3LmkgTpndPLEMwizEAMEk3Yb8NsakPtKlmj8sIpp6oDiX63VvzVR7702EiLl3dUhaLBIIs+DoED0ARHCeJgaEyKIY1FS1NW1cWIS099Z7uNci81SnUky7CB2WZamViHU3Ts8tUKl8WGZA3rtGEATOJMX4/sV++fPLf/+//vrp79/aFih5NOcMNHLqNTzSKW1+xL8l49ddPWJvm3vau3BUvt3SJY/DsoTfB+UR0VUtDGgMUhAAIXrfc8USGWAdKSeDdNikC4pZ3NzUuEjiHIjINVe2kTJ5iCBmZlymKbu65BgzZY55iphz0I+mxsJEvO8td90+JN5JHEmlYKpDIsIeQZXpC45AMFMp0voO4UyDnJoP8KNyBDUfCQdZPQyEXoqQxjkOOWolSu3Z91V6HhkPwQ3iIwFRElYaI3QFEHtXTDFJgHY1T46pMUvvPdsp9+EgReIAr1UAQAS5SGQs1NhkpP0Chzs6/5wsR8GRkSi40DRJQSlA/eX29a+fPv7l99/+9cvnX66fP3X3+fVqt92jzlyXcAIsXMp6nKUyMkx1AkSufHo+rKelzjUhCvd939t2eKqH4ypVWEjddttBnIVOx7lWNlc16xFEqN1ULW9XZpqECxEFCJIgv369b3cLy886ZCqAQDUVtEE5x+udOT3rNLQ6LJlmKklSGoBkULUIePiZwtTcg0W0W+KBx01ByWEOYQp3IokRQI8ImfMViUdEpPCUSg5ERwDkiZ/udzUFgAFfDoAAFkn5b4qw+95SjRkQmkhBZgAw96GQHmQRyN0PPuSU7sAsQIQ42oos31O65EgokkwuSiOYiBAhAib+cCgixrAsL9X8M8aCKedTuQCJiLBICc2Q0EWkgi2PyszhhIGo8VG8+wAlhoN1TQYeps9aKIdO6QACCCLWrpgZpEn5y/k1Yk4PtA8QqT9AefmFzXXGMT3KaDAYR0AMXo4+Wrb8zZFZ3Tz/B7lGDzfTXI0kYyvJ5gRIhMIMQdbi8rJdX27bpd1vLdMCwAePO7eLqh0AIqyZWhgAAEWpnCrcaap1rgCh3ZhxmdcAbFvb7603rdNUas2xW5G5SJ2maapFhBmAmedK81LXw8JI+94i6HZvzAVA9g1/+dv5f/yXn8+fr3ZHouKAFkFCqfdjKa0bjPDBlPilbFCZgISyyUMAi4FaHITkCOLU3hixaO9IPKKEkCEgiUCIEAgBzkRuXlhyr5XJJ5ngmkOZfL+YKeEEeXfmA5Cn5fc+ozXtapkdn3m8qb730TU+JqeZnu4mRbJXZaKccWUyOAT1vWcLmC+VdTePUREDurlr5twZYJipm2nrj6lBjOch74MxyMXx/ufocMyvaITcAUCAD99yzpFySYCa9rEigKC9Zw3rAQk4KqXmZIrzGzFvvXtA7w0BORXjiG6RQ7McPhCzpyrLQgplrl+mHOc+BNIdyZmySEDY94YoFIJRKLi97B///PG3P339/a/nzx/vbze4Nf79zTYnmuZ5PZZaqRaPQEIUnJZapymHb8RIBZe5mNttu9/3hlXW07qcljJx6/tt37d9J8J5rj/+9CyzuOm2bZs2y8GBOQIIVSFhoAJABJ6eEoO+Wd/H4Ce/i1yZZEdJTOBjf56dfeobrWs6RXB8U2huAVBKGTlEMVL2Um6OlCUmZstrXcPDAQMyR6+JFNeIiCxZmIumk0MYALgIwkgPEqExC8rV9ANc8Rg9AURYBpZxOsgJAKZpMvME88T3NcDgrkYeZr3rKGF8jCe093BnLgBQRCJi+NTzmM4hT07bv4s3UmH6KNlza4qpSco9qnY19e+dNX3fV/iYnDyWqmM5+31hkJpL7T3vKyJifND/HwF7ONLKeNx1xG4eDswy1J/plHtsbh+FjAMk1WcUZvljIBARde9jMIGQLKfcToRHgiMxkYi5+B1KUciePA8RIQ5A7dbNsmhCosd9kK2g9fMOHa9v97fXSyBDvpBIpqkiyfgay4jnaaqAYK6ZJwZoan3rOxFJKftta1svRZ7fnUQqYcnWnisDwfV2LcREZKqn4/F4XERY+91c1Xop5XBY3MNCyzztvQcAAqny+Wv/03/+269/+riflakisQZYDkMiF1ni/sAneNhYItGAiwVCJncOIgPkqCendkQUkKPtTky11hTS9N4BIDdm4aOEUVPicddioLq3vbu5ufVc4QaoDVLh4KgQchFGJCbrhiMKJjPjMGd3/igN3BUQPDxRpkz5TVk8utpcqIb7CKUANPVwEykJOZDMojLPzS9h2sSwtx4RZm7hgDiWR+pEVKSE2wCOB5gGDI89ZNPQcrjvgy6Z0tjcruOYTuCArkdAQJaBkkEC5lyK9oY0FPhZnzFxGizCXWMsxgceA4AIB8GFyUwTA4WEZqlk9fBIfWwORR3MIf/kmYPbRV9+Pn/608e//uefP/3Pty+/3z9/vL+ddTNwlkbQidanYz2sUaqBdHNHWg7zNNV5qtrtdr+3pFaAt7211rsbFz49H9d5MtPz9UpC3ZqZnZ5PuQHqZkYAAFMRRs52ysyKMEeoWW89kNSjG+ybbrfNepbGgonN8dx4R7gzJ9BqVOjx/QGA9PC6qfmDYKaq6R8UkaRsPqYaOagZQ0VmxlSY2WDHDMvLwCMmI5nNXU2ZubeGidf0aK0jI4tk4ZrsLyQEAmYIgK4dE4UQwUIyULUoRXLyw4z5mSQipfcH/C+fNEbm1K8n0xOzZm17gwCLkPHrkNR6+lYy1sfMpUjKM+BhIkj4Wo7TSuFwyIVb+hVTp58O6WznET0ek6nR0wQkekyCx9AU0XKqkyIkxIGQC8jb3swQYKDS09FmDgz54doj2S6304mjSAOee4hwBmZqVynsFuZWSjEzhASa4kD4YPY36fJ3zGi6BBPl62FA5TF9A4DIpOJxxzh4plwLEyi6wvntftfYGjAoojcFSZ4Soz+8o0yEYMLFw8y7hS1rVQvX5Bo5MdVp6rsVcZHiNZqrd9v3bV7m6nW7befrhQoigKota4mwft56AzUQ6fM04VEJcJnq9erWdbdeSXSLbx/v+/6bm//xP/1hfSoiEGgASUEJDQ8A9SBKID6O+sMciCOAWFwNHvFGuQwHIE/pZHpqHJJuGBDZaGbVnyoHIjbXYeglIoTeeqljzZNS8d57KYWQ1DVJ6FwkCTkQgQRceLwgPijckDxOYUhOmTsksxTT5KkRzpnlkPhfosBIJSIMI2GkhohJxvwUMy8ecDCvLJdM+HB7hhkniZ7wu7AkuVJumk9YQnZNbSwSw5hKpqIjUA5XOXvz8DBMPzYgAkbvPROWcoEZnme3QuSkGwFBtbNIurUREFkAPSdIY4zADODanYmT5hsQUqV3JWSkcA9G7AnhhITQ1ULSb/vHf/16+fTWL3o77/etX299u5kc6lSqsTAGSi1zAaB90113mnklmooIYWt6vl537dNcDvMcANa7K4TZvMyM0PbW25Zm7MM8ewlEtF236z0CRHiWQiS6q1r0iFIKI4Xp3g1REFmhG3Lr3ndLr4e5Fx4VHCZhNxN4cnwTkarWPBNdHQlz4wLhuS1Ml1+CCTLyN69qYtQUoHctdYpwJLbe53nZ9x0RHZwCI/K0ShRKFuqPuFDC5Kt65GzRmcU0Mv/ZB5rQ0lHLRAbfN2p5DlsAqHrKmWMwaSALQSLmQtrNk+easeepQ0Afis68cNwlPKEYAwQ/Jq2AwAgYhGSquRuAMfvOZDcEoAgdMxbLjAtk4ZQhppooJ538QOlma5OyqsfECdxsmmZ3FRZ1QwS39EmymyNjquJi5JtDRDBTYpCYKBDcQ4q4p2Q7o04A0fPjYB6QO7eMXAj3YSPIdRxAaMbv5RtLKDQApUPenxibER2HAU4QpU5de/7b9A2OjVx2hUHqg8bcUkLkQCJpI8x05t5aMHVrIlRrcYPWGroX5q011b6uU/Y0Fmbuh1mKUKF922HbtuWwTNMELXOLoLtf7tdSDyyyHBbrxuRguwXViQF8873OExHB5hT4tE6vl/Mt2l///Fu3/sf/9Mfj+1m4RJhZL4UBw5oBgDAllgAeKbuUtowx5HywwbMM8LGTz5WAe2DklhgjgpHVVLsWqWPUMKqzpKpgmepIgsuhPGGWPO4ZcZOMVc+EstxWM3FTHU7mbGc9cGD9XJhp+DkwRb1D7vzQfIkU7R0TbILY9n2aZjMjkhwipG4yDSNqxmmQfJhi8t3LBIiUCY35PqKZwiOiMpAigjFhginGIbVxz7lpkbm3RiKQJprsOP/NsApDs4zRt5aRs/RQTXCRkRhFkAXs0JIPxiSNn9SBecxmHYAjCMgiaIDWWa2zkIF5fuw9sENh2s+33/788dPfXy5n3V7uJFXd9j2mZZJatgyCheACQdHadu/6er3XhY6HY62lbXvret07kj8f1yrcW9u33tSmqU6FCHDft+12W9Y6lznCezewtm+99Q4gp3WlCDfo4W5A4JVJzfemOWFpqlvXwOhJFEznMMSIgM02zDRLWSnlIXwfC89BQyAcqSlM312r2f67hYexMJhDYG8Kj7FIigahKzPHw6iUnsFkU+cMEDET91ryG9ItK1IogX4GBuYA7b6VWnOpAwHIiExqigB5ghGmXMWFyncuVsqhsngptUSEeWQR86ibKcDcsUr5LnRmYe8J2RtYOE77D8TIsE8Js0iBTNXx4eBNDfRIV3voxlg4lac5S4kHcxiRUkohLElhJeSMXM9HXIqYKwB2bfiwOwJASlMersSMhof8HOOh8hyy8IfRN2kKFMA4bNlEpGrwiIHIZS94EGaaHaWzH9LnM/ruDFxlHldCQCRY6gFXSUxgZCA4MIk/DM+q1vZugW1XJsrrULupZR6OOXjvXU0JcSoUYQS491aYCpfcP4ODcCGDcDwu63qYy1zCXAjWaVoOS52KCO97m0RQpLkyIxbqaZx3Z8RlrvNUpopSIFxVVa1LgWmupRAjXPc2zXNXv1/sH3/99Jf/319fPl61IVORUs29tc6FWLir53sS6mEw4iJwRLOmgSB1DKrm4Q/Mmbk7MxLn8FQQQYQxYJomxIBHlkN4bg4YAFzV3bP1zskMs/yb1Dh5TTQQHQHQ9q5mmM4MpN7MzKZ5Si1yLSUAzC2zk75nRnok7JeQ0MHHzR1hblIkfQ6QI+BwkULEUuShj0qi55iRpjAcMEwtIEotacRR1Tyzv0tR+YGoyp4zkSdj6RXYtSGPfV2OND08Hc1jQJr7SYtSKyKVIjhKtuwzRlZd0BCHACWiWVIcneOC1MxlS+QRQfEAf1E34yrdLMyyxxGSSuX+9fr3//rzl19ef/90+/a63Q2vXbEQL1zW6hhB6BQySSnl3u266bWpBQqX4+GIxOfrdr0rhMyy1FKtw+v5ft06sxwPx7wf297rJO/fv1/Wyd0Qwq1v+wYQp+OyTIUCrBkElMq1zqZ+v28kVIr03pp5jhBBOMW3eTDkYhIBmSjFNvkJ59ed85zHwTLO6PScW1cEMrVk2eZQCIf4JefDlA+2A2Tihnlo7wEIkGuqbCbHv/Lbl1KJGALdo05TitXUsqhCiBCRdD7m/tbNicaq1R/43vzhiTAwzF0Hzzz1TsNrwyyEKCUPQubMF0Dsvec03h1UPcOBsjMiy+Gj55YOsu9GxN5argbSOJMzxHQ/wGheOI1o8OBTDyDtEOZDjnE9krEQKaCOcKklveY8wjcQchAPAEiApNqTdQeI5lZEVC3/qFTFSsn3La9i9IBIcg94Lg9yLAAIROAx8uqQ0MJEZJhcGCmjZnJcaxr4EP8iMFEK46dS84fPeyXcEAAZmZj5YXZL5hzCftvVvDAJcmVGj6FMjAhA0542fWEmRFfd206EwlKlFhEmMY/93txcpjJPJSLO92vTLszLtDDKtu2t6bLOdZpYZCqFuSTcn4WYaZrLclrqQixUJu57c4h5rYenpS6VCFtXZr5fW7/Ap7+9/OP/+e3j//x0PyuCIJci06C00phFj7xuhFLY1dABMWoRCPBw05bm4XhoQROaFpFsGUMkVc3vd5Rd4x2EyG45C65Hb5G6UncN1yGKfnQZNMTpJFJyDm5uag0JmVi1p6ygtT2/SDMFIhGmHJsAptgjIpIIlB6ePK5bbxkh17rmqi2byHzy8zhIKffw2LpDWmpTjWNpTMs9l3o4Fx4przHMKj4sESk8w4dZASh7GHcAYB5BPvRQiYykqvyEsytKZXNAby1GxyPj+R/CWQcCFoKwsfKA0Z8JMwIwY6CbR3opShEkqVxFGe5++/z2y59++/j3b5+/bl9eb8hyVSWhaZ6CMAq1iA4+HabluDiQKlxvrh3rUpfDWqfSzC0IuZyeTs9P7zC4d71tVqb5dDoC8Hbfu+l8qM/vnmoVD79vOxWUItNcfvjxuVREMELQ3q1rLUJIbe+lyDRV8+geuypJsQjdu7s5OBFwdn/55mdV8YDQtN5yrws+OJn42B1aOGSyTXqehL7nWWU2w9jsY9qJhQkTfVrkoUmjETIxREKEvam55/5f3ZIa21pPr1/i97UbkxAnGSKvLRQRS5x7wEOPhOEWjjZMCdlOPnhrOFA3KYSj4RSNfWuPSjeFSU6US2+gxPGbZYBflioPmQeN1EYiSvY30LCVDxV//miIloyMR/xmwksBsTd184jvrqtI6Xdq+1Jpw8TaH2PKCO0qIoiQvVj+hzQGxzFNVdXikZeUE4n/pVPGPIXdUp+agmYULr0bAsmDX5brtgzMS9tI6pE4G5kU7ZrluImL5E1LPMbBAKlDASRycFN9rMmBmfbrLcJT1npaZi7CSFKEiwCAEDFy0sfLVOsyJVYwWQhuLtO0nFYSdsCttdQgE4dZ3K5X9y4C61qrCAIKyTrN63KY54WJAGg5rM7QrHVtwjgtFWvIxOvTFGHzVErhOpVJyrLMEcgg+82h1bdPt3/8+fff//bp/rZxIBMxCgRmFRGRrt2ACBtDVB97FEYEIBIhkVrSECEJLs7pvHk8RDgBoXkE5yOBQz1jnj2DQ0CqgBDRIYDyg8SR+puT0Nx7DUdBsOTUEojRBucgAz6T7JRbqgTteaYSpk1dky2TlHDmnEQJl3AnYaJMtQVt6h6uuT4cqoaM4fUcyw7LDrJIFj0yVQISLgioXXMGxUS99fwMAVB7wn7BvA07TzpaOLPjER5RkXnvau9AOADso1YLSC86ETF7OMBj/cYICBYOEabdIwCST5mAo2GhCQZ348rIoNqF6lSW2HF/6Z/+9fXnP337+PPl67f+du7M5X7rMk3L8XBTcwRj5FkOx/mwTh5wb3a5aFORsqzLYV6WBrB3u20aEcs0Mcvtvm9qGLLMq8jctp2ID4f6/v1TEhdut2speDzO81qfng7r05z1nZmZOhaCAc1HLqLuam7ZhjESYGJDXDXXDAA5RcEIIwxiEpaEQ425RZEsCNyHtjh3s/H4WgFChPN4NXXtBpDpzZ4NlltwERZRM8vMTh+GwcgY0AR8PtTxoUnATI3Y96GN55hu3PRjuBR5KmZJnhVqanvqVFJAMZc5y9whwxjRoUNqmOZkhMTJgdRiY7lIiESCOfjBFP9ARFJ/83UFG+rMwDFJ1KalSvadeVUARoIJhwYoIK0Z5pSXlRSBkT85JJ55uOcMzNTyby5Fcuman1Ey65PkNWwQqI/9zHceHxJTuOVnlSVhmDFxnsII0LsiOTObmwjhiNjG8dVGjC33Ix+OKKFgltuBNB9EuD3g0gBYazXrTBKQROVgllQJEo/iFJGlsKk6yrX3wtwya0WIa1U1QHc1KpK94bzMqctDDPXYt305zstp3bYdhIG5CDNy29XRm1ktCIKxxb3vQeFqUrnWupd2PV+Xp4kq37e9CAQjMtS5WkSBgt01rIhgWMcdkJ4Px9Y2M1/L1G5Nt/0TYO/7H/79u+PTQSbOMj/h41kyiHBrXSSZEA//LqKFt94Cgzin5+MtIkKHTJcZ8oRSa3gkDjeH7z5EOBxpMowYMjDAAGcWGAve0QUz55x8qCoBgoYKa+BZ0geWASCPiGZFHj5ASKACjbcrAtrepBR3ZaExfM/AMg9GyqSgJCgwkwdYS0VyqukjwyqYKJKWnKVgApNxZPkCOBAKSATgELlRQtQxmImSfhPhpRSCTMnmzFGIhE0yg3vv6mZIwCRS5JHKl68hPnoMTNXW99EBJs81xlEjLM06MhGThgcYoyAKGbfLfvl0Pv9++fLxeru3y1WduE4SYdqVi2xqe1NglsrTNDthuHWP282uWxjycVmOB5lqyYwfrvX0/lSF2t6u92Zuy1KlyuV21t6XQ316OhB42+16uyPF+x+emLkIN9Zt2+pU9lvXvUvBMk9bi7a3Mk9QWfe+9R6EdaodTJiFUfud0EdjOWAtiA5E4jCkMiScBWX6wh5qoGAW7Z0QASwpuYl+yQCJf9sWjK7Vu+6AgJhkzDS0R8oXdKiUM5M91C3MiRkfA5xhOzcXkoCHK7YIBCAB5BofcXhvEWIkwtNQZwMCYGsbCibcDxGHYsfRVKVKVhSB47YzU6li7sJk7mPHRsxumkUGM1u3tAgHZkDlIFeo9VIlEyK5jHjunPMMAbhwuBKJmgZESQPOMD1aTjCHK9giwEUKE+oANkcaEUwzitaGI9SDGKRWN2UmG6ze3J4H/FsgX96u4B5M7KARTsTIj0IJjTBjinMsyx7m4Tq2NKObZ5K933JwXJiD3FRLreO+8MAIw8TqGgK4+vD1wVjaQd6Zwq3p3rWpljoB0TQXs+jagem0TvfW1M3MCyBGFCIWYcbe1dzSBJlgzt6VCJZlWWu90J6Um1IqEQfgtikSm7Xr7fL07ml9Wiz6tu+HY42C3e3e29O81Gk+n8/1sII5I4lMDdQma63nyE4A9/s2LwWA3j6dt3a7XW//7n//d+8+HKVwgGbyHzGZQu8DTAiIJBxmmU7JzGkJRgSzeKgqMRxSF6EpdQeMHiQCAGk76r2n1pEHOwRTNJ37mJzamxuRWO8iEp4rn1wyO3Pmqke+OeFgZlyGRRl55E16ONrwfGVBgzAYRBBDPxYevRulgh6ymyTPyBcETJQuI3x3rUNAoDbL3lm7AmFSxtq9A6JjuixDrSdl0zLEfKzPPQJMHZOC/qDDq+UqkiM8ZQd53UIMgLfjYIn21jM7zz0QsNTJupJwkCOQFLHeITP11F0wPyMRsccrFjbizglIFS5fL1//9uX61r78+tYdNrVdfV4nd77u2w7ohm1rDn56mudFkEEtbvf+tvXmGEwsIhNTpb23t1vrzdbDyoRd9bbfSWgqdarVzbjQVJbDsU4V7/ddQ0uldV3Xdb5fN0Rs+8Y4YCF1LjPJfde273VmnuWy79uuKISIRHXXu1P0UK4MYRgcAEDoZqgQSft6+GzpEUQFBEVK10aJMVeVItnX9d5FRDW36D6oEJH1liAgDKsFmFtYjo1TJpaUVkiqNgv31hgJSqJ/I4HMqQl2oJHt7lEG7hti6HnGhhMQinAEtNaZJB8aYoZcQatD5icw5kEa4ESci0kWDgDTfH7BPUQkvZaQYJ62d0hWWnia+4lYffBmEdADzD0cPIJpKMFjCKIftQ6AdQXIrKWENY4RKREmOB4Dv8+qGNjDLJyFc30Xg7iQFoxR++eTnX6C9M48UpIzMgURRnYrjJd5OJaFCgSkwQcfmJ+cRjzyBlK/iYAYFiLiEaq91hoBRUoO0VKqBGPik1a7gIEXH16C/GWDZgTRzXp4D0uRfBhos0yQKIIYbt6nwiIUYeGWhT8TEqOk49e8t2bmUoXYI9y0NevTnFVYb30X4TqJul37jhVTjF4Wno5l73cCfnp3BIKXl7frbQPCeZ60KxVxDGKqU9LtwsCkFiSsIqYKHuCg13j59fbX//uvv/7P37dbJ65AFOl3hyBJmiXlKe/uFomqN2aGSAgPJgYZHkZEZkyXQN7ViSXIoQ8ikkjuonI0X2o1c209hdJdewBp1+8L4Qgw03THeHgCdpDTBhOP+jubVXB3UyOkcOiqKULLeG5Iwmt+oe4swkzI45EZ+w+k1hoEqhkzM7P27xwUB0hdAHBhIkp4s7tJlToVqWWghnwsGrjw2HDhwEkxo6nhiDIVEcHAiHC18DFbCgjMRZLlSmyoAREz8JJTjZGcjMe6glV7jDuFiBmCmEsKYT2CS0n0VMVCKvdv+vUvn3/77799/fXt88fL27W9Xbfb3qgKTlOLvps1R8DSDU7Ph6fnZVmKlKrGu+Jt85frtqOViUBIwy7b7drudZan04RAb9fr5b5pWu5BAU0KTQdaVtp133W30Aid56K6B9htu5OgFDJtpfJyWESKupfCZZoyGi8AerijN2sA3lTdqc4rzzOMPDhMc1tu0k01lbi57815Y9cWkXwESMkvIbnZGHJERDgG5iAIMPgRHBsRqpqjDiRE4kzkzQhlJDTtBJBrHgB0S+1MzugoW93vG69Syr63rCLdQjWHOaltHGhCLsJZYQSWWlwNAhP+jI/eLk/O9Bdq1xgDUBpFB4Bputwg6S9ZCUEW6WPclqCbvCURwxwjiBH8oTl9/Jc27r1hOhytAGI6nnIJHoEwqmwID1VDRK4cKStKhWS6iD0NpQ8JXQBAENIjLhjdQlisK0KqSPIwwXy5htWTEskTMeQolLIle0wP0lKQjQA8cpNz6keEubrsreHgA2T7lf1XgksNH8iCjDlDRFV1C35sgF2tlFKYl3k6LBOYZglKCAjuZsI4zVWEIwZ/ZnCmGMskwrTdNzeba51qgcRyuplHKWLu18tdrdcqZSamYCELN9cySzkUkGjapmWdDwcnuu/WTOsyL8u0t9ZbBwxiBDTrrasWYSTqpg5QJz4cFqbSb/r6ef/bf//421+/XL7eKbhIRWLhYuZAFIAZFUeEIgKIwvk8IQCoaoB/35mna4YZRTinPu4Wkd1ecPagwxYOSI/SXgoC1NQ8fbftOMTYs8V4vJC093xRrXeIIHk4nohzaJ8KkCHZiaSvkJu6PbygGRHsmqqE9IiOGeNgGYVICYDWGhfGFIlnM8EEAaaWaoKItFypu6YHrffOLN9/2TRNY/jDkhfe9/fTPdF7IMxSBDDGc+rhfTy07maaiz4fUsUx0c5zCHMq5SPVYBi5000aGOoutTAxmAlNhWbf4uXn88//9z8+/vnbl18v17tfr+3eNKiQ8PHp4Ai31nd3qYwQ61wOa12rmOPLpb9e9Ntru23eNJZpKpUD/HZvmypCLEvx0N7283ULhrIUKkEVgh1Qy8RckJlYmBien09E0Pfd1My0CHqoVJyWAgT33plpXlcnzMog3IQwzNW6mYXZ4flEiBBZFlBvHeAR1+xGkgfXSGsYYcjwUHsmcQgoIkqtEZExRBiIRO4a6A9/KAwfTypARiUckMylzP8ZxI4MWITAIE43OwOghw8DjQeP5EQvpTxOacifNCeuiMDMUiQT9OChaxhlsxkzDnPfyBOm7Bdz95CCOnfPSj3rZgjoXSNASNAjk5jSneOjhMhCHjLjxTPGCCkdK3n6ZhkC6ZrLMaVkWt7wyEAM5HGGHI58nAQwiVRzwxgorhiEicGA+/47A4xmI/fiNpIax77XAcA1QxJyUZ+eOGtKRAMkZy5F8jf2JI/S2JYkMy71rzjk21lYQWJD6LHwHUInDKaSPyQxASbL998CElwNqYhIbz0Cpqki4XqYt20XkV0bmDlh106E01zcrQhpN9MOFMScHRh1TQSLLIfedmEKN3eTUtYVtdvW2jzR4XBorREAlWnvbYHlcFhMm0J3gqf3z1L59nZtTbU7eqzzul2uW9sxbFoqMm73FmjTXADEVK1ZCDwtx8+b9pu5+y9/+X27Xf/lP/3z84c1UcCA6I+6Zppq2/eEMXVVZgR0ABZhd0/MelofRaSrFimqCuBImScaPiYej00CoqkTDTgHiQAM7aKU4mYwfo0WKYm3TLdURIQF8sgFy45t5LTgEHIQInJByKF6pI7PXZEpz3SEpLblvjrCQ0qxboWLhYcZChMGAxm4IHb3ER5CLIzJVQcA4ZLrYiE0C2ZRM2FmIR9iweQIoTvkxe+jAhvgLCQMiEy8ySlEKh1KKaYdiKxp7qXCI0emNHJpMhUDpBRVTZoFUSbkQECQcCAEEIOI83bevvzrx28fX7592rZzp7kqY+sKBIenAliJrHf79nZFLmWZCsG81qenGQDON73e43y12x4O9MOH4/G4uDXr3tx67x+ent19a/2+bcDx9O5wWus8VQdrm09zOT2t3tv1ckXC49OxirRtdzcInKogRJ2LUGm77dosrC6lq6n2bW+TFAg1DXSvIt0NGYR8Wqeh6aBMnwaIkFpiSF/ChywtSXAGARFIjMLStafUOIPYmKnKpNaR0DUgJbZuDyHpWBQNQQ4CEsEALzA8PAEQoWYJQcm8OBtRVnnmYJgbgrtnXG6CzgLRu0qt37nTiMmzHYWyubOIR8IK3COjHXL9kTr8eNhinEc9i5KhyhBAmP52SWZ0FkFjTM4EHuBD8567uPyhx9QkUn1s32NeAEcwZNYZROTqXJKrl4adSHsBEaUaN8sWZiGg1pqwDMedyMgGgeG69PCMd8i5zff/EBEIKcIgRqcvwnlqkzAGpNeORlY1AmKoSi053cP0hpKo7pRA/UyxIaCMYkiDsSECFmFEcBiSLPBAwfAABqR4oJWyY8okKSSm3vuwa4v0yFzcgATgK6AHMrAgEnfVVJhPdVY3ZkzVNmSUjRSSedtuXIRQIDZzNAdCmKZqZhNTQjtOx5ULv7x8u2/34zrP69L2vbmCR6h+eP6hTA7g2jpT1LkghKtPi4CDMaF72/sVdhEp7r3bftGf//Lpvul/+H/98cMfnupalMj6Vqq4uboGk5ljRlOpE48EZqGUWgQglSIpdOt7I+GsGNw8lbgJHU3GTqkFeFhnXCMF3ZjexHzOMNyCiPPjVh2JzZCGfnfHsWRDQvBs+jAyndE8tX6uLsIpRIgAV5MiCBDdATE7mezQc1zbtY1HyA0T1b27ouV+2MMxUNVKreodANQ0sWI9e3Cgh4HAR6GjjvhwqBAi4MieVE+Tl2syOWwoqQCZyXSw6SNsDCXNiZiYXAMiirCqMiIQuWmGJQCjgRFn1AEBSxgUmWGPb798+/WvX66f3s6vt7dvvcz1OE2tb4pwfJrntXSP633/9OXWWxzfy/G0hPXjuymIvr3evr32l7NrZzV4fjc/nQ5mfrveeC7qcVxOKNN+384vZyR4/+Px+d16mAsR7XcrkxyOs5leLzcieHr3BObtvt2ve9u0zlMaFOok3my/76Z7lSIkLXYCL0weMNfa1EMph+hMMFeZlyqCHuFqGIgehOzgAKjmYUpcEIGErSsgwqg2MKFvpbJqRwIBcoik4EakuYscLIbXIq9pUVViAkCCR35UcjA9bCChUFhcHRmRMMxTgOuZjO3etTOLEBORmkEkvtqztX2oIVKENuxnNEJWgAhSF5dc3uQ9DO4QZaUOyRfinDtFUKbiwcjFlDHAeewvh7ELYdxOOZNyL1W0jxjYVKo++HYAI6ZuyOEAcRTIABH2uC8e3MEBkM4lBARGuNdaAPP4Bg9Po29etikDtQdoGrOTQEqTUVaOATGO/oT/eHgEWNRaLck1CMncosyKeog6EMm05xiHBB+/FiDDPF1FOOP8HJxgcGWJMAiY0BAjLI2xpiaCpUgYiMgyLxfbem/ENJXZIswcmKd5DrcARyb3vtSZCFJ/27qaR41ap3q93Iiid9OtE4QDToylztt2n8qKKG27J7nIw9bjQd31ernf71J5OZT1OPdNt75Pta7PR23q0Lrq2+Xct15TpROxHsq8TtaUIghBFaEby0iQb82DoJu3rr/evvRrg//3v3v/T+/KwiHFww0CHQgAWSKMkTMBetSxBK4eGeZGlAQSEobhjE8ve3KkB5Afkdy+R+AaF3HT5HyM7BSmrOPMNJWczIREroqBQuJoEcFJBU94Q+KJEh1IFG7EDMOQAW7OIknxSiYEQCBQb12EU8/DTA4ZIAVp4ESwHB7EkP2w9s7EiZJPGY6ZATizUJZWJMyUWRtuhsTpJhl2aMixf+rQLFcYRMSEFuGZW9IdiYCCRUyNgS0Mxt1p7pZKREIayk6RGDuSYSgFd0SuUqmjvfVf//Trt18uv/7r564WYSB8+vG52d56l0KH90cA37b2eu773Z6eD//hP/zT7bZRKctaX9/ub+f9coXL3Wvhw2l6elrM9NvrpXscqSylIPH5bQszQD49ze9/eH5aZ0DbrpuZlyoE0ZoCxOnpVITd4XLb9+teZlmXut12EV6KXO6bbg0R5iLbrm1rEVgKO5A7CKMlUZg5yOd1EgJwBTfAPPYJeRhVEFMPnGnhPiY5RDmdzuoXhkIT3ZNM7NlmMVFW+iLi5ohkbuBQSv1ebH+XHsUjLUg4IxBGrHpmxYSF5/CeqGeeNiAgjmcjkWjgKbXwyCzbrHuw1KJDBpYQEheR3tU91e1pAh+6VWRKSAsydbWsisODJGNVAYnyAR3tZ27zLGM3INxS2ebEGWzPEQHxb2vYLO2JKYLG9uEh5gPE/G1zgpQrKWJWU3IAgHxAx9Y7AgIMHYnC8/NJtU86ikfxl7pvxARA5PAUB0V6mO0fOm0MEuzWicnBhzzKhgsUhjRqbHdza5DQhzEfG1/mwL/EYz+RY60cgvmYETsiEiDXAmHZA2FkUi6F+21XnTYWRvMwD7I6lW6NcqKlHZllKuYwIe1NzZzY18NiebKYb5vemq1WDsdFSg2KeZ6bq4Utdfn68m1Z13mu7pNZN1NXWU+nb/bNMQh5rjId6t7vNzy3W9vu/bxv60LrQggoLFTBrUGEFKOC94uthxmYVLemYA5Isxu8fNr/yr/t9+2nf/9+eppQgAzMXITT4OTgwIkSfMwZIYePmjkiQZkpQNoNhnwsjd/58Djg+Jyzy849W3eTwu5gqhjJ5g9GSgV3RCSqIdRVe0CG2QzeaqLYiTJq1Yk4iD3NaAgEQUUeRoG0ywdEcLrkIyCMWFKv6TmsyYGnpe5CuVTtHQ3SAKyqzIyMbdtZBILcfcQiRV5JARDogRKI4KYklJPZ4V/R8cMkXEG7kxC6E4uHIYKr50zSwVNn4pDKdFRrImVsgImJuHcDBC6squBRZCbiaHH+eP/8l8+f/v7y6dfX1pWLqCtV2lq77fu6Vp4qV95ver31231/Oi0//PShNW29/fDh+O3l+uXbTbu8nveQWmdZD9UMXi5bJ5jm49PTc6jfbl0Vhfl4Oh7Wss5LIG23rTVPi5Mb9tYO01J42m+7bZ2xlMnef3jy3uepEmG7a78rIk0z9932vQEAMzKyagCCYxChIzftJcBzCz3KxDGcMYAAf+Bcc5jzGPg6uFotk1GKPlIGmlpuyOkCISGjmuZU3t2R2DJCAGxMlSMsmYbukc7WcbODuY/jhRIW6VKK7a0ImRqNqf/IHSECZu6mAGjWpRbweOxHmQh7H/bbfF0MImynBMsjUqnuRkLaR6xF9gzjqASIcCnVrGVIALhLRCQ+F4HAnBAh34Scw+eLYS5F8nQzh1rEzPIWyiVDILgqCaWtJg0OAxOUn4INkkwunbNMz52fm+V+LHx4VWAgHzA99ykkFebAEeeaN+5Y6xEkloCRUyoaEAMsmpcEIuU4JQIZKdBVcQRfoGuQECI8YkKDiVOekTgBiFz2ZgyVkQh+t6pmPi2EeY6z0N0GQ1i1EDcPBg6D29YAnQhKkXATYsQAIHVvdz0IIWP0WOaJqyChdje1utYZJAwtfLu1rn1dV3cvU5nr9O31rchUptp6m9ZyOK7atrCuxiL13dPT+Xx5eXsVoj/+9GNBrksFA1NA6E31JCux7L0xRnKnIRQDZkAzX5cZDWW3821vLepc0eHTz2dv6k1/+t9+mJ+W4GBgizDVWkpgaM8zKD+UyP1b5qOqGVGqh7MPGIwcd7CMriTMLGlzD3cuEpEo47AOalakuGuRGmEA1FpjkZxAMqI9cjJyj5xoOBrsc4xIutZ3gP7oNsbiJ2MvKeVJ+AC9UWbDajcwlVpT85VKOTfNwj+thmEKIgigaugktRBwzpeSP5EqneyWWSTcshMnHNCC77TzVP5E6tkAUwxmTdMRk0vmVJpaROrTkWh0wmNAakjYtZFwLoSJibEwst388tv57//1l9ff7m+v/Xx2nuR6vc+HykLb1qa5TnOdD9Pmdr7u57f74XQ8nU6AeD5flrV+fb2+fLsQz9/eGhI/Pc/rzEXoctu2zbDy4bAi8uW+XS8bMjy/O00FPnw4ivB237Yt1OzdcZ5K6dttrmWZ6nbf933zptMkUiuC9daWddVmt9u23XbTCIRt262HsExT3buT0K4a7pv1BgTEXODp3VqnzP5M2QUTcbZlapbqLpHi0c0cc6gPaN4BEQJ62xEhoX6RUCCIiKxfOI94FnFzlhLhhDmvDEBgphhXDKZQPbfQQuwQEBnsg8DoarnaJcREc2c+MTO6J2h9BOFl2kw+papaS/VH9pdQAUJQT8Yy5UIom9rcSqbml8kjGPghlgmFTo/kvkAQfDD6kQAfYJyIKFwyVRURkckxXDV3wq03ohTHBGdRT0RCblZqrnZzlTWAzNnVckm9tqfWO6MwmQmlePh3T3zYkMSlu2eAYmhIeIbzk4bsSITcDSjVf9F7Z6Gs0PO4ySvK3BDGFl5EkMeHSoRcxNwSVJe8H8+hDwJipHtgBG9CYirCrJdawsPDhYRGeZiPEMgk3YEKEdE0VzJvfZg/zaO3vk6HXXei4Fqs7zNX9BARNUMGtzguyxbNza3pMs8Q8+vbWc272vFwVLVd9zKXgy2q6dkj7YYcyNR39cv9x8OxyDxP09vb+fz1cr/uh+N0i/t6mpdlPb++mfOmnTs4GLqV6VBqJShhPQIqcNu1VuwtBKA5XK+bT0IYt2/tb7ePyPDh339Y3y0oGGalVgdz8wfLjB6CfXxoaXJfm4EJCAjaRupyHnZznbbWANBcEyvuFoQghVtTEipImLJ5SKqgFykRwYWTvpD9tYio9ixciEjT98j0kCUzhKX3OywST5Tw3sflgVkypRWchc1MWCB5DG4IlKMAxIyWlSFcKiU8Hsg4ivCgCLf0T0BaN7tmA+PmieAKM/fEyXFCHIffDTBUh9YsxlXGJBCW+cbaFRCZ6RECklanyBBuG6eIM5eu5k2RCwb53X7/H18+/uvH3/76ElET2lNJgKWHzcs8C3c0J9/atju2gPc/PhVZ9t22t7txbJeL9lifnl9eW5mm56cjzxjuW3O1woVPz/O7p9N916a9HqZlplLg+f0BCc/X+7713vS01HWawowRCpdta7f7nYXWU50Ku9N2GwJQb9b2FuHzVHp3QpwPMpXZzbsFGqj73qNHWnuxMq1LyTxZgLGZSyUaMmYrlvMZs0CggEDHQFAzEU4BCQYNS1dElhfI2PtOyDzYTPiQyGDkDC5i/JG5Xo5AwG5KgLnRYZQA6E1ZBNylJLE10mCUQKF8jsBdzaSW5I2HGddiXQGpiJhruJEIkZgaBYzgCnWAVMUNbyMAErOqBoaUYuYpXUkZ4fCFqSXhEiL/28ypHmPFsFAcwdMjViWXD6YuJIXLGGKYski6/JGo946RkoQh+U8aTy7oeAzh0t7JuRgzV7dMNeMxeqEUSo/N3mBxfJ/5RKhqxi6rWQ5zspyEB9U0gXGImIU5jfd72BayQo8IV3uw8fI3eaQIpJ4oJ/7p2AFjHqvlVIdGOKNkx4cPvQEieLrVCLsZEsxLrZMsU81D0MO33hCxZVAncbgTQLivhyUirNv1fCtFpLC1dr1eudDhsAhzbstx2CzocDhOU0XH8IGjOayHZZqtx/nbhVGqlOfjcV3XL9/ebve7u1/v9+lYfvjj83KojtajS0ELv9zvFsZFgjEYHG0+TrKWw9M8zVIFyQG7ww56d7v7r//j829//nj+eveegc/oMWK01MDcRIqbBzw+HCHEDEqFLIpT1hIOCMjEo6Rw9/AylUxlSDVLnUrWGb13Rs53j4kTvGyW9O8RTNDa7uFdRxw8MgGNuO3WdtPMSxl9W2ROsTsiZFTsUInmIioVdZTnQjxSJmNYPdN4OXK9U6irqsqpKB1bsRQreC2TZckfw0syVEYB6b4cI06AhKkIY6beZ5GYsjjVFgBdu7kTS2FJH6ZlFiUBZOJ85oYyI7G611IJiijfP15++S+//vanj3//01dzvlz719frZrppJ4GyzHVmTzwjY51nDecqx6fjbbt/fn39drneWiMuP7z7cN/cAT78+LSsjBBIctv19bI3hVrXZv56vihiXen0ND89H0Xkct2/vFxut50JylSa2m3fkjRz3W5q+vR+eX63sARxTJOUaTJVd+v3Xmc+HWcW5ELzMjGhB7R7v+9dDQxRHZiYAmuRupSkvSTSBSl7zfysiZAzmxYgSYDMRQhRSEw9y7jAYOE8z3O/qE3Tsm7hatZVU9b88JuMpwgQS6kxzEyOSblXM/PWW++t1MJMQOBhxAmYcoDIeM6UumGKwyDMLDwzL4VYEjUKAFIqPpDUxKxmjzRDf9xtw5cQESJMCOEG4WaePiwY1wMBAAGMPIDcX2gzZhrWdhumsLxLGdnNImGqEZoJFxDJhsyLJBXWY28cAfGdMp2TWzAPGqU/uGdujLMQF8rgGg8nQMj0oCRGoBcRNyPhiBARcyWQHPWgj9jCWiSF54/RPoa7gf8vMkEvxAjs7oREUuhhDSPK4+kBF3jIClN+a6ZEY22NTBRBWBCAiYHzM4mAqMwA2LUzsWHMh0nmmykw+DLVrt6bWljvRtJKISkM4PNU3KxtrcxleVoA8X5rgePWTNiOqwqTVA7AsChz2fdtmifBGu7TVFStNTgcplpLYXZD3/123U7vlnbfjs9reNy3eykIWC73+7vDsp7WwN2sL6fT8elwv92btggnhm5KgW27l1q163oqTDJNWoAEp723aPH2SR1vpr/K//7T/Lwsp4khTNnCGJGZW2+p25GaQ9XUshBmT+pjfo3xkF0yEqKGIXLf99wMmzt6rlpJe8uvaW97KZw62qxOzJwYS6naWzZ8Uy1uxsL5AeaYhxDHEgvHbQQOXCSjuNwyfDUewEgy83DDgev1QAx3LtUjTcgPIJUHMEQYIZHIgzQOoE4jwqWYNUR0zcUsxAhbDTMD4lxyIVGEEbKC5/bCMpX40ZmEOYgkLTEALIxFLJClpF7IIZr1vI9VO3HBQDCKBr///evbr9dPf3t5ednum2vY5b4HEaotM9dF5okjgiAOa1mmsvdepU6FPn68f7v087YfjvMPP5yO0/x2UUD6d//yY5nKt69v8zJ//Hb78nIlLj/98GGS6e31zSyo0Dzx02nZt77d965mPUrFwzoB0Pl6mSpSEXCrqxwPh/VQ+n1vbSu10Cz7rbnifd9P79fTcd7u6mbzNAVyM7211jCaw3VvHYgIk27y9LQejqsUDt8C0yZI6gE0/CJDIJBoMk9+AmTZyMzukI7rPCKcwdSJOV2GnvkfDzPXd04DMj481dG8hwdgelAIfXj9iDhH3AlJIxzPT9oiWSRcv1fuDpbmJAAwi723ImKagmaEtDmPB3WcqyycqDlm7l1NjWvB4dwEyqMsE8eYcygE7iQcAZI3FSKm7hUe6j1J5aUHMIH3B3cufERrwhhNISTkOVnQAEAjnvcxhX3YPtOx6wM/C9mu1lqS3oUj0COpuZn0Ysw8tnmEbsbEFrlk9gSlJvZyKGQfGqzsNhAH4zfzdZnFRrYwBYAwWbi2Xook9C3/gfI69OH369aFOCdRXVulmn1Ea42ZmCArL/cwcwBHQO29lFmEqnAQgsVt7xFQJ3GgbW9mFqAf1ufuDcDWdb7dnIlU+3pYIbB1RQhmWtd627p5r7wKdQsMwFJLYYZAKVxM7gABHoHaNSy41B8/zOe38763Vef1sH59+SZCP/30w/lyRtHe/bzdJilPz0+MMdW6LvOyLrfb1V3dIzCVlLD1bT2tN9rQw6zXSdhDPbVucnlthAH627uflp/+409ymLKMcDMLz9R7H7N1RoxJuDdHxBHg7s6a1TDkg+XunHnrXLp2YWFibSZVzBWQ3JQHiwqRYvjgDbiwq2asCiHDGKs6MyXo0iOIMN/j5PxoMqMIR4+PaJk4lj/PGBQMbBxLgQh3L3Vy91SLxwhFQIcI9QxEMdPcgQ23eTYa8ODdUgxZXhHTgMBSJUkYAVBYemsoXiQfSM8mPSuqCCh19lBTw8o5g3D3ZP9AwENi5JlrFE7CGLufP76+/HJ5+bh9/uXterFb09dLNzJCFEJkqFWmKWVsShJS67SW/bXprr3x16/bbW/Lofz0h8Npldfz/e3S3314v6zzr79/LVW+fLt/+v0iS/nDH07rTGGdmFFpWfjD+6cI2Hd9+3oGjLny+/dPs+C+7dMswgpk8yLP8wqh2nqETZXNlYEpPAirYJkF2VtrRGiAXfu27T3jqAFq4cxqtGiF4/Rcj881rCE4RoRTniQwmM/Du1tKcXcCdA/VBgEOqexkD2BCf9SRyTWBGOd7emrdXURMVZuWUs0VkRHDMlGyiHskWLPvLRkvOaa2rkRi2hApj36R/KnggShDzxj21BExozsEZhjqd1cBErqFmqWboU7F1BDyZ3toK82zWAcLJlbT7IASU0qJGAowdUntKmSYF1J4SJEwDwQzr0UQqZunHRch+07ACBLyEUQQBOmEHKqY7FZy+p8E3ZzGQC51E3Rn4GGY/5y58LlkzzwQGw77/AvnRWKq6KDuALEsi/ZmZiyUnqOuYe5MnK5dfPzA6QBK33GRIbImRoio3+NH3BOjmkmNSGkdwgznwEeUErNAuJpWkXwUxrCNOV2mMRRNPk8VyK9vd0Jh5v2+GYDUMkExU0Jq3oXReo+5TnPdtTHIbb9KlSDoviNR0zYvs3rQhBNMvfdlWcARMIigFAYsS6+GwgKq3SF039Zp5Yn23l5ev73/4fmHH95/+v3jG8C759PXb1+37WYWp3VeZ16Oh0IECPMyA8X1/AqIVAQs6jT3rW1tq/ME3idli/7h+YPD+fzyBsLCvN/0y6/b/vZmvb/7lx/n0yqzoFBudKRKPnzp69GuyJyHIhKkFT4fE48R0/oA10QEmCsCIaOZ5SIpUdtm4+0sItkHwEP5M/hR5sSAyK4WiBnxiIl4dB8MrRgPZJZQ7sHI6TVwd5SctmfmDP+vjz0AtN6JEZ0DgCD1XpxqQkbO2K8xpBphGUHIppY1HYysSnCI3A8gYZXae8saMGequcQkziRN5KD8i9RSAdBDEdAtEIPTKx4+FLQcTIwQ+1v/+qcv7c0+/Xx+fbl//np14Ftr960vp2UqjBSILEtFHkzGIKCpXjeFUi5f9fdfXm4tluf5pz+sc4W38/3lou8+vD89z7/+8kkV7l1vZ31+Ok1LRYf7bTfdz/ft6d307nTywPt1e3m55gT+hx/fz1Np9xsxqe5ScF7KYZ6QYLsbghFDkcoM22XP4fpcpdRqTQEhCE1t23fL0g4B0RmDcFy3gV6XsiwiAqbxXVcyIJI4DL/MAgHgAEyAHh4sJfHsxAw5C7dIyFVqsTLAmkjif4kcj5zd5VLHDWkoTdwpO4NwI2EwMHNzZ4Qsz1nEx1QccuhEQ70epoO/qelLVy3TZOqZRouAeT4jITyeahZOMH4eYuHhFmWqairCvTVEVDCzoJFV5Xmq5UyJkSR3wYl5AEQMyD2wmyUg18zqVCEnNpDWee59Z0jWGkWOUdM8kHmQeYAyPgy9qfmhfOElY08gmCVb9fwQs0AjTkI1IkFE5HApR2zpqQoAJmx9FyY3NFUW6b2Ndx0ihtPnsUkm8gjEECIEIGImDg8Z+FwgxCDMbbO5CYsPfCOAOybkC5EZCaGpEgCRqPX0so6NgQUVymkyERLHNJcrbnmls4i1pk2RhThKSRgkT9OcWZXLNCGBujfbialpX5fTRHjbtgi+7/e5zgHetS/HeduadqtLDYayFiGohXvbAZ0Y7u3KE09S77fr2/ntdDodTk8vX95Y5P37d8z89nbdew90w+thqes6tXYDgrrObuqArW9dW/r49rbTRMWFzF79fHxazWy77Yx4KNPl+vpy1b5/uX7TP/zHH05/fJaJgSkgkoifycAU4AEcQ5/eh7o/EAeP0NSE2SBXsjZPE1CGPubLhSSI5l2T1AgJ/YcAMxWqESGluikSPQ53cAMgJ2IgcHdMoCZFQKQPDXIwKPI9c/QxPHWgwQfIGKY0uJCI7YaUu7A8C77rxwEZwtwtWEp4pAEl36dRpZoFYRHpmqAtIKrmDSDUWi6fHlKhgBxWAER24MOzFplskwURI3XvFJ4NN6AgIjvF1t9+eb187dffr18/ty8fz5vG9dZLRXNY1kIcwI5CdRYWAEIDnZdl289HpMBy3/pvHy931XKUDz+u67F6qLu8f3eCgL//7dN911oKAE3rkunnEQTIqno4HI6HihD3q76+XFpT8Pjjf/xxXoqrTdPU23U9zs8nmQohQm/dtAuDMJWCve3mSiUweKoVge970xZuuN92Dkqk624KgCgFHMhBQ1eBD394Py0S0VkkjUzIYOAAoF1Td5d449RoASIhCddRWUMKvp2IMv9u+FQChvUXPDuzyAA4D4su9D0DMW8HAjBGTp1uEIK5kACEO5RaUuJsYfRYAZtr4heYxd3VDDBrHbSuJJIXQynSzVxdWJjRFMwcgEjIuw+ZMiIQqHUuMkaO7uni6l3TrpY2eHcjFgeXxKwjjCgczDLIBiQ5w5PdLSwgPKO23LWU0k2TtZBVWPYvo1QfMokY5RygMIfnrhUfoS454x4ta/KK3NDBAYNZuvaxWoCIAM57L0Hw6bvLMPqIGKpNLywJzJpkShwuMQ0zWgAOfxkhBmUyZcRD2DtwDuFJqwchRkqPOAVAQoa7dRZiSNYuMrH54MSm1xoAqIh1p0rr0/L6+ayBjrAskxTaN83Gh4gIwtzWaY6I++16en8CRtu3tvf1uE5Y7rfb8fmkSbA1Z6ZpKrf7RpJ1cb+1rbA4KiGh0FoXC31+evr65VuhyUynudzvW5mqVJa5fvl49h+Xp3fP07Leb7dUoJ1vFyAnwO1+B/TKXI+Th2kL60pE63HZt+304bDv+3bdQ3Q9ze7We7vdCaFYYLvz28cbB3jTw4+n9f0CIgouGEXKtjdiRGK31DoPYT4LhgchqvpQXgeYGiCqK2juWkcJnMYxIiql5D/nPH0M+hwzHi+RPoBo2qWUnP/k1jWflsxxyVY1YVLqlgUeWNqEKcKlFB/UFSRmTPNjhAh5gKulr+q7Lg2JrKnUCUIRciCZaXgEAZmzTUQWYeoYjsIQ4KHfl5Hh5qGApG5s+YwOv6SZiYh2BQxzHQ6VcEfnUswV1KTWWotvfft2vf5+aS/6+dfzy6f7ZTcv9fXlrdapW6bJMwIcnhc3m1chxuvlcnpepNCpPiFh2/TzpxvXaRU6vZvfPc+IvN+9B56/niFQWNZlmepyb3q93RFomWSaijfATJN3vl01wq3DVOuPf3x3WAUhbveLa3v/PD8/Hwt7zlrcNPObS6HK0DY3bcJcScBxv9p29f0eoT7JLMzqvkGsa329a+tmEOYOaO/+8PT0XHMzBABqjrnWz8kyQkSwkLYExIaq51XurmMOEYEk4Tq+dyQYPdug8yEhMrm6uyIRM7feCQcUJyczafgWAXB0H1jLYQlOZ+JDRZbsg1yCOoRrNgRBTAiYArakDEU4grSuESFSAKK1XqQQlQgLD0j+GlLbWz5y2T0Pvt0jmxof2DckAsCwwDQouoObwUCJGsBgmtNjjJ5SMwfHCM+OhGNEJtL43eGh38cB/RyCmXTV54YNESJ8ZGc/dnNDaYrDbx1qSeVmSbYqZPLRWJsAZI5SeJRCgCBSVFtqQXv0zIVUzzF6TVdZ7qF9oCghVToIqfwGJu7ZRaZpgUYE+mORaJiqjbQFOAQFM0GgWUOkAEfkLAkBwMwDMfWB67rczntWmpUlah5wOW/FwOjahHia6/VyLUshwiJsXR1JmNq2FeG2WxqZpqlYKDHUuaLmkMFKrfM0qW1I4b072Om03u8jCet0PKV/+/ndYV2W6+WMdD8s8/z+3d43947BZk7Mdarn8xsKl1rf/fjuftm2275fOhEsy8wkaYrZL1tBmWfpV73em4Yy4tNhJoftxV7ay37eVN8tzytN5Mi7qUgx6+EgzEAlV5tFSut7Wmw4MEcrmb0T5owpqnNGDEbrNr4UQrOeQ8F0kyFS74qITIwptH6ktA7V/2MpNeR7kODMJAOO+YqHmWraPilxr2ZEzA/qdUAwC6b9CjFB/2ORSDzqGBj+ag939yJ1mNsIiCUyqRiQS9W+p940LLgKuIYnoyJo+FogINyCGd28TLW1nUgAQbg6pAoFgCDCGSBQxMn3/fLry+3j7e3Tfn+1201fXm4wLZe9O/Hu0K0TQamlLnw8lrYDEm57m9bl+f1JRIwwPMxJLYrgfDj8+MdnM71c97e3/nbejs/rcT1d73di2Zp9/fxW5/n9D88suN3a5dItYllmd9LWVPs819PToU7JSwvCmJYyTWy9zWWeKqq2Ztrbvjyv756Pr1+/3W/3aeJ5OejF9tZvV217x8x2ltL2vjeLoG4GjnvzhqHgYP3Dj8fTaSJOo3WgO3FBCo4UmOQXY8QMCGAhOfAxcwpI6lQEIrjGd2FhHp0slEfPAJWFI3LOFWopZhbZA0EGuyuJpCInR0kPElMgckSYdiQMp67KhOkpAaABwy88RIaJpUxbrhQEsFFPOwRO0+TuTNRVh9Iaoncd8/ZcR2XDO3axQEAp2sTkgxMyszWTPJwRIcuinA2lRd7NetdaysjrEEYEcyulqHZEtHBBzjFqBoS5JWkyFykxIIeErg86PAKEZ6jL4MbAAL2oKTMTMTEGUqSt+dETl1LyQsuhcDopHrKn8Rc3UyY0T5spjEDkIY8LTFu/WUSYW6niZgw0hLE5ADMHByqc9aap1VKBwFRzfIQ4Zq9DMBiBQeE2ZHwQEC61ejMWLFVKtcv5TlUwgpjLUs1cTZkJOcIdGQmRFcmhHiYkdQdtdpjXQFCDdaruzIDLPBu6gyEjGoqwdmh7PxzWeTmZNaEpwg6HGQDPr9cylVrrYV26dxbacNt3uZ1v0R2ZlrV018qTsPd9r7U8PZ+sa9+bNX1+f5rmuk87EQvx9fVamctSUQEs0JF37B23u0+FL+f2w/sP0e/by95Ut3Z//pf3z//0gScCICTK4gkJVR0RI6Brx9E0UYTG0DtiRNqsUk3ApoZEmbTjEcIFwokxXTyIgKktGITRnvUEjS86sYieWBgicjcAQ+bwkFLNunugjZVdmhOBOWzgUzwABpEfACH9tCScFngWMo2A4FLAHQh620cYLA6S8BhGDQc0ubn2Bggk6N2QpO97KdK1ZcHEhcGARbRrYTYwLuz5XiCqu2rPZWBgAGJ0lToFeHu7nX99vX66X7+065tuW7ycN0WxFs0ci2z3fZ64VK6TrAvfzjciYoZ1mcpasdQg6Bq3a/vybd89Dqfln376cFP9+vnyem4R9M///IdpXb9+OneD3vR63o7Ph+WwmNnr631rAcZlqsfDu9v96tYPcz0+HcI3DJRC4DodKkcH9GVZ5nkCsO1+37WfnuanY71fbvfz/fn5NE9lv/u+bderbbtGWIIfIGDbFZHzSzf0jm4eSHFa67vn5Xisrg0FcAQ8DENP4i2ZS4xNCYJHZlVHWKpxrXURVjcgyg0QplKeeHiaAiHZM0krUK/TtO13AmQhs8iRcY4icmSfxzfiAPPRkCdAst9zjpTYGnyoY1Lekk0qRo5MwlVJBLLXcXxgbXFvW+6O83+SegEzT5RpZkBGBCC6jrI7LwILQ6CkQUhadgfvFEaJmnU9IORQ5VFTZc0FHgMcmnbZhE74o1EY8uZkYoyXOnuLLJBy4w6A4zpS18x+FxYI58rpd3eP1MMlvccemWIpVI1wYs4QpRyeCoFIsfwIRIAwBmrY3VOTF2NtmwKpIYzFERCRk1qmEUrsDshEkXnfIiOfDAmQEQLyJ3GAnEINCzSAB5hqQAAaFZgPVUrZ99Z7hwBVX5fldr+2fedCtRBgCJGSh5kQ8yKX8w3dMaJOkwS72b731lR2BoJMLw4PRAK3ULjf9vrhQMSBbnuPOh/WVZvfb3u3KwmWSYLi+LTmuJCAbtf7y7dzgG+ET6cDl7W7moZ3D9e5zPvW5rkWkdfPr9d93247Ca3LQRahwDovYVC3AKVt25Ttej5PNQiCNsNXuMWbdz/8eFreHQGRiFwtg3ZFJHKKCuBu4d9VlegYSAMHNNTxxCxkqigMnj7rUYhkyZKgf4C0xiMBxoODRgRDkp/DWaKBOwdwJFNNfbOImLVEKyR0JccC7sBSIvJ+CTcjQJRUdqZoInGSESlgjhAp6VZLXRwxmnrGYbg6ZZg2AQFkZOljH6ZCghgOoN2IqasChA7iFpkqEXkqCIlS9IlEQoKlxq73l+v2++v2+Xp99X6H1y+3zWFrcW+qZli4NzU3El5PC4IHQsoB6zRxKcsq07K+vL7eegea1gO//3CYlqoRv388n69NO/zw4Xma189fXm/35gBqcXr/VBkR8Mvr9X7rQvV4OKr1l7dXRvvww3GZBCimqa7HKbxNs8gsbfflsM7rZKa970FwPK1PTxO4tt6neS5FCpbz7XI+33QHkalQ2feuTXftQUDM2v22tWuLDmbgQvj8bn1+d6wTAWr2vtZTJOPuhowE4tpJKN/XhBW7mRTJe4KI7HH4RgJWEViKqUKMAjPXjYMsC9R7E6I8PoGDiVvviFhZmvZx+KQCXxUAMgwkx9FuPmynqcGBGOS+CGIiHmkwoQYIUgoMVRIJl9b2tMYCF9UuXEaF8cAQeDwouYj44C8ked7CHn8dyqqIAMF1RHflaik8VTxpxrFwpzRRMCFEmEMMsXPaxnL889gEACAIc9qwBuorL6y0wPBYFzMSAqWcIwv2wYwbq3AsRfJKZCEfxHwf+n4AQDDzAet3FyEzS/R2zl5zFDh+Y0qIa7rhojAT54swwuUfJN7AyPYbHuQ6sD5O/xj1fQxDPxMgMOHoBL6zpcbPzNNxkomE6bBOTNh3a9rN9PXy5mEiVUoJiPvtGmBSCAP2+1ZE5sMMBNvWTE0QpTIW2H2/t7337hZIQiwAcjw9E3NvaurrsojINE3WvU7zuw/vDqeTsFwuW+ECBm1vBOHW7/ctPA7zYZpmRMltNJCc3r179+O75w/vylws3Nxr4R9/+uFwXKhKV2t9d0J1D4JS+Onp8OO74zqJqp/P19u9tx66QyjqFuefv33968f759fYGypMUoW4UAUgUw8MwxFgm3jecTznk55+dw+EsK4iAp5jFuCUVAOYmalyPkT4qKfcXRXhcZ3nyZ+Uw6yGxqKVIjyldfnOiDB4mFpkMrNHIUYAIc4QgaQvZNsXgGZuictNdyg+cOHhGOAAGW9MwtlxsmQeYcajDuku4FgQE9NwhdKYZWUFlb11fipjEBpu7ohEjhQSe5x/Pn/6r7+//P3ar7Jd4eVlv23WjS6X7kZLXbe9Efky12WdCxMyTEspIoBRKnNFA//29fV82Q+n03E5ruvBNM5n/cfPl2/nplH/6V/+aV4PLy831SDEqU7vf3gvJBr0+8e3t9ddO7rit6/fUPz9D4d/+uN7BL/dbrf7ZTlMLDjN0/PTs1ork6AAMXAVpFjXui4TABpgAGTS1u3Srpd713h+Pp2OB/dQg7uqAdRJWFAzC4iM2JkhzNfj+u7dsU4UodYbAjKzqw2xuiNgBmDFUKuM7Q5F0ptz/avOxJwpIowRYa5ACAimYzDuA/A5sjyzVk7VrvaeUPi8uSMiMUEknKiP1vrwDYdLpTH1TqFqmn4eCrGIAc8npsRcmmm4EWLvjUWkcE+oOFPXnkexDpxJDMZ1/gye6wfIFC9mSR4ejuMeJHz4ZpnJ1DDSLZYfSjCTdY0cffhjKRrj93LTnK5hOnkI8vJRs/zj0UcvDx5JV06Jqzu4a0bQDPTCUFjBKNIgEmGaqMicwGq3MgmOyCdw81IH6N/MkRgB1LSwuI8k1e88Vfkerg0jli/HfCKCiKomkicRhlsqjhKoN0wcw1fsgRzgqe3InouIWCjcc7HBzGlvLUuJsFKrNZ9msahdwUyTwkFCGMBApS7MPK/TvrXEj9Rant4drKNDtH1fntdjOchU7rcdQfZ9f3c83s53QujNI+j8djudDk10mtfr6xuhCZfpOD/R4fWl16lKqU/Pz29v39xUiK623TeTIs/Hp+t2v51bdAKKHZ0olnlC8Xbbb9t9i2Cpy9MKpWzX+3ZvwiEkaM4TuFo90A94uLzeDXzfg5gnEjek4O1yD7M3QFQty4SHAwghkroHRLZ0bkDgGGkbhOz2EDAnocicRARTzVrYPcDB3EotECn4AiayrgiJKaGH3g/GfYJJrJO01I5/+UioyNcvrVXwGPEFRC2T987CCJFMqoAgIRt94VhzpSA1zxEDZ2JEtt6ZUvJAkRHzhBCOhGEQEamKolQCIPa9M6T4LSgy1B4RUSQXqpb+G2FCFrWOSKWwAKLF77+8fvxvv7/+43Vej1Oly7lfzj2oXG7NAUjk5XYxtFKKCAkDMqx1mudyvly4yK6dGQMERf7wT0fE8naNT1/P5nTb4NOX1zpPP/7x4BHb9bptHQDWpyNTaQrn6611C5iWdZlLjd4M8f374w8/PN3ezvu2L2s5HCsRTqUg9pe3byw0TXQ8rPNEbd9636HgVIsUAcdlcds6WOz3FhrH40pC7W7d3CKYZJoWIn59uyFiLbJvFwVA5ML09O5pmighSIVnVwi3jJIlRPVAs0gIR/qwEcc8IMlOkdz4kkIaZP7u2c65uUhV7VyKa25Q05zYvzuNWCTnfmY5+MA8Z9JH4AbumTYYBh4ZsR6aLW5vmvdO701YUoMuhTP2vaux0HBvIRACIWpXqYWIXVVYtGu4SxndSe6uWNg1IHJQT5mzBANAlP8IgJFRfA8qo3CiePK8VlWHRPdx672IILGZjeceUCNDDDAiTDVVlsNcA2Pr/NhBY578QEiRE9thrIcBFsVwB+bvdXS+K5zDJcSuvZYa5lSYxlI+X7ChqxWmnmFhhASk2sYBjYgiic16EFYjp4QQYRZEkNUdDbBLbt5NNSQ7HQTVyJkEJ1f0ESr7b0ZnpMDA3DGCI5BUYeJ936GjqhET5/Dh3thwv+2H41ykBCgjYQAX2Vtre1tPS9MuE3VzDzCzOtcg9MBt70DUu0/L2m4NGd0pAL9+eflneW/C79//8OXL1/ttR5Yy0enpcL3fr5fLYV2fD0/n8ytPxR3cfO8bN5imioaqre/7+aKF4flpPh3XdSWLOL98o95wntdjQQiZpN03FHC3soptHgrLOnW1r18uINivVgqRBXjUWvbLHn61rSPjcjo+/cuPdV1YKLia94jgKil87qZhkbrsXK6SkLkhp+CKs332CGbiYBhlByFkgAy7G7jnAKdIMVNi9shABXZ1TLWxOhMDo7uDJUU93Jwy5jpQmN0MMJhLOEaS44gzuo6IWDjDucYgK5237uAQST9M7zGBmZZSeu8EggiJfgz3jJoYYlXAAeJVZREAGEFpDmlMSdeDiOx7E1AIrFgLkLf++a+f//HfP33+5a2/2TNR39tl7z2iO0RQty6mXbvMNM+1ViLy9TBFhgkIXfftMB+Oh0mmSiyvl60rfPmiX8/hprdbYy4//vi8VOm7ni9XZvrhp5/c4HzeL7ddu63rQeR4325t37Tfj8/TVPF2vtz3/end8fn56LYxUtdmfev99h/+/U+HtXq/7/t+fnttrZcyTfM0Fc7Yd9u77uoGh9Ph+rbf7133cKQ68wQiZb1ebtq9Nzu33pxSff60yPFUj0+TsLUe6a8lfvRaHomDDA9AUtPU1QCmTYzSr5cOKnfAR34Jk6SzBABVG7FYVxH2xF9GpE6SiL5rxrL1HxSZrIBz4IEBNLJ/kwHuqsKcqjgpgpjguYEYMLOM1kBERgkzEkaGgKAife8iAg6pq49IdpCP5BUcahezLNiRHN0RwIglwMaBBY/zM0dgSBTgY1dmhoNtO9bhXTXLZCQwNxaCgKY9b5sMM8mogDAnYbfMyQsIMPNaCwRoV+JUUHs8klQRh/Y5Rys53hnbPabIQlsoZ98BjowRbhGpEHJ3M8vfx9yIEbKcdGcujJgGtmE7HncDDsxCGnYkj3RUtVJKssI9QoQYKcLdQc1qKR4mpbjpyAXNtiM7ygBX5SrD7IZEANZ6uO97E6os3G6te0iRJRZEcO3g4B6lMBggYymENLV9n6ZamIFkPU1fP7/t2w5MDiS1LiIQ3E1LncmldZWC0sr9dv/2cv3n9eBUfvjhDx8/fVzxgIjrOgPG7Xr7+NvvT09HM2umUvj4bpn2ySPa3motp9Pc93q93sPNOux3292kwnI4mGkA9s08LMDW00yBUEivyrPsl5uTlLksx+X13i3iulktYK69Q52p39S2iIDXn+/Xr+3Dv/+w/vQkC2tQhKUEGVwhgIUQKdAAghJ6USTMwQIJIABFIBSy3SMhZhzWFUR4YJUpCIZ5x91hROaiW2AiGfExyYkxUQHEjDtOjml4MFdzGymFVCIC0cI6ohNSXiFmKlwdHosNGlEWMdwGFBZcCCKKcAAkoxURQrUwIwP444wgSAFJHjfaDRkjkFkCgguqqbuVyqUIR0GguLQvf/36y58///ynT/c7ELFsaqq3uzIXdesIzWLbN2I4HpfjoVrv8zoxoXlc7hsgHd49vf/huRa57/r17e3l9X7f+bff7sZlmcrxuPzw04EFt623bvM6n55PEHi5XV8v90A+HA9MU7s1IYYqp+en09NESHvb16Uejmvrt6fToVYmBFmmp+cZKReZ+vrylQmmmd7/8LQK9dashTuYOjjUWvpu2rsHA1MtWKZFDbR5V1OH226Xu+9BJsih70713YdFOKE6CWdJxNk4TAKQsWg0QBQmIlHTdN8mFxMAc1iXTitmjgDAXLqacIlwAEx1Ym6PAZyZDR4cnuQ8mBNzFgAYwUMhBlQkkcmJ4nG1IXBHdA8pCB6DThaQhscxoIlgIovQriTs6jFGT0EsrTVmyQRDGHZbfuBFE6RG2W0AYQKBfCwyvws104b/IIXmjDs9UKNyf0xlRSR78CQlRCS8NNRUisCjvXqspFNgiYCYCNNuKoUfq+EAAHd3D0sPWUp1ER6e3/EL84dzc+sGgKppX84AttFYwJCPgntgYPpLaexjM00+IoyZkEB7BwDB4TtOBm+6P1g4teSIUEQo/XEAgFhEAoOYzUYkDpM8pj2YplYRzgSrXHtERJlqWQoW9DA3A2Ry9GZTzU6NzZQwk3XNTStzqYU5VPdZSrtv0LNUDAhEc3Cfy1xrRUBm4SrmHg5MpbW4b/Dl69v9eociyzr//vGX7XadpmWqNcK/fPn25dtbnWaz2Jq64eF0Op1O8zyZdYt+XOs//+GHp8Pp+to//vLy6bfz5a2pAgC5YyCZauGCgMI4HyZZSJbgiVHQUJkpPPYOb3fbHA1k2/12sa3zjM/tDpfX+PlPX//2f/39/vsZmgkyARNgLntz+2rpCiT0cERUM7O0l0cAJpotFYEYEWYQ7qpp30B8LGPSyZmOME+uFbFwevogLbZu+VxnC4rwaEkxB7CBJEgVZcEyB0kEIwkgJxweEAm/JykRjv9ZjD8CwNXyb/EoDCMHUBmPDElbD+ut57o799gEEebCxeN7a2I+hOS9FAEFBNK7ffrrt7//t88//+Xl7dVvN+9WPn29vlzvSMWdbpu1DiEZV4vLVM0MMUqRrbVdu7mup/X5+VmkvF37//zH17/87evbDc7ncJKnp/kPf1z++V8Oc6X7bX95uzUzoHK79o9fXi6367yWDz8+rcdJSI7Ls1pHdikUCD10WiaZZNuv01zXw+Su97YtczkdDoXqvm373lpXYvzjP/84Vbbe29b3vRGi1Cplco9t2y0QmaTwPBdGUIvz5Xa59fOtN0dFBAQRKgynd8txLTmDQQiCYMQx6w4AhCKSwLWcY1uOfgayDIgZCU1tDBWHEZdsmGYzzkSyWodRQASz5EjG1PzhTi6lWFfr+iBgkg+mt4ZHOGjTAEDOsb6lvHgY0pEAMQMJ8mHOx15Nc16TuRRIKVOOgGSDGzH1vQOgO6h5+MC1AYJqR6IYPrgEG3uRkneAdo8AybM4IvIlHIvThPMEJKeTiBLHIYXNDQDzhc+9nXuCtr/PQ4d+eShVTXPklh0Qk4QbIhKjqmV+d1ZxzDw8Yjh4vBEBOC5SgCjCAMH5zUaUKtZ71v6Z852zuVR3jB4tXIiA0FRT8k9I2rVOJVEq456IgJEqnG4lS3gIZRSBCDySg4CAAi0hFphPgCaOIv2H2XmxkKGVWYrw/bq1u5VlBsFu6hHzVHtr4Ni1k3KEkUfrncs0Fu3hQmRhz8+n8/m87/s0Twyk2qTMDtj2jbmWIq+vl3may7x2tX2za9nu2/7u/dO+bd9eXi3ghx9//BBYpHz9+iZE62ndts3NN90LSZlnd9/2fbvdEEKCplI/v7ap4O3S0V0mqkVqLaWUfb9H3wOkd52Oc638/ETfPp7X9fgZbsum1601hddzm55P4U4Q+920v2B3ML7fNNom/+33/yA4/7gkthwM0qKRBPMBVC5sZkLsBIyUaVmIGGYQ+fV7fhsohI/Fe2FEzHAuEeEM34isdByQ0wkOasPsLbWGB4tgCigA3DQigEjKNC9PUo9Ape23dj+beoSWQl2DOIgxvfFCFADu+tiqUcAIxCFLYFGEZ6MJERjuZi78KCAiVA3TcAPAkl4SJByeGiJypMIogL3Z7Xb++o+XT399++1vr5dL7A2bQQsVIb9ZeZZN/bZpCydBgjg9rRFRq9Q6Ne3BiIKlzE/PJyP48nL+6z++do+ffnpHNL/JdnyCH3+YpbKZv73t316vh+Nyeve87+38dq2H+Wk9qgVS3F7vbYf99gIU757nUhkA1sOKEGptWcu7H95NlZre5jpJkalUs1277ff9+f3Tu+e5CvbW77erb71IEZ4V23m73e5bN8OC67wwl9dv997269W+vbXeY7O47oYFJ4L/P1N/2pvJkWXrgnsyM3d/B5KhUCizqlB9DnA/9P//Nw007u3ug1NVmZIignwHdzfbQ3/YTtUVkIJSQZF8JxvWXutZhga6n8/l5XXiEu6OB/oM3CIdWRCopphO4jSEhoNjKTzUwl01hZN87QIBkFlVhWWYpqczO3YhD/VHRADMPFuGcrkACLXPIjbEALAx0mDiR1skZtgWMMY+pGYC2Y+yKULQ+HS6O+dx2RNWwQeVFo5/F3jUFwIiQEiR9JsyccQnPhkJMSWKEBGEUHNAHjqStyZVwlSIyT0IMXe8iEBAEUrHQvY9qeWJCXXoQb3wrKQBisMvH5RSWhDSUXzDbG6FBTIsRphmjDQnmRoLezgfWi1Zfg2CH+F5Yvo0lSZ9I896lLenQ1OKACIODBvKwplbdgAmRARGNrOU87Klp2ufpPV+5BsOhBxjMgIPfyEdXgFkxIx1Ipjb0VWZ2bzjjpN1r8fJMRAQg5B778iFi5AQkyznmjrX8ABHYqilZIGDhRUhDwuXwsw0DXcRgFb2bcOF8rozzXMfY3dV1TafIhmoczuZ73v/7W//8v7+3QCe66BQHYpAfcd3v7nFy9t1XpZvpd3uDyplOZ0yrudqfYzapmkqxH77/rGOPp/n/8f167521X776ITx9ddLVCilQDidaity//mxrU+ixcbOFRDidG22ex8xzCPi47EX9j4CCRW0ApvTusOusP+vn1jtm/96+nYhoUzLQwTycS4mSmLrMeONcGIxHQgeCUjwaFPLhj9CFBHrnVgqt4AwpogQRkeKIA9jIQBsremwcC9MUgQhV29C5jCHGhFgbkPDkZfT9fzyrc5vBrSv9yexRriBoQNSYFrIlYk9gpEI2CJXgXQHkKki0ideIsfFTvn1gYkK8FARsWMyHG6e95Z0B4FpKexq5zYxcr/v7//x/vGP+/fft//6Xz8/PsZzg8cwIAlzcZtLeexjVx/hRsEEVZirqFlFcaT0m8ynZUSsY6z7eH9//vrty8vbhZz/68/315dyul7B7bHZ7Tb+/L5++fXy9vVt27t2JUYiCo1lmrduFrZtKzJfX5eX11OYnq8zMY1tm+Z6fT072PfvP1lwnqaX1xdy7eoecbmel1MVid7H4/7Y135dliZT3/rzuT3XfeuDmE7zMrfp8eh7t9uj//jZtx26ezcry2w+WGAf28uX09dvr+dTA985qfFBPgw5P8hpuHIRCaJQk1LG2Fmq2hCpZgm9D0LK8FMKDFxkZDUYU4axk76pbkwMCOkuiaAAwMjGUwgLYPjMNmEWYyiONBgERGTzxxi1VveQwtqV6IhLSSlmxoLmCIEsZKAUxyUAkTwsJYp8oId5yQ8HZ6KvIcAtgJgFhCXtjBBgecBNW2QEQJgpBoipw9Ffdowzw8MhPIIgxlApkroYAOQDzjFvbj/hERSYDNFjcH7kj/M+kQVPeCSzjuYDt9wPEIL8k33hnztnooEQwIMAE8qa0K8scWWII8YN7um2tmEiBcCHqSTU4fNjhYQ6hoi4WWEsLJF5jXCEoExtp5nEPZHIEGA+SpGIyBTYUYhmlq9W5qSTipr/40+inAgfR0HOaaUBAxOCI4hgH2YWgdnw1+/bGadS2D3MhtngWnzb+zAEjrAwa3Pbu+/79vL6y9bH/bmt+2YBp/Pl8XhOp4WlvN8/LpeXfTymNo19VYUYTqWFw/1ja2WaTkvgfrmyuY2uobqcT6fzMjZ7bg8GZob6Vt5vHwyAFC9vS994qsXVn7etb7ycKpBb91Lrl99++/nHn6aKBMP7+lhDZUQX4ftziOATR6s8SXG1sQ+fynBQ4nUfyvH7f3xc3k5lJhlS5grgQKhDCwsRaR+t1RywZ7AXCYAkIoKyk4cAvbWS1cGEkWVnRK7mVTjbJQsRIqpHLvUY1qogRhjnGxQQmQUJpNQMCFVqYk71PF/fztdvbX5Blv10KXWq81X1of3pY8dwH4ZcCMmtEwIZZBqUCJtIHOgSMjMRVjME8nAzEyZE9DB3h8PgkIZXRgIpxX0QspkLEHUlQwkaah//8fjH/+uPjx/7P/7jffe2mzzGcM68MUJ4NBnu997VwdXKNE1LE87rkKipYSynE7L4sB8fH7VOUvnr12ud6o8fH6dzO5/nPuzHj/3H+7Pv8du/fHn5cunb/nxuPdzQpdbLcln38bz3+31Tj+UU07kAep1KROzrfjotp+tSBbbntvX9dT6dzpO56dj3fZ2mejrXwhE+eldT//LLG3ns9/Vx2+73VSOW6wmRGk/r6us6Hvf+/rH3HXo3ZxQRhbTqqgD8+i9fvny7MvmnVp1uWgIKzGYSzPyd5lhRzSKOVtcIxxxVJsyFMaeM7u6m+FmbnnVUERZ4zI0i25uzLuLQZ6IUiTAPZ2JTR8DMkRELJkEoD4mWnhfKmVEGvpIhQUIRAAwUiIhjKBwcHUmXTamSP9o0mWz4V1EKBGQsMUX0AFfNpf8QRdLewsy9d4q/gookcAijkX6b3ALCgwkPuKanmzTvAJm3P4YBOTEOh8wTQAAgHFk4xIjgDOkCQoSaExFifAJ8QtVJ+BjURNDhuKC/rKDMDOD5oCAgN5K8TCFRchzRgTnrZ1AtCktuS4dfJwKJ0rGa/8kR7URwc0wiS0S4CzJk4M1cSjkU25RoBT+FsU+0V9LcmCGzS6oBAmFExETqjkypGHi4hRYUIPIxahELMFd1JwohrCxZurDtO4lcz20p8/rcqIgU2PtjOb0gswf2sde5NY/bxzOQzaxN7eP9drm8jEf/+Ph4eXvhQr13QlGD+7qdTnMVud02D3z9+na7vQuXGApEvfcA+Nd/+fu+nQvxn3/8zjVe3i63j3d3J3RmmU+VoO7ruu99fZj6ihhoWL+Wy/XaH+vzvgGVearrvb+8LASq6s9V9xEjXNUBg4A/NrXAMdQQPOTjx/bP/+8PhzGf6/W3F6Dgkj3AgcBzkXzHltqYQbedhNEtAiA4rZ0iDAAeKoWFxdSwkLu3mv8+wIxLczchDKJwQ4JPGQZ1WHo+iQ/RL8CQJRCLzPV6acu5Xl9au0YES0WUtlzHdjfdfKw2tu1xRx9Saf3Y0weMGizAJJTcNw+Uo0CckLNFwHwQIgjp6IiQPz7SnJruCXfXKEKE2H+u2l2CabL7ffz+f73//r8e7x/b7Rkj4uOpu/8FrFEWUY/dTc2RohBXYSKgwiSQVdrzaRFpqrr13QNrK7/++nZ9PT2fe61lQnne19+/Px/3Xeb5t6+XubV9HY/nPrQj0svler5c9t2fz36/bR6xLOXlywnM9tVglnBeluV0Wghwu2+9P19eL9++fa0FbF/D7XxeppkF3PrmYRH466+/gNn9+/Pj+8fWRwSellZr0wGjx/1j/f7H43bbw1AKN5C998o0VF0HUDDG9Tp9/fpSpnAdYYaR0iCEg0LwsV77ZwAJRNggHMPNmY/Ubjojc038nEgddQ6AaEOlFkQGCAcnljEGwmF9PNCeiGMMEmbi3rc8ujBjkTZGj8QC5TmXjjQSILgnRX8ciVQ1JiKiPnqGEdxUWIhw2/dam4cyF9VxiFbMeWvM2QMiI0EAMvEYI4+qqsfOZzo8/cd5fCUCQHQQZjL3AEhCAmbNOoB/Jtrz2F5EzD0sBZEsUTkiyIT/PUXw1F0RWQgS0UX0V23T4e086taQBBKK4m5cJBWk7IRBThy/JRnfzWuRPgYn658kBzlSmBNyLWjJxIUQ4WSL5fIfny9RXgmSRUpEwBHuQcGUb5s0AwMGfQLL4kAhHZwJYyFEDM8uVozjruNIgOFE4pByGZlbGJRWzq8n2+5haX3CYSZFIgjAEDCA1nXjNlORxuQefevcRASBrFX2VRF9meZNIxDMYZoXC1wfuw5lKafzfF8fb1+uPnwfilS6RUGaLq/weD6eHU8807R1ezz217evz/15/Xr6/b/+8+f7I/D+eDxqFQCzYc/bY5oqMzJCa1JLHb2L0Mvrb/eft953Vba99/v4sf0xn5d9Uy6laIRZFQ4jQZxrUYUd3czYKTCqcCB009wp125c+ec/19YoLpuYl4ZxqoAWreIyAxgB1VYCjQinqaQ3AyzsCI0Ts0QY8aRjCHPoyCsjcwEPRg4IYdptREQRYSk2FMIDPNSyH9t1pM0f0GspVGsfCFKl1NPlS62nQAZiRJ2RZjxv62mMNcbDx05A/flOyUoMZOIiXKUyC4HsvgvatMyPj0d6PyQ/Uu7AkBVO6fPIFBvQcXiCiCJCFvoc+x/P/r5VKe7b9+/rP/7z/c8f2959VXw8N0XOtptuKhgGAeFb165aCsylCjIDFWYHD4I6zy+v5+8/Hpt1Yr6+TP/6b78y4B+///jxsTmQ7tv92UPpy69fl9OMiH3447Hf7/vbr5dpmknYNJ637eP7XTeXxi9v16nR2PZpmYpIrWVZZiJ83B9Esczz9XLtz83EBZ0Y6lSYIuUIltJEzFXX/ef7z9vHvdQ2nadlmQhoW9ef3+/323g8RjjUuXiIWieW3i1TGA7jNJW//9vXuoBZJyYIRyRkJPUI90QaIQElggBHH1IlwQsiNRyKlF33nNTT53HeRqINDtcAC4fZEbMi1NEJafQhIiiMFoCpewNEqA5AFC4JuFQf7sZCQmQe4d7VMk+Q3V92nLkhG8aYJQ9wro5EEahuDJTJGHN3H3nadfK8nZh5rl1jDCklzA2O47pHEDJAgrOOnOXn4cnDHZCSBkpunjpQrv8RAOm+S84BZLYriAGB/DNIdeTl7CjVOxBvCODxl0N29F5rdo7HXxcWhMi2TDfNShd3twQ2JEIiHDIoDEl6QU0ufEYLKCCCC2fwHhFUR/pzaqlmg+lo5kQIPJZyztRfREiRgzbz6caKcBImZNPOhVUPxmd4kNAYQwpJEQKIhIEHsnyaOSC9gznQ14CjnlyqOERbaoBtQyvPCRHb9lGFmUr4AKF17WUqk7R87UfX6TQT4rpvJHK+nm7vj8tbW5bZEBFhjN6WCYDWrWsftZVlosfzFk5jdPOGTutji0mm88XjPjR+vD9Pp7nOsK1e6hyOv/7629vX0fs2fCeL28e6tOn8+mJjR4Tz9eTuTFiaeA8q8eXby7o93Zfb958Qvr5/7H++u3ldKguP0NaW9dnbpcwYm9u2uyMTtk035MMfQAwFRYdG4cdqP/9rtQ/HfSwvtT2pngv44CYlCRmgWfaT238aY+hALANGZBszl5peOVNlKQhBlFdrhDACMDdCx0BhRqRhCp/wQQQMHdklXESchMiltTqdqLYAzEAAupdS1JSEY2TD4AhXCAUA005chBlbQwACzskRlepmRIhBLIcm0GpFptBBQOaGmPNDHvtOaX8a3uYZTJ/fHz/+97uuITAet9vPh/75c/1524HKMDAAU/PcVzzKVABgWKgHl7wOY23FwXrfsIowv/365c9/fl93rS/15bS0udzfb7//109Fvn55xdq+/9d7nfh8vQDX/dkfj/V236Z5+vb3b6+vp8e63x/9/c/H+/vqI9o8vb1d51Yf94/WpLYGSNveEeL9YyzLJMxVGjqez2eUof2xLNN5qaZbeCyXk40eoz9uz+fH7f54nC5LKWWaGhNtq44x+rBtGxAurUite3eHTFYHcclC2n/7f359vQL7oAqHC9jdMqbpLqV4/v8UqDFnoYCZHCQCxKEdAUTELJLxmc4R+yyddVfikuxoFtZcuJFAMM3lGVR0NBJyVSIOC9Mhpan2TKU4xF8um/SRpKgQkZgcAoiMjKgPRCACxrqPnT8thWYGjAghzOmfPKxDGaMZHQlLLUQ0bBzeaEQEdDQ7/O6SPAXEkg7sDEtLEuPoiEeHagZPknpm6CiUdZS58NERuEyfpf138ZYerdZOzJHH2wgzLbXkXopMmZUAQDUTtPikCQFTbhiYBgpK1u4xlUhcRjo3k8GQM1h3B3dAF6EsbWBO1y/mNSLdI5zwpqNWzJlJh2Y1poiMsVephUqYjdiZyUxZmLO01hw85japqRCbD8y4KWFYUJoA4+AZdN8z70ZZEQzh7qVw3o8MjEXYeN+7h9dSWFpQYIn1sbepUqHWmnfX3utUzetQP001zuA2GFruxIiQAWNA+vh4IML5fEGg5+MJgc/H+svr22M893V1i9Pl+ri/72vf1/7nnz/nuU7nVif45eu1TWU5XbNd7Hren8/71CYm6M993+8QMMBY4LycSVBNgYEYX3556fuT8frx8e49+jqqYFB5bg8bsI/ggnUpJXxT32CQcDcnQg8Cd0YuFVS9Fh5r3Nc+I7eQUAd0Aon1iYKMFQIIAsN1jFzPSapqpwgP5MKp5TGRmWZ+3s2YM8jnhunvd8RwVaA0OFiMEW5UCgRiuIUBODsN0/DAMiFQKVME6ujmQcjuHsYBvq33/f5B9hj7x377ibGru+4bNyIpQsVVgXKpSeKIYIV1H2EgLDpGOExzddXwAEehZhDoMM2LWwAhNQlXHEPv/fF921bwbu8/x13hfe27w1zmbbs7hAMTUSItCSgh0ZnDJ4CptfSSjd0uSy2tfP/j+3P0OvHfvn4x8/WxipTL62vvvq/x/T//3J7jdL0Opfv7bX3uxPTlty8AUEQ+3h+32/bjfbu/b+Ywn6faJkDY115KOZ0vANC33bT3bRfEL1+ub6/nqeBprgH6uL+/XFth0jEYEaRSjDFsuz+td1U9Lcvb29W6SynDrPeuw54fGzhyxpcddQAzmTmWqmEjLOxxuvx6ORfiYd2y6ifJl+mdR0IwJM5oZ5JayNWADHPBYQJI50xAWNpNcuU8+skjkBggXJW5mHqVmu3QAHFgcrKbJoJyKff4pIYkfzDSgJQWGUz/0GEH8HSaFCmmAyCYaYzBJKoakgVZyZx3YWYWjQxxpl8RDqi+Wy0tdRodmiOQtFamkiGC7uGmOeJFBPNgJhY2D8kILlGqFp4lLXDcfdKjiqaWh/0Ah5zBuUPWK4Znuso9Gy0OlSfciZGZdSgL52KdBh4hpGxlSx3fXCRRMGnpz/vwZ7VkXgIIXZU4owkAEAfPgjnM3RyOWC6NocLsEeZ+GFsxQ2QBgFkq8Dksch2jlJKiITKGAiAwcjjApwHJwsEHEqkpfbpL81cKACbSochyZKzNmdmGooRbSCl1kpdvr7//549t34TnWuqu1s1KCXWfp8mc0Id2Y0LgbCUlM5vnGdZt6+vU5uc2THcqc5srddcAjbi8fgWkXYeF1an2YUS+bvtz3+s8ffz8MMT1+/pyPeuuj+czAlT9/fYAHM/nPi3tdJmXWpoIunmH98etFj7NS98HopdauKBjdDfGqEXU7Ovb5bZWbX2+LD//+L4/d4cARgt3ifHsgRxopcJu2FUZRYjH6MKZkYZAL0UKAwOAeV9hv/XL6YTrwBJ+e/I8sUOAkYdIMQp3V7UqpRYZ+0CkcCORMfY8f+TbDwVd1Qlde6lzmBURtxHJH0FHQOsdEAHMXZmIWMzUrNAwYKLKJGJdiXtQ6DAPQIh6OtsY2+3W7++2/4j+HOsdvCOC7xuIREfA8NFNB1MFcLBBKmBGZkKcpgDKGKTuEZwVj8l+FhFgGj7CHZy9h606Bv7zHx/rZt1kDViHo5SfQxXBgwwiIoYNIFA3MwMSRvIxlstUazEwDZ9aLdPUu/358XF9O/3tX76u69q7LaeTBt5+Pj8+tttDeaqX6xszv/94BMT5epZWAqhvtq7Px9rff6w6QgdPJ2nzZN6HojFOpQDLx8dHXx/Xl0sr9csvl7fXk+teloUkno/76TpfrjOCQXgVcLP9OZ6PJ0F+QNr1chKQp24B0Yeu+7h/rK5AQFOr7rh2TQbC0Liv60B30tfX9uvX19O5mmsRYBIbI124lrSoYXigDcA8ONkbjAigamntJ+KsdEBEEVa19LgQASNlEWl+mbkXKeaavO/SqvbBpUQYE6sm4sSkVlOFgIQzu4di4GFJP5jkpoZC6Y7J3C8iu3s2lbJQGJtpKTXHDEgcAOrDIeDYijKxG1Ix88CYHbw58hQiP87NREcWEj6HuKP3UqQPDTDMnjMkynMr4DH3yDsGAVH6hw6o9OG9djcCCggPy/0zaQ256Oe2RkTZ1l1rVdWcTacnaahKYTxCXFBEVPVo/jKHT9HezUvhfBjhyTlKnDRk7CKlpKQrFRb87FInJteRJmtiHn1UkeQauVsoEB9+p3x2gtDTeJWaTzl60KQW7btwgUznRxZAorsLcRY0emTn7QHoIOZEm+U+0fveTm1+Xeb7/vSdCStJGaJhWx9IUUyFuVQmhFIqgIVb33ubl9H31ooOCwgmwPD51GRiqXXsutrYxocUlDb1PsC5lgJ8nAi48el8+v79x+l8+v2P76d5Pp+v63NDwLlNe3/+/HPDPx6BQW5FpBZupQpxWG/n+9QYyS/cytz29bmc5/vHnYkA7B+3nxhgugXC6bLU1sLi/vGYz/P63Ou59GdvwqYgoLvDCAWmzGgAQCEkhFbEbThSQdYNvLo9OofDThGq97UzytR0OLgBUpJwIiy5m+CBUazvgBmlbu4O6OGKAKaDEZgy/MJCHG7hPEaXWjBNxqHhroFlRnRHM1codTLz0QfK1rsGitRZuzHzEFYd1rf19s62gnd22/cNwkJ3fSIUJSbru7qXMoU7C4FZjM6IoYOA3RRRQp0RixRzExF3APNQzU4/QipcH89VN3zc7dnhtsXuMYg2AwQaoQCs4cik5q3W8EBmYVnXHhCtlVrK2vc6FUeo8zSGfdzfX67L25fr/fbow0+n8/Pu7x+P231fdwCul/kFGN/vq7u35TQct9veu3/c1sdzaIR2LFLqSU7X17W/F2GFYIc+/B//+U9Cn+e2nJeX8+n1y4nDXl6v8ySE/suXl2khokhONyK47du21bkWbs/7vU6tLdPt+0d3j+Hv7/fb+/N+W1Eah7C0260PA3XcR5iTBRACQ/z2r1+u50kKZnZi7EoAXEoafnKJgIjAzxhUHAG9NNJkYNwjji4YCEQspZhZ4kACMVsbiZNYT+ZGhIHZy6YHSRRoqB4ctIDPISggETEn4PlwujsEwudHwQgRhTH3GEAIKEcFuhIyJIXAMuvq4ISUowU9CHRhea3MSCki2cj8mvluxAfQXk0h0N1KbaaG6LXWfO+lbV/yach5bwKNGdnNRMRczXPVxTDPGw0JEZBZdrRS/mwWSgIGORAxoKdNCBHt4FwHceLgIaU0YUoukVour8FEkAzgI1UWidzxMPocTSNjykr5ApmZEAGmqoNHb5cqApq7MKqZZI4XSXXkJJoQmVHV3F0qpy1PiBCRibWr1EKI4F5KAcBwikxncCKBGRGK1DQTihQzhQBiZiK1hN8jBmZ+sBSBgoFhpkBCHDDCA7Srw/O6zHOZzFXVlqVqDHfvfUxLyaK4MXaWAuD6XNvpBBiBXqf6+x8/lmWucxOpY7d5ksL1fn8817V3A+HlcnZwYnrc11rLy+tl21btBkGFp/DR5lYIAOI0tyY8xl74bK5CPJ2m2qQ/ex86fj5K5Qwm3Z53Di8EblYqE9F6f0bosChTMfP5dPL7fmplXd3ZLcDdW5FGFOaFCcArATNJBAc3QVDb3lcMmiZGCt92LYGuVEukayKBDY5uihHIAmHgRowW5sYYTuE5HQ4PCDQdYZZ1LoRorgRgew8EDAwPG3sphZHUeriFESOZmtrT1VFmd4zZ+64sUoSR2fse48niY1219+jdfNj2dKQAGD3GvgY49C41z60diNxAbczzEmba+3Rehob7YBZ93KkIAfq+I2MVtuExMFb4+fvzudru9NEtRIYZIMcnbRQQPLwWAcQAx4QUEiFjbQJEXGS4IdNQ39etTvXlemFgCwDA//rzvn7s3cKcSdr57YqI7++PMK/ned103fu26baOrZsZl8aBRuQE+LjfuCAyrevOjKMPEX79+uXXX1+Xc2lEp7m2SrWghzJBKULIrts0ldG39bkCRGlTEQpTAGxTVY1t3YFkX7sNtw6tNu9c69Q7qcJw7MO3oY+hhuZmU/jby+mXL1dhNt0ZMX3eepQSHvGRvvfcCZDQxkCSdE8ehaOY/MGI8AAEcyc4BHcEBOj7zlzAD2epO7gdvU+InNh/NcumE2TMcLowRfIuVYklDlRUlFozI0ZAiS1AzML3w1Tqlgf5PKsfTtGIXEnIzdyUC2dwDLLHIv9bAEtFC4MQXT0xR0Nz4pXcMyMm+4wZI3G4IYAQZqtdIB09ZElTcPcwSHEmO/AiY296hB5JKI5V/ohH0WGYjQxAQzjhcZpGQjOTIpDFLMKQX+d5MHdCNDXAwAPbBHS4izzndVIlnR4oAtkBYMbMmTpGZjfDCERKTmdOlYsIHqWPxwKdqOscDSFEeGQPMBK4qoUToZkeIqCH1OIRadsHAGEeY2QXJloeEI7XL11MIhxuyOxqjtGatDMul1nXYQ947qsQF5bn2hFJt4CJcqRuI7Z9SM0+AjS1IlznoiNOp9O6r0Rh++YZfgt8uZ6Hed8HiUxT8/Buo83NIfquY/Q2T6rDQEHDVO+PZ6TIhSilIDAA1DYheBE+Xy77uI++vp4vQlGLIEBtzcKf62MoM+C+ba1mfw1W4sq0WRehWuu+Pj2b0BmL8L7rdW7vfZuEu3VhmguNPQRdh3qFc5FKyKZSuBb03tGL2Ziwjfc70ykIqRQ+ZMs0SvjxfrAACD66GjFcubBpRIbITAkJgSNi9A05S6RJimjvhGRjRDLcCHXsARGmwDL2bgEB1Ledy2jTm5vut1s7LW7DxtbXBwYg0Ng2swFhoKOUhu4MFLaDDg8gGejU0/vg4n0Qka7PfCD95mBap2Jmtq4x2ABs9FrFmHSH/gEff+xDcTV/7DaCrBsUwqDhmtdUTIGCwdTMPWwEkIURYqmlm6r26VTmafbw83l5fb1IlY+PJ5J8PPaPZ0+yb53r5cuXALt97BC07eO5rY+9jzG6uodQqUQIoVNFAM2PkVvs20Bw5DIt0y9f315fz61xFbmcyvVyIrQ2ke5eG1cmRlDA9bG69QgIj/l0QoDHjz9JBAn6tukwABy72ebrU0GhSClFfv58rHtXhMeumwUyi0Tf1m9//+Xbb19bE/ddmDAwEu8ekRPHCHDNjmgwN4KD9Gya5b2R5kUAd1cmJuHRLZkjf9nWmSgLG0hImDQsY8bp30/gvIhk07WOwUSRd1ARzusHAiGYAxL1vmckEBEIycMIOGmYZgqALBxu2fNFR5kMUcLws54XydSI5dNIFoiUGdsssI0sLHJFIhHJxrNSxNzMAtEPAvZfEPsIcXPkI86aJY8WkWvlkafytP8fAJ9jGyEyB0FGjAAQEncjYg9LlDTlncvjvwl5foQX4iDiBibT7Zi7HGiVPI+nkTPbQtKpndtDsp2JKYkLkT8vIOfv5g5gmcD8q8/PXJnY1UiOwQ7QMYLPBV11YMI2AQnBzctUMuMFCDZGrW3onoFSUy21uY9CQkRDB7Iczt4IJjFTBHC1sKhFTAcTSsFhBiI+AgimViE4Ta57V9U+twKAwJEJxm3dSpmREMxJ8NkfxLJvK89zKZVFhsW+99YagJRSSpnujweEVaki8qCNhDyoNNFNjTUU9n3PF6XMVc1EcJpnYAgNNb09fjJFTu53U1SfpL68XK+v58d66dtuqoAhhNZ3cAMiBReidVjWuN0/nkxNpBSpHY0IpobmVpnnQm5jLlSYRqCAAcRUqyCxBIi3qWT2OwIISZ87FSytBiNRYcYxzMAwY4oBXLIyzsMs3AMUKWyoKSSeP8ITnHtMnrLiA9C6ZswwgNA8m+siUl5ScAq3bd/qRC+/XLZ1u79/r6cJIKyvNjZAWB8fQBjDRCRMhQUAwsLGgEAMCIv9+TQdaC51sr5HhDOXOu3bGkQsNJDc3bcOGapHs9D1MbwL6PT+5+OxDnXaNZg41VoLDQR3zds9H9IvmFltzT00IAI8yyYjhNNFzq1VJP64d0d5PPT+NAdmkfm0zJf5/f1nN+waNqIrJKknP8MWoyAzicM+hpZax+iqMZ1rk3p+uVyvp9N1+eXLtRYsTOdTOZ3aVFmKqD2loJREPkaRsvWt77sUJkQKQTREHF2RwrZhFhjOwN4RlUmICr/fnx+Pp0GYQY7Z5rL8fPy+XOq3f399e51FMmIJEQEWJOThMZyLuIG7p7wMnwK15dLhR+oiAnUYEyOSDcfPVUuYh/as0AqECGcgU0MC9CP7+Vk46ulqCXdEAqRww1yL48jEhv1VjJjIT3BzRkQSs5G1KsmSyU71PNfmOzcDaHAEFx2OkKxD5M/0VLGSvJl9MGoqUly9eydGRB463F245HfzCETSoQjAhCKFk62bw9UsLM6gMDMf4S8/0ED5d8p4S4C5Hu4dxsM+lTrJQcUE/0TaIiEj5zbj7vmNI+BIqR34IQgAV2NhYsyRfIJ9iCANT3n2SdYHMQpzZOyzlIgwQ2Y2cyTyNH26I9KwIcRmenyvzwRgSljCRHx4/wFCpERSXNyy59KOOvNwMylF+15agUDVUUuBg9vkmfYspYSbObCQRwzVKu18Xb4vH9sPC4RSuVBx2MVFVW0MYN/MSjsfPGMAsKOip05lbBpI5lanNsYepdSJCfH6dgYqz0cHcMaYlro9tmE7BItAACMV1TFoSOUMndpQLgQO27qfXxapBUODwMKgK1YCgnV7quoD/HSZdu1taURBjLVOajtmql5HEQm1sWstVceYp7N3fty67WN0lyLu0Qqr2t4NdNTQKkgY08LhoyGyeGvIAkTIM9eFqAkSspQIRTUY3dwBI0IQI9PwTGQeOnRZ5t53U0MmHJQzrnwhINESQgAHutnBhbgPzY9JvoMSzpEUYKRgAnAFBNABWT9gfR/PwkKIY+/hDhiqo5TipmYdqQBhYTRQhgABAABJREFUmO26EzO62a5w2KzdhrpvmG+d8K7mqh4AtXo3qQJquu19jEDvCEITjFg/Httz/Pjj9lxdmIdZk7JnU1Uk4wWYChL6GBA4lUrE6oMIhUXVAnSe2jzPHno6n0b4P398uOO66jbMCc+n89svL7uOH+/vamjA67o/bvuwAOQAcHQmIoixbyJKBMyFQLjwaamn67xc2svbeW7l+nq6vi7W+/XSSiV0H2MbFqXi9XqtQtv6JKFwG6OrmdRyPp1akefzw9VDvYePTes0o2Pvtt3ehWtA9OHmjowB6B49Ipg/+oe5/vLr5X/8H29fvy6EPSMQTKzqoUduNqE5zKna0zGSdCOkcIPMBUUgU8oPCSPAnPxJUVO3IEq6gwpz9iRyySMmsHBSfIhFc2aQ6xqme4UC0iMakFAawIg42sSQ3N2y7g05acfuBpF2Sj90iTjStURiYOGePs4DVg/gmi0FXUrLEjMAZM7Wlkh5hllUNX2u6X7MTwcCpFqOCAenlEVUNe8syORDiThppWEDiQE/21RUuUhrbYzOxMJibon9QTgU9rRcIR5hF0A0y0mI56E7T9+H1HVoahkEzpGvH6jWI7uQlyn0cFBPF38ur3l+E6JsWMpvIUkEigiPOpV9dM5+ZA0RQQq3OLATyW/5LOTM4AMRADGEk3DCLsyMiZEweXMOoBkFBMAAD5NDeoYIGENz0mgRLFRrQY/S6PXL9Xu/7TfVsPC0J9myTPvTPNzCt21vINOpmqpUGrtH6ESFmYbZtMx9qEhrhYWliIyhCrEsNQLaVK17m6tppFehtkktilRhHH0UaXWq22NLDM48o9m+3tdlLqfTuYg3hvO5tUlMh6rufUOEVmUW9vDuMbRXqc/7B7n70I99R4vog5HBUajMEz8fH2Co1imAEYd6YaaGJXxuQgRzQwIo0thN2KVQbSyFp3OtM8skbZbSJHw4ug0Dc6xciyCyE2VSxtx0mOUm5AFIIgyE3SzMAMBMU8MFgkAPcFeHBkRIgaqRMcHPMV1a/8J0CwM8WkFICucRpLU5A2TMYrqHg5xmBN6fd6QIjGFb4eLaczxEhH3dU2IiBFVlYnPDsOxCjaFSxfaeJZe6bSAUADwDAvU+7rdnIJIQs9dKATwgNQ7Kj6QImXvh4uYO5NkX5cbBpp0IrpeTeZxO526xmoXD7b7qcJLy9sv126+/ftxu63MXqd3G+/tjew5TZOTeYz5XO4i9TMKlFCRsc6vSpMj5cm4zn65tXmRucj43BFuWUgoTmDSqTbhSm2qtomNHRmB4fDwiqNS2zDMxqu23j9v9/YaEfR9TbdPU7t8fv//j+z7CIdQNkba1kzAGDLWutvo2dLss/Nvf315Pc63uOpAAHPQo/4YjvpNQhMi2qTgCsQBjDMQEPEQAYRhylhFlfyEyULaBQngt81A3MyBI6caGZQkoEoTl+TIyw+RuIrWPbW7LsB2A0hafAQJCRCSzT4E6W68RdAwp5cCjpZ+UDjROeLhlYMUBgo/CSEzjg7sJM5M4J6chLA55JzK0a4ZEqspEib8Lhpx+M7OaHgNwQsmHZMPy/ISMaQY95DTT9HpmhAEI8ldRUCSM7CnGg0mE/N9m5E9kWzqpjY9QsZVazYyR4POIf7g/jygk5mcz/jKJehwUObeEJulQooOyEuAQ4BCUtPejjiYyBQaEw/WTeJ1HcudAJAx3LmxZF5xgDHckEik2BiOaWmmFmcfojGRhgoLZbQtphY2ckyOgHXxTTmyRjl1kiqO12C2sTtLm0qaKiohFFbhQBBBFmUrfvZSybV0aq+pymoBDVVO6LkKkTgDneenuYMYESGCGgrTpIEJgYyKQfHOMy2UWmX6+/6x1EkIC7KNPc1tO09h3d3S1qZ2YgQi2dfVKdW6A5OFcmApImRGtb30NL0WEAYh8BE3NxggE62am22NTjXDZ19VcHmt3R3La9z6xzE2628S0CBMogIqwUAjG6SKnUwsfwlEat3NBcpmJZy5TMyNypZLqXCAEFya3fR8BLqVmewQRQnhajIjIuQS4mzMdmG445GArVcKNCjNh7CMOdDiVwqYOR3GrAQpGAJUyzQiMSMvpMp9etO9mgMgoAliX65fNP/a1a3QGBqrm3dTcNACYS37izS0lIc96FzuqaDGgb08E9OHEEg4xtJ3ORab7vu3bAObet2VezBQtSGZ7PDVCRxAS8+coGBgLB3gAqh6fZ3A/X87qwYU+HlsfOl1Pf/58f+69ztNvf//y+vXS9+35WMs0PZ79+fHULQCYCQm5zIQIIpWJWapwYeF2mqalCMu8zNNUif18nYRBJIpEKyxCVaC01qrUgsTIwtvQQjiV9ucf/zQ1abwspyZkpo+P277t5naaJkEszH3t798fw9JmHpfl9PHx9CBABqJ1PNd1mBiR/9v//O3vf397e5sZnYQxDPDYqiPCAQof8KVIanLO/QEQMcvZc3Z67ArJIctWH9VUbZgpUNwj3Ckv34VVDTFZlqRd82SqfUgpB4MuXFgsLJc0Fk5fULrh+z5YyD/hNMQU5lwKITke4PlDoEZMq2E6GjJRHIksSaQPEjOpagAQExGPPgiAifpQ5tL7KOWoycoDdoRrH2leUjNIR7x5eBy8M0J0xywCADqYoHkMSIZczj3g2GiDWXR04oPXH+T0F6RTTYTzLA8BxBie3etKQK6W8/oUdyBC3YmS3ZDlDAeD4Zj/0X9z4iA72eETHPQ5dkYCVyMGAM+4mauXKtnNDu7IbGYISJyBaUbGMQYLs7DbwHzS1dU7saiOxAGZeSKACNktEI5tlonMTdPqG0ZwlDiwkKpJKQFHLuMTTw3ny7w9DIHzrm17p0CaEMgRAYUKlT6GDu69n98uum+EsN6f58tSCpmmygRj3aS05Tx54HArlcJi3R7fvn778fPDYUytstTnY53mCgBsWCvXUghJTWsrNoxqIwRTBQdGwcD7/bE+3q/XaV7q5bLML2cEu79/DDUOIMLRndxgGEcEuoj0oUQ0tt3Vtj1URzg9105AE1KYtirsETr4YLERk18vk2sXBiAtlQRBKk2nRgUcvZ6mOs82ZN+2TOhGxOh7AEiT0mSouhsKWXiYA+PQYeiCAJyuUZRS+r4TcLhxLRGBQroOEgYMFkqvtBvU1nqMAIyAPgYxMXGdl7acHRBrW778jefpdnsfvTuElFpPl/n6BZxvj6fk9I1pPC2CAiQA1rVTYsMyTl8k1COh1hYpNmYcf9jwYSSVhco0P2+7Gu1dTe36dnKQcKoGz10JHQOqEIqom4U3qeCByO7QdYjwGAaApTYgUovuNgJer6/fP+5b1+U8/cu//92tP97v4Xh9u/7x4/7x824DwaiVggVbq0MdKZiJSIC41jItbT5VLjQtMzGVhq2VNvFUWQqH9XCf58vlPAOaCLkrIYOZ7iuxb30Hs7mSlMKIY4zn+nzcHzq217fr/nz6MDP88x8f99twp9pKm9v60KHhzNvuP963Te3t5XLvt5dfrv/698u3X04IGXAN00CwpN2l8uyIiOCuCYZAxDhaTNzisIEm6oCZjvWWQIcm71LVkgBhbgiBjGSkQ1kk94OAYOIj3lVrRGQBrecGkM3kuaUwBBdzRcRSJa0uHuHDSdA/pwGek33zg0UB8AlNyH/GMANmxCQhZl4NREred3IInJ00LBJx+EdTjMrMWpKU3exIKSIQQhBioORan7Ai92BBPGgQuW1yQOBxiqYcU2AcFKBwQAJmNtPW2hgDj0b4/IlgEal2QUpacRB+AeFoROFD0M95CdGx8ke4mxNBgucyQJBTFHcvRGrm5m5RaxljMB5VxohoY4jIGKOWAgRmAJ5kIyQErhXwuFXkyA4DWXKWkHhgZC55vwtwgBDmcDugvu5BgIThaS3V/DBHgIURAhH+5RXLmAkzIyEVPL1OEGiO+vOer526tcK04L6NWoUF+rBiihFlatp7a03NTksdGOpRpRhShKnudZ4q1NvHI4i20Z/bygKN8jwShDDNjVFonrdHH+qqykDCZY99ak1Nwx3DXWPdei4uYbI/+35/tFku17lVsd6f98cyNQ40D1P1sY99JYLC5fJyBiUbEGHP0VHdelgYI9RGy1LAyQ2EVAjnRVqhWqFeZgZliTaXUrCcGk88n2ZgKHOlVoKicGMRs2E6qBT38ACpNUFSzAQBSMyoGBaBLILq5uZJ9Rgc4Q4gTIUEEJQsEEsR66423CAcPXnvTACRJXwkM0+vAKWerlWpngvXhUq1cBTmIq+/vrFcqAziKXCE7a0t9/d3QTZVoIIYTp64GLaQyh44xnADJA6DcK9T7eveu6U/Ah365gG8rs/Hc0OM83Lqig5UFSP23oOJuxky9aGmjg3UgSAsDsVVpNZETQBu5rfHejqf//h4vt8+ysTfvn69326vL9fXX1/ef9xv926GhAUi5qlw2vMQaqFSKjN5gEzT+ToDOSBw5TKJCJ8vSxGYikxzofB6nmuhOhUHF0Q3q7Usy6Rj5yKuq/YdXMdwRtyHaYx1XbX3ZZkghtsA4J/f76OjKXpEEd43e6x9U39s47GFESyXdltvRfxf//3rt19fzhcRDiGEwx2e9IAAQDd3Q0sPyMEsIM0UUZCZlsqZy8dERiJEhKqnrJ+TAwhwDynioURpHxdThWMMmqNdPxSnz9UpqXNmAwEJ092eVJwYfWTVfDhAAImY7cJlqOZinTaKPN2nD/6vuhjPn4MIhOqag4fPv9vRzVVyY8gTBxFRBLlrTkT2vmU1UPYiEwsimFlOOrOGNNyDBJHRPSAS53BwVzLhxszhR5NG4jz5c6E/0hBD07N/sKMD1IxzBECY9DRETAH8E+pJKRkdemsEcYJ9lBCzjjfcpYofYQK3HlzQPJg46xsBoRTJ3wQIwx0+aybN/Oj6hMgwkqoxQ7hTJAcoCBGIMo0cx1jYaymA2G2vpZpFbumuVuph+c+/AoKZEfCgT5tB6tQipk6EABjmQNT3ziJSwbQTBjN3H4nZY0JpZRHetl7bXEoAUd/3+Xxq5/J8Ps512Z59OV85Yt87FgrfEKsAlFrhEutjK3UBGwSE7st5KcKt8FB3xX3duFEfnRiJs6FgqnUaY0epEYGBoIrgoz9vH+tcaZnLeOhmG53KMk2Xqb3/+BhjX7cepvvjcZyrKGxEm6ZH317OZ4o1nIdG78rCtaLqdpoqOAnRshQpUasI+rwUQnDQUrE25gqITgV5FkQIAmoiBdvUxs6jEyDl1RcApBTtg6WEOSCVUod3wuIGUgpiNheRVI4A7eYWQEBEpVSLcAeu4nsEADEDJLQDAglZmETaOaS16TKf3wIWqBMiEU+BlcskBd7+9j+gjwfcqE7ghE7MDCgABlyYpa8bIXpABJIUG0m7YiQnYUZO/Hly4AJQtdc6Q2AErI8NLaal1qk+7tbm+ryPvsGpyW4R6TpHZCkBiOQesesACAScWutmsxBS/cfvvy/L9H5f971fXud/+/fftnX98svrL99ef/y8/f7n++jxXLt1bG0KAyasUmqdMjYtwm0q7TRzIWRus0iRWqm2KuCNhcEFcZonJCiFWiVwC7c21dOpmZtZD9v27Wk2PGLfNxJ2Hfu+gercKti+3h5US+++bXp7X92CCHX44z66RfdwEZzgOs3/+7/+MOv//j9++XKpby/zaaGsawRHAAKAg9R2kBsOnyUc5GYnIlOHcGbRYUQotYz9CBIKi1lYugQdwo8DeNo6IrLRJeeSSVRLUSVMjZmJ0D08HBwcLPvdVEcp1UzhEDMQPIjRPguHIRwiSimmKRJgQO4shIRqGgh8dE/aYY1hiXCRmlo+eBQpZpawHBYBT2jW8ZUADkCY6MRSTLuIDM3fNDE85O5ChA7B+Em/y0tK3kRSggeg5GgkY4eOxhmzowDvCPlmNQEiM6V3SpjzT3WMoyI4KUMAkWwvwqGDmQKDgPJy4HDgVfMKktsMHnPgz6N3Tn0dCSk8dAwRtm6lFvPIC5cOdfAikt2/eDQ+QxyUacibYHbTEnPigxL23PueD8uPYTWZGhGqatK27ei/hHCgKm5KiJBNA8juWenkzAxAw1SKhPk0yXJqz9tNBGurgvx8rtu6z0RtKm3G0XuRMrV5DCtqInU5n3rvjXh0BcE6t7os4UGuwj72Z2HxqaW+LEC1FGEMUxZW9QCVyghcv1RG6n2Y6nSaxu63x1prSY2FsQgGASCou73/fM5V9jDfO0BIhev5/J//8ZxrY5l4wFh3M9vdNTz0CYCP9U5SLpfJh28RFs7IxNEKmBmH1Ylrw7kxIb28Le57uJKgu87LEgDDB1FxcIijliuIgAmF1bSIlDrZGOGWQ3hiwix1QBzWybmIpFpqaizFPTEWNgAYGARjt/z4Briq1bm4A8uE4IEwERNIaYuS1PNFHWS6orSwQWWW6VRbmU6ntrwMv2mQtKbb8GBVR5LH/VZKLSSlVN33YGIpIk0NrGsQuSkBIBMBjaF92GGOqLVMdX3sz9se4E4+z1IK1BbDOlcvEsJARFWmn89Nx0iWmOW1Op8f5hROHfHH/VGnaQTcHvevv15/+/bmpi/X68vl+vP7/Z+//1AN1VimhZeqaqZeW3OLPoaFIRFLRQgMBSMWZvRSUBgwFIOZilQpld29Fm5zJQQgn5alSHHTMUbfO4YhETIxx8vyUhhXHQxIhfbbg8BKnfo+7j+e62304QDOiOEAjBAMHGPoY+2//3z03qdKU6XzUmoD18EUSOIG6ePCdG+XEnH0g6oOODK3ke1qnjE6pIjQ3vNAUQ9yQwASAgKFWcJN+C8XjYcxCxEnetshPaBHI6y7R7jUqn14BBOrWXgMHWnGNTdwQGJVDUR3ZeZEFGW/SBbOpMmHiQITIHsEqgIBAzDIwwtLwIFdyLtLILgbEn8edpE+bU/hQEjD9twFzQ9x7/922UXvIJ61aQQEaAfhBxHAzUkYMecOhw8HAP7v2IZSJcIPCw2iZ7rd/K8gckCYedZhQ3zOiukARaQAB4iuRoWzm9JNpZY8k6VpKcF1uZdmEAEJTYMIkOioYoCjySZ/N1Vjkd73zHkjs6rVViwszD1r53NSnRtcxh0Yi4jqQEQijnBCdAQzlaMnGg7jLWWlDBCRqkKy6A6z1zE8iciiUaC/gueEU5PTUp/ebZh5fP31lx8/fowxALwtzR2ee48PmE+FRbL8PADdAj8pY3m5HkPjSdPp7BqMBBQspUwtzDJ8wUiFqDbed0VE4apDWymbGiMa05eXV2Q0dwwsQqFuEdadgYROrq4YQGj66N9vf/g/RMrYQlBC0Qft+9jXXqeCgK3NFKoKZmOpQlb20ZECIYatSyUhnhohKdGYTpVnqFCRZL7MwATkXOo+OjNwbQABiGphZlSKYMBAc1czKqL7DogBSCyqDhDAQI5quq7r6XoZo4cZFREmnKd9H5FvZiIRVo9wDY/8eKmOwkJIgVhLA6oIWuXM3CDatLyYuwHVdmmntza3y8tLIAcJUZM6vf/5jy9ffh2Puw4bu7XGXErfw92mOrv5UDMzN2OR5/a81mph7krEEMDMxExSkMjUx7ZH+PWXS5G6PZ61CRq5QZX45cuy73B7OgGcWrWgHHuqOx31hBYiu/pz7OYOREb25fXl9eVaRFqrYfDP//z5436jwkQo7FXK+lxZyrw0c/cwVUCWUlgYiwhClMLEiBHoRk6tyjTVWnleai1SW6mSWPheGpdWauHnbR37WhtjlAHW6kTM4b6tt953cNv6BhCA0ntfn/3+0fsYpWT7NW2r5pE1Rqxb72qqWoX+9rfXb19ffvl6nufMaHq4EQkSp1oiIgBh5mlvDABG9DCiklUTqZhnMXo2MLqbjhEReRfs+37wmpDT0unuEJEZK/cgzI7fbE1HU2MWM6+1pHueEJMDGogOzsiRUSfJvDEQIUnJajB3Q4IEZBFnngEsAvxgzWWIAYkDHAgtM1ge4d5qUxtmRzt6Fo/mQwYkMyU6GORIVJndLSMOOSDIHNXh80qCm5vniTUNpYfMehhSwwmypDevh4GRmH4zh0xpEuZK5+FSig3NhRsOLj9EOHOaZw+vDhKaGyEyMR7zdDpejBRwhklCHwEAcyN0LhTuqgepPyBqEzNjIiYcQ7kViDA3QsisvEg+I2xmxALZ2GcDQIqUvu/MBODMAhhDM0caLGQa5oYRUmscFv2DSkSf8ldghH320yIklAIpS24FIAcbDJCIUzy/nlxxXuKP//h4/3n/CCuVxxgC7O4shRH3vUeM6+VazyezDSC67nH3yy9XEjbVtLjFPjo8ZTohRKslszRj7zZs76NNjUnCorWiQ0ff9nXUJtNcCKMB0DQ9n5swqfveBwNOdRqO1rv1HH/YpvsytfOpje1mOiBNchqEIuGbjv2uj3WrbSAAMjGwm1bB1so8UxPH2F9/mUsJEROpLFEWouIBzghUaL6cu+6BcZpPQWDhAVFqZYwcrCFTITArrpYGTUTnIpgkViQEpyKu3YZvzy2vwcQ0Aj/1yXBHFgrzQCThWoubZF6AmBwgQFhmcwJkolLqIlwCBSDcjWR6+eXvVeR0Xh7v7wbY5un9fR+Or9/+/o//8//d+0ASItE+wlHaRNwYVbX3PlgoAub5hEjMPNYVBDWMiJEpInQfZqYRSLRcGwbd7lupSxHeu55fW0R13ft4MhiXqgEOPHoHwGE2XLnUx7oXbkBkPpjo5bQs03yaJkT888/3fZg7L9d5OlVA3p/b/tynOpVahur62Ig4HAMtLMB59CCuwlIKsVCVMk+tTnWMfTlVYQpXDDZzCpAi0zwx4b53FCBBc4MsiipE4fdt3bc9IFiYlNVxjK67Pe99+GCENhdEeKzbo5tB+f6xff8Y79sDJaaJX67zb9+WX75Myyz5uUfM7t/jUIYAas6MLGyqKaNHGjvB3cwjpBRmGiOECCkZ7wWPUV/oGKU0JDRVB89TCGBIkeyIFwJ3c0vbfkSqkUOJUO1IO9mw0lry49yNSwlTyOIQTKpPYM4+icPzrI95pnTNbkjwOBxN+xif8EoEiJrOzLDMn0L4J+E5cgyJiOomknnaCLeDzE84xmAiytrVhN+ljGsu6bNMYSQTWzmLcLCj9RwxwwuIwEymxiVFq4CDVATZLZBaOAIS09CRnp/sbkRAVUuv2bEEu6cZNCFrUjhh6rlNMTGV5MAEHk2QlLsjAjAR4IGSVjU4eu1daj5H0WqNo/MgjnsffPqaMtIVwQBjDBJKu6CDYwAxuRkiZoI/3DBfKsTkcZOwmbkGUp4IjutYHkshPNAJBTLDYllwhqqGaR4QnE/18fiojV/PpxEmBQlpWzc3r0vM5yoMRn57PHbbr9fTVJbH+ijgY3R2RWSWEhrDFbyg7tN8YhbsWlh4nnZf5TQl00QobYLARPP1tO97huRz6D23GhHiYWDoGDaEBDlUwnpf1w4B63Yn38V7ERZiVTczRBKRNs2qfW5z74ZMuo25+jxJ+GApAL1W0t0Mtre3C4S69zpVLiSThOvYHrgTCCyvV3N38JwVAdLo2lpDxN5HAJRpgm6e/v1Dt2cEoMZmHsa1NnTe1jWGhTki9rUDITPPy/K4PZJx22rd1k1qHdEDAki4CACFM9VG06k/+hjYXs7EVYHnUjWUuQ4eU5uqZJ6EgxnIf378OL+8UCnbGHvfT/PEtZpqWxaPcEQ3G2MAIrOEuwcFgIEPMwMwD3PlWsDhua7q1k5TdSKh0JApz4mGpG9fv3z/8wmkY6ylNENHxzArzBoQZkXaplZENjULlUrzJMs8McPHc/1xuzOJcJ2mdrmePcb62EN9mqYiyzbG+/uztpprpY4xnAtJrcgA6CEsRaiISJEx9svlNM+NEUxtX9flNNVS52UqhQBjbpMZZoIsAEuRAPvx893BmQsT2b6Zuapr1/XRswOrLVJrfdz7c/cd6fuPx58/x3PTYAD3L18v5xNfT3w5y+lcRBDAwoIAjwFs+rKz1IQcmdyUmT3CIWI4ERNQmOlBIgDtXURyKTC3UkvBkkYaYszhMDOZhdpglkP/iFQ1kAjN3d2lSBwEBAcAqdXNiNmsM5WkVwaQq+eK5waMkQDNZMiDx+ENjsgsQJb3qhqKJGHsYIsiZOIqy8UywoaIpXB8TqEJOZNfpZS/gmYpzACQjs7CAA6ZWmYydclnBA8xC5jJwQkAAs2tZhFYVl/BJ0gPst5d3YKY/opBAQYTO4RFEvwpwqQWNweI1OsDc1zqR29K8vMSqW9On50rmE6bpPl/btPwWXCcPB9GjLymRJg5EWkfTIhEvfds8U3fUUqRqkYCOd4pzEyfDs5P2QuPnQzgaB04uqX+KsR0BDiyfEaY+/cBqiHGOEgDx3zGzSJC1aiUfM6AEC3KxBQQrvefH1ZImNtUpzYNNXDQTbkxsYTZ9lAmOF2WZTkBRmgw01Bjt+l09ufW1xUhoM2GQ1j6+rShxGiuYBlYgSIlRtwfz3mZJQEmiFOt+zZ0dCTWtWe2g5CH78w0em6cvG07AqKhg+z3sUyMTGMdFp2J6lQrTK40Nt33IQWEAcGmWUhgmqbCJqeZQkfvZSKp0i6tVHGAtsxlLjpcPba+kxSpRSqPTn1TQBhmTCDCo3tf95x0DSUughHah5nJXAFh2zd3QGGkAITeuwjX1hC57z2tXWYGw+a2lFJNB2CMocICCCKtTCcL5uls1vuwOi2qULGYATKxC1MptWBiRUqZyvL4UwDqt3/7n8yTRXBtdT6RVCbpfS21qe1531guJyK03rkIhENQIJfW3HYiaq2t95Vr4QBTRwIpvI0VKAQRhF++vEFgqbH39evX83Bimbvh7bb/fGzmgEIeYIjHJZlwmqbWKgR8PLb7tlvA6VyWqZUq99t9mJs5U0HFx3rXobVVptp7h8DChZmYEMN9dB/iyjw1YUKI07y02piZAepymprUJh62jw25nE6nMA1gCiJIwcBuj1uEF6Gx99AR6hRAEd4DDBB4aSWLOtauPeLnz/3Pfz4HEiBeT21acJrw68v027+8vrwtzGGqUhiYbRiL5KE23P8aW5qOCDyMPgAJj0GOzwytH5fVnAIiMHH22gKy6aDDexL+mcY/MOYMh+zuIYXz2c4x77EHIX4eX3OiAEN7hHMhhMOsmStM5K/hEIAZkAIIYUKm0TumRVIOSum+rSSSRGgkAsLI/os0t5hzofxViY6YAhPr6EBobq5GLITgoYToYQm6R0RTBUI5QsREcKTiLf0tEMBE/lmLCAGfCJ0sXc2bSpYhHltw6lCug5ACwd2ZJQ34OSImAiLOZymvApCsPmQ3yzkBRCAneYOQIhDAg5kPhSr8QCCZOyan2lmYMKnMHOG1iKqZGiG6BzG6B4IDBELCLegogHCHctTUYOQtIbkcAZDz9zj2PAQIz/HMpyEF/8JOAUQoIQURpuYoLMMUwGurAWAaxBAeqsqtvH276oh2me7P1YD2NWlTVfdgAuS4viwQZjG2fSvCp3mZpiUwVH3ogA0BZJqbeSDgWJ/A0tp8npcddh1WpFiYR1gfvSsRL3NBcJaydy1cXEcrhEr73hlR+0jXlqlhQABwFR3OtUUQIEUYT9UgQk3KFLabKY4eQDLJqc18h31EeJcidWZAbXNhtPP5VMS4QK0E4tLKfF7yG87zKe1UKOAeeS6RwgHuBm4Gju4DkEWyVhoA2cYoVRIKbX2f5sVKNVNBcjcpEu7hpGptqaRsQ029FH72vj5vXCXnfkyQbPHSWlvOe0fEUlpxBJQidRIprkoiyNiWEzGFm/UhdRr7WPe9Tsvbb//Sf9xUvUwTlTqdFuv7x/MDEEVY5jZWB6TW2rPrGIOYddtLLQDgEKWwgXu4lBpdMzSUsoMwL8uyD4Og7bl1XZeF59Nl76BRPm47uLm7MZoDIAGiRlQhESlF9ojb/TnUHLCQtDIjgOnYd1OP4AIjtsejtjrNU+9j9MFcXU2kiFBeEJkw3I4UDwIT1lIZEQFLk9pKKXnyDSYsUrR3Qtz3ve9dR3c39wERLKLWc43MxcQVwhCx1FrG6Or4fO6P53Z77PtuZSpF6stypgLb/rHM8PZtPl+kFBBBADY1pCDGCAeDnPyzoFuoes5BXQ0P3kwEposGE/WIiID/jSIISH0fi7C7wmH7cfd84Ml3U8BPYyTA6EpMmNpOYPKfcz6aJ2mEQGIkB6Ow1FvoqB0+2Aep7GA25iKzZu4dGDCYUB0QGRBSg5KsszfL1mmzyL65/N0OjMKxDH8acwjdopSSVlGCAyJNCdwnCg3CkKzZQiRzR4LwoIMN5+6RffKZXfa/Fu5PdkIpkvVkCXDOa0IAODijqI7WalY1wfFHeKz+cCygfzlt8snITeX4iwAgu1wcMJLqB9n4E5+lxGkPBSdJSBwD4D7GoTshFqE8oQdErdXdsj1GOJFN2TB8vBMwS6ICmSnCwI/6L3fPKVN4MLG7IR5jDGJ217DAkq/0wUzd9k1qCYs4hsYUEWaDCAMGFiSG+Tr1oevaiQsbEfM8N3djB+/elokiCbOoar2Pyq3UyoV7H6H76MBSwtUMYtPcx8AD3JmFRLRrRkYxkMAZyXtvUtwUkHR0CBSi3k2IjwEaUfIJgJBqLczbaoFsDktpGkrgfexYpjqFaw+I28edmSpTQXTFNiGLzecmEqfzaaqAhG2pIiSNWEimqqqADOVYDqRV3YdBgJrbICFmUE15H8MtXPKSZ2Ok5ZdZ1EZ49D5ICsZgRKlSirSp2DBIAK+QKWx9Ldwuy6y9A2EUygpAAAwH4jovLx4jpDrg3KZaz22+eKZfEIGRiRCyCptLId1xG/r27V/n5fr4/Z3rNPMrIc6Xl/ff/zm1E4Av16Xf3gdSrW2oRmTwxRFQXZHxfD1nSRMXUTfzkMrTNLl6RJRWnZCEVMc+9jrJ6XRGmeJDYfh54eeK8FQzPJrhXadpKUWIYdUxuqcZ93I6TaVVZhvmBLfnTlLch2rUUoOgq42hEFhAmKgItyaEtpwXFmCmIkzoYCY8h2uWg6RFgrmp9VYbEyBS37fAeD5X1V4Y52kibPenq23C0kdHCFPt29ChOVPdu669R8THfXUWnlA2Ix8v58van7zb0vDX3y5fvi7na5OCkIg1IEQOz5kQ6K5UOKsgwz8t7IT4CTZGQEROyRtRENEBMkJVyoE8AABzR6T0woQZEas65Ti1CABGWOr+RYqD21AS8XDwoMJJ6CTEbOPiJAhRpg3StQgBQZyxXvgsHcu0l0d40vCRMVfIz1GEINIYnVkciXOFQUAWMwOIbJS2CIQoVVwtnyZXJS651R1Q9fBQC0EigXCs4qri7lmdQInFQEyJhhhZMBzcHQ0/L1Bpisd06KtZhCOhmidNX7ONHjDCSysZVPhE+xwgvcM8+lnGljeDtPkn5jY+OW6lcOYpUjiCCE66YM674cBIQJCbZ1+NR7g5ixCmXfUo8SHOqb4dJwDC0TW9/2GeeOqjKFnDiSCglJLXLD9y0Q6IairCkXURxG4eACwl9w8dg1kCBhCZZtokU9aOWVdAAA5l4vOXBXmUVv/4x4/b+95aYRSHEGIkcg9Xra3wVIuwmo3nMzDKsrRSpRRz50CCQBE3DCE3xRhMZDHQwAwpYm5V+0gGMlOC0dksYjhY2DAIKEzgpKr82dnARL0bFUDmMk2669BtN3CLwojcpkl8f5Y6qfppEiTH8DYV69EKnl8aV6wVllNF6IjAc2EmKmymz22fljmPDX3dSSiASKiI9D4wSHdnBjf3bnVprpn5RCkSw60P7dt8OR0T6aHCiETAf6miRyY9B/tmQ5jdggWYGRx9BBOzhDqYhwcMVayNyhSNIyZuM0iJcTTYZcbQw1wduQAZBJc6v7y8hrM71/n0vO2vb78QleGhiFNt8+myvX+wNG7NVkNCJvE+AGlqy3KaTG19OgVIbf22CReZi5uHQ2CUJkjUx9amtglObV4ulz/+uLUqtdAKIQVLo7UbBaHrqZSpsBNs5sNcw4RkKlWkCJGN4QGP54ptEmnabZi69wmLJsQUAQHaXERonoWI28TuFmGuHaMsl7kIMTMjCPM8NQ/L23ar1WyM0Ul4fT4itLRs8tL7/QGIp9PJxw6F1RQQh7lp9AHPbdv3vY9NWNppJiPfn+uqzPzxuE2Cc/N//Z+//O3b/PXbyzQTegRQDvYoycam4ZgtHXAcQ0nVuDBm8DWAEJml950ge9kk050sEpqmz8CMoUai6Y/cD0B2ZGWM6ZATPA6uHAImxoeJA/2Ag2YAITAiug5CDCIHU80md6ZsLHNARFUVlsAolfNKke5MVw8EkeJuFmA6cuplxzTiqPdyN5ai2ou0rlvaUkwdj8HkUbZ4BJZKQGaDhZNBfVwdAEWE8+pE5WAbHTwegAg8iA5/SVfuOaEmYuKEVwAjBToApgDNIslehhzWuyJC5sg+NZzD5Zp3MULKPk8ixJTkEsoYoGZEaBYFGehICYC5lBLhkCVlua14EKKqRhwtawDHTRYRs89ShzJTYR463KAUyQLhcGA+Gt6pFNUhIuBgpiy5QSL8VfZLlBUFAJDJOE1UqHtA1DYBBACZerbQfHZSR9IikbMK2a+/nm3cf/68q9lyakL0eO7EiMTQHdDd1KPUSaRykdLX3vu+PrE0kakwctdeeWqF59fT9twYK+SN2MYYSlTOy5fn+s6EEFaYKPuj92G9mwYCTOW8bncfbsOSXwgOtchwbI2Ga9btsjDWBqal1ohQG+OxR7fwQRhSiN3mSYgxGKSGFC9zYfKyVAqqc5FZkBDDx44RGACIbBZSmoOqjqQPtFp778jsbgCEHDYG5amHkABOp3N/rgAVHEXIIgLQdKSYGoAWgUy6dx9e5qbWI8doAMKyD9V951KJkFkcgqQCSR+BVahMhVtdvhC3FBaYmRPPHYHITMzCYL1blHY+Xd5Mg4SIKpVpOl/H8xFALGVaFhuuFqVO03IaWzdHM0UhBJjmmUtxGyyVCNfnbg4ORs4EMUzXdZ/OS4CqWkS/fDlZh+f+IHFmv55f/vz9o37EMokBBLBAzXvKw1wt3VIcRMLCgEOHqnkw1Rm5rNsYwxCotBKuIhQWtVRhFCEAU+3nc3M3omBhhKBMtIeJ1NqEEHUMEsyFd93W1kohUevMFKW0JoXjdu8O1li0a197GADStvb322Psuq0Dglhobicf/lzt5/v+5/f1sXcI+Pp2Oi3l5Y1+/dvy9uuL1GR8AQAgc76lLV1hEYdKgJAFQDlNxGOZIjPzI5oa6SaHyHu851rn5khQWDQX+E/BAACkiLkf7ViqhQtktNcDWdyTKR0QIUXc0m6E4cGFXd0DkZNuUP8aA0BeCRAJycyRsfeRGxUSmDtx/pERIZgd9nKICBOu4T6G1tYi3E2ZaFgHAAT/S2ViJlXLmwGmVcackMw0CM0VCVEQNQiAsnTwqHJEJCKEA/uTJ+XU0SgjxszCjMiZlnZzpgPNn6hrCMg+HVM1M1NV1US5BUDilvLJRcSASDXMhoZ7olbTmYuQd7hPvGi4HxcrQqQ4iBlJe0amjPUCI0omBphKkSRapHBHiCJCiInzlXTeQt4Zye0zBGfJN1YgAEJVdXdmRjxoG/kNPY0CgGoj/azJhU8ZLEF1ROweSGQRBzEpsp7WkTxglMbL3KZaxrPvQ3/59U3NrEeRMs1LPgH7uts+CPn8egkEcx/DfcQhcUUQYSGa2qT7rmOMvuoYbh3B9v2dANCckQiDCF0NwwWoMJUiY7+jO0UwIWOgKoG5juQ/lyJMJFQQodapzecBOMyHhgcH8higButjT0WyCLWlAME+NmSThlx9fl3KXJGktJlbnS/LfD6J1Pk0IVJS6RGPDONQM1ViQiIuAohuoJb3aDB1G93Meh/7s4uIEDMiM5ciFIhBwlxKQaQqzRXMXKQQi7qbHzDmfdsBcpDE0iZgNg8DJC5SlnK6SJsQEwHJRJxiIwHkeI1I3OF8fUMSFlG3bdum5Xy6XgEBkZFgOS/7vkdQqaXvXcdYt90cSqnMDMFTm3OxMoV9V01zekEHD4TlZclRJEAsy3S6zoHRxxag7czdn8R+Wvgk9LrUb18u51M1j9tujz7UgZirtFZnEQn3MVxBjMSxmEYAsxRErCIsAgC1CGEQhXtHUIKAiCKMgHWqdWqlFjeNtKC7kaCHmam79d4zx0qMfR+mOrXGxFtXdZtbY+FQ1TH2rT9u6/vP+/3+WNetTXK9LNfriVgeO/zzj/Uf/3h+/Oy6+/nUljOVsn95a9frtMwVMqeQwD4/zI2RUnemfIEOEHiAe8ABZOY08iMdn74IMPMA+kwpHQUniFmdmJlWPA58DnbMpUouhoHoB08fD4N8TnxzX4E4lCKAiKS5kZulRSc3FjOLDK+k/E+cwOTCDJY5WfrMBCe6HkVEOFER7OFqTiL5YEopcYSnwiySjuzuWQ0fiUOIVHWOAgMChGwA85xMgIhkH0C2/mLubzmzZUEETJqN++eJm1POTn8nAQQLhQEenGdPjcXMCSl30YDIwPTnSCbn24EIDsF5xkNEAncjStx2RARBrq1MjJFD+RwY5GNCdDcUtsxrCCefB9ARoPeBiAQc4ISUU6fPC41nVOS47RwM6MBjDmF1qqMPLsVDIVCHZUJEWALCTBMk7B5mLoCAoIcbLLKXLjAQiBgBQoKSTZ0AKRbWoaXQ+W3yEQxYuP18f37/+SFStq5+cweQCjVCmgRiH10KX7+8qHYC1H2YWikZILLH/cmMEcN2F+DCCAHRNQqKlN2UmKuwh0OE9YGAroaI4GH7oLSUqal3dAALZhlmQFiEVINCMAgDqkzDtoDQ0QtiK0V1gNO+9jBgGa9vE4bVk7Ck3cK5ASGbgbkxEx3XRnDD0lofq6+ATOBhZulF1t4hos2zu6uZsJQy7X0LU0ORVn3t4difW3qzidnVLHTfx0QzOEip27oCs5mrrQkUdAskGqNHoHblClJqm2opE8rCbSltcZgIBYIjC4tyfgSH9olA7qFDWRr1QVRqoW3diMvpdEWgx+3eR1+WIqX+4x//vMy1tfa4f9x+3Hvvl9ezSBkaRHC/PbdtV4fHfXcLc72kWYjA/JBuzZ2EqUlEUAG9jTbNxF5Y8LUOjMvbZbm+/X/+f7///sf2sLFaBJcgFDpqDlWhu24juFZHiGGZypRSuFAgllIKUpjVUkz7NE1FsDASQC2MVIqUeZnTZi5Fcts0cwBnQgduU2lTxfB93YijlFqFgHDXJzFbWF+35+1mo4+1f7x/rM+bdX19fW3TZOr3W//zj+c//3w8bgpYloXnSsuZCo5fv11++ZeX69uZJZjFTYmEAiHcw/Boii6qKXs6AnCRkaPRSC7QgfrMhFYgZJ9H5m9HH8zMwmMMhP+WoEcKAHHsF+GIAcMNAQiTWkYBmFZIQAw3iDD3BC+n1JPCkYdDDjMjksycjFJEknQGmyMhASGj+5EJyBIzTutaIDGM4cTELHCMJ4+pqkek8G7mCGSWbZSQHtO/SA3xadkh4oAATfCBgbkTirkBIjKBY1aLBtihf+UuAeR+nNqTfHf8YDr2WzxMk27uB9Tfgxj/CscSZ2ckMjEQECbbOuNdmcoF8+D4NAmYsxDEZ4skhB9+nGAkJmQiNRVmZPAIAvCARD6kEyv9mUwY7kdKIntg8PN3pkCmzxwJZNlm5hh0KEeIcKo6qd0TQe9GlG006b8yiGOvyn/ILAIiTPOkqdwhJCSOSZBQtRNzWgVUlZlRbLm0/an0gbu67k8iyQ/Y9XVxi3ACwH3TVkAa1DZFqLkJU62l1srCmVeoVQxNd3ODjL2rB3m0Jilfat+FGwjrsOMpCDc1FEKLWguihQOGIcWE3D0PL44MYIFItQjhZMKh4n0TJkQ22kx19A5I5mOutFxqKcCNPcb22No8JbQyKcFj3foYEbFczlOdt+1p1klk3/daa5lrcdm2PvqeMiiz6NDKNSZRNUaqre7bPrYxX08eYcMQgZi1P/cIIGrznOguN2OiOrXHtu/u0moBeP/+Z337su/bVISZApmkAAqVJjITymelhhDJ4WoAAESRimG7AqFwmUqbt8eP5/NR2zSdL8/H87GvUqVN8+32jgAvr69h1p8bYNRa2rTsvQPEUP94f2zbSlTGrtNUBKi1FqCuLqXU1lTNMRK/vD3X3vepNRaSIqZjG9u3v7+W+eU//uPd3O9738L3IHQgRAPIofm2uSEEYUSYDnD0AGIoJSu8nYPMDdHdrFYJNwRhQUJA92lpXESI2lQxrFRBQmIkgnRJuA45T242+r6uz1ZLm2rv+/1xU90cHNz3x9P6SAqQML69vtKXojbWh73/fPyv//37/dY3DVVqhecqlQOsv/x6+vrb9fXLXAuCu7pSspSPo3/CC1BNI2k1GgFh1qkIprMDD04ZsoQFIpkqCR90ZURiykxPDg4ZKYE/yTH7HGRCQKgPyEGuOSGRSAoPZhYGSOCeiVDMaTIiRJoYgyDsCIUR49GrSACYWbDDskOY+H53JxZX5VrCHZlc1T45CwHh6kgoUlMvSYiFp8MeggCR0IcSSURk7UrAUWCJAWbOwlIowCmQiriFEJHnFDl7c4alYk6fk4BE3uU6S0hpWTUzDILDJHp4cAnxryBVmCMzILiFh5UiOT/4TMwCEQHlpcyIMN23yXDPsEBODtKVpQnbgTgMSAd9Gd2Dg+IT+xfuZpY8v3AHYhbKO1ne7OIzcOAe5AdJJqmB8TliIKaDIHeAxEGILaxIQYAwR8gieCNOGxIQSx+jlAJxjK8PwDWEulIOQoiYslNezB0RAnS5lNu+1sa14nazMpVauNVKEtZNn0HYTKLU1lX9GbUJcnj45dRYSHXvIxILDAFTa9TYu0/tpPs21WX0DllY2jsZAGi+rEkZDPUqzIAeFqMLclAg5e3N0S0o8nKrnjoWIiMjBzlgBR9CWKJBQKmVi5epXb5M52uVGsDh1vPGhpTuygYIdZ6R6HH7/zP1J02WbF2aJrS6vVX1NGbm5u63+ZqIyMjIjExBQIQUEKgZDBjxC5jwt0CEET+ghgjChEZqACUFiEANKCqKjOaL73bemNnpVHXv1TBYejxK7sTvFb/mbnbO2brXet/3eS/z6YwPuzqO2pkImMzU27JKKaUO9zeAmDaW0rpyqohOQDCMpTedz7c6TK4KhNBd6gAQfWnEDGitawAIMxEP42BdEZCIp2GfJRBuqqroXlju/QKYn0/KRtRI0CSGbcCZ9A5183GchsKfL5cIL7U87He//PNvu3FYl750HUd5enry8PU2R3jTtXebf237/c67ysMQQb/9+rI/HKbdOE51XVtErC0vDRwOvXk4yMgB3puuSzsc9+rODAjw9HSstZzO59eX82+/vV5urkpIJRAYS0Q4oAWSCFjcL6GmplzFw6z3pOG2ZJCImGthpq2TtgIAMwtzqVIHAfcySn66iZALsyRhjLN/pWvz8GVZrtezh41jrWXXtV1Ob6oNwnprTLQ/PqHH7bbe5vbnn758/XRdZgXlfSmyH9waEVaK7757/vC73fN3x3EsEZ1ZABkptHsGzl1NqoR5ft6lVNdAQGT2brkKd7NSsoQdAiOyBpoIAS1jSUQpGYIBFwlwiOxg+caUBCmlt5ZRPsDA7fa3navEAhDMqE2TB7+lnNwROX2UqSTZlkLNkq/0Jt3N99s2BL6lqfBuLXHT8Mjdo6l+46211vD+/yEiMeHdf6lquaDeXJcRAMAsiGFw9/XSViqJTKAm99ibuGtE3KkG6fUECOciECiM+ZxJg1Auy9Lic9/+588l328Z/VUpkgD95McBJAGT0ngf5iJkFmlLoi2KTISYLtc86Lvpvag5uUBkZpu7EzEhE5SX7Wxry2mFKJvpYWuu3z7GWyJji94hcRZeIgQISbceEW5AxOEuIlnrSNl5mWVAgX3j/2xPdbwD5tS0SEVEU/DoGVAACMyY25b9g1IoNCKi7vjhw55wJaIynl9frrcG69r2x30RiMgACLXWPWIMCTJo5g5zXY7jvkpV1bZ2nRcAQnVmYWa3ta0rAXjWv5bq6lXYHR0pVN2NIgqROaipa98KoIkRqQh3tamOq5m6mToBAnHkJBggpRIhOkTvEORBXKkOvjsMUrkMZZw4SM0JAMBV6uQRpp3qiMKVaxmmt69fzq9vu/1UhtK7DePQ19abtnYbdlNmd6UObZktTLX3DswYhKFWxtKuCyLa+To+HCDCwwixa6vTmOxuW1ZKmkpfNdzMClOoUhHtNu0mMzP1es+vGCAGlhS+iLMQJkWdiMjW0xTuuAzjVMLXbkqFKOr1Nmci8Xq+/u7pD7td+fr17XI6R9fbvHz68st+//zueBjH8eVy4rl9+Xo+neZ3H59LFTUz92Vdl2V5eHicr6stLT/q7nB+vc23tY4y7ofT2wU99rsJiS/n9vnT9cvX+bxAQKmDuKMDDkizNgVqjpSLmu7ezDPhaFEGQgxCbF2HQsIFE7/mzlRYCCA2WSXrfIs4OBORIBPWoYY7k0hhFuqtL/Ps1qWw9t611yKuvfd+W+aeNegAUioP1d1fv768fH253dowjs/PZSjj0nRZ1dWvNwzr0248Pg3f/+7p3XcH0zkTjZifBEIhtI0lA4joAaXULKz1wFAnEYQg4EAmQjd0Dxbpa0NKokEwy4bM7B2DpNauTTIAy9shb91wu/IjBGYySWSDAgEgsbh7QCrPqX3m2sDinqNCIiQw0/SARHeQTGICIgE4IxOVpisEWhggmGeEFiIv3CKx7T8gMFgoRea0YqYpE4O3DXOesJSlY6KqmRzL630EBKCDoaefE7U5AOQ4QxbmviW2KF95hwAnRjNNWKa7BkC4ac/PCpFvQIgk/GAWh9G3LHWej+kBJQzw8MIEiEnmgw0ZzenANO1cONyZgEUgwlRJSmwLGdswTzmmpQEgM1woGV2NCARKOz8zmxmwAIDb1qWZNqStLBR5a3XEICI36xbIxETAmWJjc0Xib9EFdxdiIMxKNrzHpzdFOlyYv0UTkg5yX3Eh3BdyKT0YhwCGW5kEqe8fytoqB57e5o5+W2aEioRxsSx9LwSllGEo0364Xi5tbZc3qLVIYTQgwLDw6HUkDuzLTUjAnRF7a6onV0cnQnFnMAhVAHTNqEdWGQITVa4ta9LAe1e0CDMIzOvDvRAB3JUIuHCQCkpvK4WWwqUAkgMFDRQhriZDiWaAgRHuqtqg4zCOyPz+w/u3r1+1aylSalnXNYUcJGbicFM1BpJh0nUVqTlHgrsStHkpVdrSkivel2UYRwc1091+b+5JuOQ69HWtUiG6tdZbFyJndo+1qYwFgZCKlKpUIigCDQARmPKJjunezqc7QJBQj6i1lkFub6frclt7j973+1G1XU5vdajTYe/L5Xo+78a6dH17uXIc98Ph+d2H15c3AL5e18v5VuowjmMhuFxvqk5MT4+PBtHX3sxKLVIEAdwcyafD3syGaSiFZRxeXufPr+dfPl2/ntqyatAYXU3DCRN9Z1np4YiU5RZu3biwEGEQo6h1YQJAVUVOZw1mpISnodZiqswjF8HkgHmwBQomnp1FIrtkAcfdyDSsbS11kILrsnjrgVEKI5e2aFBBpm769evbp99+LSTT/lDKNIz9dl762ubber21vvTDcfzhLz78+BfH/btJ+xxuDl6QITAzHBokKBqqrWU7CjIHpXa+/XNvJMx8FiKCW0J8sfdeSu29s5CZ5jVZvSdNL12SaQNl4dx1m4cwITgRepY6AgSAu6ZDZnMcQYRH7y0gmJjSye3GmAA0cHQqBBHhkYckGFgokqcjAQFl+9NhixMHJoE5775MnPL35r1kDsxTDwjJwjDA0tAYHnmFTWYyQi2DWtrBo5barREScmCQ5BPPTYkxAAgo19O5BYoACKA7TJWRgzfom5kzEWLaPf8l5MyF0u2Us8ZdrXVmZsQs3GQhBMpnDyEyU7fOzASAIpjZasQ7wwfyr7GBFzLc7woQxIzAAeFhWypgY4Gh39sFAgIIIqKWoq6whZopXzZTv9vGc0cUmVwjYQt1D8ZNzXa3UiTAGRkRPALTB8WSFQipsyOAWzAxgGepEABIIQyyZlyEiHRVoAhhjECOw9N0ee2Hh2G9LuEeYSQI7mr2/uGhzSsBUiVh6d2l68O7B1uaqYGDq+eWw9QIwNYOaEWqSNG1ZY0YFa6FfUFXNzMCFpbWmpu5KYblfrKwAHlFNiN38x4WhJYiOUY4uCEhMyChWxZMG3FMey4Uw8TEwZUC8r2BCGzdIFDXxqW6O5oRcF8aEYZgELTWRVgGIhC1DuwYtDbNrDx5YBgAqPXMNCJQEdboTIREYdiWFQA1weiGbW1lHANb6nsQiMxTKbfLDVUdkLiCetoRSKTWoQx7iILEHkDqXGlL3m/okYQAegS0rlK4Fm5r662vy/L2dvr+w3fC/Pr6Mq+XH3/3h6GWX396NbW2Nu0GQY8PT7vdgamui3rg9baurX33wwcWXufl7Xx79/wwjSNLuZ2vGk6EUgWcbtc1AFBkf5jmeYHAOo23xb98Ov3pp5e1lXFffz8dAMrbNb6e5rVH93An2HKUHh49PK84lJ9WiHVdCQLcChZwZxFhlMIAUQojhlvfPT6UyqUyEwZBBgTDw9wE2UzdFCC4UhERQURY16VIkT0uN79cz7fb1cHcPJqtS7udbmb++PRciFzjduvX23p6XV++nJbe1tW+++748eOD9pP1giHEAERJ78WsOeGMK0BgbJOuu2nPq7Z7BuA9tzfmTiRc2NS2S+oGLTPEuDuaAmELeWAa/NWY2Sn32BTmTPduxW1QxICkh8aWcTInZgRAZjcXlkToMxMklZIwqW0A0HtnyncuEdPaem54AN2TDbFFrIIQsHC2l+fWPOPKiChJo/Pt1o+AmH23ljRPT1ssERNC9pkn5jkwmCSRyR6g3ZlJ8qdzv/WnjHln/uSvAXpTFt4E07xHq3NuDSwAt+AaEELExt9HDPdwR+bMJcfdyhsOENsBnZzkVCfy8VYIAbLTBwm3pkW8PxACXJjVlNNkidm7qxBITPnkqKV6Nths+Ict8916D0wBY6uezytnMn5S10pISKKgtJsQmYeUpOhhbx0ZLRx543vk+ouJwzfdOIMYzOzqKddsuDxwZsIAh+CSRz+aA0YM+woufenf//h+Oi6n021dmkX01l8+ncbDsNxWiGGmpQzl3M7zVUolAog1hlq40rqued8nZyzCROt8QycElEJVMIwMDQkpXLj0pal2ByKCcn+ouis4MpW8fZgpAmOEIHj4NNauThgBTuRQc6fmtfq4K/v9uD/gdCzCcS/qISREJqnkRkA4DpNbhGMdRuvd1UoZrpcLQjztjsyOhYHCu6paRkMCwtykUgnWrubBUgLcCQA8hf7UeupQ++VWa0VkCGQSQBCCeZ5D3Rmn3bTcbm5QKnGRy+v5KAzuLEU9gsQiS24CKSNzqSrdebuQVgUfp1HXBgiGflsWDHr/4f389ul6uxDXDx9/MFtOby9tnWvZZa6eS5l20/V6m+fVPJbeH58fP3z/fl3nl7fLUOrj/sjMt+sy31oA1qGGxbzcapnc7fBwdAXr7kaXUz/P7eXLZRjq8/fvRMZ5htPr+vX0GugWEWaIDATEAB7mwZwVRIkMg6DIkVoI27o+7KdaxF17MxmLW3elKEyMwygREWF9VcAqBG6GEUTUW+PCuelNqnNAzNcbgKl21eYWpVRG6NabNe3GXK23MJ91vd3a5dRuc7+c9HzRcSoffzh89/1EehvHh8fnIzMZKJgREIuAhYcisnXDRBHLEK4sZA6E8N++gaUpPDwsQrtmbh8ipBSEbGjBcPBc/TsmZUCKuMXmvXVPM7yDM6M7uDlmbxACBPTWZKjM2NUQQYStayqbzOLREcnMApxZetcilJdgKeTmhGLmQSCF87xXNQA0RwbMoKuqZQ3S9oBA8G2fgUAhzLZJr5lhNiQCQGZxdHcrQ3UzMw8PYrGwiNCmxGJuiABERAQeko7RBAnRBgUjJNjSE+DhwUKEaA6BEB5mnuiIzE8hYJgnxSXlB3dn4RypIPu3U0BOKaQQxEbPuDtpAxCT99lbZ0ZMdkKu6e+xiPt2JZvSjJjcNB9KRBDh+XxWVwAITRAjQjZeI7mpEAMAEzp4RKTXioVz6AGgFAYQyTMaJQzmZpr1oVIKEbTeh1Jaa7njA0/VDS08fzwIqNoJwc1KKWtb8intkYbUSBOq93zwYJjJQNOxWgNefZrIFFRbYGlrlyJlN4RzBPelT/uhFBICcBtqYaFQy9QGBQxTFRZbG3pAmEjVbq0t0+4YMsy3Kzg1axjG4ebOjBAYiIU5iSARAd6EaShszlgAkZxVxMYqEV1VSUIEqzDAUKqNB2HWcZJxN67zhQi1t4KVCyKyR0y7fVuXOoy997a0pq2Oo6/tcHgEh+t8uZ6v034CIGvNPIg5IKtZXc1KobWZmQlLU7XWkZBFCvO69NYUyRLaA0jmQQGa8pI6MJm5ABeqHRZzzV3guBtJxuXWzYAdWYTL0FuuCjE8SLbE+mb7IkwmYESE2zAUV11ut2kcq9TTsuqqT4/Hh4fjn//h03y+CdM4DH1VkVKGIQDWtrIwIRf3d08PEDFfGiJ/+O59Gcr1bek9u1mKMK9tRQAiGIfB1VUNqV4utz7fuvrT8/Hd+3f7p6d16f/0pzdQRY9uikUA2RUIgZiBc/uLDmrdSEhVS+FBWN0IaKw1l/5uPg3DNA4EMUx1/7Bz1+U2A2GATId9EEihMkiEzdfrtKvDUMDdVYm5DPV6PpciYTi32WPrHdTeowMoVqomEIRqfLv1y0VPp3adV+0x7aaHw07b+fby9Y9/9fGv/u2Pu6MAh7dgEQgE09j2qMGF3UxY3NUyNU2QWVRNmBhABsJzL5/U2G1zEJHEde0tA03Wdahjtx7Jrs9LvgJlcSEEIms3YhThgAgMITL39JKaB2AQsXZlIe2asAFEUu0kDE65rw4IcKfCab/2tNzAdtiYWR65AGRm6JiuIQDItvaAcNxyshHRVqVtcRQJVAaDiJCtIIyQZMs/MAOSmub8ISKAaJqDz8aFlgBMhZOD0hifm2pJTrqHCJsaV8mEyEaAuzPgtk9Hns5EQI4BzAUR3TVg00jzBYSALM9mymp1Nu3bkL1JzxuxB3DrBdv2a5lpgH+BLpXKcK/QFCbPp1GkARRSsC2J3QQIMwgVIYc7egIiPTk5hHwLCvaupZZwTa9S77rdBTwg1XPAImKbKwncXESysmZ7LYkBgrE0nQcZTLWw5DWDmG2z+yIyhZtw8SybxXh8v//802utVHiqQz2/XbRba4FzjkRABUTC3CoIBiCidtO5ebgQ7oaRgNLHkBB07V7LWBiihS4r0VCF+6LMotZKFe+JQghGyD4wonDtSCYAkUEZMimOjAGNCaapWhiyT1PZHwpIMCmWEHRmCDASMtehVABbbj08SKi3mbm01y/T/liGwc0CVAYJ92E/qvW2LMM0UGHAguF9Xd0UKKTwNO66roBYpJAIdieBrh08DF2qRJhZuCkz2db/p8QJY3EhbsvqBU2b9j6OO0bgYcIAlurOQMylIBcmprEKl8zrp9E5p9Yt9JPR0LUP4wDe+rKaro+Hg2C8nV+7r0/v/wjaXr++zLfl3fsdMV4uV3UXsrfr6zov53lBLofj9Pzh/fn0djrfjoepTnWd2/lyvc0NMQ6Ph1LK9XobxwEQLWK5zKVOTf12XTV0N9bH43H/MKm223XWdenemWggxsoRRLtShrL0rgbdLJwioFQGcMmeVQQIKEWGWvLKNo41vRV1LBFhqjCwmw0y5LKUGKWW+45W0rUhuS8Kv54uvVuErvMNCMdhNNPL9QYebTY3FJpan09vy+V0m5f28nK5XZZS636/w4huN/L+9GH/4ceH6VgADQwIJTFhhMSlZOV7JkAT6OKUneqExFk8HhE5luZpDgAbXMFNkDVTtYkRAwhAFrEwBGAibcoi4EC8+cWLcNJcLFmSZohotB1obk6pGZh/u5umGgpbjyEkN2I7jiLAA9NawqzgHsCMliDSTT81vKvxEYbE7hYE1s3dhjokekdEIMATXGGbn4cSiZYaZBrnE/lpRkTCYq5JKmOm9MLkWj6ZObnNhwhHRvCMDkR+LXdPTSMFCeIEZETiP4HgnqH1rK9M6FWaomgDyUWkUcjv8xc4EVlXSr8kgWcsAiOd+7nDUe0AgHfqJwklp4iFVS19aRu56N5Zlg+kvAh07elmu8fTAgKI+B4VgdRs78FkdIhSa4CrmdCW5RNhM/dw4QKRm0NHSSpTZAMMCec4to0RbmmWTbKrq2XoDgkIKMvMIY26EOH5qmMZ6f2Pj7/86VObHcj3j9Nybrdbu/ni5l070nR4HCMbv5BFOEyz5nQc6rAbGHC9zmvXyqUMVZcl1ADBzYgCA/rawgPZCBAQBNFRel8Ys+DB3N3bigGIKlQQg8KHWswboqr1dbkOe64jThOOe9kdpjJKQI/oHmZ9hVLAjAt7awDADK7WDYwNAGC5Fh6KVO/NkRxi2O1G6+fX/vbydnh8ksKqCohlqBC6zE2oSK1tbWam1pk5ELIuLQwyLBnsqem5g7tvNs5NAfKufVlXglBTbIuxPBxLby1QkOl2OXN9GifJBD8xw3bbyuEzP07ZRqeECMJM5t21N4g4PhxaW263K4Y/Pj2/vHx9e3s1iHF3WBY9X9ZxKKHh4LfbGkCH427aTV+/fH79ejX39x/etcW/fn5bmzr4YZoQcJmXtTUSrGW63mYhud38fL2a9mGS9x+Pu2lsrZ9O/cunyzobC797nj7+uO8Wc7NmuHTz7FjIHCihwR3FZVYYpRRz7Y5TFUiqCgIzJ4iUIAhSULNC1UyHOjJRAJQkikMgZJ2czUsLBxbUHrvDLhfD18tKgb27qgPRLz//dnm7rIvdLuvaWm/2+HiotUTYershrY/P9fnD4d3741Y3hcSC3nsAAqFqS58FbqH+9D5qGs3zhisiEWBqyPcAXy6NPRhZc7kPeYNlz9YCgPSygW8hXsj6Xs+jU0nY1Db/dvLrAQIp3EsRN9vqrczv+PrcpsJmNkUQ4dY7bDhISPt/65rvzvwKyBxhecXNMhVN6iU63POzpVRVy119hnwDttrgcCBmVct6JI8ghL6qVAHfBg21DpgtPYCQXAbPLb3EhkgFIHANyhkgIr2cqX1LqaZZthaJ8UIGBEwFHAjCnNP1706QyxAhSivbJiHerUCQei5sy9ZcG1EGlIlITTN+lhDL8JBCuTVJEl62NvOWagZAQLh7YSMAIRNkJMh3Tw5gEHN6YHHrmCRAMFfaeNxbmhw43L2UAohDFYfEReUYls9MSqY0c/qIg4STABru2US2eZOAiXjVmbmEOzhuag5Ayg8OgOAZx0eCdZ0daPcwIjVYYpn7uK/MeFvMVcG5t2WZPYcN67JSMIIgVBFi0qbAWGpZLnM3H4WGqYYbczV0bR0BGDnQOYmIQEAQwEEYpq6RHgWGIGI1RYDCzMhhjaGHK7pC2Hq6gSEqEA3Eu6Cx7MdhGCHAdZiv174sbV4ZQGohZCftvSV6RZsGo5tLraUM6zr3thILl7Lclvly3e2nYRjcsLW5t3kYd613VANwBLJu6dpRbb33IEbnvKxFYO+9N0Om/XFa5lspZXc4rOuqTfWyXOe5DvV2W8cJe7Nxd1wd1rWNy7ou63B0NyujQKC7/wsvHmALiDim+yBU3f02L5fbrbk+HA+vv/3WlmWotVb57bfXNl8OuzEQzpe5rbrfj8I039rlPH/4/sP+MPV57WuH8HfvHjzidFp+/vL2sJ+enh8q0rr2eb4d9+Ph8eF8ukXQ+bzcGji6lPL4cBzqdL2019fl9a1dTn3aHWTEdx/H61XfTi1M52u7Lt4iLMIsmCQIharBmqwDIimcJiDUbrtaiGCstQi5KtZ0VUOplYUQgYmlcF7gNHR32IX7Mi95lerrGm5AUUSQAcCWyxrqVer19Hp+nb98fvn89VWX3psPpe7GYShlGti1L0sLX3f7+PEPTz/84eN4HD2RDA4BkLQ1RCdi62phzIVR1K2v611ZzZN+u2mD5JGVJUi4XSuTuEAYsJHDSDgXz563KUC3IEE1HUttAZCbXUtbh2v37O807SKiEQkV3kgCQBApfYWZMYiZIjG4d+2EyLL9Fwre2MDgQAwJMA1NZCdzcddIx5GnFMG5b++m6ZaEFF4iwmHLiCVQGnLyh1Kkr60ONY2/yHnbxYggIGLS3nPPKSLWVQCAEM0dHTgnkUyUueUONPJngQCxETcRsbfOktfapPqAQ6AHbivlrJux+4EX9xaBb10K4B61iGrPR2gpgpR13hxuLBIe4JHE6UwVmznRVj+JhAGhPVvG8mtDBnVdU8NJUYZYyHo301zyJMQfMLETDOnnSRoSoHmawxARzU3VinAWMXrkhGWpDVpS3gy28yI8RWzMpuJwAmzWhIWQnCAiTA2ZAAJFwjVH0ZyrAIELVeLAKpXL3JHA13BFYR+mQQp62No6C92uS2GOiFrYMWjC+XIbSqWhEOJut+trAwIq5GtmcNgDwhQcwBP6BEyUOy4IS3VamFHd1LFQYUYiB2MZUugvVICKezMPaKuaXdYFXUMX60Ue9wDkgVOpUyl9UdMWYWZGyLvDMUB7y0mIlnkeEQFAWJirw/rw7rG3frtcw+3Ax1oGhHRe5YVDmSgouNC6LIxUx1KGGg6uNu6m5TZHeKnsEW3ty7KICALM1zl3lVkVCSXDnrbM1w8//oU4vZyW1vvOLYisxz0DuJkrYiOXbHEwDiTEHh7uyzovt7nwCEC35fZ2+vLdu3dS8Hw+vV1Ov//4IwDe5lutMoxjN/36et3td4fjIcwvp/l2m4Vra/Db5+v5csXAh/1+LLXfltu6dG2PT/vr0tZm57Pq6k3t8LgvghH0y6fz5XU9n3tTLMMITI/HcTZcXtplXT6/Xpcui0ZQgTAmBFAKcl2DABmE2c3cqZSCoMzCm1fcqKIwMW47c4JcxIIIh7kHUGVmNNOuHRgYyLoOwyAcakoUqno6X70bmJ9P5/m69K4IsRsFaxEphWS+rdY0eizz6tF3O/79Hx5++N3z8WGkgACKMAAgZPDIKx+ktTMYgNQ0IHKyb2qBUEtRS8Uzi2YTQ7AtgpAJ3En4Wwl5pp3i7uJh5ggrg2gzROpqacdkoIAQLr33tFSa6raIBkz/CzL1riIFwHvrzJSpVQZiRk3uG0QYmBqRIwsnAsFBuyKACHuWFlF+GCMgmAkZAEBNKe0zCc6JLUiVF3RzF0IP3/pxIyCiu2VTjUg160SkXZPj5rZ5fDIdrGqIKEyo7giQiS3kdLxhRAjzfaOOmN067oCQCa/74Z/RKgoP36TdwE2TCQAg4v8WHDTlNXY1CFjXlZndjZk8HDcsbcaNA4EhtoAu3u1OeKcb5W6Lc/nlQULubhYomSoOQiJOipwRcxqC3QxT/nAgJGH2sOTkEQcxuWHe5ZOjXUQAA8KBAAPNtJTiW3YhwoNFXLNqB5gKAkFykHKSciOs3VqRmvyMZO2FGcaWZGutE3GAQYAwS2Ef/VBGQfIOdZByJVVVM0Agh+vFjmlLNxcGLDKMUoqsl1kEHMkt8cocFoEeEeHgDtaDmREoVIVQRBBVPaiFg2oHZBQEYSFAoHCwKixDVVUprG3lEiIjy4i4Wl/Cm/ewuYXruTcpNbdhuSTsvWlbu1mpxaKWcRh2Y5iBAw0jAbqqs7EUNxDGp6d3L19ebufrbhxa7zhIqbXUulyuUlmbmSlQYKCpzelkANLWrpk1NwOiUlm79mX1wsyFWfq6RoQIoXA4FClxBwkOu/3QopQCyBghpRJJtuFtYi+gh/M9MwibqY66h5lfr8tx/8BVlmW+ns7f/7t/DxgvX7+O0+7dh/dtncngYdpXkbfzgkjfff/xcBjfXq6n01zGClDOl9a6htvD8TBMo6t1cyJ69/xOPW6X9XJeTm9GJHU3YNC8tvN1XXu4ElKVkVgkKK5L//y6/PZye73qtYcHoXB6F3PezEsSARAgI8qWtWzoDgiM989JDwpy2WCLES7DwCIOQAHWO0F4wbmvEM4jE9RSWCqHKjEt6+18ubpqIk5Xswa69LkM8lQf+6KAcHm7tlXn680ttLdh9MNxfP/heDiMUjibB4mZkMPCs/Az402IJJzWwQBwt97TlX9HOBCHJ34+YNs436+nmHIlEoFp/jqluu0J4waKwUOJ1k0TexxSBQzNem6oAXL3lRvY7Ycb4cISbtZ7KTWyjyUQMNyNRbLpCCBqrQmWyKnyTkSAe47KHe5uvHybMecRl0WPgNp0W1cgkoMjApgjC0SoWh6STKLaU84xW5hZVYmZmFQ1+7IS1rB5ONUkEIkgLJkasN3/ARwwk7QsnJWN2T/ALKkWANCmfW/JX/Kwu2CwRbHdzd1JyC0v9QZAufGAACbeTEQQwrLBmxjDs6wxSpXcN21eLggEYsqBS9SMCT2slqFpY0ISMvfEfG5PJkiAKPi9+MzNhmHI1ZBHuIMIEnCqIpicPwjc+j8B7mghQGAUyDaMzXkmmZJTt/AAUmSCcNrqcbbfmXutfFxFunU0n8kUECLiGyKbUnOepqEvVicxNOvcGyEX7L2pEgyPT491qiTkSx+HWkTQcO09HG63ZeBSWabDGI5SWYHa0iNCV3UNIZrGsTULDMRgoSEorHRVDMOAMMciAADIwlAGCrsIBLhOeyEMZC2FhcVqIa7BMO4lULU1BwSH9bYwUTamFmEGX5fZuhFBkNU6BeNyu3VVcONBmKSUoS3XCJsOY9f2+dPXw8NeVMIN9wzIvRkB1mFYlzVbYnrrnI+piEzB6KrMJRyHseqqYKGtucayrPvdME3j7TwHEKDvDxMT3c7niUepo7uXsVo3mQSJIYKIt7zJdufIETctDwGIQrIuy7Jc/+pv/hVafP78Wynl/Xcf+/n08vnthx+epdLr13lZO4HPr05oHz+8ezrsvny+fn25ElWH4Xpdups1++HDu1oJHW7z4u77w94Rb+frfG231d49H0nq5Xp7e1sUAZGWtc+XZdpP0343jaNH/Pbr9bffLi+ndTF2qIEliYuAiS5n88AIKVREiIAFEb0QjbWQG7gNY0WwLGmtoxCBtUb74T7wwzIvSEFg0YIqlUGGoQ51KIxqfpuXeb6ZWzoDXS0sTD3fe8R8e7v21ZZlvV1ns1guqxRG94fH4Q9/+fTuadrtR2FxSIpijn6BSJym+9TesonQrAzFLe0xaVLvcufzlCK994AAByQMNxIhprSwp+MeAqwrpi+WCICKiNtG+OVkIhFqXvwdAGyjG0Y6pkxKyZrfu8IHGy6T0DyQ8nJN4IZMLKzazbPH8R5X2y7sCERwb68MgCy8RNxkbQCHCCLsBrWmPxtUlZmICCTpA0YIyJT2B6YkWxIJh+VzK2LLEIBlbwoRAJiaEIl1IyaWvAtz/i1VLfXV/AunXRJx41KnPQIBetdvOa/8HkSSZRbMGQ2Gb+ni9EoSo3YVoiBITHQe0LHNB/kybIewb4JD/u+QqrdBpGMoVT5CVu9C5AFukVOYW1fNfBOaBzOysKkTYqaxkTYDExGq5pwVIpJAiXDHWt23rINvfq9MdmxPLADAFDZS06DtVc1HICSGmshU092EW6YbNJxJfLMfoJmnx4mYIpyEw6IUBnUSgDFUmS2GUUYLQFhut2mslQAH6ctiQC61jAxuFK4OYQ6nczhR4d1+RwboCAMZOCOZWpi6x7o0qdXDi/CWugqL8MIDFAwwZAd0qbChtWIe9gMhIiqSE8H0UEsZVpuHYRxpUG19Vu89QScOjlh2x6O2rl3X2+p2W4dW6hCbzAS69ij9us7bZhNxHMZru5zfrk/PD3WYwoJAeregCPQiwzzftBuY7Q6H+XYjlPW6HB8fwzA0ALF3DXAmsW7uMY0DIGhXJL6er8MwSB2GcUqBbj89aAwQlJ42N8vakJwR0iETafng+6vrHtpaa0+Pj8fD4eXTP1/P54d3T9NUf/77z0Tw8bsPpv72cl5aiFAB+vD8PI3lsrTXl+vnz9f94XFe+j//9vn98+P75+NuXwVxaWv6C6nKclle3+b1prunx48/fvfzz5+X1c/nZhit2ePT4Yc/vpumUd3N8befX3/7MmuncdxHD1twXYIqFwZEsFAAYiQhBjAmDFeMMA0qFdF301goMr9DJIhgquM01LHUWog4zHwNyHemOwkOQzk+HMZdjcDW5s+fvgqJkNRaSwEp/POffjmfTufzeZ4bOKzLcjqfrdntsiAiBz89HipHqdMPf9g9f3w6vn8iQTcNiDyIaylE4BuHJmnERiLW1QJ8aUjU/Z5XJQTECNgoN2m9EEIMyxwofitMzLAO3MXLZP6QhzsEZ9V5RKnFHcI848QJddDec+FO2ZuLoGYMLMK9NWLJrFZgMDP5dp/DcEcnImZu65oWA4AEcTIxmudGOlQNEDNuFeHICfVNm78Pw5CwAwhgkfTLILJDCGOe8qaeG5T8FhLLn2dmLqvNNDdUCEhp6vQQJIz7GocIe3egkCIRfndmhprGFm6yAEqeqpmlrptTG2A+p3PfH+FoZqlaZANOeHK0N2ZcXhLBAwu5GcFdSXdDJhEhxsRrxGafT771ZkOKbQkQ5oCJb9lsAt+0Cti6LTfG01ZmCZ6tC5lFQA9I+TrdTPktIFJvXaSobmi5++Vv+z1MGLE1yJtFAojMgjkIKb49A5LpgQThLKLaEHFLqAW6u6qXInnzMlMW7q1j4HK9vv7ytp8eKpd3jw+XeZnnxV0REJGuX196lVIGFu5dtduAtQojAhOOu4GA3FCbXvobIoOBrsFAy9o5GpGAyDjWFE501cIclbQbEjZv1auU4rgEGrKzeLgiurarVODCAVqLIIUUCuSuHbyzSB1KRCAjMGizvrb5dJVahmlqrTXtYBpLeNg47aQO7qrdgJAI2rVLrdNezLo2f/t6/uGHXVcza70pgNU6BCiTJB/rej6XWm+nGRFv5+u4P6zrrF3d3cxwIHBrTWk3AfDttiKRm/e1ra2pm4fvnp7Gh/dzYzfrauyW0Xlh2fJfgdkhnkmiMHNXKfW6WG86DuM8z7frBSO+++77puvL1y/vnp4eHx8+/fSJOnEdienhuBuHoo5fT/OtOwyTAZzerujlu3cPj497X/vn1/P+ae/EXORyur68XNe5Hx6OP/z4/NvPp69flq4UXJfl9vTuUIuUQg4ORC+fL+drZ6kB6r6ZO4ZBkB3gfvGI7LG2tC9XIYQYa3EzkpqX4ggbhkqIpZRhHBGThRW8KXdWqoSHo9cy7Hf7MlZT+/ry1tdWS2Vhbf38ep5v55dPL/N8ywxnKbLc+tvLab41BBgGGqQUlgjn0ONhePe4f/f+sVYhAiDIz35sxhxPBdIDuvYNxslUiSJyHZrWu1waI2CYQeIhAAAos1SUbEnKNKYGEEBAqaJqOZ3caS7YtRcpBNHXnu+WNMJlpDy74/PI3v5I2qxSGz7IFJnBUbtKLX1VKewBYEEIvTXhwoVNNW/CkTEGYusdGVKUt6wgSCQBgpvnysSTtQybZSYPnOxRj/x2TCnAzEph0zAzU2MWz1SN2WZ5TwgCYX4viCTZme6ADNB1Szxt6P2ASOpyVnIAEDNGesug1CG7PeFOTdw0gS1VG7DlGOiOzExeVl4QQ3ImuLtuATwHbg8XQABo2tFdpKQlFRE8QoThW7IA7zSgLVfMeP/3tOto8psIIfF7qaSkihCACQd08O25kpSlYOEI4AiA2HyimOajXO9QLdK6EqGZCwvy9vhkIfBwdynibhBg7sJsbozY2kqcL3BWXEF+/QgPdxQJBw0HiMLiVMLwp7//xd0//vA8jMP07tiauXu7Ll2jMi/9Nk5TXn/6qrUKgHCVbQ7FKEMSrpkieriuVorYau4OvQd4WiV5lAg0MApR766rc5AVHsmjMQFxCHH200WoAxchB+tdgb2IsKO5h3frwYzBpObAJKVCgDVzm7mU/fFhuS2rrgARMDPJfr8j5q6NkA4Px6+/fh4PD8Nuv84vbWmff/tUp51UYqm9XdZlkSos7I46d/dwC6myXldXB2RAt25mKqX0pZVaxnHUpoCGCNMw+gR97a9fXx+fHwFgvV7GR5t2e3cFwsAIcEgXYUS6A+6e8fg2yGYc7Ha5IYZ2fTu9AeHHH767XW638/z9dx96X1++vr6e3pRHhiiDAPHtclsW/Yd/+jJO+7e4gscf//Dx+LgD18uyACMX4lLV7POXVw8K4bIbPn+5fvpyvt500Xj/4Sgjfff98+PD4Xy+nc7zusb5shhQINahgEOZxN9uHtG7IhEEEoAFEACz5D2tt6UOg6oepqEIE0eoBoQzYqGk9jIP+c2aqhSRURRUexsfDrvHPTEs8/ry9bMMdTiMGDZfrtfr9Xa+9NsC7k/PxzIUbf769XW9zN7bVHHajdPAQ+EidDtdGej9D8fn9w/DrhJGuIFRGKAg4NavCwGmlkWQCOEGwMgM3tOTEsRi1pODkqxNVy1DzUFNhE09J/1Shm7dwPOQUtXU45Ago/velYizpTxDrMLF3Sl93sxuDpjM/XK/K0ZYKHhWzqUpMbsK3DKh4gK4UXNEIKK3joQWWUa25RhYBCCQoHcFwBQA3C2BBYjZ1JI7bQCkWkrXDgBSiq4NGVtbs+yFKB2uFBGliAeAJrHOUl1l5q0uBjAskEJcDYURsfeemVtAFObeLdu1fOtwBzdn4XDPipwIB3RACrcAcjW6x+42OQECCVpfixSHKMRbfgrRLQBTvHazQIoUZYBASADC3JgZCQMskUGRwVD3e/QsiQ7pzM0zPVI9zs/tBvSNSJQN5IS2tT9m6C+XP8Qb2shMjYm0J/cCAkK3ylBgZsraIMTWky0FzLJJ8A5w1wnp7j0lxshkIEAAcBFwK0XSuVzLkCbUQMh7CgsToLpjRB3Hh+d3r5/mT7++Xa7t+x+eh0nGWhloHMq6zl0XBGrLrdRaxxEifO1lnNa1hSMBCol7y2tVEUmOinZjZjcnYOgOAkyMgkhkTs2sUum6qHUgLwFFiAXKwEMByzcCooyE949Qb6up5k9We+5IrC8ODiiSCUNhJi6EoBoi4zDCsizrugqZq+8f9yyluxbC/ePj2tahyvHp4fx6ul7nplYL7J8ehGtvqxu5mYcjIaMknQkmWOa23m4JK65D4UIx0/nlcng6Csnttlg3HwKQlmWV4HW+TfvDeptVbdgXAmESJkFAFsH7PioBELH1pEKEEXHvertcpsMunFTby+uXh4fDbjd9+elndJ+Genm7fv38+nKaG9pf/PDE4Nfr5eXl8nruPeJpkC+vtz98/+7HHz9A2G8v53mdn4/7cFD1L7+9aiARI5XrAi+/vX15netQHh73pVCpVZhO53meW+v+eu5E5a/+6vvr3Nzw9br+0z99ySm4CJtZAAFiFclYXLiBwW6YqlAtJITZIUpMQ2ERLqVmDVxhocz1mQObL7r0NuyrVLKu7nC7zSKMBOeXl8vl1uamfW23GYmw0Ol0Add1VWtaxB8ey2FX9tMgwtZWUyfou8dyfJ72x4Fxu/alxMpIkQsZhG750SN3p21RESmqImAAmjqz5L/m546Yw7c9gXWFrTEW1TUwpPA2FgSGBzHm2iQihnFa14VIIoxI3Cw4ANHdgMhNpQzaO5jzMHRtxAxhSEhYIIJZLK+/Iubh5rEtXhSIITY+ZvZiMSGzmCViPdeNmDt2IopuvgWJOHWL2GDIwMxE1HsnZkTa1mLat0JEQIJswUPLwCtSSJZfEhGZe9osU3TkKt5NcpBx882iAqjdEdMdFZuFc8vFpPuT04u+rk0kAVIlPCgL6ZmIMcLzCgaAtdRkt1lymGGTSEWSSoEAkY85d0B0RjbTjE1lJX1apzw878u56Us/AGyIN3fLuDwwb0+BDADQFmQE7XeaWOr0Wc4QkLNbdneRZOcyI2DWrJVS0jZgEeZd8sRHCvc0GtD9tph/ZNqz0oUWvpE/8yxxNXdDNBZhYnN1NymEBN5jwzEiMJcc7upYP3x8vr6u83X99MvbcV/xaV9GzsSEzjdvDggkdZiGOgj3UTXnAAwqQcZEvihK7aqFC6J6WJgWGZARMR/XXmslwm5uzqrKhYGcJZiIK0rFOhKzYzAXolLavHh4HbkO9fJ2DrGIPBwpkLggYXR1MxMSFundfZ0ROcC5FinTjsXUVZv1Nl8uMtQIaGRAJFLn+ZLdjcTSmw21WlMpJdgyFApuHbHNq2TnH9jzx6fXz68inCvsOoyIUKW2VaMAEJZaIcDdp2lclqVyHUpdbuv8dpJd42Hfe6vZI5hxQaQwBwxPkAkhQhCJh4X7umq4Pr57+PrP//D69vZv/tUPUvC3n/9MFF3Xt6+vf/7p9e2k/+Zvnt4/D2j6+nL99dfLl/PaAx4eh9cTPr9/2O3qp8+nX359maZy+OOBAV9frvMadZzmpoh0vc0vb8s0Td//+MzozIgkt/P69e26rHZrFlAO42BmAfHrb5/OV/cwwmitE4qwIGIghjsBoykTM4MIMeFuGBC0FomwIlQqQwQVIsZhrF3bIOMyr8NUPXxdFtmV6WkcKjf1y9spIkSgnZfbPN/meb7OpjrUYRrLfJ3n822ceD8W2heIIliOh4Ec1NxRVlZ8GN7/+Pj08UCF3J0QipS0abpnvU8CDIK2UlLNsyhxW9s5ft/Hxn1htElvbpr039QAU2bLXT7Dxq7Hb+IdZCZLtd2bX8HDgdA1cOsrpywMIURkVuvCnBtsIvKwbctEaG53Nlw27jhk3V5YrgTyBPeM6aa+GtkBJxm3ykcdMqn2lJ0AM7VgLFtPQKnbb9Nm2QclXHpf61BclYSSHgQAah0z4xlbmD1XW7ndcYwIyOZVJ2YIN3OiTAdjWu/TEJbXxvyG01CmprkWJCLzYMKArWTSugJCrcXDmUtACrcA4ZAVCBC4xdKCJJfpgYxSKdy3WpjtTh8Z8SUiSooVRMI+CTGjVD21DtmyGJjvkvAIZ5Zvlv9hqF07ZuIBkYW1d9oeP8TMG7abKAl8RMQoScHLHRRRElKJEGyrzMxeBnBLVLcL8f0NuY1NsMF/AAG+lYsyi3krLKZOglLYI1SdBIGpzTNzGXfy8Hj4w48fP395gXA0Y9cPz3sksgjCY197X9tyNaGIrlS0t24N3Oq4OzJQLTvtV9POIrb2/fh4Wr8CBKKXNKVFEIZjl1pRCR3yoQaIwIiCjgaMPBBGMHKQlmGsQwGw8+mi7STDoOuyHYtuZRiZGJBEiAHNgoR3u31ry7q2ZWl+m11jt9+Nu8mstra0Nq/rzFyW2wyAdazEVcj3R5rP83y7Lav2boeH/bg7vr1+JVqHqWJimoDR0bv3vo67CtllHbTOrQxlmTs7ra1jQKllXVrkhdFBW0jd9eQKtXXY42qu2kvdpd0ACYFcNZMiGKabJShgnRem2B0OGHC+3kj4ux++v51Ot7c3XdbrPP3pT1++vvanB3n/WEnXr9flesO1QQT98S9/8PCH41D39Tq3Xz69QcTjw0GQbzf9+taWmZzrarjMq1vsj9Pvfnw/TRNi3G7r67kvl35r+HZZpci758O7x4elrZeXizu4aUkDdNLjMUopAKiWN50gRHAAYCH+hi4fawXQUJ2mgdyjeww2DiMQMIKF6TJb9EI1NC7rtTfr1q+Xsy7L+XYLd48uQ333/RP0/vb6ZmufRhoq78ZSBwyNYShu2rvN50XntYzl8fnw8HQcx4ELE0C4mysCpcWEKN0lG0RATQPs213Uk8pCG0v4jnoGiABgAEtVL9nl7n5vc964SBBAQuaWjktLb7xHGqUiLMtFXA2ItBsxAWiGzlIKrnUwU3MrUjIeSkxI2FpLLQIcAuPb1GKqcH94pNExDwaP7Tdk/hSRzJSLaHM3Fy7pF/UIogAkzZ2+GZMgYm+NmFLiNldK+j1ABJBQa4q4QSIAwAIBIDOnabfZklIREhYkxETJkk720EYgysQsoLtnngJThbA8hiFVe7yXcyFEb70UQQRzB4eujZABQqqYKlEu8yI9mjlAMNHGCvWIADWlreAFAAEo3xYOABkSD7d8AoTh/TpP7lZLSVN/rgjQN5F2e4rANnp4RDKI+I6XRcTeGyJk+myL9yFGmprcth90bFIME2UIMDwVwpxS8zNGaTqOrGEDgHAuJdxEJH1WDtF7Z+Esf4jYWOT54gFY2Y0Qjp12D9P85XqYuFClCCHXeT08jIpuXY+Po8iki5tDWGTlGzhE61CWZt2xiwxgLhxOdFteAS0A0s7mmKkSIwCNHoQsmIBtqYwMXBUQh1GkknCWG5CjInq47abaGiI6j4V5nOelzfb29VWqkNS0UQuPpmZ6kToMdZAyXS7neVnmedntdvvjbtodpI69L4DQl15qXdfZImxdp3Hw3ehhfZ5DapsbAI11p+vsq2HhUgoj9aXXqVhrETkth7D0HsvSSi3LbWEpiSdpS0NkJHKk8+VWzhcsdX47B38q9SiHnQghg5sHEKKjAxFnGoyQgQLMAUB7V4+nx4e3t8+3+foXf/y9d728nHXV8WH/+jr/6U9fhsJ/+29/eDiIOQfA6e2szd8/HR/GYTmfn56OxOWXn77ebquM49PzO3X8/HqFKChlbf5yuk67uhvru6fDbqrr2m+zXpd2ne12bhrx/t3TdNgJ822+XU7z9TbrCoxUR3EN56hDMWREaubLoq0bIHpglezxhNzgCeEyX3ejAFFhYsZBSAAxgoBkLM6+rlZ3te74drkt8wwYfY3z+fzy5aWbmtkPf/iwf5ouby/turrqUGQc6vFpV4uEm0LcrvNyXa+na7/1Uuvu3TQediQl3DAQCM2NqXhmlMLNopTinuJqtk4FZJZ3k2E8N/45yicF2nsvwr0b3DXJAEeCu8ddt0MMEloT6RIETJ9QIKAIZcGju7NIVkqEm4UzoQWER5WSZpBaanaDl2wNCyKSLQKAARuAAAmQi7R13TTYvNYmyRkpSxtqKbpxBsF1K87qvZMwM20FM+kTUWXmrg0ciO6FZWlLSe02IkULzo16Np8DbB0tkIjf7alDzIEugBieEVba4i8BQRu3J1fh1l3KVp4VW8qJtixDbMpu3nOHYfBwVS1FSMhMhRkQTC2rlvMlTCNlHuvqLjl9Q0jhxBuBbzFEMy+F79xtzJwtAmQGOB3DwkBpHvBMMVBExMaG2AASTBRBsJUsJMnHk3iqni0gaGoskoE7omwLMECEQE7N2TIcBwTomFu2DXSazZIW7qaY4WQ32ZA1Sojd1M1Y4G6KzZcn1UWMuOctEIiot05IVPn4NPk6auvjWIh1nRciff7hyR373IL5+HxY5j6NUziYuXVlRFV1NaMgtGE85u4PPenWzgXdOxB5GJUI8WCvXKWAdgIwGVgqSuXhUIYJmI04rJmrmxpk4aArYJh2gAhnqfIgx920N1vNoVnvc8OBpKCp6dqBsEy7p3dPXddlXdd5oYjdcV9kQBIAd18AQhWKiKNqB2LZPRzfWgf0tjQkkgKBuC5LMSFhGlgqWQ8ppbXm6hEU0YnFmk3TDlGEMDym6Xh7mcs0RGAZyvU2D9cb4PrD8/frvJxeXx7Hd8nKQZGNrgr5Dg1iDHdXM1UiGKfBos/X67KuHz9++Kf/5r/867/88OXPc+86Ev7DP/xSqPzwu/1hojLUl0/Ln396O8398bgLMFvn3vTDdx9//fX0689vAP7u47M7fHo7v77NZny5tttt3R/lsK/vDg+Hh8PS4OV8Oc1du7uC1AHV22rqt77qagZBiPW7jw/Q9LauBEC1eERTdI11XsOxm5ciaSwxsw5exzFMDXFXShUuQghepGRWkYsAA1XsqlQ4iJZrn+frssyX89xns67Y4eGw2z3tI/Tln39DtCK820+H3bSB45d+fjvfVr2crrdLf/l0qozffVejw+HhsNsNzLli8IjE+m8fLmHpXTM9k20gSFSl9K1Mu1MaLjLri2CmFk4sPTNJjNodIN2BgERmCr6lUEspahYQtVbVzlthVGeSvBtleMghMh6HAMKyLGspJbn3booEFpYTBrhLKWb5d3MmEib1PHA9ELw1QCQiCzc3RErmbmxXQ7fwRBVtR3ypva3MkrBLD0ULJMjVS07bxBSOqU6HWeIHwi15Tfl9FBFVQ0CPRAl124pd09GWuSiQhOgHAEVkP5e7ZUMBcWbBIL032R5/z4YEOIQHEwOmiZjNlIkii4KQkvjWTYW3tXw+3ADhHteK/HqbQzT3Phj5VM4lTxGGCEo6VFo57675fP4jE2Ry2UJkE5AjS8TuhAl06qr5AE7SHBgiYamcRfYeYe5pgYhMAm74NtxioQnhYGKk1jVv97jBWjdeXoBBRDaFBYRZvt6IDsCAELUOG7M6I9MRgCgifV2lDGY9ZUx1BSJdOweAeBm5DlEHqUzm1pf2+vnzYT8icZsvACBDaboKcx2LYq/ToKusl5ubWa4xYvK+eHcIAgNdZq6ljHsQpBJSOQKAsU5TAbC+ehgQTI/jsK+7h5HYdL0EaZwdXNO41LW3tbvGuqx1rIAAyAHBhLUUVUMP790BkIqG9puZR63jMAyCZb5dXt5evr687R+Oj++e625fpv31fBK2tvTwwCLtdkWEOgzLvPQ+m9nj87FMI1w0C76IiBiAQ5uVWm/XxfvqoWUaSqkBvtsPy3W+La27cymnt7fd8ZGHAVujwNttabMqts+//TY9/z4vHd9GMURIsQfvLSJI4QDIKMyfT2cWOL1+/df/5l+h+ZffPg+7cb2uFvr+u/0f/vhuv5Mvny6//jJfe396GvdjqWNps/ag86X9w9//at0eHw8Px4frdX75dH57W7vhUMrjYXr+ePj48Ympcq3/+NPnn/78FaV4t8Nht9tNgHGdl7fTzT3cEJnevXtAhrfbmZGwVAtfVr3N3Xusa1+BPa+khIRcRAjdwxMZi0zZ1xYRwjQehjoWqghFqGbROXjY+e26LrfXL189qJaxjnL47iCF1bWvdhgEqRD6cZxCXVX70tfb+vr19Hput6vOi51e53eHMu3q/nE/7Adk0OjiBQAzV79hBSw08jBVIgQiMPeInjWEACKy4QAARCT36ZKH49Z8mPTmrLgw7ba1BWzkxI7MKf4BQiqocd9Fq3ZmTvVu4wSZMXOt1cykiKkC3a+zCGaGhGAJjw5mueeKGBggMtiVf0lPsjQRRpYYAPbWpBbrXmtVM3BnZoRI/wsTa+/CbJbnuEMgENZSzTUjWmFZBUkZVjDVrMeJO+khzY9dW56uOT9teI/8BWwiJVrX5FMy0ybFZP1HLnnyeRqeRiXMdT4CbppK5nsx0XIBpNpLKV07EzIxfEtIAZhbLt9zr5ePJECgLSXA90KGfAA6ItwBSNmtHIzkHpgGAoBwL8JujoBZd1CKbMQ3gLDgIp4CSr6sEAZBgdFdNTmCme3OZxJ8Sypt6u425RAidtd0BLpHEd6mU9jieRCAzKZ5NuUTaHMmEzIzq6YSHkiIgUSo2kUYKRglIoAY1RCBq4BCSAx7qYUF6fgwqvflNjtYUx0GhihVgIVFym4/Ccl8camFyUJrm1tvN6QSbe1NQ6GWYdw9ru1m0UFvdRzKMAK5h5cd190UJOGjaSsU40SlsJTCIogaNuNu7OuNETQMg8owGBpzQYLeUwAzjSgDTLs9k7TWtfXWbgA47SczD7P1dqUi034fxNfzvNzWgJcnej89HB6e3rVl2e387fOL9T6N0+18A4NhGL6+nMDsta/vPj4Li0csp1OpBSoTcQ9rt1upo5kB8DAObV4j4nq+uANK+eWXTw/HB0dce3cEZEGm5XYLQCQ+vZ7cPRDDNCQd9CnfoGcTGAIgWqZvCNRMdSU0sP7+4w+//em/vs7XcRxfX94G5ufn3X6ql7d5nsGAvn9++Pjdw3Jd3+b+eprHw/HPv7x8+XQ6Puz/5t/91fl0+fLp7bLE9dYfHg9Msd8N754flhafPn95Pa9N7fBw3E17Auhu1+tsCAg4jodFFbq3bqfTiRmQAWppDuvalqVfrqsqWKAhEjNnNArBvAFAC92NpZaSnztGqEWmwzDsilTBQZyyCEPN23Jr67K0dT0cdtN+QoRaxXqfbxdERGu1cBEax2Eo+8+//rws6+c/f50Xv9z619fFEda1D0CHp8f9cff08TBMjBARxboRMTN36+gJHMJwEKFtU02JxtzyNJvfAsFtK2vbCkgQwTbXv23wXU6bvJuySKYKfLvrZgDIiVm9M0m4RYD2RiyJRt4QmQ5Yao6C3x42+WuAMMvDPSKibJYWSN0oERGmmQfaFOdSOGVtNZNUj7ezjnL9zZXDvPcOSGCGSNl3kMbF8GDGvHtx3sjD8gQ03TjSwmzp1yeMjHYBJh+DkQ3SncjbaAJAEbLtjjFIaKNfEeGGhgTYenQDAa0bl0wdM8CmdQJnQa5sZLSAnAbyFZY084alMT8PcULYIDyALKSqTKn6hvC9kmWLcG2DmyN8y+JuDL5/cdrkRstT2ylFIixX+eXbBj8MIFJlQibvWookmq3WkhtDYclpwNxLKar6bfWfRs907rPklJDM6i0+ysJ47+9JiiwioIfU4v5NNoBlXVJcT48zIPo2ENxdpHlloOKQgUaUSVSllhCAEJ/GMkyY6SgkeHx+ksJUWMaBMdxNhmKq4M4CTKTO5GoATIACTAHYUcJNXZ0NBiyllOa9VKkP1ZF1dV8IORwDyHubuzqyoxA52Gy9ra6OSKUWV0WkZWnpcFDtSILEBLisCyLsHg58W2Qa19tNu7fel7UVKXWadofdfvdwOp1u19nt06G14/MjCS/zglxefv06DgWd1rnVSu8/vLt8PXHY9eUsIjKWOtR1Xtm57sZSC0QQxTI3Fi5DbbelluqJ6ivDUKaldySar7fpeEDipbe1J1/E6zCyDAiMLJDzdS6GExWeZpPcfELo2pfb/PD89PLLP3748M76/PLpE3ggwOnl5bsPj++ed+ttfju3z6fb4Xj4wx+/+/rry59/PSnyMO1Pr7e//4efn592f/1vf3x9e/vT3/+y3HqU8vz+nQjd5vnf/Xf+9ctl/ae/+6dPX2+Fxt20ezw+gOpqvvYeRJfzLQLct8yCVJp2w7ouXEcNuM1r11jVuwcIY24HmBCMgEGVCxUkQShIRXgsMgrsx3I47vcPhzKJTIOhr+HruqzrbV1u7s277Ws9HMfHp525cSnLsgjHcpuJXAjRTa++zOfb6/nl6/V6am8n/fXrJYSIQSie3x2+//3D+x+Pw6669wzTE5MbbDBkwiK1905bHxYiZ6KFuFDSfrqpWmfgfB4n1Tw8RFANtfdSazYy3fPbRLKd0bH1gADk+jtyHyDp/ogIZiZGiOyT2VqJ7vQ0MO2wqYObxVBEckmOSQqD+wDpseGJhHpz2MRUSZuMqX6rR2QhUzO1tFlGxDeJMQwi8tkQEAGEuUK3cPcAy5QuIUTGBSCAWLbcImN4fkE3c2RiljBHot5bqcXDw/O9DwKEQtS7wbbTj6RqwLfdR+6s3UuVSPOlh+ZGnrerfPacREAKM5vNSF2EkdF9izZs5iIHI0cIFLZEQdxniPB7Xiyv60lmB3Rzugfk8ieV5k63TNCly9+ZwdwAghHBMUnUFv4vo4IFcZQq+YbbqIHqERGUayjPFDUhJkytlCHA/+VEyEgGhKp+c8TeZ5kMp/BmOeMsMqNSBAFUu5S61UttJifY+Bbg6JlXSCYphEf2i9ZpkDCKDmquHVyGSQaqgruuxmXDCzLxUNi6rRAA3rULgzEYeYSXUpChdzNVgpa+CA9vvRdrhbjUEmD9dkFCXWbv3YS70LgbwtTa2tuMoeEdA6UICPbW1+sCEcQ4jkMGDh2wra2tfdZ1afM4jh4udUCIUqtUdIdu5hSqOl+W3YGeP75f2+26rLfbBdCPD/uHw/HL5dM01etpDu0Ph8P1fC6FS629r/pyenz3bIv1rlSpAPd1QSAqIpVxhoiYbwsVCQR1QwBEPzwdP/36pY5DnYp5f3x8XNsaiAAeGMeHB5FqFhwevdVhzA8CAljKicweIaW0denapOA667zMP/7Fh9cvf7q+nsP1em3vn3YfnvcR/fXUXt4u7949fPzx+fMvrz/98mV3fAwc/vHv//z6ckXkDx+OCPrbn0+3xToMk0z7/Yeff/7p8DC9nJaX83y+rEz8/P5p4vF2XT5/OQGjMRWWYdov8wzhIpSe49Z0Xay1tixttVBz9VCggmXLxyNSQBHK1S4PUnIMNR8LV4lpP02HaTpM48MOC61ub5++ztfr5etVijHB4/Pxu989j1K4soUHA734OPAiaH1t84xuwpXFH3ZTv+jV17622D6Z9vzh4fvfP37/w/Hh3X6Y2D3cekQw1/uRBQDYewvY0Fvh4WCBuet3JDS3HMMhMMKlZIgyuIiqMjHVslGOzcCt1uJuENjdCguwIIU7mDkFuEepxbwn78vMhNE0AaIbZiggiMi7IRIxZU0sICaeII38LCVTDKompdRSzAx9Y8axCOLW2PXNyPRNPXUz99yvECUmnZiZulqefpzZKZbW1loZEBGhpp2EBO6mKYSwCALMHT4EsrD1QEDmJF6kxwZEJPH9gFknTgIAqknz3wrcCbd8hJvyxrjwzMIigKoSQl7nidiyCGy7X0O4E4u7ZlFyHuW5R8/JC8BZEGBrnLnrNJs3d0skZPAmOXSe2WhKzDciIiNl/4x/W9t4XnJyLsEtOQcRvmWTCXIcy58y5Gpoe3giEgpuKRL3XPjiHd6QCYOWxjLgze3qGohbC0I6ZYehmtl9eQRZORkBGeAmIpFCmMH6IOGEzeWbIFtQsl9J1YiDCGn7UIRrQ0EWNFPigZikihDCCn1drTepg7WlG5s6hjGCQoLttA5C4NbX/MFwHXjA8CCNcHBS7cv13LBytUpXR4ywhoQaxlDWYawDJ1W4z936us6z1CzkBa4V1Cz7r9G192EaShEiVvVxGFdtANBbv+mtlookILQ/7onlel1a73Y5cV2f3h15kOvbPF+ura3Hw4EZl6W1pbd5xoDj/tC1L20mA1t8qWsZS5WxLYvsR6Ro3gncletQ0cHWXsaqrbW2llJPb9cyjuMwardSuYx7LMXXddwNTRvIuD/s83IX7pxeDsTYPjUZ6kOHCLPbMs/zUsfx888/ffz4kcjOnz6/fPptGPjhYYqVvLXzZTld5t/9+HG3Hz7/8vaP//zr88MTQfn//N0/947H/f7d83F/lK+fLr99mjvSDz/8cL1cfv7pZ2b+4YePBWi96DQc64Br87fl9XZblkWHcdgNAwuF23G3U9MIatrdHIOFyrLocuvBWwA4LS0BIUjEWDIIhpz2PkSigN04Mvi4r48fDruH43jc7Y7T6TIv83W9nK6vJ5vXaSjDQNNYJiEmn6YdVrperyIRDjxwSKVDIYJ+W+e3tqt0I3y3H9sSq1kPLyK/+/37f/O3f/jhL98NQ0+yP0YQiqkHxL3ZA1JbzNnXAAFCOEOjgLlwB2BmZDFTIAwFwDy1IU1EqTtiAKOYb5uAoQ7pNTUHvNPlmFhVwx2JAVCY3dQ9kCi9N71rGuJpM4a6gQOmrdzvHGl3N9zcA5IL42yZQci9PLa2QjgyJ3CCGS1CtUspHpBJfm0aQSyUkNT8zbQttNHDSimQ9jSAIEDfNA93hwBiArp7gTwgIpVqV8Otg2VbnrtnnBOIkLJfM59Ibo6MEVElPUmblHA323q2YqY3gpjdbOv4BdhcN+mvzwLMjRm3va7hFoDfmrnySbjVckUEbZDQ3PrQ1h6zKUJElMJvRCBQhCfcbZsGANNkz0QWBh4klPwTyvQvAECe1/jNLwXf7EseyJtl6E6mk3DPD7y5ZkpbSskeDW3KhV3t/kUwleLI8H0OTBqZLjazrYWGxc2KDNq7iAS6q9VS7l0/AhjEW9ZORDDhehClFCjQCefbvJvKMeOghZMIiIClkKq5tfmytGQYOHII+Vb+5QZt6YLCwiTMFXfT1NxM1ZGZASDaslYuBMwU7hphTMAIGJo3BULv85KdDURTaysDIWFfezR1JAtvTQMjkAHgeruqORFKlToMECSlRPjaG7kgCzM/PT1YQEASO/o0jbtp9/b6utyWW+BQpQ7Dzbt3efty7XP/8Xffr5e23m5ucHq5Tofx4/cf+7L2pdXDMJbxdp3rCFJLu61EXLho2DSNvaswe7P94zG6ffr0+rvnZweoIm1pYYFmhNjVAsK61WmMjJt6bIgx2N5+iZsP8Ov1tVQeJj799h//+e/+vwX9r//445eXFwU/ny6Xy/rd9x8J8R//8deffnn97sP7nQz/r7/7yXo8PU7hUaeRZXp5vbpMtcrX01dQOx7kP/mf/Ie/+evf/xf/j//q87Wfz3653GqtVZBQHh+GcRrdo83Nwrs2EiEUBwSitbl3TZFXhB2y4ytUFRmT9ps3rO3KjFwE91MdRtxN+P77d8fn/bCfZJRlXa+n8/nt5fr1VTjKjo/7QSo9Pu6GKsIsHAA+VfZK1i0GHOsUoNfzDcGit1A6HqtDnK+tXLE3/fHHH/71v/n97//qYxktO6LRw7tSAUEOj8wJsVBYIKNqAGxkly3CkOcWJsmgUwAShTkzbeeZB3LySHw7uiDQId2i97XBtjNhIdO7VABUuSh0NXPTWqtZqBoAlFLc0lZEuUk2UwAkodwQJB2ZiLctDQZRyRkCiNIaqKq5utFuiQYyMyAERPOkPW9bdymSLAYELIjOmB2WjlGSsJmJAczAE1lG4SKIGQAriYalmQfg7o/h7dBLRl72zLgbcrgqsQCAbNuue+pd1Tws151+P44pSSkemEZ3S7kdYMO+xaYTxD06z0nvz1fOJYGuKZ5ghgA27ZeF8Z6257vDFwCSdpRuGTMtUjxNSEC5EYIIZmROqhH31rgIIDJS3IvTIoKEwAJygHBLQSYHUwQkppzUPMC7ShHtyky9tXEc1cxMmThDQLb1CgAgunuRmhCbtP9jTk73pDElWJg5CLXncZ8xNN1o2+5cONSy7z73WvdtUvp2MypBu+Ph1nvT3pSGcQKIrtrXZhrCm0XVWnfqiGQaGDjVUQq7NgSrlbwrArsqSJ/bmYdRCpdpEgE3M+W6q6VUobIuZxpg3BcRZEYu4u7eOxEDWaZjGNlVtVtYlFrnZfWwcdqreT4FS6nJIu9r660fHh6BiImnOkREW9XcganUigC9q0WcT+dhHB6f92B2e7sdfvhx2sWn9dUUGcb5ZKfpwshurhbtutRaLudzBPbFeHCmMtYhIIap2toA6PT1re535l64NLN17hZNm2nvt+tlf3hg4Xlur+frDsfHTI0SliI5n1M+9/7lEhPbU8HczefLXMepr59/+sf/GmL5+P3Dbbm9fj2p8bunh3fP8Ppy+enP55fz8vzhqQzj//O/+vNq+O559/i4W43Kw/H/+p//l4zDpelxlB8+Pv3V3/7lf/gP/3p2+N/8b/+Pv72emka/2eN+d9wNQ6WhDuNY3dzN/bjvrb+8nSx8XhYHCCBBdgTGOB73dazL2sxBm4JpHriVJdw5gjGmQY6V94dy2PHD4/jdx4f3P7zbHQ8ovLT29nK6nE6nz1916cOB3j0f9seRGYqwaicJ9RBihj4WbGYysWnTtug6h+q4q6iyXE5MCOAIPhT++N3D07tDmVB1MdCIzPNwPgh8q/OF5Png/YLoyWr2SApZdpMQYvB2UGx2kizkyqj/MOQZqtaYBSiPzID7otxUkSksdfG8cWG3FhFMxFyTXoNBERtxBBBM1bcVMeVOIlHHYEHESORqcR9BIvtrLSAcSTI+aPcm3m8BUffAAO0dKZVaaK0lowIRLcBNMxsR7qoW4IWHZBaYaikYROCx4bbMmjV1ZREEQtgajN2DciYAEGHtCrkFc8StKIyEmRxiKzIg2gqXLZByiIntKeqRl9MA0+wHNmUsiGRqVAARASHh0hCBKdICCBHSfakXTkjuhtuij9wDEYTJ77TTbKqy1PQh3CHbcTLvl2uoXGYlXZmFU9PnrNaCHHq2v7ZHMKOZmwHxdqvIaTNlfQQICCZG/hfwSKlVVZFIpLgZArjfg9S+uZcyGRiZGelaufbeck+VjKN0ASEhiwSCNiuleIDIlgUP862bBAkh3ELDsiyeiPIbdI+5zwpaPUx9XVpUaq0RQBWSIgAghUPbJsuH2erX20oFMXC/m9q8quK63LiI3hQBSNBWq6MQMYLJAGG3tt7ULcBLJQJDHrjW1hoiCINqd7WeKxQWmca+tNPrpfd5GMbT6YLYgBgB2rKS0DSNxFRE5qVfL0mKt+l42B8PdbcvXNbWluuspk70MDw+vn9/eXuJBkMZ3/T609///Pzh++eP3/38T5+0tbEOp5cZXLPrh8Cut16mlQW8A/QAAWI2733tgOGupdR1XoZpCkdhPr1+3j08jNP0+cvb189f90/vqJMrhAYjT7uJEcOABoEAFMrEuW9Rmu0mEeYAUIeqfSTQ159/bZeXh0OtVT79+ddw+/HHB3R8+3p5/XypXD4+D1DGv/v/fTovXUr5/ofvTlf9+bfzl7/7ZQm8nc4Fy/d//d3/+D/8d2m/+8/+b3/3d//02/k8Q0R13I0yiWP0iNLXZW0rBKqZBoC5Q3hgmg8BQDEiUAiHWtx9KGVZewQgU7gKV+udEYtQJdyNMk10fJCnh93zd++e3j+U3Who8/X6+vp2eTmv8+xmh4c6HYb9YSzitco6n0K5ciWpFCziq87Rb90dAHW9RTQm4cLEI8mV0GoRAahDIQGH9XbVWh3RRIpbEJbk7ubqNy9AAIABxLjVk7hyKWFWh9pbR6AU5QNCzYiQWdS0FOxdmSQ8m2IDAKwbMjHyZsSESJJrvqoBW0QUiQASHkyt9yIcECLcu22+Q0t9mNxz85GGb+/pwffs3hIzQ+DeO2ZxoWRZcY8AMw+IrBSO2L5sRhlUgZACsm0lhFFElttchSPYXMswaEspGFQ7Fw4zImqtiZS4t2PFnVzAxPc1DWyjn3sKq6oWkJ9RQt68QxAoqg4ERJIoG0IMhNzkJJgpoxa1VvDopqk5EJI5MpKB5ZTgHhtdj7ZtCyAQb6oyM2fjO2xdY7JlLCmvw5aE0tyHRADENrAAQnqbXE2KMOe3FVnaINl0k/RauK+VMBDv4MagniwkpDzGASAzX0Hh4cxESOGBzKa29Q2ZA4J1LXWrzNyeLPfJC2FDSgBgwqXXdQUIYomITBuS5FLPANC6ZX5irLW1VZhoi2z49oyFoMQBmWcYLV9ECKCElFVmLkjkphzwrSVDRNrapMj9uVtmXRAjYXmtrektwnC3zly9zc4AiMvlxVZCctOOGAk1QiGEYToMtTLQ3QBGxBLLbcaAvq4a3nqHwGHcL8ts7g/vjr6179E4jsu6uFs+5B4fDupBxM360toyL8Dy5fT29O7dtH+83S7MdDlf53kd69B77B/ffQD56e9/++3XL5fzMky702Wx+XrcTU0jXS3IxRvoGkyEEX1pJDAcp1g7CRuygwNDrCZMVOp5vjHKbtqdT/M47td1rUVsxmVdC2Gf9fHpu0ac90VHp0jvDBBgQoNTHstsABOOY+mXM+iV+vr4OH759Hp9u3z8/Yf9NPzy0+fb4ki1d31+Ov7zp/nry0xA//7f/6u3t/6Pf/r16xrLbe3anh+O/8P/0X/vf/Df/9ufvnz+P/zv/rPX03o4Pv74w+/HQgUjrJv327wu89pbX1ZDJBHa7Q8eoF13u0ORIZWAFFqC4HS9EiIwCA+liFlPfU0Ii4ggHI/Tw3F4fBiensfHx/3uOKnb5XRatZ1eX1tbdF5rlbLj3aHWkZlimmqYVtFBAgF81aX5Ol/X27ze5sLEhB4d3HfDuPa4vF1JaFmaWqtVHr97PB6mOgmgrdqFyTHCArb3r6a66m5C7AHaO1d22ExxbmrutsxErL1nsaC2VqREhKlmF7yImLqr5T0XU0EjCjcu2b2BSBt3Nhy4cO9aJa/YQAymBu4AhdBNNfE+21UdIKmcsBVDQziwMAKYRVbyMrO2nvsij1TgMNJJzgQKIN8QpGSmyQ5g5ghwDS44jKObddV8GOQeINceOTSwsKtFYIBv4itCuDNlhXN2sVieUbQNSUjMqfukB9LNUnG/8/9JKEddAN6YfNmwCOmpJ+QNGJ4hZ7ojmL+5pO/w0o0kt5GFNhMnpZE0f0bCSd1gosAADym0+WGy8i37LWHTj3MmEMBslxRhYeraJUmNOR+EYy7QA8ydGSKMEN2dsnDGPNPFuToDiC2MlnNAbPfvDBMgIRJUKeu6FinIFL5pBgRo5k4BiFlZbHZXShDgzqlWtXEY3DOQEVuiJKyUktCurj3T7Uxk3sOdiHPmyGcy0UY5IiJEQ6bpsN+NI2kvYKUwOPRY9NaHaSKU3jTAHYJICIgq1lGsde0NQMANAZkhOJBJKjOJ64osaKGqibkrldyAC4tQ3QlSMAmVgYv3vnLhWhjc2zy7x+XtxFQBqE7TOO3WZaYAD01paVlvZiZSetOIOPfL4/tHdRAuUykacZgm5vrl9cswjMfHJ0LaPzyf3l7eXk5V6PT29enpqR72l7f5fLr89k9fhzJ461N5RKbXt9enpyMwDYVMDbneLpc9kzbaCWsDN4vwaRrV/Ha+Zs9EKcM4DtZ9nRt4HA8TAnCpy+qtd96X1/Pr4flACX1MHAjdC4FzsE3sR5asmxeKtV2ur79WCZjX5e3yuN9VIjR/fHgYBzm/fWZmXfDXX1+r1O9++LAq/cf/+On1ti5MQPh8OP5P/2f/ycPj4T/9T/9P//z15bA/fHh+evfdx7/9w/fLMr+ez68vM4kgo0UEUam+rouuZnHV3ue5Lc1RJBm5SR3sPVgqeGBEW7upMyXigdKLd9zvdsdhnIpUgfDT2+u6XLT3QO8993h8eKjCWaJnCFALYyixjUWKUK3S+tqWpS+Lq4IbMrqpMMphwghftM1LW6ObE+PT0/THv/nxh989T4fRY44gQvZmhBLhJGgdNqi+h5G7BRBuvsbNEpKhTkEAKgIY2QUGBN4NM0GGpGoYkd1bWyJVJHdEpUjoFuAoVCDQwBBwS32al1oS+c4inrk/2DpCICLC77UtGz7SujJjJAUmkc5MYZYgLbNUC7Z1Vm5+FCL/e0SoKWfVOydompDYPYuPgQBT6yUiJlHvjIxEpg7g+c5EwDCjLEFLK62b+73VvCdKhxySNO8sgoGI0VVTMIiIsFSzTe5SarhpCiYpJW/nMgNASF7Mk40sSIFpmQcAyihvZgkB8uq9nf55Um/RqtyAuWwqcWRWi3jbRiUNg5lUlYLyh1hEwr2UvN2DgyOhmbHQffTJHe1W9JyrrJywEhCEiNpVpISHm1FChGJb3uH2VQIQfWNgoGqXBHx/82veC3M24QXxjvEJgEBiCFM1KVxQPDwgiohHAGGV2tqaJigm8RyYkkwdwFwQ8x2sLNmuE9oDMRAtnyFlIyAWclRdvbVxGLGSm4ODdQ0wFAQKqjQKy4QrEkYHJEZe5wawlR+woJTB+o3YiUIKF5FMXyLEsK8oZTiMkGR9cOIihLquVKgMEioOZZiaru6Ba1sJgJj72rkwCztELTKbQfjxeHh5OYP7599eA8IRyjQA8Kue67h7evfxerudL6tZvJ67O15mX67z5Xz9w48UMtYdvv9Yv/52ef06Q0T/5dP7p4e2xjKrY6+1ElPXXJuGqS3XayQsKsuCAvf7HTp26+axtGZAr2+naTdSqUxUy/D07rEb++U23277p45kaRUJJMbEOoWb5atPhETARC0UwS6Xr+vl6++/H3/9+8+ufdwNBbmvMA37f/hvfvny+XZ83P3HX17daP9w+P6v//B//r/8vz9/PYXUsqePDw//i//l/3w/9P/1/+p///q2joWxwt/+zR/U1//iP/+/W4BG9B7mAA6uAAiEUYfy9HC4LasIj+OOhYXILJBlbdqarm01yd3GRpAnpFLJPSINdWitL1doa385vxlCn2qZduNuPw6MZRzGSUrlcSqI2OZrmVJkdZbMeDZXEOLCTEMNoSWcKAoPAWEWt9M1nHWZlzUsggvtH477x93xwxHAkclaSn2CJGYNYttQOyhz8XDASHRBbLW6nrzFNAKJsJnmxba3XAEldyA2Pyxh1l3mSt/dGfPWHF11oCHVXUJqrYsQISKTm2ZFbf4v4ekfiSp1Wa+lVtcO/9LT65AcfN8SIrVWM80WCrNNsc6y2+z4c9+UpICA9E9mbilQalYPgVsQkxTWbojIImZqrtqNiSIM88jlJPxQeKQQTcxtXREZUk3xIN6OuJxUIAAjlV0HDObS1oZEgZHCqqRSbqrZhLpFqygtw6amhBSxJY9zvshTYzOuEGbd5T2u5sAZliPYoMzAhBRIjACcwjKk9J9/HDoRMgmAM1JI2hmARAC3qkUM6L3XoYpsFZe1lrgH4YgJKBugARO/B8SMtK130l4ESMSMyZzN0GBmDwLBu4kIMISFlJIh8LwMmmtijzKzt8WW07uaDy2i7BXIF4nzwUAI26RmLFsCEDFJgQBAiW0iIVdlkSxUJhGIkMKxuQ4cCB0hIM6Xi6hy2CSxtgVJ1ttaxsIM4f9/pv7k17YtS/OERjXnWrs41b333fsKM/dwC3OXe0aITIIEKQQKpRAkEgiyiUTxD9CiQztp8Q/QyP8Aekg0IDNSiCRTgGfKMyLwIhKvzM3dzJ7Ze/fd6pyz915rzTkKGmPu63jDOv7eu/cUe845vvF9v8/LvMt7PgwLS0xgZr25VCHhZVnqNLs6AkAoEbg2IqYJI1zdgGJ3mMu+SJ1Njcq0dbWtuZ+niT2gLV1EHGC5XBCp7kpvmuEvlvxkmm69TAUZd3M9X1bgNh8PwqU1swgUXLVPU+VS101LkEy73k1k7h69+7LBh6ft53/x9md/8/7rr1+/uL/h0IfX9+/e/xoC1tPFLe72FaKErX3tu8MMAa7eWseKpjrta9saemTyCed6Pq8iRT3ef3q8vQnt3nqLE7Ru87yv01xk/vDpeblcImnPlvvf8fhDBGbOolpzM+2AHmZOenn8oW/r+x/W7799q0E3N7vT8+IA63b+s//Pz+5ffHF6tO2yUJnubh/+8A//4v2H5d3SXjwcfvrj3/73/r1/+vGHd/+n/8t/uqvzP/pn/8bLm+njx+VvfvEL79kzK9AUwQuBozOBe0ylBMHz5bIunRncWE+LJNKAKI3STJxQ90xIueq8nwK8CJuamq+trWufa+wnAMHjcZorvX5zVwtJLSSIHKVgnVB79+pMHQEl06Stm5vSEoClChXsEfO+LpdLuEktxMI1to8rEKtvARAEd6+Ox9u5TESsDlCnEoZmgdGTxAlIQBTmGbVJfT6FkUFIRHIwQiThrj1LwYrU1jtEIKFHqBkAOkSoIqK7eWCYpk6QC+WpThDBhGmvqKUEuI0XIacKkW8yYmIR0960ZTdcBKJhrbNZZ8JIGAExMbm7Jz3YIiREqKsDADK5dQhKNTv9gqZGhd0CcsIAGBiBLFsMj24sHD52hFdc5jDTjJowDxYAIjBlGQQ9IjSH3E8wS9fOyIQQFO6g2kcCQd1ouPaJsDdlIcEryTmlB2bO0tsB1A8gZlNjxuyY5yGwcHhk4TBkAta9SImhEmGC0lLQGLwnw+xk/qy6AAARZr9DgCOAgaegFQGAISyQ9t7wWkpSkACchCyMrisFkswVBmIatkadZgRYKBcZ1LYM2jEDEaaLC9MUbMmEyshMwDAGqWqeAuHAggCDVjSqiVVJGAFba7nitnBE0K5ylV+z5AEH+pQsRnaMiHOe8mYixUwRgKUgjh55AFRVGoDyIGQu0/PHs59OhWI31QigoOXU9gfZHSfdHLxv21pKKcz5eyyM29aYiBlNe1CyPVqEIYb3vp76tBOuYmAOgPPkQLvDjbu33q33bupBlUUtzudTlsSul22aSxG+nJfNbdpPaQjmWrsqRJDI4Xij6iS0XLaAEkxEOB/m3WHPZT5S2brNh507tKX7pVOhuVOtfdodfvV33//6F0//1n/9D272TGX+4ouHd2+fW7eV9bibzutS2dM+MJcCLgTEhNZtO68ySYveW5+maZqnboZGrp2RQPHmeFjb1lv3sN51d9g3VRBuzcBRm7kHMJcypYALRAiOAX1bpRRKLYWpt61WCm1Ndb2cv/zRV71b73DY1+9+eZp2x1qn5dwuq9uEf/JX335alvePj7Ir/+yf/jf/3f/Ov/nXf/6n/+F/9F/ON/uHV198+7cf/uTx4+WydDd3ExBBebi5PewqI87zNE1lXdVB17Vt6yVMA8uAw7ghoDZ1oFRF00AsRTDCjNZtTWgVIajaqNJgEKq7XX24PxIqeDdFqRFBhZkIXDWiQTRtLnUOw+4G2s0NI6iwG5KQqYOHCGcKurdOQBhMQJWnE26747S/nXa3c9dzIfJQgIBAktzxJj8yLF2f1s0CMVuhqG9bMve1K4uEqXUnQhQJCHfNI7trZ+LcJUfa6tyTBWTaqRRXTUUFiUxb+m0C8o0YXMS7QoCbiwgAmnckUW0ZKNO+EXGAI3LvWwAkIzbTGO4J93XAkFI8QtuWp+WIjV6FlCy0zZopzFpAGD8xABAW64oBiGT978HApVTA0K5xpZ+lLSG9TPmztiSq5uENgUSqvTCbuqZehFHqFFk/Eqaqdapj6GFHAMm/HwYNVQcpzcJDGR9dl2QeCAmVHSAEuKJ1aNj9M7+b0hFkbCHNs9nJHu4E5BBEmX4ADwfEwmKuCGBmycNJow4LDRHLgSnf95AW1cGKHowgcFUWjogsusx7lUVUFQnH2laIfKCRiFl7IyJIJrh97hQDYtRmiJAVboCk1qcymev4IgGStgGAGKDWRSS1Y3cHQiHG6yJdTUspEM7CZs4Aho6Q2fcAGI3HgJGJLyBySFULR8lPNpUTlf28v7l5/7isz9tTf56qTNMOw9AAkQ/383K+tNZgCqx1nkUVM4eFiNN+2lZlpt43kcRgk1sWnyFgTNMExKHGUw0ALKUQRhiEa9OTbswkzH3bslzMVPO4AcK+KhJKkVqJUHpXVS/TXA+lbTpNpQfVXaVJgnE+HlkmllodHJAcgCcofVm2l1+9kLl8/Ts//v7t4//7//Ynf/ZnfzPR9OOv7x8eHtrJv39sXnHpWt1L9TrNUkpXJSEUblvfVXEPICJBDDftrBimACKMtci6ra9ef/n2++9yjlZXLPT+h3fHl18fbm4tEASjBVzx7klzhAAH4FIwwrRDBISGdkLSzR8vz7vDobsvp9V9+s233z49+Zs3r5dF374/PVqs3X796fl8OSPBf+Mf/8G/89/+/T/+kz/9f/7hv2ildq8//O2HtW2EPVprXb/+6vXMuN/vRAojSq2StpMCQsJcRQrRkkiluELwU6ciYWJwS7Mcpl0I0+0RqNoI3IFzZ8vMx9udWyey8/lEaK0Jl+J72e0KV5oLTzKbWSYmo2lvq0OUwm5BIKHGRJhbeHN3nOp+3RqFWMe2GAMe7g8vvryb94hBADHYBkxMZBGeJykzgEG2DRKN3uZ+rfoJHx2HQDkncKZBmZnJ3BHoswpNSOoWmoqKEUlCIRHznd5GQjP/IGA3A/dkGSSsjBCBGa/FiqodAq5Pb7AU0JP4gmDJtB/MUQQAJIZsTGFP6heMZzQhUkCGcZ0LM1NabHLJbJF/TwQIYAwzCJRsD/57OEOA5mI50wwYVwzlVCaPCPD8D4xsATgRQ6on7r11LgUE0w5ElLgICAfJqwpGTnn0tGQVwzjWHVgQAfMv3VWZWZtKKcMCT4CYgBzPVXXiHACSiMC5D8/rjolzPZv1AMPDhJir/BwGcOB+Ut9J4DIwoXuGsxkxrCsXAfAc9PIeSuQ/l6yvMbxiOoTZ3XKsMDXP5QmRu6vmRiGrYDScch8gCOZOyIyU9bk2rEdEuU8H90h7MkSiQLKGJ1dS+WdTXtSR3yQAYE5GhTFREdnahoiSvkPwKlV9PElyjiFCLgIRGLi/uwvFj796i6EY+PhhKUx9C2DmYlRkroQWYdFWvZy7O5hqKcJFmNBMtTvSBMSuWneS7yZwCogiTMwspNtKUw3EMtVaZVtWRwQOWzVabzY8TtlzcDwcl+clHLKmjnONHsk8MQCcj7ud1HqYgiUI5uOtO1wLnVEtkIGnwhMRFWRwQ4zbf+ff/bf/8D/5F6f3p5/9pf7B7361O84vX+0wCR8WpU69R1UqnCESJyZAdIvojoR9adPMda5l20yNBHf73bYYCQeASFUP1Y2It96+3B13+2OCQcy7XD2/kN5EQjIAAFMLT0EEu27AUI/zx3fb/fFoHbbFTstlO9Hjx/a64sdP6+OGC5b3l2Xttm36B7/3u//D/8E/+6P//I//7/+vP1WmMnEs/ePzScznyi9fHCes37x+iWCL6Xlbl62dTks4ugO4T6UwU4YHmbjsqDdlJlerXNvWmBHAdtPcVSNMRHprEWEG4YEOKFy4MlsphBDa27yvBKbrhmTaGhU0k77ivC/zbtrtpqns3HsV2cAJJyRgwUzYIHi2abTuvWuZ9raZd35+fHYjdSOM48OhzEzkiG5qwhKemBOHoetzXPNfPvJW4eGlZJwKiHIXqIkDAIgATwUWESgw5VYksBSrhYCQGT3AuwUm/FlpxDaJspgvwMPSesRMboacIu01ghPDng0AIqX3DcCFy5VYMGrLIoOB4eEeSByO6VgBsnA1K0WYxd1NNSd7HJjoyFabcBcuZlcLHQIx98/AYAb0fGEj4DgTctNpZuHABflal4JZ/Za2w0wgubMUCzM3KYIU1tWB84nsqilEX5fAAFfcZyBAmCfxLYk94UlXZTfN/biUAjhyAwhk5lIKeGRcIL9Nyf3Pkh0zJSbEQXDNGp3rPwARIcLh47omDC4UHpD8NWYLTSN/anVBUPP6IfHQ3MYQU7acmWXJDIz0QICHmVopkpZ/h3E5j9By9lIQXP05CEjuEe5ci0GYBYQRExARkatJrdE7JsYvhdeeJcYpL1EO6flbQlcoEGanZnYLAHRTztAwpVkYVbuFY0ApAsieXgIC6y4oWPD46kWh+unbH57fPl1O23G3c7fdBqfHtZSYDxN4t9XCTA1CHYW1Qd8aeHiE9Q6Zo2YmASbkuRKSUJ2muZQJiJmQhNfzWooAOECYGQyeB/etWVeIUNNpmjfEMhUkaA1dPTeuVHhbu9SKRFiEyuTAVGapEiw0yuK9q867EkjaNNCXZUMONLu7nb7+rdeX06d/9V/81eX99v7905Htxe1uPS8FPBDd3FlOz+fa4vZ+t5wv+7t9GElhLhIUGEAM7bISAhSCRQFB5rqsaxitl3Y+LUvTw82DaUcOa02IEa79zBGpgY6QaaKtzXpvtixuzbQTS+vem9kMAXA62Xfv2qfHfn/36tPH7e3HdTF87O28LNb6/e3hv/ff/6d/8bO//c/+8E/OqznB8myu+vBi/+LucKhVwxjkX/7Fz7d1K7VoaGEhJDe6vdkPi4V721qpQoK1liLp4ENCtL6LwLV3QJy49tbdAZEDMCMpAYEAra0UQPupCIUZWOOKGDhNBTlMFV3dYVsU3AhCxbT3k2vbnmuV3WFHSKodEeo8MUHfOqgLElr0U9tWaA0eny7rut1/c3+8maa5ioC7SW6TEUOVWBBBajEdrI1wByBTIxYuoqaUHrhu7m5mwgII4WjdSinuERgs1y54Bko/OmJaE8Oj1NJbU1Uf9ZDkZo7o4YXKMCsSexiLqPXc/abvOfkKSJR4ohz3M0Og2sdfBsdLSKQYdCRS1cGdYTLTqVQzywmDWFwtcSNMmFmr/H+4G2EihM3S0IUYPjhskY/5DDwxR4Q2TTZzqmdpdCQiB48A7To6CcdDzTAAkSEbuojMjZEB0TUAQwpLXuljdQkjmDD0nFRjyK5rGc08saklqGYY/H3UJeazFxDdlTDZNXQVMZCJwN1MB1Ui3MPRMzQLAECFwrJV0TMDCB5IwMLRLUEUiDge2lnC4paaVQI7805Kb0/uHCL7fYiEr3VoQtHTZ2tZbJ1zTFowCbG1XooQIeAgEWWgOaWdXODkrJOmUlUHykV/IJJ2zQQAYE554WZSS/4GXLl9A6ktmSMHkJLrpqgsSNi1i8C16hm4cibngqDe7Y/t5ba6dm/qivT82GqnWuLpcamC01xZKALb0pm0TDMAWesRxiIEXCfpTW1TKMgyl7mGD6+LIAGjd+XRPbAmlq+vPQJqnXpTgChTLVjDVNeO+6qt192ur51YyswRACSByKVO+33Z3WhE3R0V3B1ICIjBIdwIKxbq3RBintkYT/3CUs3t5Zcv/sm//Y//7F/+xfNpvbuZtLe5EBJSKasrPDfmcFeWjaSslyZVALCGMBMW7lvLoZaJmenu4eZ8ape2kdCytMdPzzJNFGSqQN56MzPTLlJKncCTLZId5DFSewBt2wgVCBwMuNFEy6aItG26bvHdu5OU/YePJ3O5GC7A537uZkD8Bz/9PWvtn//H//nj+eQs1ct+v3vxxfz7/8aXZp2bffvh8u1375HK/f2+ElWRUmuVWmYBN3CoOyEgD7ucl977+emi4OaWJVZTnVrXwlMAuFvmbypPvbVQR3Cpxa2jWQhZeFNH2pm7GdZJ5l0lAUADt2kuqr1vdokmxRGBkfbTjRQqVJBi5uIA4KFb662vpwuXYue+rvb+u9PzBVazrnZ8mOfjHN7N0bpJqRiB2RmeIpWbFHYLG+R5CEc3JSYm1K4sDITajYsgZY0XIOEoz1L7HOnPfFY6s1VVhBFIVQNRhg3Es54knR2mypIWkoiA1joxDgtpRvERcVR6GIuEGSKqKWKUkqWBeA0qRdeONFQUJkIkc8dAZAI1gDzugYS1N2EZ5e+Jjh+McaBrMA2yjxY5CQ5pM4yIUibVBoEJp0kadmI0YeSiIonz5j3TwojoQVwYA1MMRwRGJuJwT5A1RMjILHggoV/trp8Jl0wYERmryx1OprcCAgETnACE2aSWOJtcaQY44kgX5QkbwzqZh2rGBdJyESkrC5U8RonQBmtoNF4OKtyoj4+0naFQAEC2LV4vSRhHdjByautpfR33EHOuBCiLeUeUMhfI6BaBIaUkSyQARguEx+cV/GgbI2AcpGuAyOtoADeYYGSnnYRNjfAqmnMKQaG9T3NFYLMOjoHgaAgAY/sOzJwJF3fPHlFhzGmAdrx/fbMsi1lfn7fTad1U+QzHvdSCnS2CAZVrDSdtLmVWu3h3YNSl4w6noABo63YzHx3CIhx82zZGDOkCRUoxvVAyEZGJA1na0oiceaxGiAB5aq0ty0IAjVs2UXAp67pNMwOVst9zKSRVkC1QFbgQo9Rpsq2LqDAi8q7UKhKxnZ/Xw24HXqXuJim7ed7tj8/nT6tT5Urs+1k+PK2O0ENr8cPtbttsKkTk22V14/lYuaBjuHVHYCnhhGj7/dS3+OHdY1vDAS7L+s3DC+vb7d3NtpwhPFuoCQZHN+vgIyDCE/QIDGWW89MTwha2eW9t2zSwN2yXuJxNZFo7EuD753YKPLs2cyLcTce7Fy//w//ov3j3eJn382Gabnf73//H//DmUN+9/c37d5/On867/e2bNy+FZarTul7Oz8vMFORPT4/bpR9u90vrWRyFRI7YDbZu29aICYXW5gi0xkXNGZCZwIwJ0mYGCF3bXKfwzTHMQoO6BTWFiHneubttOu1rzv/TtOtb76t5skYQZGIwsOZlx9pzbnb0iG5EZV2tLbE89+WphVTT9uKr+5v7u+PdQWNjIGayrVGSWiS5RBEeFsYsEel+Tvwj2AAdo3u46253WLeLcFU3QuBR+wEkTGmpY7ZupabPG2ot4Z7IdxbxbqXmMmjYaVW1lKqqQ6UIRBkNhUScGALEnMiH8RQREyJJ2Soz3FZwJSWDWbBkUMthOMeyYrr04frXUgUIR9g54Pq6iOEm91FhAgAUlJONqZVaXT0czA0QUxzzkUsPRLKuJHlyAGYzlxkhF2HV7CeEKzBt+JGGimUOaZFnJvMIjzzJM1zglrI2XQlE2TAwSrgigtLVdI1LuKX8Pe6tUsVc81ZItZ0IPSDzYnnNZPuVexCCx0jWZf+nZ7G7OxfGUWDmXEiE3YY4OO46SKfXsFUNz0z2ekawcMa+EtuQ36DBnY6IGAX0bk4JmxAyMwzXQDAjzsZ6QABKz+yofcBxMETkngoRR+fcSFGMySGDDhBxXVejAxCA1MnMicPMilRzncrc2pr3bp0m611Nw5xFAlFKkvfBTYtUnODuy/u2bUho6ut5m4IvAbErtcr54kQR64bqBBh0IrAyHMRpXsZShCubgS4tIrAQV47WuLDUYq51V7UrV1mXVUrlHgB4erokLYeyTVgNCMDIAOYyCUtrzbpO+13ZzSiFpAKUQAZgRqF5Mm+qup4X5GAi76bN+qbEJLU62c9/9jdcj/M0/ea7t5enCESLOK9bpQkxHk+rRjyfNil0d7+fYr74JU1+ri3UdO1hwZMs63p3O+928+Wyde1cGSCK1JOeCsuuzrt59+nxfNwfiCpisea9NZmnweMGTKk3ocDgSkSmbT097fZIqKgLBejil6oMZVmAZf/D49nMzz2US1PrFvM8v37z6s/+/C9/+PgJ0Hdc3zw8/Oi3X3u//Pm//ut1a0UEkSTCL+vmcOofmramsG4bDm8APD9fYAzKqGZcaJpmKfNUWlPt7r0FomdrimcwCcm8hQUY1KmUIoYuRQK0qZXNnp622x3rpqUiYjHXAN/vJ/DoWzf1tnR1LYKH/Q7MWEDqpGaa4c/W0MPMmlK4bKuenuOygE1W5+nh65eHh5vAkMTFI2aCFyFa07HaE84+W4DoasQS5jR6pYgQe1dGbn1DpG1rAGjwueY1EGkYLOGz8zs/lxk7cmLW1iBI10YCCJSelEDQvmF6hwCTF8TC2Z+V/5vGSEIOiJRq8jdCVa8vYwQI4ZK5TkQiJDUNAMbR7pL++NSYEcht+NQhApEzRhBXo3wEaldAiABXS2JPnSbVjsj5mgSA3jukFACIgmFOQpld4cKmWmpBrAiokaEBMv0c4xjrcusKiA4uVDw8OXMZ2IKrBDTElkivOpOHJzk50RY0kmoOI4cWlI4mSjodJtwY3JnFx/s9i4wwm28h8iSETDnR9dHFiCQ8+lsY3T0bFjE3KJYpF0hj6EBODDME5LemlGKudPWSalcUIqTPoAt3F6ZInx9gqvPJesNwFgk3AiQpMKxNlrWi6RClgbkjG7TYXHhgDiv5TUOiHIBgbCdSjxK3UYzsqqXWbNAkIYLStRFz/q5oVw8gj6lOairMbkFIKIgIyBQYu4fjXYulPGMwBaqrNl9cC90CyuPTuyrF3KuUT4+f5kKH4zzvamtaSsmdIVWejrO1lcCZGIk9tLuydiTOemFVBwBVRYZSZZqm7Js23dzMzCKCRUQYA1Cw1nk5LRNLANR5NidzXZ4+1elApYQRM6m6bwtNxXXTZl64r4TCpRPy7e2LLy8NPz5/Oq/+eO6fPm1luv+0PNtF7+aI3patd/NC03nxX7xfbu/oAGzqDhi9PX863dxPhDyViWvZtvbh47vD8YFEEFsEXBad97ysq6oRT6B+e/eAwEiCSMKcv2aOPpC0BDA66YzCtst5WxdvT31rpdbTSqEXN1Es5+5NeVNczedpWsy705Gntem7DydEvr/b//5Pf/zj3/rm+9/83adPz9H8xd3dvvLtb1UJqVNd123bLs+n7fncpVYz0O5d3dUB0dRa8zoJYGyq4KEGgSW8M0vvHRGZGSJa112t5uphunlgMCELMpM5uAXz5L61pjfH0i5NOJhoWxoBTlN1c+tqahBEUs7P29i1R0Pxpt6a1jpdHs9EVObdtm7Lcz8/aSCHxe2Lm+Ptbt5VECVGd3PLlAVliDcSzeYhIsTUtlZYgDnRe6ZaSmmtJdpEpLRtJcRSaustX5kDDpaPWg8frbzZMwiIrE2n3QTgbu6QTCAAwLGvxQF5TrUWAnrv7sFMIqKqps5M+QvAhB5h6ZxBctekIESEa08sTeJC04fr5g5RRFrvtabsnKZBR8iOkATMokeG9dL0R2bXFzNTcu/Vxq0GAFKKqgqRmn4+qwNgtCkIpoUpi35TJcvLFa8TVxp1UkVJ+5NHcCmCSAktSNBo0koTmgaZPs5rgAiGApKv/QG7cPOcyyATtjlGDrRTohqGLJ7JNHOngVIG5DQdJUFoYKXzOxXJ7cwlewQScqSnB5LUEu6BUaWMpoKIzBg7JKsHkpXBnGqapXU0dTUbqhSmYTWtroScO2f3IHIHSqJeuEtJP74jcgwyR1JYh+Bj7rlBgsiyinC3MpdMhEc4C4/tiFl6G9yNkT1dVpCeUh95yDBCYuauHTBfEJBhAsBo2giEi9+8vilMhFEnef7wvJ22y9KW7QMAQthckIg3NApezblYEaxlKqVEeK46deuQQRMCUy27CQG0m1oPACLQ3nvTrlqlIhIJk7mHEUuaqXLSKsKhbgZceX937N0ZuLUWIMxcS6ESJGNqMlWRYgq98emC5xU2l6WvFFgn+XBhQgnan9enX/ziux++e6q7PUP58On0O1/swpkINPzj8/KaCldkweOxkrkHkCNAmGKdaX88eDNV3c830zwtm7bWAWm3n4mLGVqHx/eP9Xhz2B0IkUrJp8RA83kQgbrB9VekLatHc9en9+8O+95bfHrqUeTdh3YUASin82VrvkEA0M309VP7JZNL3V3Op8Kwv7/90Y+/+O2ffPXdL3+5XR5v9/zV73zz8uUtux8Pcnu7f3h1e1mX7XL++c8/fvvtpwBpHSzgdFqQeDn3vhkIoXtfXa15QLfBzoxwQHaAMCAgptIjhAQQK0KYeoRboCOBtoDLYrfHysLEFdmZ2dX6pdumm2wAEG61VmbZFhMWAHo+X87Py7peqM7zbv60reu5uTvi2ru7olvMt2V3Mz1882J/fwjUMA8c3DXCMLOktqQomoe1dhMZQMlcbAqJdkUEIVJV056L0xjH3YBcIWLGvq5VvYZMpoaAyCG1qPZAlCqmI4aUdID8zOcZKkUiDZTEDIEIqh2AmFDSxzn4BjAeqAMMJ0m9ZBqJLQTMvWBGLQQ5IITF1EjyHZz5W2itIw0DvAfUUiwPC8vBCKUwhEOge+5lQbtKLRBOAEl3CAoYLbMIhO7u4YzsHtmPngpNqvrJokAYoGVX5yIeAUBmxhCS10laIQEDBoQD0i5uWWaWyNY0e2Z81xwZ8xLOTUBEqFkVFsmmheGRkiLJfkihnD4/4d3BCACZ8pLM9Qtcv+nBxOEOCMKSwDw3Cx5Xtwj7KBGN1H9SPqIRinCAKFybdSbMH0YEBADT8CnGFe7lHiwyLlX3WsuV9E0YAcwQwx8c4YmOSOXOI6IH4PC2evbPQP4Z6SyGzHBEIAZw4t7CAdFNIaWj6+dta5sIMDJGmCkS5u4rDQ+Q33gPEQ4Dh8Aauy9mnO74A3roNM/Pj+fzurl5GJgForNQRewaJFGqHmo5X7b9oZijVPaw5LxyqSLMdZLCOYgkqYqmCYliBXNrqxG0pFm1bQULRKy1shQgXC7LJEyllH0hjqYqRiTg3hAgLJcZ3lZdNy4zny7Lr/7u499+v/zlX5++e/Ktf1C9HO8nRvqv/ePf++aL4+72y5je860uzdGt1vn9yuS4nypgiEQgXS56c6gapMsG1mvhWnk5tbqf866NABZpWw9F1wCD4/FmWdp60d7t+x/ePzBzDTAtVUSEiIEw1B0N1fPRpF2Xy3maEcA/fXp895vvX7/eFaYicrn0T0/reSfLBZ4237o5kbMsZmbAUnuPnVB5uD3c3j/cf/H+ux8uTx9ev7n98s3Dy7v9Ycab4+7Lr1+4drVtfX5i8K/f7OsM52f99Gk9X3S/ozpNx910Oq3npbetsVRkX7aOSB5BxL07C5uHMGUmFtRISJhRCIDDzUFzJmZiNdNuNpVUwpfFiiCJbGtXbADY11ZmQ5bsGQHgy2JPz9Y7d+tdl9aREZbTZZ4rMFaQr3/ndYjev94/vLmZDmK2sjCkhZHyPBnlrxldSSk8e7xJCMIGSmD8D/XeWATQ81mdZnFXQ2Gp4uYE7NnKazYeqJ/ZXPlhVk+qPjOLlLY1qTC647th6s9Eo6wQwNTzVEkqfp7pEZ+lXQEw8EEiEJEIx2xkp4waeFbDml73l/mcDFBtmSRN9nvmzsjBXRFRzUot1i0cwAZsJttmIoBEsvI36ZbjvCJIS23qhK6uaIAwIlwBItJaQ0D3YCE394E+Hc4dJEQIjAxeAbhFoGfMwN0QRT0bTiJMEXPrCxqeJsYIYKb8EbJwmBNjHXwNYBlCCRJ4xh8GJxndgjmZl4xZMAljJWCm6ABEybIws5rJABwB3dRUIhcGRJw7XoQk9+WvCAKkvRcczZUJw6O7QpZMQ1pdAT8/JQgzoZfHd6Tikc4BGmPgEIgoO5byHkGkUTcPo2oIC4tD9NbyyzdzgCi1hjsghvlgDCOY6dg7IRISMKgbE2vvTEwiwpITSx5kQ0kkIqYci1AozLBSua1HvtVNTz+c9w97WWR52nrE+VkdsQpZobpDID6fNy48z+xAqt6e2nSQXWHrER7EQojuEWhcyJamfWiCTNz6mj2gCEAkkGXqgrmpZi5yszOz9XTpVkmEq0S49qaq880DghJAb01buNNvfvn2j//013/0x9/+4vvz+xO2zqRnrkRsYdtvfvHdqy/v/+FPfnp4+U2H3fvv3p0+PO/q7A13SOIE1qQUAygsanw6td0c63ml47Sueru77c2mCTUgMAh9a34+K3NBUAxkZikCge66rE1dW1sGdCQ136yaC+cium2JWAnV9XS+nJ4/fHyapd3vfRKYd7VFPz8v4dOmBkhcymrwYXnX1N38sT0tle4f6vPT0/nxKPb48m7/Oz9+UwUIt2ma7h/kyx+9WNfzt7/4DRVF9zrjLbAg7Pc3Hz9cPn1cm3Yu9eZ+N+/Ku/dhaboDS+twhJuGRxCLOao5gDPB1tQERZgLlVrM8j3mBogG6rB1r8UBYgMPAwZEINWoVfa3uzTfL6tSkeW8fXxcLmvfzYdPz5dm1DW8tamKLn23w+NXBzzY8Xb38puX811FNEQHQFeI4fiDFE/a2gCBpeRRAIJuDqrEpPa5HRbMHWW0IYYrCYM5C6FlW+oo4E2EAfo4kYgzzTpYZA6BAVMt2ZxVasmDJcxSgGfh3Gl8NsKYeXrNrxrGMF8SsbY8DwEQShGPCI00IAUM2lLkHEqYL3y8logQBjOrWpGSEkUuEsycRUoVNwcMJlKNlJU8/5KIMPiV13UxQBEyd+2dRcJ7qTVJ+CychEoPbL0TM6aF0RKCDOlpLnVyU8jBEbKygCg8cnvEhEEc7tl1zpxPAEgNXYiTrUrEbsFMJBIQJFmy6B7k6jiOSsBAB5fCqVolKDQgSuHxvR6BADTVIpIizTDkupsbE13LQekzZJTH0ZxeI+SsWoax3R3b1/GCNkh+Jw/uEiEFBURYGLGEm31eD5iylFxk11q69myAUfNB8onAa0AX0ruZbRWVEbCrJksjEdOtNSLsvSFAwrsHJzX/a/mzQXTdJH+EEBFepmraIFDdWISZRisXhgir9bB8khiEAyNNXKbDy12td8/tccEt3n37eDlp17YsCiLMotaEpNS6LsoyxdoRo5bs2Is6VUQOgG1ZuBRgKlMh4r5pAGhv1k2b1Tr11pfzBtAAbCpT9FGa0rZWBUWKQxD4VApnMQuR9uiXs5l7j3WFVeXtdx//1R+//Zd/8Zu/+eUHr7UHmweyNO9VRGN9/+Hxhx8+/uKX7//Nn/7+bt7Xw/7p0+mk60WZ3LkeZpL9xBfdgOS8IV5It61WMYeu6IHao/eNEHprKOIhRcpJ18N+er4ojddclCLgENqJgsCJJJ0C2dCd9HKHWC9LoJtbWy6tL6enx3g5f3z35Ivudgcqfb1ctnAzn+e6KLhB8xWcwEzdt1XrvL68u+3bed7Rm9cPuwq7iQi22+N+P5Ntz/tdORznrpsQrwUCwC4LAhwmXmfWp7gsqwWdl83C1dyR5jp1jmy2EgF1T9HXICLcDMLMgSwcDb0KMwATkSBTKRxE4eQgata21sjR3Uzbue1u9vs9QvC66uXSEf1ybssWm8qmuGm5aDPH424v1dDa7Re7h9+6u7vdPbx+2N9PJKBmyKSmwoWJVHvCz33UV+U4OJbcqTabuzB7uGvOB5GcfvAQFkTKcucRASPUbsTBLNo0nUWIkGSd1OLVTJhtNLKCdRNmUwcMEsYskgwA8AxppvUREAZNLyJicPaHYQTHr0aWiySTDjLVZek8BGKOcGZUjVSBYvjpAYan2EgQEJA4zAhpHP1SfHTTQ1qJ4FriKMIZOtXeM+edJIJSCgmaUmKGAdBM0+QyxBKIQEcAV0dhCAciLok+RUv0p4e4B4BhRoAQLbzIZNFKqWZqkcvbUUqQjS5po7SER2MgjJMxRZW4ruNFCFK3S6ofACY6zUzDpOQskZtvwIGohuGWyR19sneIgEaTTi5mzd3diJCItDuicYp4wzJkpVbtnTKtgZDVNtlTNmw9Kbq7ZbcBDOQ3QaI5zOw6E+GIMOceZcj1AfmoyUWIx+j+JSklf9KmykypLAWEas/2dqLMVIVUGSgPwN5aLRMiELL2joyjuSww3NLoFIhuxiQRwVnQRqC9pV2ad3z31W2/mbdP6wPg4aLHT8ty0d40gRePz5fDodaJt+ZURDC29RLEQH4gXB/7zvd1N2VAfJgKNuuqYZHfnNYbAouUZbnUKsjc13Vdm3aVuRZVZJoPe/KyLb24A7qZ96bgZTu11uG88d/8zfd/8q/f/Zd/8v1Hda23LFLMy8QEvZsTYOEZfSsIl3fnP3r845/+9k9evnpx89BPp6cIcddPzV7M/GK3XxvKVDbTfYiGVwB1m6d5WZe2xbQXqYJUIlBb1xWkkJntZtl6Y6EArbXe3z8gUQJtwj29GZg/CWAAhzCkaNuqsFmYdfVNXcE2/P5t61HqvIdnrcQQ62Ha8YTnx0uEmZn6yiI3x/3d/vDy4Q5s/Z0ffXM8AiFMwvcv3uwPBbmsG3UK3s27tmkx7lGQoPnz43J4fa/9eX066ebPy3mzaGoW1G2d6kwkKOgeEZo8PuQYykMMV4ZZCKKauwZU2s2Thy5bRyfSEKH9XGqt3regqBLe+Xz256cnUAoiCwbz51PzoNOmHou6r9bu726ANpT+6sub1z+6ffXm/v7VfdkBoKvlOzAQyAPCNHu1Qj2tOxCBY5kJWdZEyBGR6830CwUSYJr0cslhiGTqkIOOWTpHM5GXnM7eNS1/ZgMABDDStsOXkXVP5jFqntHNS6kRnhkuEnK11JEIUbtF9gEQ5pRATMTCMKLCSGjdEkCUwoKp51M4oflq6uZSS94oOfSHjRWERYw/SBXN/drc4moiFSC0KwkjZvEZqUJ2e7oFQOCoJBNzy+MpjyMS6VsbWSVmgFRZAiWRbpLLACJmYe8mSalOA2UAIKBpCwhzY5GcA9xtLHvzJiRQtQHQSCqeWUYPgiKDHm7ZlGnCBBjajSXN9U6jfzaQMeKzdx49nILCTTg7HJL6kLcOXFEKWcIeRQQx65cTKuufCzCvRV2cm+Geups7D+420DjHMQWNCIPMJUhW/FgAtNar1K59mmoEjuK6z/pSgsLGLAIAmF084VGEzT1yAAJQVRKc57n3zsxhnZnNw9S50FXa86wbREYUdu2EBBRE1K0Ti4MxsVuAG4mMnR5ESmEAwUIBwPe74/3u8OKwfDq3e1lP29O787bqcmkV+Lx0NZcq2owKTvt9eMOg3Pe4mqmVCC7VzVF4d9zjsphIhG+XZTktfT0Tl1zomFoEMrEcChGFxlSL1IqlRMDyfPEImXbg5fnULo/906N9+8P2R3/69l//xdtLoMsO4dDPUwChG8AZBVRN5qm1TkK11oj4zbt38zTVaQ/L2XrzsLWD7/bdxJ2XZiRwWa0cWcEry7KsRIUK5b7K1bObr/eNS03P7m5XAhwDtJuUulyW21eUty2kHcKBkVBI18UsiOnx6XE/MSK21kPp53/99h/+9k9++PT2dLanS++Ah3n/5s3h6Xl1qUDi3j1MRKTup7kg+ul8ef3ylqs83N1+9dXD7W0VcaKY9pX3hzJH7cJHwOh92VqJSQpDLIt+8Xp/PulvvntvwSiIRldCboB74cmK9obuAUg5nxp0BHBDI4Bw7ygOIgTqsvUiCAGtq6tFeLeJzCtxa60UYBQRXnXr3YNiba13P59XEgHiGzm0CNQgWfaTv/nm7vXXt6+/eXFzf0QGBxWWtH6EuchkPeFgyV1wFso1WYQzoaT5ElCKQG9J5WTmRAqGhxTOFS4AhncqguOki4H6FUkzOgLWzFoSeTgLeUR6aTBlBiAAMPXrRjc1I9Tec/WYfXwAnpz95KOl4bIwNzcmQuRkf4ZnNjbyS6ilWDYiIYJj4PAkIvMk1cyCUpPgwQsY5B2M/z/ohJkJyTCju+ZqJM2b6RkRka6diZABPBDpul2QAZMgggCMqFPV3pN85+qADOCMPBwxKSx7hDpEiKvjmH053AnRw5g4Xap5e8SVejHcNRGIQIKmMYLTEZJFmYTpmOQy/J3DTcVE1xWNmUKECOd8BqkxRTCxuw2YTM5iCBER5lQ4iYfJZ8fP7p285dBziZ1/z/G+dyNmNRvMjvRowuj3cTeR2nsbe12HoRfB2N6ISCBMteY0mIIdZdSP0MxHAnu8NYAIc3ZrXfHqFVU1EXYItQ4E5j0ckPMTDK6aFlvEgJx98smRKbscZnP7zNlAna8eyxZMwsHPEGE1BcHwCOTpfp5vp+d3j/NFsNj6rPMil09nU1XBIgXQLaBMMxHLhIhkDn1dNlVDL1MEUNPOxL3pcrqoGbojMRIj4rSbCaE3Awd3zX5H5ILmpN69rctqazve3gAfTk/L2ujpzH/3/fMf/1fv//znnxaWQO8Xc5sVXlUqu7sb44vtrG1PKEHbW3t+UkG3bYnt+4/vfvzN16+/fP346dPTp09Pp8vdVC+TF2ft60ZepGyKpUIACeVRyICo3aTWvmzbpas5c0y7cml4Ws5c6mJboMvEW2vJ1eJSx6/JAKMHCJGOdiPtHUzBYWv64cPp46e//niBp+f+4fHcFeYj8DRjw+fnkxpYGDPWOs/74+1h2u8qF9qcfv2hOccj2ssldjMc9/W2Hg++65tCYeMgWI9fHJ6eLvvaNz/od6dPT5d5kvtXd/G0bBFeADpxwd410JtZurPCoakhOAYhARNxpWytSMQFIIdbU0MkIarzRK7gsZx0niSDpQmHaVtfL21bjUptHpetoxSU4qHPdnJryP3mbnr9+ubNj++/+PrFzcMRKdwMLEw7ICIyMo9RNdADAbHUGmnSGp/E5KkhBJoZMiOgqtp1Czqq2IlyJM2XPmXJF5GwdO35T7q7eUghjwg1JASmMM3WPO2a8kN4XIOonhcSIJRSTBUAEMjNwoM4rnSDdGd6z080XstoEDEhZubX8K15YuCQMrQkRVwtPBwMEk6HSEQiPJzFark/U1UCYiJDijHtKHMZRVsAqsqS8bkoSap3lyKEZOaMPOKx1w47D3eLLNVCJAOPcARsa2Mp6VlC4rxRXLsQozs4BNIgyREOa7+HY3L2IVikt1aKMJXWNyLqvYuUcE/VHgDNHcyz8QAcaCAtk+sdHoEGJMQ8dqTuTsn8SR41EWAk0y3cSpHE9YyQbQxmNQLgKAzK0xzD8TN2I4MCuZbJuz3ZbcLFhpxCiQk0NyC08FAvIpEhZ0EEzIgTUqI5wEwRqUiJbHC0SId4QJgpOExTVTPMHwACRF5LgADhnh+GUkrv+c0h0ybzzu3zzEh59WYhMDMTY28dyYiYiT00eXn5awQQgOCIKfS1bhGeUe1gdneZ5PD6NlqlHZ/fnXzFWvn5+RkRPn16mnZ82Mnl5PNeaJLWTSpLqXU/1XmSwl19vSwDcYfIUgggwMo89aWbqQhFFj0QLEsrtUid3ePxh0dHcGRvrda7renH9/rhcfu7X3385XeXX7+7uMRu5tOnLvKgu9/6B7/z3/qf/q/+u+ve/h9//JsfHru2Zzy/LZ/+fP2bv/Dz83a6OMTzoo+bvryfjrd36gp1ed5O5TkqukTf38jp+bzb7QOJmEnCXZE5HObdHAhEJJVJuKlxLbgaIUEQBYLHNFURnCZOuqIPVZOAyNpKDIgMzAH06cOH2ztx88PN8eNj/82vP6iX5zM6zU37+8clxNyKA3usnElV8nkSmeZAXru/+/bjz37+rcKfT7sXxwPfHqa7m/nNq5dfvDreHqTU2O/psI/bG/Ko4RFYUPjmOF+2Plc63u5oVdPeVIGrOQOje5iumLVSBAHk7hRhFIhBBMlZyfwWY64QQS1UfRICDFNdt24ERQiAzM008TrR3bsFS0Gqy7rWnXTdpp2//Prw+svDlz96ePnmYb+fAiLMRh0TkWkAJpU0jXkOQOFh2aFyzSKOvFEW7UZEdkMBAmd3yNgcwHAkIiBKtjkhQISaegSESx7WmTRidlVEcc1FawaV8qlOFuZmo6MvIs90N0+yakQQExXx8CvVnK9nCMb1tVdYLEX4gPzkmupgR0EgGgwRxZAJLPKUS6m79z7GfTeW4m7ETIx5DeTudypTS6Iy5v3NRJyyFYsMSyogBKnZoEwSBgARmuULkCh7ddzDNcUVvO5OwpJxlJqSEqCkSh4B4THUMeaE4ubRnOts9zy80LLTEdI3gU4Z5gpACAvizHtl5jsyLkCMEA6EYY6j05IAPKsFikgK9yBpqkJ3zZWAiKQbLDwX6FnTPcyt7hlT6inLpwkKOZhzmYzjGU2IQDH6PNFHOlfCYHCQhDO6BUDhBkQkBB7MpL1nBoJZXLNrDd1VaoncAtEItlyrMRWJap3cOwCYG5MARri31rJbjjK5ahYeLJLByByETI0Y3d0ditR8KanqmJ4YQh0IrXcuBXOvg+gjKpA41SCmbh3Cqcr88ohMvnrZlfl52p63vm5hBsGhYR5hKPvKleZ95bl4gHuYaWLyAMDN+qZICJZkb3aLvBt6UzMvhwoA2nI7TQ4hRZrDusYPP/zw4VF/9W75219/eP+oS+u7XV0v51r2cPPm/h/+k//x/+J/9Pr3X/7du59/+/jzj8+KW6uX5+Pkuy9vbw+v/u5f/to2N8O3354K3+xmKmU21YmgWdvvJnGzgP1cwwGIA4jnEn3RrlwoEExtd5gt1nZamQsxEUdvDUs8PT2/fvPysJumw36a6jXbSZQfH4Bk/xLxVAsEMIsbMnAzuKx2uuja/elJq0yO8PTcyixr081UeHJXBTzs7uayd4fnZXn38enTp8e6EwDwd98hGBMQocDu1cv7+5v59Re3u6N98+Xhi1d1u5wZ+kT1w9uPiOXpeSuz0LZVovubo/lJFQDMQzRTOhqB6hGBDgGOCaw1cx9rs4GQdAU4qd4cpkuz/bQ/3kwADt18W3PY3ra+bc3Nu8XWNiyFuADS/lCAG7O9/u3Dl18dv3zz8OKbVyIEaATU3WmQFQEBCchG4oqScplj7cgfaUBG7oHSUC/CBknMJyTqTX10F9h4egMgJGM5tRfsvTGJmwHTleqDYcZMaW5WtXwEu6XROuUcyEYX652lpLg/Sjg8zA0JkCi0I1KYIw2Kb5IReGDK0BzTNkrD4Yq5f2YRbQ2AAoNiHFCfs8QEwEzaFQBTojHrKe8QcfI3W99gvJctG8ryZoXIfXNwusDdEUkKuanbsM8TAWYzY/praSDbgdCvbedIBJgWRsBwYs50KSJg7/noDjfDawpWiqQniXAc+rnVxiTmQ+BwgkqEiVCa8SElN8SUhkZhDgxzKwaQkLmzoPmIBYhIXmWZnwbGgOi9IYGbAqGGSl48OLhDNCQdHEFnABHOZV5OPeGexq/h2vFAApF0vkaMx4QHiqoy5aoYMFFIEL31wSDycAqHyOUBwLjMs/smrxphJuYk0ueiZ2zkCVUNGaRIbxtnTqIrETLxUJcAEXF0KUQC42LQ8kzzh8KMCTkBygFWw6FIBQCwYBnhlEw4lyI8FbfOwvKq9KXtbnf0A+xvdudPTxjmBMuyUp28CAYzlwiyHiiogCzC6mbaWkuTqDbXrSGRWy9lco2wnISkr1p3dV36qH2r0/NT9EYff/3Dh4/93an/9a8+nNRbyOHhdrls3QR2X9nhHx2+/P0/+tOf/fP/63/yw7ufff/tX4I/7eIZm51qW3X+n/z7/+vb//nu//C//z+f376n4stih8PNfo+L6badjsfDsqx3B3FxmcQ8IhiQEIlLIcbz01NYL7Uys5RaZiARQlbd+qoYWKW++er1tllRKvUAISR1EPcCCSiAA7q5q/n+eLTt0rZ1Ohy7hgaakCs60OF2Fw7Osiou2rt7YLjFfDzUeV7NT58eT5eThmOlZlxlQgxAN1C1uPTL8pv1F79u9a/huJ+/frn/yTdv5l3002PdFYD49PjpcNgXLveH/fOlny5bYd60k7BZftY4cSaYRps8LXAk3QMiPNIIAEiBGAbLpjf7spmeV9hPgoRBvG3rBmgKgNVQscC8my6X5tZRLGJ58bIej/svf3z/+s3dw6u7UvPD7j7gBDDIjwTmbqalpAGaLLNJEFesFuRrFBFMsyZlaADmJojEbNpTAg13YkFCbeahyGzm5kZpAw/U3qRKRsPcPeP4qc5nCBcRACgAMoGYASOE5MJ4co6RIGsNVV0KiRQbpYFWa93WTUrJbWV+cjEd/ZyrOjQfjNLe2nANWQBYqWLdhvs+IgD71iE1ZA9mNrORDEUEQrVUXCAiRmotvTwIyAMwbGDggQNfZOFBlGyFGNqHQ0SQjHYIUy2FfdxhwJI2k3zck3YdGw+/fsvyNepmYLlwiUy9Zq4P+eqXGs/tZEIM6CYRZjMDEmQ9FgJ0UwgTKRGepb5J4qZEjaGbG2dt1RD28RocQ2JK2H4ms3vXUgXAmSS/UISQkhsPjCyHSwc3ZxF8wrIjqXlBWWUMqTkmR4gREYGBIreADoBgYUhYSlbKEInk7w1EdOuUcxeg9yyoBncoRQCAxtwDRNhb7mZDkqvsUUp1VQdnIQQO0PRqRES4EzFKdgAQZmxZrZYaEBmhBKDhDSBG5HzvmyW0zAgZcj2d+3IKcHJympihYon5fq+nNseOwgURTXfHad7ttmXVsOOLIyL1rtQj0K0reERgb7039WbbpvM8MQsitFUxGIC0m3ksl3OZdzHXtsXltDbnbcFvf/X44dQ+nduytel2L7xzF421cQW+YYi3f/Gf/uUfvt0evy982utThZNgYw0heVrXP/7P/qv/zb//P/vq+L/83/1v/4Pz+ZEa0snu9vvb29jO0bTd3RwQTWFrHogaoRiMmL7eKKVmeLyZAsHlsky7nVkg0uHmYAZ3h5tSdh/eXW5fv+Fasvslrov+69DMUqppn+fdczBCub253Vr03kupajbvijmU/e7p02XVbuBJ85t2N3Pdd22n09Pl/BywBhbiKVzMOTwAGQE9NhFAUMboZh8/Pp+fT2+/v9zdTDcVphlLYaxl+XDZlXKYd7Z123Q5ryCiZpgYsGRBAoBjeL4e3D1pK+HulLEiImBEAJmYMFT9dLYImEtFcKnT6bxEuDZQDWSZy7HBk4OzAEu/f1lffrV78fLw5Y9eH2/3pXImhIAhYRmUWZnxWodSplS0EUFbYy6JP4PrawwZAbImFtJ2kazE1BLSL5/oXBgYR/QI9Ihs/WHW3hEImcwcr9r3IPJqH3KFmYiYWsEyGJlEYT0QTZ2FR4LXPCJyOnH3rsq5Cg5YloWIVJuImCkhISM4JNw+j0dm9hiV5pgLPaJwU02W1OiPzxhXLqFFSleD8PzRdFWMyGwEMplpmiAAgRgTAJeRWHUzt0yjZZ6uaxfm6+8sBBNmGgDQ3VgkyInRDSCgt35tRwAu4g0kF7ORcQ1L3p7m634IEzE+F6ltpddKpLgrAI75LiLDXJ6Pbs+DPmuJIddqngzJv/+vQWHWgVsec0MMpgLBlcQZ6aSF8LBa06QPHj6EvAS9Mcao/rHB8wNAwnDLLyQCBocHGCKKSCC4RuC4h69yTfZdDEmmt06cbvDgMdxAPiLSqJoh5xgFmZBx8wCISIOmAKG518Lm6KZchISIwEznOnUdGCJ0B+QBqjCn8ADnMVI4MUJQ661INTdmyS23mRMCEwc5jCYosO4oCAHaDBBMnQV5kiCfjhMzy8S+NuvdL35ZVnPd3+yl8La2aO4RpVZkNHdbOhAJFyyMBfZH2pY1Is7PlyqTqbnnuxK21dat+ZOeV+s4nZb2/NyeV13aZdrVH31xvP/69anVt98vzeyk3fEJn//V+h5wOdU4T94gtkIJhrfuxtz/9D/+P/4H67949c0/+v2fPPzpH3+/PJOY308PBEIgh1qP07SeP9Za8rTb1kVL0Q1EYJpn3S5r2+ayQyBt2+F274ZmwRyhDkhU5HD/0L77ZYCcz9vdDbtpOruJhJAcIelayyUEcLffrc9dkcs0twa3x7vl8qmwgFNPQiazgxIKAZZSLfx8umzLKaITC9Ek9eiGgAWDACm8ZeVWeHiAFDGOZvHctX3sl6nsCokgsArBvKfztrR127qSkLojoLtG4CA9Zvts1hKNGWDoFdk+AmbCghhmUWoJdCBY1/ZdazNz29bwQIvevc47kfK8Pnls27bdHurL19MXXx3v7+cvf+vN4ThTbsDAgCLMWaqHpcQKAExkauAAGGk6ZMqidoCInPJdAwlI0CMAHQkz7uPmnO/93gGRafh2AAdDwIdrBnvv47kLMFD2EaWW3jvxmKnDQ5gRQIpoVwAohQNQe0ei9BMG5ZCUBlPiKTkTgBBIFAGFJdx5qmlYSm/L1QZ6jRCbAZCUoY5ctwaeadvIvNJAh5KZJcuuFAlL4lm+hiHzQx5WatVxP+VoMjDy5vnkJ0QIDWaJCBbJAHCm0qSwBUEOZBYY6DpqRYgGnFhdESl9rnL1n2L6QcGvtBsANxCm1lut1dE8EngNEa7Wx+s1xzcETMxLFsXg+ImmNA+EGoYISOgOue5W88AYS3XJ2oCh2Fz1HBp3gIcgBQteXdo4iEPAzPmHehgiFJFr2CQoEKWkry9fRp9hfkQYPuYYUwOICKtSAlxYkmMBSKUwDJIzp9KX+ZEAoOyjCMNAHMqgII+onrvlv5K3Xe89+YKpgWY+vLWVhDycgfN3pWYMPYNyUvNN4+4sU4AxsLsFeACZ5RIFgBhx0PByKcJlPA7cfKA9zQjDwkKQZwkMKdxX6lngsrWlUJS5Fqrz5AGqCgbhGMTara+90NRNPbRdug0ttzHK1jSQ1qUDl2Xpy6Id5Lytm8bWjUCPR7n9YvfyzQvDimc53/HHU6eJbLlEVw4kXjk6gQIHABAAMkdojaBl+9f//I9k/7P6sP/qsF9W56CnT59++x/8mMxtfSSA/W4v0SDa7sBzgZubebeftC2unQjneYeEXZuZSqmtNTMIxGBAFKoTIpepXjbtFsQCRGEYHsH5VA0khvB5vzs/fZRKHx+fH58XdVx7XN5/0tZ7AyVau6mBk3i01vrN4VgnWi99vSzuxoRCc6n3td4RaNcILOEQuBPsYeGggDI2atgdYQ0vMWHA7NQ71JnWR63VNatPKcLdNEuoLBenEAFAeSJcNXeXwgEe5tEDiYISnd8DwwO7W51mb21Rc+UIC4NSp+7aW3M0RLt/Xe9f7m4f6sPLu69+/GY+FoBw6/lUyVeXZQUVIuUuEAIGWB66dghAILNeSs0nJUIgE4S76fXpCQGRnPZM9afgbJoQAeotPaDDQkoEYJAkdwpKhz4GRbhICfCpVPNwa1IrYMR4/KWBM6u2831L+dkcbNGISK5ch4BgYu0NmfOvhxhI2LYmlZE4wswt3HO/AhRuzsQOkXVsTOQWZZ7MfQj6Zp5hVA8iTEQTAmZfGMCVl4yIiCIcuUXApDN4riWIGTwrRqYcQK698BBX8Boimpl7kBAygiECJOduLEKYU2JOmCUSD1N8IApzh7y7kmM8Nq7M4r0hYWutloKU+VuDyB5qoms61y3GK9xDCmdnKLNkmCBvPAQU4WuhY5KlR2HAMBHlmUaZfiIgAE+cKVxDy+xuETlwWP5983thaiLiWQ82lsa5VkhrGn3+3/C4+lxp/LtZ3UWYtaUeOkQ9B6TsD0EOvCYPyN1hyIXj4ZAra0CMsKnUrpqXWSS6y4yEmZlFLOdK1VS6rnDNMlLggIiRfygxAoEPMzKIMBGbKcsomE5qHqSbFdIsVHMsrVIAIdiRK5YAghIolYUxWmcIriRVylTMLQIIycMzXdK7a/dAbcsGQMwFPXrXrq4U69rNadusqS7Nt80vbQmpSPTq9f7mYTZo+/v9w/2dTDfPf/mhTKXu9rxwWxrJKhElCBwhDMjdHNCyGKMwIpqB6Lb2t6scu0OVSt1IHVF42h/UOrpOlRFxt5sClvPpTAxuWvY7sBELDSdkyY9agLszAnItHkgoItXMC6W/gstcI4CJxjkmpK0Dwrw/ns+XT09nZFaPOgvzzbJ+UHdXa4oB5N0YaHc8zNPkoevyHLZmex7XuchOhEx7mKs31Qhr7itARyIggWAHBeQte5IM76RUYhbbtEeAK7gj9OjqXCYUDcfhroOMskOgQqCGQQBiqCphAFKAG7huC+DERAAk2QrmGMi9dQxsq9VCqu1ud7v05zQfP7zcHw/y1VdfvPrqxTyLpNgbAeAEhHx9hRN5GI65HHKPFYhgnjRNDBx3Qz6/wgPC+gAjmiokWiY7xiElegAIGSQuSZ4BEgmxJaEy30AZFjM399H+omP/Ryy9dSmc3M1IDGeGrzIJnMgdCE+WZ66CVfOLGsGdAAR09XCDnN8d8pOVsX+AoCK58879pYGpRRUqtZjmljZADYlcjUWQMcBJyBSyNYuYwtxtYOAgHIffB7X1LKnObUFERDjSKCoYsVsARHKzq/wBwqzkEGBq+fpMedzzkGFi4Sygl7FTZfYAgMivwdNWguhDG4rB4IyY5xkhttaiSFp9CvNoUPGICGa8+m+ySyDV/kDCACcgGn5/GOneQQQKKZLTw9CRwrPuGRJnkVSmnAEIkYKJ03sztgbpQQog4kRBBAQC5aWbYh+kAucD6jMycpTfu6HeQQQRZZww/7m8CwMihT/TbDTHvqmU7AUbuRJwyGuplArI6oZMkPXBmbfyAB+7unQZX7/q/IPItJPwda7Mv3aEuYiIFEJUU5bStbPI5y8hTVw4MLGQmDnK+msIUwsixEChWfaghsxI5NuGZkgRCG3rPAtXjkjqpWV6wj3Wy1l7hEWCtLjItJsv57au2jQQRT3WtXXz3vV4rDcP0/3LXd3xfHN7fDhWKZelH45078eyf9XcfPkE/YcCYZTBRofcyUdaodlR9hMTUTdH4u3i865GZ8VYWhdmLhMh0abTjhgtu7YJoi0blSBhLsNKCAl9UkdAT+dYkLD0TQFCtb14eS/zDoFDA0sSTcabiwJFCgP2srnj0lUI6256+OL+b/7mbUjBrtvaVQNAiIEAplIj/OnTY2sbhBJPUm/megcYfTn1flG9WJiHO/TxynGCIAS5/vwoIDZdLtwjeJoEByYkiBjNkVjVWYpDpNdYzRKMM86vTLR7IEMQhFsRiTAUNncA2loLNwpv6BA+cQnLxlMgwLOeAWKe8HC3v3uxe/3q5uVXD4ebOR8G6Q+EJBwjujl4EHNenCkMDmETvEpNFb615tcy6MicP0aS3lOoEWZL5RPJNEGTV3nBjaWgARGbu5qaezZ1J8xgJGCJzI3GLtqHu+KqHrl15qKtI5GOzDDm5hmuzotBho9B2QwPN2ORz4MIC/etmwVSitpEzH3rxAGIrkqS5Sywm+feGlJijPEzfF5KuXKSPR2PiBnN9cgvGDMtBcRJgPBSa0QgBQC5hzCbO2bjITNcgwJuls1cgtytAREjWWaEPT4DjmDUqEPbeorGEpFVwn5lKAVgQNIt4LOhB1R7/pwAQs2KCIzrGkbXCoSIMJOZCjNgxhMAwoqI53NluGYGv/PzY/963cH1DY5EmPbhIBRhU6URAYDMXJgF8zhSEQAJ8ibwAUXJjO3IB6QDGHO/HIEJokqnMGKYl1IS95aLQHXNPhxMO2CkYcBzTJIiOR3UWiAcUWLwV20gPkTGXmSk0hKkTREe7lhIqACOJ0zKlxkZIGFIh5lbOEphIKTQVHncRooDPGqpkG29lJdE9uSMqLP3kCrp2AJkYkKktm21ChC5OVYBN4QqhAGGgm7dWwdCEcGZ3cOaXy6X3vx8WtZFIajUUmsNkGXtl2VNpQ8QBfn2xfGyrvW+3r043N7vDwe5fXW7u5nLrprGZesYnWO9mV/OUy/01LsGKyKEK6SrFNFH0EFYqkKAwdYNMEotu7u77bKUcrAer168iud3x8NOsdWJCvb9XiqBeme0IowIZaq9KwFxLbY2REZEJqyTlNnOp3V3uy3L2rbGMgGAiHAdZXsuPgYwQEQ0C0RYt02qFOL7+4fv3z6G6uHm+GlRd/PITxtzLVJkWS/NwgmrVI5SeI8g2/Lce1c9GSyIxuiQQkcIInk4kDJAIOcvvvZl6x6KgbMwE7I5ItNAPaQmQUAOAcQIQJhEREhs+gh7OhrAcFTXJNsQEwI4OIC7mTBRkST+epiZutpUkaQ+vNzdHuf7V7eHQ2VK2JkaeJgzkakPPgumyxL61hNdkHGicLR0ssVo9CuFtSk4XmG6TiSZ/dxaywanjA0nGT/cKYilpIvUXPNJxhm8BzBX8GGOJ0IILFJ672mx9wAR8XAIICrpW/ewIslpd5ZxHF57yoYBb8gAjH8v90cAcFu3aZ7VzNRCnflzDzAiYTAXkd4VYaD8Iw1hiG5ap9qbpZE6nboZVAYIQsYAEXIcI5SpAZpZlk0BEvXWMilldmUlUaaplImEuOcD393RMy4KOGSdgPwPXvn3BHq9MgNDiNDcCBkxPMLMay3h+WTMpz2FuxBDUm+u8M6x/DQDCPNASrckEpKZiZC7i9AwJ1NQQGpnUoqZZpo9/04knBIKjGd+gkScmEoppj3VeTPjEcYdv3kAwZidxjkFICERc6rnltdqNmSPYLQN3QYBMHJWcDc09AhCcB2RbgLyiFqLg10f2mPGoQgPA0BJXx16Fs5EhHCUWtWdEQwAfAw5zBwAvTcRTqy0dcOMABJlAiPSueOBuUQRNuvCNYEYOaZAQH56M2+GWVAZNmrdAooUgwy+o5tCIEkIi7vXWgMwIgwAMKhKEAF4GGzaIWKaJo/oTd3dLM6nBYi1d0BBCNPYegsQgd6tu/vusAcgtQDEpp1rAaHdng4HOdzM+5sqEyUTxyLu73aXS9ejFWzQFusR0QEb55cfYJ7WkPRfjrY2KCJlKoXNttY37hf0W9sA3Jb1uUYszyeefSp76BdimHfT/jCLVDWd97vL83OoEXIHXS6bWuxv9nWyt795//IberqcNgtkpDRDjymKEAgDSRjR85fcA5nl5asvbNt2uwME1WkW3pk+A5KqC0OlKYL7Fsu5te6lCjIT7RCin0/bdjZo3TekEECn9C0QiEBUlhKKKDJ2OdCRpbcthKLBxLMwjCdRQAQIc9oiHEJVkcDVcgtENFyVmYRMn15XBzAisAjhxKEzBoqI9W426vMQUATn3TTt+MXD9ObN3Rdf3tw+HIggtDlIFg5naj0JWSzsYYSkrRNzWACiRwoHAG5EzEymVqWo9lLFuyGBNncMTjAjOBEJkoETSX7O3JyQtJvz6HFKW7swa6hHno9ITK7uqlQKJTgSg5hIGCxcTaaSd+2yXGqZwEcWbIioJGY6fHOc+wzIGWUsCMeILwhQyqxduTAhmSqTuG9Coua5N82fRbLqmDnQcRgjpbcGRAQ0LiczJAaAEScCzDX12OhzahvD2w0IUiVxwiNya5YWdrqm1oXZxm3lyQzOSQPSpcKczDIPF5bM0HoEQUjKvkVq6wsAiPDwIEJOnA7uxGyuibtBxMAxdkX4NS/nECiS13BgpHbkQ8d3JaY8oIfWQbmGSZPyCPq6XUc/ACSkyPi1l1K0BSCycISLiJoNdSNinL8pVQ01BiyCkVJ0Gh6sCIwg4d4VEIpIprdE2IPcrKS1Lj0DTbFQZcmKuFxdwcAtARIQcISra6qTKBjhxNi1owAQrq1LIQQkZkQKN2IkkAjQ1mWSrABNbW381K9NA2OyFkSifNq33guUcZ9HZIoyzAGjlAkwuraMvzXtkIqC9vytJULwTFuAUMYvOMJZOCQoPHpjFABLGngp7Kq69Xl3OG1nBCbwtiliYebWDRmlyG5XpuMEBgF0vL9b1svz6Xk67stUbh9uD4dapypT1a7nxTgCtYVtxyP96Kdff/uLH3xhU0N2tGzPpDyFEYhRcGCxsOwqAQbX87rqavcvZxGKsELEhLUQO4LrVLFM8+ASApWpmuG6bu7x/OH9/v621np7X7/79v3WnwBLEN7e3GjT9bzNuwNx6a1PSBFQhGGQuiEAuipyEDMB/eibH337819uayvEh+NhuaxSCyxaJwlUFjQncKDwYyVACCCQeW0QLVQhGBALguTqMtwAGGkKpECiUhwcEpLpBgBSJ7CuZmHrXHeFax8PN8jYFCICoRB4eJIQAdC0IzIQEBEBA2QaGvO55A69e2H4bCEtXJBB1y4UHsGEx7upFHvxcnd7Px9u5loJMMAjH20s5NeOWLNwNcQwSBpd+tMB3DFZOiw5PANGem883TBqwMQI29LK5wruYf92t8iBnsZhMDo8UtPx4XuOWsu1AQYFq5tF+AgEAbp5OLCIW0CEoZWS3SeRjBlmAc/3MUFcn+QOkI9eQAgQKcKkoZHO+BRyugcFCZubG7BAKsRpPB0+GqRRVSKsPQEwVES2reX62SPCFAFLqao9lZdR56AmFZlL70qEkC+/q+/Ds4f18wWPSEDjCicGQdcsJA8W0d6RmJmIyNyzDSYC0vXDQta65JDQtSMTeECSVIeIASPDxgjA+dbuXWstEeEWdZL0AOQygBDT1A8ICI4sCHlrERHlgZWJxHxDjKMVry8XhMJi1ilFef5sE9aBjRt4bhfhcB8CemS4lzxF8ACgEEJTFxYPIEI3vWLwPF8udo22WdJUiMyz+AzMR5GvhyUgkBiR0NWFJV1ZeWKpGlEk2EeYAZPyFK5Wilz3YcMeh4iWNzYNiSrXxUKkZogoUjxf50goOWDl5x3LxInjZyFAikz8UPoHFBmZWHLVaQbZOwAAGJ6rtiSrqIdEOLBgGAYRoCMVysETIMy5JBGXVUPXFkFc5vNyaQoORhETlfl4aHqhibjQ7v44zVMgTMfbw8PemaSUOjEJqTmZzftdGD0/be20tvNF33336s39qx/tf6NVLxVhAw5x0nAiR0y6ugUJRQlEdwjEbW3zXL/6B1+/fLg9zsW3j5fTJ8WLHOn2UI67PaFTEcRw7YHQu93cHlX7s7bbu9utq6o7zNNuOj2uQRARh5vbTv5bP/nJfn8LNhDo+RMkxBTTAECkbOuqPe5fvtguFylFu97d3a6b6fJcJ2JB7a3sCMIRVS3NGOHdeD+1bVVla0FASFPBiHRYRLoXSyRpCDnAMCiQAk24dG0RThgWQaRdt2RaSmEiShw6IQIjBmYAMJ0TxAJpUgALh4jItkIHL8SM6BHmqGpTFfNOQlSwYBWGuTJTJ9IXL29evL493h/rYQKJUEUiRnIPU3PzdHhjPnWFEDCIAgzSM81UqG665azrDkwUFNfFHjpAlssJCwGpalp08rwjxvTsASELmbmm9Y4QADCNb+OjHqoW4aVyCjHElPT/tPrkzeduhOxgFj4Ca5HE3o5ALOmEDBbWTQnAzJnJzIVp27pfuwWJ0TS3zSNmxETIFN0ywDzNk1k6bcaobmoeGVD13CqbuTVlyURbRGyASEkjJnR1JFI1IpTCCOjakzINAWk6ShRlIHp3Zk70XuJOEQEZe1dz53BGTutj7x0Ax2Ahkvjr3joCSD72IZyBHLKjMC9tMLOxaPfUly08plpTscU86BHG+e5ubiIU4VLErwNJLkjDxsMTEQGTEhV5Q8Jn4xCAhiJCGgMiQLXv5l1vSoSE7K7gAcRu6f/lzwjwFNSynNI93C0dkHmO54WMRGCeijwVyQVUxoEBAMAR2TxjVhgRdM09uBtHrgcjA2SmVopkXoGYRomdiFQJM84eY0wzQhhoLZN9bi9CdAuHIEYSVu/JtlXT8Sue+fhS3M27Yi32efpzQApANNNSC5WKSASk0bOfWjivB5dSVDty7oKyK3R0HnHG+iLGOr0UACdglIhwA+vakdm8A9FlueQ+eNtcwstclm1F9GlXDrf7aVd2t4fsxJjAkaTu9123bbtUIOdYnk075EplmuZoOsHlJz+9OT3NT/qsqyM6MZJD8vHSP+DdAKSbEtA87YTlzZsvXt7vXtyKhC3nFaMfJjlM6LoWqefHj4fjdLw71Jv9+Xxq21YF98fdwnxeG6IgJhuAqYrQxHVWj1LLiy9/FCBba0dkbbrbFWEZXmTIRWoAKIsQ1dPTp7dvf9gdDoZoEdva/dntAei8BkVvbgqta6CYQaAIT9GbWQ8kYplo7rCEdwx0AAAJFmDJzx8Gexgg2Uh/QoSZa1AxRwAjDgiFQA8Aza1w6sOCjKEBxN7TPkYE17k8W8MhsmYuAgXYHBi4Nd3vCqGnhM0FSGKu/OqL4+tv7m/u591hZsIwDQgzL/w5S88RQFxUtZQCFK5uqiyiqojoap23SFA+JDzRrrIG519DXYkZGQI8c44ckA+yUoqOJAGaKjO7jydNgkbHsR7uw8XH4zrM5DBx/tGUGv2AP3r6xQOHRRUpZQCg4cFA8FxCACKZat3t2nJB5FxWE5F3HYO7JYIUDMC6liq9aZ7FKZ64+TSX3pWQgLn3hkgipWvP0xIgahE1hyuph4iGGdJiVJt4qOtAYmCav1GYW9sQmQRx/CvIRZK84e4AwYXzvHI3ZkQCAnQ1LByWo4wTyaBbQmYEEAdqDWlEjdxTTx8WFwBElCpq6sMKBm4GAMhACLn5zw+O6Sie/8yajqTwRXi2k0UkPC8D2VmhwkzCXKeCEDm3MFJrHZIXnVMuQvgVCJoz8GDzEVztQ/ngNh0g8rxaEZEggyeY3T35f6lApBMLMIRHD2UGvnPpnXI0IAQCiSCEiAQAJR4cIr9RnmYlSOkmEvGNyRdN15N70u4yEhHubmojH+k4uuPRzRKqOr7wGPIWMedVEeGlVm0dic2zvMCYsBZR07QrmQVJISSmwsQEhAClFmJUUxImoYjoZoFBLB7oSIFoFmYBREi0rS2ny6YqlbbW3czDpll2h8oTUuFANTRHr7v9fNyNFLzp2lpbmpsv64ZE+8Px+bxul5W3y4++uvnd3/3i7tWei0R+P4SpIDGWWgoLSQlA5HJ7/+Lu5cs3X755/erlw8NhmlDXR4Ctou0KkvXDTtCVgIhFu3rEm6++QObT0+PHjx89QKSoKjju9/uA2M172e9++/d+p2nnOh9ubtRwdzwiM0Y2XmQqCNIwl/7f/WHHld++fXs6L1/9+Iuvf/vr6XZ3++LQurWmu+MuANf1sm1nd5U6EU11d9zW7hiIToURdwaFuTJPQIQkKCVE8k/UUZ0OAYJQwgPDgFwKT1WYOBDMFcJ730L72DoiAI1HW6LikUtKPeYWuefN32YYAWf3fDIHMiPS6HUOM1MMmyodbubbh+PN3e74cEC2MEMmj6wD8QEwHrbCjmk6HFh7zlkfIij7afMlHOkA5JTde1cAZGFAUlOzcHMEEprSKXc1d+ZDzZN1nGrzMI+mNAEBHpKVXgHa02IfgGT5Ebh6KyJFpci+RsiPNqUfb+Sr0qdP5p5r+HSF9m1jYkBMEc1cqYhqj2uW1T0IKW+dfN2m4p1cim3rALism5oiMRKqKxKN6D6iXvujwgeNbjxE8TqQqyUsb6xFCQGwa0MkzqpERhZKF/CwgV3jFEC5Loos0DULGAe556LX3TJnIDRydBiWFt2RsMBh2cktN5h2BATwAe3DSHlaCkGAhSNdIxGEblalZuw34Z9pME1iNAAKc1ct5cqnBjSzUkuuUsM9/b8BkaSQrJNMKT8jZhGBFj4igmNUHIlfLgMThACEZs6FCcHcWMjUSQQGqSlE2M0AMyBG2jMs5kycv4IIYIGjvNjDTZOTNPxMqRgwmodI3lIAnmqYk4yKu78XuwKvYkvJBzjDALoGBDKP1rD8LUgTFoI5AICbmymzsLCZInPvnXPHRILIrW2EHGEIDEjhDhTqDTCE6/iPOLgDkDMxF/m8hHAITis0RN1hWxpKxeIyQd35ZdVAzOfebl/3N3OttSROK/zm7uCAhHnRFLh0ZWKi/e6gjnNlOIB2eXm8f/+8fvr1t69+68c/+Z0XbJe/huXD20e9rAYRygAhUsKwNZv2+/v7u1dvHkrF+7vbm30hVHJD14nDdcVOtVJBuTkcZKJpFiFjD9f++utXH9794N20RziRlelw6N1DwyEI4Ce/+9O379+Cwe3DrdFcdgeRadrNqe2N8Wh0WDPtcNuWdWlba7/z09/+4vWLn//sV7HatvXjzbHs594VgJ8fFxIwh0kYg46H2/dP7wkzc1A5ijABMAzWhwQShACTuQp4ekUQzK0BdAgDb6VOaXbomWlhHv2joCA0jAmpJapZIruDwtwBPDS1pix5xisgnSA8QN3S9URE1tq8Y8QoBecdTzPPkxA5piamUWp17eABgA4YpkgcmtoBuimkD/AztNIdLEhEtdVae+95Urt7tmfnMwUiSpVE7ba2egwNIw3QiEEkfWtEjIDMQjxatcKDGB3CzaRwuItw61pkNAAOLyUCE+qwHeabdsBAYSyqM2ZMZkHkeV0JIeSXANj6ttsdItWbFB4C4HPz1OATs2rHdJFDmPZSJzUFA0KcpoJIEenqDhYmod47Q5YhZdEZZq1kNq8gjCbmPD1MdWyDzYixrS5EEdG7FuF8N4ASpZ0voJSS8hQAIJCbEyZ9xxNuhjQu/nQzjlL43G3muXZFLgNEMI+1CiIxoZkyirmVIqmeXI82gCyFn0p6N0Y4hRnB3QJ4dAgHBiKqjx55GCQNJyDrSoRqNtVChGE2vAZIEZFMfEQEAjDMJ0lhcfduxvz3dlIkgEBTA42crbR3yCwZc36XU7xCJAhjYSQIDTMjGSUtqd3kYuezV9UjACIzdcKcw01eWaVMvW9FChGY+QiJDDezO0SYc6G0NtdpggjzMA2AkMJqATg65BBw0JoQAaCrM3PuzEVKXjlJxcqxmog9xxRAEtauGbwIN0BWXYVrh57vL0oT1QC9ZttRIGHhapm/Zw7kOHcW4VJ738KxSukaN3f7MvPhZne43Xk4V5Yi82EvUwVE5gIRrS2pGS69i6h5XNaNYHc40Ndf7izs3cePb3+FP/q9nz78k9978eLFL37x9sP3706ni2t3Jy4z01x2Nze3Lx5evrh7uEHc5j0vz+/a5XJ6fH+Atot2fLGbyQ8zuVstYt7W87abRXt3132Fh4f7jx8evTsAqelyXqfpUKfJ1029He+Ov/rVb87Sdw8v3n233ksBESDKF9/VwYxmipHwX3x8On39D775+sv758fnvnXr+Pqb17//b734u7/+9V/91c9LgRevDlvvfXMWmIX3N8dze17X5yJTD4BYAtPylYVEJcMoibEBD4QgcAxFXAEcwfa73WGqFLxunnHOnMNDIUfSQHQMiLFbCsaMrCCSg+eWLdzNBikLkUyNiwCAdXMCF0CWeTfdv5imgrcH/upHD69e7UtxBvNQxIKA1g0C0rqdeP3eGguHOxEQSj4rwSFrAQGxTnVbN0DopkCkpkA0SsGQmMXcXCG1zTxVaynqBpmYYVFtjOmhSKZNcdcUcyJA1dJ6BwTEor1PU7WuwoIMqo6DFsx0bUJPBFkkr5GQCR2ckn8F6DoyvdoUBc1dhGuppjow/RDuzrklYsqmPETIvR0ARAAjxgghcepm1215H+SJ8IgQyUk9iLmrImTyDWzkv2DkD8YunQKGM9XNpmnqraNHrVV7q9PUe0Nm7b2UoqGWfhB3Zs7yyLQd5hUxnDuJCQ1wc0nRIsbdZjj6TUaHOwQkz5KJAryU4uGJjUslK23Aed8SCxJnFM+6FhGICATiceJwjlfgxMxIgKAD+3eFAnrUWs00DzUCcoesBUsxJ6OPuacXGnYgppGpyGi1u2f8BAd8wpkYCcI5E87p9QsIZrQOBEPjYmJzFalqnRCzhEtKpm0iSygR0M1rmdw0HIDyIUAZZyMkty5cPPnVkdwIMk2ukWfIy93GnocowN0MgbKOmNMCA4iEnpOBj4HM3YEIAtUs7cOA4QBqnYXc/Io+Z/OcbNyTHUTsYabKwsTZbxfebVx1EUCUvwYOCJ5lrBxmri6lFgmbJottXVVmCkKuZSp48+KOk0BHDBBhnUWEuNayCT1+OvWL1Xm6rI3QmOiwj1f38vwMl9PpF//fn73+5ptvvn54uDl+//rlpw+n8/kcRk68m26m481uP03zHtn76bS1bfv0dj2dHma+ES+ut3uGcPWO7h8+fXjz5n7bfN22aTddLos8iRx2N3f3z/6JeNqez8i16/by1Rc/+6u/XsG7dax1XS2CzaJWyX1e5tE8It+cmSIMh6en5c3XL29vD33rf/fz/x9Tf9ZkSZZlZ2JrD+eo3sHMp4jIqMqqBNCNoQEIWpoPpLQIhe/80XyjcBI2Gw0QQGMoogqoIafICHc3s3tVz9kDH7beyE6ReqjM8HCze1XP2cNa3/rNbY5/9r//R9/8yad/+X//t3/43R/ePZ/vY0/qGm2/zTmttSXobe1w4zl31FOv7OX+ERAmiCMpYYkZOZHOYeAkMmace38+L13Yg8zSLJkZwigRMo6RQQRqGYuftxY16Ml4vGtcOfCHppu5XsN1VWWoYl3b5YnXs16vcl5g823fHMnAIRItmhaDw0KUVJkAVQFV9J6D5RgUVA5IBjLHEcl3tL0Wh0nezRFOTEJE2pjJDSRZrslasxFojlmqDWY+IAqluSmV1JHSmnIgZLzWWsTsUZqUFhVycjgVqDyxLFRNtj2UEYUBTj7mDfVLzX2q6qG2JBFlUKiop4OCSVCJVUnueUyWQPEwPkWC3PIQp2jRG2vazJCKkalgj/QCPyA8Wm/mVna2iKAIAtfP7+ba2xxDWM1cVYu6VpD5iq5qveOgRmo+UKMliYl47FyPOoAI8AxORpJWsQOm4mjXbINwGEY8g2uJj2iqteaNSHNvB+Y6wUXdEyAYOTMpomsHBwHuR05m7VarY4iIhOeRm5w1aq+pVqa3rqXLrKC1Yn3UZO0I5ckkUDnCSQ4gXUnduMId+SjPPRJx4BnKf1AmgHqJqpCP8EQq84GXeIh2kgCCzQku23fFAKUIz7HXleMe67qMfXh6SXIrGL1mI4WLqPV7of7mPrQrF/yUDqGGKFeZw1yPaRy/nngAAQAASURBVCFaCSjDAVe7UKqD8qZ4Tj01t2qB3Wfx+yjMS+IW5gmIaOErqGZJQMYx39DWSh/CTasC0c7pEft084wK7WMizHRHtqVLp8v13HuvL0uUe1/NY2y7NBXGHPvYx5yxnK7XZx73AREinM+ybUMwT6v9+ffXr295u92//OVf8qk/ffruV3/y/rtvP7qRRSSYmyoJg27bbbu/8tuPlyVpfnl3po/XJsgFDX6/nPq7j+983G8vXz9//nK9nk/rEySXVbfbFum8dlZNivW6fvndDx+/+461raf1t3/78oc/fH7+8HE9fRi7ffMnn4qVGRZUSRi1taln1GzfJojfPb8j4tvbKy3LP/sf/sXT6fSv/6d/95f/5Tff//LTmP773/zuZdwqdjqX5f2H97evP9q8B2xZlUUi0sYokSNHF2GoRtSDGAEjRLoxoje5nvp14XpMtrtzpMrPqeicWnBcHK7lRyPAVFApJmIgmCQcIEiZyxKotQCLCFhRnzJzCFNf+HJdLmc+LWCl9bIol9EEqlIxiDhYZ/Dw0udFREQQB5NkhCOWtk4btd2LSigoLC1IROc+isLmReCKqJkYkNJ07qO8WgBpU7dSncqcu0gzM3kExKpw1TD0840BEi3UPhNy7qO4m2W4z+K7HZbgOCQfESSl7q6qMpdlGWMCaL3lkUmeBQEtKREJ3LOGaPTg08x9EFfiIY9hwtS0jbETQZpSAixzzhKJhNUHCRCpqtlkYgtnEQ8n4jmGtMZSI19jbTgOkzxqtUgIzzG0NW0tAj9DMjKCSJq2MYaZZaI266KVbFWae2aiggkeDoVCP4gKgmq4VgLHEvyWAJc4K7kND25obY9rcfqY7CcS06aIIMDM0yyQ2jgiygxcSl56WNTq5zvmRYFEpfJGsbYrEKewGFkLH3cm9sJqEkcEs4x9tqbMVDoE98qhrO84WTmzYh94jlFMiIwyVGYNHKU1nzNRlnp+yMlxrL6ZiYEDG5hECE8CVa5A5fUIi6hs+01XPRRERHOO3hc6oLgVpyDcSFmTYGmtbjU+4pFVqOKts1Y6eUjfao1Q++RD1SAs2WxYPkiz2nq5DjOhWg8TEeIoAoqSzgJUsxJVqYnI0csnWBXCFCbaYgaJTB/bvt3v9+lW/ITl3NfrqS2yXpbL5Tzuu3AXVaaeklz6VOK316/a+5hjH7d2atrkvr2u62Ufdjnr+dLf3eXrT/E24qcvL7/++kLnUyafrs+I1HVRX+w2prmNe+w/fTi157V/uSvDlHxVyf1+Etb066nnyj5vYT7Gfjpp097OnZc5x50mm3lbehPtp9O2Wb+/Pn14/2GQCtr1uZ0vY9yXyzsi0aaULCqU4KbuVqisYZmJd8/PLHR7ebXIX/7Z9xn4y3/7l//1P//1r/7sWxX+9d/87v2Hj9d379d3p3/7L//THH6/b3P6uG/rgvXMSxOzyPP59eUzkMAGh0WKiBQuMgdRivrTaXl3Xk9Nz8u6jxFBO5LlQZ8EFXy/3jcuTynqCWOzg16XCAR5VuhgECHTRJgZ6V5olSqlT+vaOwnhcpLTSs/v1utJRFJbYXCz2OmH+YeZECWbxqPWYimAOZLIJypr71iOVV180Cvz55eagtyNmETUzEXY3Mn3Q8SdJMo2hqggKkpXI12EmHiaqWop4ipTPQuM6od+rIpxVhFmdwuHsFRYWi2Ea3/HTCBBBguxyBwGYIwdJLWFbE2nTaA2w8XypIx0O3JsKgcqQXTsRGFmGQgi27fWW3pUYlctG0W1bvAah4DIbILI3Fprx/7yf0MvTlSwFQJB4AxEJDO1pT8G2kB181lRB8rEnrFtm6iUJjQ8VMUf2+maL1UWTRL7cGZVOoA7x6kcAaLHkKf88ECZj+iIccimOucE0ubUpnNMcMEyj4+YhC2sdSkwExF5esFqSkYWEYSKto5yEiaC6eDwAMeQp+xLdQRHBIKSIdyQ5eGg8rmUjqZ6FstShTrxwW4td0mZ4LPyADIjYlkWqxivw5NNIDBQOytRcYsqncumGxGt6eG3SR37kCaU5JHK7O5LWw5hAJDI1tRs9t7Creanh1Q74W7llmAmYokwIN2sAtmPuydBxCps7lIqdZUMO9Y7TADDfWnLoccogVH4nFan/bTQRqJiYZSAFOCw3j0p9ULvS+IIAfXwGNO3nYn2fcxpSUiEUJKmLrKel8v11JTX86WtTUU93HeTpWWGJ1SaC9blMuaGiH3bWu+iet8cuQkhfPTe+kUbk74Y8fL6ZvfbW2bs24t5tLYgoNwdwYIT9vfPa0s8X5Z9t1/+6bc//PV/OTWVHO8/Xpr6sp768gubY7oPM2Ju6K0pySrM4WhK/d1VVL/+4dXGNsyen/rz8/Wbv/+PfvPb34Pp9HSa07mHUHJB4TMiwmx6pKis5wbmOV16vwrFHL//ze9//Pzyq3/0q6fr6e329vf+yX/79nb7d//2L/7N//zv0m1t7e3ls0/TLh8/LNcmaTEy1vX08fT+vm3FT5z7HhyZYHA7t94aYT5fTufzqqr78Njzvs19GFKUGa0BtYhNIYAS7iLVyZlZ/CwXq6CNyIjjPToYLcKEZBV0JZVcF1kahPy0KCME3htax+G/IiosIAszww0ZXqVM8akig6QWn+Ruqq31km8woawtIKHw9LLf1zwaWdq5JmoZrauNgQTpw+mDjKL5R7IIBZVqRZiTvVi6tYwrCaAFRDgp/9j3M1A9r0gBApjp0MY8GB/hXj6byPQwFjn2i/XW8SEE4iY+rP6IslTMQObBHTgqKuKMyIAIs0pkEGmUME/VplfRGZ4sVFpWUXUzYjE7VJ7CnEHCtc456DN1WIu2Mv3xQb+IBLTptCkq5RBWWWsUhsTS29HTRNTasuIKklCCK9ZGiMPJnVmBXyFasGXQI5GKjyytug+7+xSWyMhIPRImiKRUmD/fvofe64hhK2F+ej2uIlyDETNjZiYc8jKLZBCSlUE4cj7NW+90iHWOWXuJQUVlDmM9fgVtEhHFeZ6z8gCOjUKWY4Wi2mVhdqRnsDKsLGAAQZjTkZm9dXtYajMPCSYdIp5sTcOdVRAZmSxl9GW4g3Jpy7S9xqC1NXf3ZVnMhraGQ4YREc6o7cIkkhIa1tKmlFUixFIJR1ARs0GQ2t0V+j3SkWxm63qeQdu4J0JCamxH4LLCh1vva8LMAxSFi0pG2V4KuhtuIcEsSSSqlUYhS9tu95ke5P0k7+g6h5eSaj0L2Pu6sDIxcRMhriUkgaSJm3PSsiyZyNwz6O3r/XQ9KzWB6sJKss99PbWm+qtf/Oqv/u5v94/88vVlH/vcpkN8jLeX+/Wb5/Wk/bR++f1PTaa/7RHbynn78ferxKJ0Xtv5okJ2OvP5+XlE3LbbfnsbNs+JzHLGCZ91e3nrl+V0PrnFhN9f7/ek/fV++fCh//R1H2meolRHp/sRXApCBoRYtBGRzWljerqPOe+DIP/8v/8n61l//9s//N3f/vDbv/mbf/Ov/t1f/ae/ent5WU79tm+FGmxNzuvSlMzH9aTXk5za2fKCjLlbRpDk23bvwpRovRPjempzuBHm9G3MMZGQJE2CErGQuYcfWK1SBWeaEtqy3seWGQGy6aXhICRR4WM5w+sBUYE2zpgioh3fff/x08f+9Nyen3VdkbBwV1kiogBDJS9QkWCqkGibE8mFzShNTgaHh2eoHD0HEzyCD+5b6RRTRJAc6QSabiVvzwfsPjN673PMvixms8SeoGQgkCJiZkRorY2xlYknKVW1xO98HENUURwlKlWR6WbTj6VfIiNZD8cAkSIcKH0pPLySvwgUJRRC4BF9WA6vmpy4WR5xVQXx5vDI9JQaEnCEI6n49qUZYT5m7QXIz2rBVSIiPAPOym6ORPphio7McndWqXvsb7kSbExE5l5nCwCvIAFtzcwPQwbAqhlZGfFxIKuYQB4QAZjSU4lBOCIxmbmpRgQTRUbNf5gowpjF7OGDKLNSJXCWoXltVXRz5YEdeIOjEmdka33aqIkbEwsLUdasqwzoRJSUKuIegaSC8xWOFPnAxQPEbv6QV9KcsyboIMwx1vVUfoPqy8p8UEJlFBA1Acpk0NFhICNQMiTiaVOYHpaTeJTG8bPw6+BYEAEor2D1KB5OPgqFZOFafVXyGJuqzn2X3myOQhulJ1Pl/hzBUkXMktZACA+3vS3LtPnwiKH+0rBJcgx2VTTCyrSReRgOUA1jIiIj03Iwc19aHvdoxewYEasu7iWwY0o+DPgR3BpBeySBjZkhuSQyzdxsX8+9dyEhRNickb6sHaXIywyznIWyThE+n8/bbVNuXZa329vr/aanRRuBIEL73F/GD9cr4e1+/rZ//rwvH87hZpb2iQiWcMrx8dP5m2/evfHX77/57svvftfZU+Pp6aTsmTtYd9+Xpst5RePW2/3l9fX2mp66KpaFRXVZt7F1Xfpp4eAP36xf/van3//+h384rC3n+80yNS1VRFhL62XhJWes3YzNWVq6xspCfG0fT6fby/23v/71f/5Pf/n/+Z/+59vr2+9//cOXn16vT+d2att8yZnCEsZvtw3n9XRe7m+3hWJFW9tyuZ5w1e3tTRf+xTdPhan2SBsz0sd+H8HbPT0kCNTE9+ithwBMilp65YFyFCaSmW7zJqDpVg60qtDru2CCmasIUfSup1Upbemdw20flPvlcn734dxb9hXKCgT9sb0+DGmlpPTjWUU5IKvtjjSSsjaLh4vI2AcOKiep8vEaEpkZZZ07oa0dw9JS0yGJ5f52a0u/3+6F9PIZqOukHJKJrEENl7yjjKWonMFjqFsk/WpgAI8jrLsOpfCf0b9C5RBmYtZKW6yZSSYSTiwVoEbMx43iLqpEWWFBKuLuiaovE0gq93hSpGlTCBIpOPbSmRmzJmYU4U013ERbnen1c9YMLA4vH2Xm4dVSDfe6isyNhd2cIvu6RGSYZyakbA8EpjEHH/O3OKgWUVMQOZYZTIW9AUjdgoUyat3hQY/1THUuGW7WuImwcHsIioOFRRQIFin5/APWeSA2I1y4RRgTZ6b7AdYvikPZl1lCtazN8TijrbqEEuS5GTGq1VJVm5aUlQxEIomKGchkeFjrLbOg29G07o8HIyhDmlAk8VH4E4uNoV3DQjhZjkTlKO+JOUrsFV6jNyLyuqhAjylnDS1DVc0KGyBhrnpwM3yGiiKzBketdZIj/MF9ltD2MCUSsXBWUCUrKN2MiVRapB2D+wCLEoW0Xo8UAioHT6r+DYgKjDkUWelJJFmgqmQ6ahAS6RHOED64eGXWg/aOiJx+Ol/O6zmfkdPHfcQYY+xZ/yBCmmR6BrfW07PeljCrXhXhvieSzuvVnjIoX76+/vC7P7y9jV/+6tu+Xlmw73dmlRzK83rmflo8bpfrGmlz34Hrj7/98vzh2hf1EcT70mHjJ1Fnn/0kveXT8znS+umQRnRtXfT8fl3W5cvvf2/p43WC6HRtujbbcL/dk5C6fPzF97+9jXvgx58+iy7vv/3Fej5ndCIOUMyZM8uaX/Gah36uUPWZhJnDX9/e/vNf/P/+8q/+y3/9z3/T+/r8px9+8ed//n/6P7//+vb2f/u//D/2vSAl3W2fw5cnbswb8Lbt2/1+WbtoLLqsJ7BCG4g4Mu1lv7+8GdP9NgfEjMyx71k8zIgY006nxrWnYQgnEVhgYxfRZLiHsMyRkJrfMEVmOITTY/ouXRAC5Om8vHtq5xNfnvrlROxjf4n141VUOa22pqwCdzBiWoKkaYahSI4zklKUqMg8kSRUtHAVnXNoU0qKcG31KoGIe9NplpVXA3K3GlmVSm1sdxbtawuP1pSF56ga9kCjR0Zf2hgjM5RaFledH1g0HCIo5seZXBgKt8qMgmhlfpboPdyJWERAOGjMhwwokERSIEs5mJp8uFDrRBaNGsWUKDE8K0+0PAGZ6X6MQIiPcWINt0stXzLWkmbNOUuzm+GtdQ8TlWlWaeERdV0lyMGEyCNRBglk5cgzM5jrfu19yXQ302OGUYNxNvfKAqMmwMMiV/JQD21N/QhICeZDR8wgAGNOFREVZNp0ogMChQqA5CO3oKr+iEMUX+6BkvoKMxCgjIxalUQhbo6cGcqECBehs6xrx7C8EgiIVThBEZ4xWdTNSXjGXFTdSjLMLJXdQ3NaWYgfmVnJdFghqowhlEwKoKBjsyUM8nDubc5JgJkDqN+IK94TUUNJANNm01bSBSq2FLKcxuneW7c0AOneW3OrVQUXvNunVSI2ER/yZyJhzTQv4DizRaSH9p4ZgTCz1jqAiFkQ/7qCiLgm1GajDCmgGpJSIsxcRJv2wnUgk7WlGwmneUoVhqxN62sRFZQyOsGdiDLn4EwjqPPwDLd+7sp8uZ7X06LtQXJvXXXJjOHhw5BobcmwfRtvX162t/vpfG1tlSbn68ICmIfjfNEP3326f36d+5BGXdrTdZ3jfn3/ni9nN8SM5dS0txlbYxaFal/Jz21hCh9vYVjOnSmF6H6/Wcbpes7IZWkfv/3m68vr3PZ0GvddWuMmhbptp0WE//F/909uo//w69/+4lf/7fly4daAlglkiPKh0qu+uhK4qrdCZsSc48vnr7/59d+dr9d/9k//6T/9p/98OS2RPs3+6j/+1V/95988P33offnxxy8q/esYwvP9h8vJ3Td++TL0pLc96ctb0+18ar57PnJR5m7bZrsHoCPZgoi5r5KR0yIzhSjMwcSczKxSptbkJtOskTpFub4cQGVTEI7nhLkJc+OyODSlvvB60WXB6bz083q+qjYwcuyjNUYgZxWCwSo1toisYUImiigb9eHUVhbKBTWjw7wyy+hUCBsiNrdEFtmYtbl7X3pE4giq1czMYAJVEBMRtLU5D2w9k5o5sxTbNyn5YOZwlkg7XFSLtwNC+iQSJiFiYo1MtwM0jUD9DnUQZjiSM6j3bjZqtc5lxQeIWaB1dSDBxD6tAAfl02GmdDpOgERGiKi5taojqXxezsxH5kE5X4XCMksVA4hqZHhtLI/GJVX5QUpEuBOzuwkLONkr+TkJQQcJGZGeFZ6TGRGta8GgymZcioDahTC41PweoVYaKeYsBzyyFpZl7GahAvjJI823rghChiUrE2VBF0rNWqpYZfF0JFRb5CyrESKB7K1beAW7ZFawQ7oHURaItG4I0fInwz170yAiYURkQd/qSgBK3zrnaL0JFWQ8Wy/u22Guo4e6QCvH+Th2qeJbVA6DyRyzvG9EOHQ/TJQQZitp7qFKZikxAwOgmB6c2hQZoBxz164EMNEc+6HTCuutV3oaPzbfurSxb407EdwgTKJq7kzEXUsCVDkwpSKgIxKVzC2TSCsTzYhSRS3SzKVxWJCKCFV3iTwSULl8rQnRDiQCFhYEQPrSqseBCAdUxKaN6Qjzfbc5w40olmVdlqZLszFLMY2BPFMk3Lzeh7Y0Jrlt923eP3/98vrlPsxF5f3HJ3ff7/sc4/n5pITY7s/fPLlvuq73t02Fbvtu+/2bX3xjI22OaYPC3PZT19O7VTzf5v1yXWJsUXlTnJQBR8a0SW+v9vzuMjdM9/P5vFfpZJkcKuyM1htRRvq3v/zur//mZbmuy9NpjumWwuB+SOyDOM2TCT60tUPvkR4Jm9Omadd/+E/+cVtamAMYw7bb269/+/vtbX9/Xb/7H/+Hv/qPf/n5y80N6/nUlch9N3t3Xk+sQXHQE5K/fLlBsK59n4HISHbp+3bbw9zBenKLTK5mb/okSqCBSm2V5VARAYuIsM1UpfAi/R15tpEHeC7TUYHjTZgpC0EyWZ86EC9fPoM7ywmprYmbERHiSE0ikco3L45x6XZKYZ9FVWfOjLBgZlG2MSO81JPFXhSW8nuJcHipsb3+y4dfv0wth5axMD7MHF79jxytPOJQEAqFRa1rE6hNddf62aQY6SKtOC8VUBMerXdQ1rPaWqsDhKngfAwmK9jBUQOTG4Hg5uDDUVFJljXJqHuOmSKLY0W1DSFhG7MkgoeYlagCcyy8kHMlQi0gjbvLcfkFEk27mRXas0bxxZUBsv5qENKDVGyfAJgkzYkYkY6CQnscqXZJR0dDUkhRYhb+eVJncxJIlblAZ6UT4jrbvQJYKqEGhGPnwcXzYy56Rrgxc0XbID08QFIMo7Y0lGi4clRqO5/kYQXte6R7VTkcIloDnBr5uZfAEUDucxfhBp5RsP7supiPMgoUDS09ZjoR84E8UzfLh73tkEckkPCI1np1WNqamSmTmasKCAknINzb0mtkWcY/KvYcpTCbe0YyONMrlJxBB9Gz1XfpqF0+ofRUeQhv4e5rX50THk1begZCRZKiuH1hXn4xoZL/OzOjMEHgcGu9RwRRJkVmKckngbkEZJQ1larTHx4qmh6lVbMxRQ/PTjBnJDcOEmatawWB9HBzJIdjTkM4MrU34iO2YT2f+3nxOYlZtYXHHNMzlmURkUwSleWyfMr3wnJ/u0drlracdFlZWd6/OxPR68vX29urhyn0w6fnucf1aXEP2zfVHjFPJzmfrp/eX9bT+uW3f/CxnS+Lx+7zpprC2VTAVRpLRmz3+erezue5DUCkNUkfbjEnhfalD5tr7+7Dpn349jsX9UhtvWlnkofNG6hEnmqxUSWIZ4aZA7GelrY2ZYmDX5JmTtLWy+nP/uGfrR/f/T//r//Tf/j3/3Efu0/69sM3Pn8XCU6nsMtFhDsxtaX99MMrBWz6DuvL6XY3C8xpM+Ttx7d+fUrKdbm8vL0ktO5/90nkQkSB1rpq9qZMSY+XyGdI69tw5ExY0mFbSYIKl7/Uhu3sz9frsvbzqV8v5/OJr9fLeuauyAzzkNLpGSr6Ko7A9MJukRwa6aQEGFxOyAgmJEm4S1MCqqxc+3X4BgIQSA4PEJEQBZi4lm0Vidpa28fOSSWWzowxR+9Nmka6Zdj01jU9iQ9NRNZ6sPAMBxMbdDBgOBFuLtIiQqQRGVGaF9SojsWjxDxUnkEoW1MEEfJYn0giwpNVtMwcFfp0pPvFz9ohZjaLCFduNTA8RNlU12gNhUBMcwztrZJcM1OagrLyvKrLKeUNMWWA6CDoWRwW4hpk5GEsLUVTWSJqSBWlu51zKrQGPcV7Ui16MVt1NsUZiyizFT+uvkcBLkeGotlU0QLUpHtSWZxdilXAJMLuHgkRbl3qzk+EzclMPk2YtMoKrl4ka3wJHKsaAtIzJahsMUezRXVwl9zI3PexS8WmI4QqFMyjYtBrucv82EtHXeZEJCxjenkghVHreDNTleryyqrWW5tzapcmq+fIA1F3pPbEdFlaHj712gDXYuJBJSp4wDEo8Go1kDXIAgtq/UREFcjHwokjcbJWgCxUpneuf1FkwLUJURn5mCCZKb2XEsndMx4AQk9pUpw9ZDAEAkQK4G5gZSFPJ6KgTHKU8qgvxAst59BGLLoqZaRNyuTIZFcJPp982x21lWem7L2xcEalqMPDCbSs/YAj+ty3HeFd6fTpqff2h99/sQoDmYM1RfO+bW+vb3/yZ98T4n7Pcb+tvauiL+1+3y/X5Xx98tjd59PT2e7z3cePLekPf/N3NjfAmkAXVWWkiag2IWGf7hvmcIsbc4/p5rksfWni6WYzZ0QgmZbzEh6sfHn3fj0/nZ6eiQVlOD90cQwpHwZqgOHmKEySlNemlcnOE55OykJ5up6uz0//6l//3X/4X/76y69/j0XQFqjMyb/+w5dPZ/146sqoLtnG/P7T85e32+vb2G42bDeTvbouPb//xTuz4R6UycRmTtyEIE2AQFKyu49UmXPXhLTKk9IleTfTDE+PZI+JA48cKsIUIqSNm4oqN6HWOW0qrX3pTLN2tlxJE24shaQ6lsDalFIzXEQ8jIWDDoBVwdgzIsOOV4COZL0ZGygPE4/VZrHWSFpAMj4YfDnmjOJhSNkVqS0tk8bYExBVbVpXUYLT/cG1D5aDu161cpl+tInZrCaYHlmP9e6DQIFSeCeljcFNgRqz1AycHk5akwbKilcjD6+qtIrrqPM9KTKJaJozwCQlTgUBARJyjzT/eSOjInVv9kUqqKA8SURsFoU1i3AkSDl9ZkbSIQktC575oWiKCBCV7CoTxFKVe9W+y7LEkUwOlF5mFFS/tJ5UCYlMpHWqhgcLihl0FEIZYFR30JpkBgtHODOzHg5GBkcEMXppgGtlLxwzqupWUaI4xilAnYF8xMfXPhWVB1T+bqLjd6tlbK07PMBHXiBDIpPMTfigSJZywz2KwVxjq5yVQodpQ0TCXZXNnQ5uaGZYaWVVW4R7WC39LPYIE2kZoNqEJFi4qNy196j8PBwmRIiKlT8la3jVygQswmZBynOaqDyc5aStZxqrIp0YboFah8RhMC/3ZOFFM52IPApRd6h0iUmSAWJRCg4PcmranDxrqeURZofbzcwjmFsmJfW2XEF6enov/cJyktbS0ZrE2OATvJEPDJdGIgLIbR/1C1abLCokVEehmTGxRbIIiovIVFLdMebt7ct2t35q9uZzzPN5eXo+r9dl3Lax7XO/L6e+LI2QTLzP/bT0vqg2NRuXdxcV7roOuo+317cvP9m827yRZLiddKkfw6fr2oh5fTpT2/bNE2Qj5z6192PHknG5XIN82/3+8kqsetoCozXWJvVmZmRYQJBAPZnH0RfFzi2jqbpPN6/HtRR7kZgWZrbd5tvL3F79+z/5Zcwvt/F2m/Hb3/41Jxr7tcnW/MOpZ6bP6eG/u9/GDnOYZQwPJIlIax4xxgCytX7b9vDMZCq0/8H2AcBpSQ2tqxSNkeCeTrmoEJipW2LfZ0l4WuNSPTNj7W09ccY+9olAa9JXIEeYpYoWFKs4y0hmTbMaiY6xlzrdw48AGpE5rHDu7q6ilcYVGUg72oRMUDmiakmK8CCWOa2IyoXNOfJqKziXeO67qNow0Sbay/Jw1DeACCVLjbZJ1cNLnecWdYBQslU+2qPELEUfHadhMnMFUGVk7yePCXqwISOlCzLM/Y+Z20erkUwMRilz3EKbhCc9DlMop7mZt97MHCAbs4SYNVOiOIycEeljctOSgajonI84FhzjEfdi4FN6BkVEnaUQPjSNtdq0OXDQe9LMKamibT0LJXAoitwOrg+ppDkOAUsC0GpGCuYc4fSwjJm5tkNk4klNJAtNkyGsNoOZmEkaF6T7cGaXHFYP5h/oAA3VxAd5THjM7PAZZEnsu4cdWSXMx/KtUqsiHlojRMaDgVIvxAEgCUZJolFuiPJ8Uf2SFb1Hh/gHTISD0izMRXuuej2j82n4vXEDOCu0AOyliEgwHjGehGJt1tnBTHSkc1P5vJMKyVAfTpQLDMhaT1kYMtOchWyM1rqZU1NWqcRK9yBlwCtEobKNoqxvCSaJachU7eGppK4oqkQekjQeM8HqQGsn9E7aqZ3X81Nfr6Rra2fuZ6CxqgBzDvgObUz3RPiY/XKxcecZFPr8/kP6ZnO3OTJi7juLmI8kCg8vGR9FST+TqC1tbi5NsCEjbi/bfWyttX7qotqkbfF6Pi8E9NZM3Oa4315tzNO6LmsjCiJel84kPmfXJceEuQAkBBun08oseARYhw2mNcPX81k1xnTf7+t53fexb3G+Xm+3Ee7Lu6d+arTP+23c5x++/QffjS+v+FAAO2YhEopCd4kcCOMSoasUxhuRGSgyMwLV/ruZh40x5txZ7P/wf/x7//SfffPXf/Xd//v/9a+/fH796cc/kFJO/vxlXPj8k+9CaMK325YMFWm9sTYzBvW3+z726eFwSsK+j90cIGG1DCYJTuKKOTHiGu+5FmJUVBmL6OYuAiGbkcLNLJKiL8rkAm6KpZOQtdYASzbLeR/ETXoDRJKOjNnH1LQSBg2gQiV5Jip4g8UjtAkySCiDSp7i07gxgrS1Ofaw5MIvWpRKsWYsTASRPLIItT5JLkpEeOk4INmq8uCizgnRA4cAsEqYAeTFbLBEobHiMQVKEFXwVgA4zGiRQIrW7R5EPG223tytNR37ITOpjZjNKarhs2jxVOBl85o9SC1NkcJcoblVfmamm5cEkVWFKUDlYEigaRv7gIJVcOTCi9k8BEIJ/vktjoxEkBVn+yEfyhos15UWCWaxOUs5WTaNCuAsVVJZ9jLzIG9mnUvVaaDg81onckmgCiw85xDm3nrCRcXNy6IW7iKHM1j1CBN2DxWp/XJr8jjsIvMg0GiZAP9omcbxEReMIhGPAXodzUAWM7aUyIljJXuAoAEVDgbKP1KcPJWjmQLRAzw3hzXRkskj0ZranJGZEa0Y8TDSymWshKMcvlH9YQRLBXSAhGr9Eu6icthTilzE9DOesLqXssaEhapGLVSOOoi69uObZgg3QppN0e4R9aGGeykaVJmbhg+KsoaJEkV6zpQmRKWNrsuNLdM9qGnMSG4pra1nbWfuJ1nPy3qV9aL9hNa7rq0tBIY0gpS3LmwKj3n/SgpmRLie0n3ntgiTUrf9jUhgxMLCKtLcnFhQawflANzd3ZBgSSZwU4UL6+3+8nbbX2+vH95/GLtJbjbGfdsQU1rOMc9P53VZtvt9y9xub8l0fabT5UxIJnn9+mIebz99Hfu2395OC51O1zn22jZR4nQ9177KtwlulqDU89MlIt1s37Ztv6/r+fXtZsx6OvX15FuawWZeP1yZGxG7GYkUHvigW1Wg4COpo4R39WqVEDDMa3rs7mEeYeuyXH71fT/TD3/7w49fn//Jf//f3W/3//Vf/dufvnx5eQlu9Dag2pry7fWmTZn45XUzCnNyyP2+RTFeWaZFa0vpqkEUBYlMIAtBEAxIUxYgvOprTmempKl5UBLJExltFdEWaeel9S5Nszc6nfnDp+Xpeb08L6fLuS/CDSk+vVIEPCiEKT0JVSrWDjPDTJi5XihA5dEf1I4rgonb0pDhDPdZRV8gJcvio0ck+jGyYMoEaNrMCFYJO1z7BK6ZRrle04OYIuuf8dLejH3X1ooXVJStxwy7VgNV+HKmE4vZABXeDJnJhf4nJhbKsDkPjwKziMYcVV3Kg/VSnUEpdCIybOqyRBizch7jICTGNlilssBExM1FJBJlk86IWin3dSkyj1m6W3X1YGJCBUWLUB3vKAWjJD8Sv7kCtPHIRgyElWO0zTkgRHm0MsfsLvyA4ovEgShuEVEKopIbKbJm+lIza4uptXY/nFDHtL1moCJUljw+Bjo4/l8mFT4sYJ7gJDlYt7NGb4/INxauOXqEV1JAaUDlER1TOtVijOeR+JOZoEcT7G7AIaOMMKlMpWM+gmqTiai3BYeigIhgbgEwU9OeESqUILNZrO2mOqe5eW9cb767l2OPlVHi3sMQV5dllgi6VuCRwaUWyWCiVoOpTGW29GOG6PNgkeKAVQgXVlCCkB6i7O5CtfIdwGPfnkEsCJJFkxGOZNjwJAUR9zWIZX3qpzPrtZ2v2i+6Xoi79JPKwm1prYMZBQdnSRAnubsjnXLYntCwumKFsVCVDHuNPPscm2e2ZQUw5k4hRgSQtgbURKjIyUCS1CMwbYxxWpfTcvomP3h4Wnrafh+9szF//fHr0hcfk1WWpavq1x+/rudzhtv9bdwHizBwv9/G26vfb09PJyHL9H5a1nO/3W7KLWMHMYRjgpDMWkukTOLGEvry5WsycdOxTdGVFzlf+qQLiZzeXdbnK4giUpClj0yP6i2qQcwDgkPpWZvGdGTknGbHCDh7U+3njIiI8bI3bv/8X/yjX/7ql//pP/2Xv/j3fxGZBOxGn9+8opWe3j1FzO2+t65vLyPAzukRYwazgoSWbkGBqmOOPf8xJuUKNjkyofuiwhBCpgFMyZThht7bKm2bFu7EyaKXcxfO04mb4vtffvj03fXdd+/7ej2//wBmt03FYtwo78iRc3dEYlbaWPXZhIoB/OP7RcWjNiMWJimnkP0cxgIg0sMJ7EgSNp8iGiWgEA7zUpSzHPGJ9dJpE5uzaQcw567SAnA/kgNYK8iFpPg/QF+W+VDdZKBG8xUPZe5MJU4tjWaW37PmM6qtTlWR0sslEbkZ6luvkEThTDIbwurTtWuV7YWALknlEf4uUhEsTBzhGU6s5s6l+0biyLUnn05MeZCRsvY38cjTLXhnDS0yK1K48I5Savja9tdqB6CipCZZAeQJAB/amXrZa5FZMQk1hUbV2Vkw+dTaHxAT/I974EivuF0QmAv0TfSIyosMYT26qmoLqFYlB3sEB/GulLNU97KolGqKABxoi2ytTx9HtiI/EhGRxcOrv6zW6MxIOMBceNYIEFRLHVxe1uytRYHLS/FzuGhLZgoRDbcIrjuwCq6SRnsEMTWWTEhjtyAgKeWP6tcUJmZFppu11upTFhUmTMuIIGZpLXwc5q6EuT9QJNaWI+6GwOBARH3yiQp7TkSoqPYeZjhSyZhZfDo4y8AWIUmdlzNfTnJ+R+2sp6v2kyzn5XwmXtp6IlJtLZNJlZNBwqBpnknFuhojSukUJaYlERbWbvd7HX4izYf15ZR23+8u0s9P3ec+xwwHR5hHApFwdcxg4dqMjc2JMMcwx+3tjbl3VbB8/fI13Pa082UF8dLX3njpy/62ffOnv/j6+XNf9Pp8Fll6U21q9922+9wNM5SCG8eYp+dl7oOZl8sJlLfbm9+prX27WxIJrXX4n06XOXyY77Y/vXv68cvX8/UCjiUxdlufn8Yk0ZW0t6Y1zkuvV5dAVKNqRNahEGActpks9HsFSSkTMXFTZAyzme6ZY44gmNEffvfl//u//Iff/eY3iLxcLzFhmfcRi8S5BXOqcm+rhWwzthn13DsIyQR2zIiojHhRYXASbM5I0sa99YxNlZsKI5sqwUHF1se6NqewGCTEYBZkhqcLpTb58Ol6fl6vHz99+NM/a0+feH1K4jAnzBwb546457jl3G2/MYdtN9hgTiavsqUiMzkDcRQoJXWbNq2KpIr2TcThGQQRoaTb6SWUTHqcC2W7SYgeCBMzJy6tRzIJq8aYAObwakRY2IYdO2HI2EeNvAvfSmBVBiGsrFi1HCa36ulTmM289mQsSkRm0zNU9ZAJAUeopNUiE6W7XdYemdr0iNitw7ai5MEZXlTHqpVJJCOoPgpoOSqI2MyW3r2ifQnS+hyzCnYzq9s1yjFVlWWpfYBqUJAwm02bh1UDVEOeirFSKbFsRjgJFzLW3XG0RoXBqJBzDlSMIVTKMBipyuZGlYNUUy0+Gqr6rtyttRbuXZVruMUQITcX5cNWWh96UftVGKjvUrQkQChbWcWHhR9h9HVHFPtBhFTbmPuxFkIlEhfwD6KSD0VVehTFoAoUVTGPogDW1IiIkjIjmcvY5aqtfCDCzCJe7zmBmcL8WHF4jczi55VRZixtmWbIOAgecEQSc9os1UH1QxETh/U4RYRATJlEbZEMf2iMyW1SEtIDop3hQaIPi16SakwHVSKxuirpGl35fKV2bqd33C5tfaJ27uslqWlfWJv2TiQEUumo/xB7wmcQUUIKNx0Zk2ROs4oZCGCGemrQ0lcFI4jYVQi+heeyrGkcMftyznhLImbYNGR4TJSmVx4B1tOra/bt3vsSjvv9/vLlrbIkVbI35RBV7etpupnZ68tXZCpLIqeN+XVPz/W0uMjt8+/211cyR/j51OccTGidbe7LafU07Zrg86mTttttY5Xdps6AyPlyfnu5JeNyfXJkb+u+eRLoNt7/6S+1dWElcLW0bq5c0r1gcCQODJYnuGYFjxAFP+AwtYkS4khCePGaknX6/PHL69/+3e9937/9/jty9ymff3jZx3a3+ZSsqqAkjjFn720b+7q2EhzNoMgQasTgBn3Q/z2TIdIUSM4ctp0WrtMQBUIAwYMFEqmC9BTVGTEt53RI3m57rPQsi2ecnt9fP32vT7+g8weXE2uHB0VKM4qZvvNqPm582pCeyy3tnnbL3NzuFOYxKUHpVPuSir3OUgnmHINVa3XAXJrAmlokEWIGV5SYoGz2Dzo95ZF9UQUbWUXdqs45EgEibeLTKoGMhMccQkSkxFTbtRlBoFa5uxXlEBnuJeXsy2JzZsasPT+Tivpx8bO0XvQ8OQQ2TlyKmIKVVixgVmbivu0VzuoenOlmUXw3d0IhBmZfF09n0UcvlBkJjt6bZxCV2a2ONtrHFCUWjkBE2RLIplc4gUdElD6eS+Fd3H6bMzJqk2QPYDcR2RzEJKzhdpSbgDAX0Lf8B5GPXgTQ2qeyUJnoHl1e8gE9Pc7BLME/UTLVu9C6ZjpArbcMPwTU9U8/9gBcOONMPby+FV112CuIGQjmA4VWfDkgzefhNz7IoJwZoopIwsMwrAcnt7dedItj1xHlPfl5hFVLcIdkjfIQNe/KnxHWGVHZ1eFeWQ2Vdm3hVH8k03x/mCqPa6kYeBlovWfWr6+V/YtaUyr7nMVGZ2ZPMApU18MnEVjULeAZkQWszRSbnGhOTLrQefV2btd30dZlfaK+aFvbehLuoo2grMrSQSysnmBWi3ST9AjKEhYhKsG1aFUx3Ee1eO5M6GEtwUlJLNw5Y1mfwnYKSUtOo2DbjNBt3oklwpPAnUvxIQcvL/fbPQPmNsYkVjA9PZ1/87c/jjG/fn0rFLDN+MPv/nC9noOc7gdbxj36onP6sp56b4RDB2b3O2zm2EVIOEHm+8a9MS9wv90HKbfWLcAqY5un04lb412YMc2Xfnr6+OF237UzwntfMpW5b3ss++DmbmkW2qj664hkImlKpRLjOGBYxEzk6R6RlCxMQnNMynRPcztItCCBckzh3mnBjv1Ob1/HmK8R2Gd40m68k1pGOFT67e1tGMw8iB3Zu/ruTCDsYVNERVRIh+3k6Vm9L1gopoeScWK6MNhDlLuyEKnA3AUYYyfV3gQsoFRpl4u2vq6X99mfv45md+m9ZSqFMLXOQul9IYQnTNSRI8LUJ1HEvFPs2N8oR9hb2ubjFfPGcMRMd2IPNxFqSyvPP4vY9ExCRNWqReDhR3p4hlWkHbOGGYjrM34wFsXNivkr3IpUS4cdDJkljGbkI851hh8afxNlDxeukRUTyC0cE5SV8FNSTpuWQJhra+FWtiwWLqBvZoDyAE1WUG4ccSnCkpRuLtrcvWmDYNbE383NaipFIFKq0QVFvaMc7lX4RUbT1dOR2brWBBvhpa2gJBZyTz6GJfXLHtMRZOUqcuVchYUQQYQSjgdEp3DLVO0LIus/NYsiSlBFk2fW6L5G0kXMAzHX/jXSWUiY3A+de32ypJwRPo2l3M+eeeQn1PFPlIeqq0DmBD+IfdVkV1FfVHHKKGdZHIh/4azMh4xjUBXBj1SzBwqDIqKEU7Vjcas7IIAj6L1WQAAhHwEazBkVMJ3MsDF/zqzHIzmgPgJi8cMKR8XNB7JAKGHO4OpFwv3IIWgVJZ/1GOEAORnVLoQzYlKgNFERnkRJ6eGpFdjE00HrObXL6VnXqy4XPV+5XcAr95XbSVonFpYmLLU/1/J0sXhmBCdoGhXyzYM8A5QWUYEjM3JH7Ga72TQTIM0W5iuHCgk1qsVcAuQkAIJYiMVmJgnSSYTC02r9QQSKrIIl3alch8zaOsxj3HdTPH24fv7h6/m67vvoKk1FlK7XcwnLji83836792YVaVT95csPnymckdfnS4xxf3lVWc/XC5DmlkdYr27b5NaZ0Ja2D49t59bncJH+9nZ3y+0+uvdtDqWlnVfPHNP2ORvxzBBloJLRpGZBx/8Vek+AA6jGNcerRtjdQTTcbBoRRcI8wj3iOKQIeLq+//TNp68//WS3r/f7fvs6uC2m8psf97Hlh7PG2LqqDwPSZxGwIMQePqcrCzGFObditUe4cEXmIcAZYA+wkO/jfD4T4AZR8gwk9d7IcbeokSyQ69rXtV+envvleWYffLHB8WXLztpIVReRRqIOYe7cu4AQDelmRO42KS1PO6VJ2txfNe6xv8JvMd/g97A7ywDNMUfNY4tawyyR4R4ZYG41rxbNTD/O00RNXwtfxXJweDwcTJGVuh61lhdVysd41Z2E3KJYx5EpcvgJmIWFqtwDAwgt3++RsUpr7/sclTmMTCZRVrN5LI8jHTX8CCIJ/6P0qLYUtbAQlYQTFYAPRBzuSGq9UuyLoRDhGVxO/Mp6OoSDYe5hRy0JShy6fhFOr5ASeTRXycyRVZHDZyU/Z2RWaZuZTOJurGq7lTCd6MgjKw8xsaRbFiA6qLxmdR9ozZyoBJVIZGFESo1TJJw/fgRV10ulcFWhf9wsAMDCZXUhERYgDhptWaR+XnODIFU+HziNw7MeD3ULC88i6cfRl9UMqhjR5VIBMixwLBii1gesbMPqX5UeLMQPbFM9N8SMsh4XrZAejE8CkpiFCMwwT2IJt9IX1aRSVNyPpLoaKx5WAEb5SBOHm+j4U0kZFB6NW6QxsowYmRaRpGxO3M6hS3/+qMtFnz/Ick5ZdDmRdFlP7tz7KUlA3FuPRIUfELEDSVS4R4tS6vKMmAEPC4/7vgdhTIMoCLdt3nwOc3NXpqd1WZPOKkvrq/gqHSmcuwTMNiEGCUlry5I+jvqqPk+WbbvXGI2AYM6M3vl0Xre33cMAEqLWl7HbnKFdnvrp++svXj5/ZjnQ7ffb27qswryufez76dyfP75TVQof9x3aRHjfN2UOtzRX4oiYYy7n1tpiY3iEDW/LiYnnPqUvy6nt2+4zprvnXNcOFtVtTBOS231vhLYk6WlaQnU5XWTpaSCV8DgQAu5ZOgp+3AeV+xRVzGTFw9qcHpmgOe3QjzOHGTGH2Xpef/nffM8n+v6X3/zVX/7Fv/uX/6p34dZUOtPYx8hze3rSGl6C0+6moGmHkqG3xSOFRJpaGXlZpIIqpfKRmII8E+a9tzlsXXqCPEGsgRxmgJRSM0BKJI2XdW2nUz9d5Px0m273Yd5y7tmIMda+tEaN0IRPTVdhBTURUlZmbkEUC9LG9BisT0xGc0cOtg1+9/0l99eYL5FvmSNzJ3NkXZ+VkEFMECkdaFLWnJ0ysqAvcww50EA1zciS+SXocVBSVD1XQ24vFIT8zMZo2uecE8ZC4cFKZeUT4QjLyIisbLTdhkf4NG0UHpaDSeoIo2NoW6QHeeiJCEzI8PBS1leJKcr+yA8sZpfNWcAHSswR2lpJRh4m0eO0LE5aZpIQJbvN4j0ckrM6cMIfjqC6GwoxDRbx8mazxAExBUv9tAe2MorYr/KwPjAiKqQrj389ZWZGMtWavzqCUkMSMqIgrqXLLLG/mx8SOVBlvJWxrfbNrZe6KEFUEv4MVGpYfabH+MWp0nm0ac30M52gmd60uTuQHln+HHc7/uThj3oYCETCAwwRIWazgcOfSza9gsPqXiBCrUfqFmVOtwN7QQxhLtHxEeKcGRGo8DqtgBphImqtRm8AiKXShpOyQOfCWjOxRwQrMhO1lYgU1orXJKhlhsPM9dxTWv/w7Xp6ltMHXZ9JL7qcqC3QxRPa2/HaN4Uos7inBfvPtuPEtAikRzrYkWPmTNwsds9tGzFtzO22j23bgg6wl2XsHr3p+8tJgdPSyavYR1J95jp8NlF3pwB7Hs4b7enm0y1meii3St9MD25c7+K2jeW82pxzjgi01pg0/L50YVWErademJpwW0/L09NpWdrpsu63W1sbESRTe5/bvL292m1HuEbmfbx7/257vZmN3nqYzdjcPTJ1XaW3fjpFIDwZWNbzfd9yxLZv6yL7thNou2/91FlqHQpdF0T29VS01EjAa50DLtdnEQVw9LOH7aN05WA3qxA3yfR0qrw9AsIBBGHO0Tq/e7eYP7/++IWi3CGj6VJX9Z5x27ZFtDddT6uuyNzvMyG8DxPW9BQR0R4W4VljiiK04HAnZmQKIUFjhhAFhhCXzCKSCPx6t9ZbRLJFv/bWWzst/XQibQ4K5D73l5vPtg7e1vVEm7HgvHYV6sMW4kW5KZ9VxHNtKoBQoAvnoktQWup0d17MbUfb5DIpdow3xJbzNba3mPe0G8VOcFBYmGhzcyVilbA/siMzQrThALewKnk1DpFpJqrEnBSR2UQqEUFVzV2EmLnmcGaWlKptziEqNoxFmOSQhCpLkqfXehIZfekZsfS2jVF/MDLDgkUQYdNVm8/JXKdHMYwTCU5i1Xr3iOCeTGitZ/HHAAaRMnmdfpXCNFkOK2hGMqVKK/sqEanodA8LacokDgdlOh3DH/bMKrwLw4ym4se6sOrnWnGzzykiSaipMhcnhMunlcWbS09WfiCMkoDSeSUzxc8XVR2Fh9Oq3oSicpeSqZ49qCg400oXFcQ47GUZx/FbBffDEHjo5IkiwszK8StNIr2pmhtKwEMgVCICEN60W9RE+5ERlhUzmVSoA1D4EaubgXQQk7IAmHMc0fAgJNyT5WDS2YzQUtqlyHHNklCJoB76fQSy8SMTFdlYyxCb07Q3ZHqGcKvpGWVWCFHN06x0Z4DfI4SDu17f98vz+f0HubzjdpH1IrpIW4jUQcyazIu2DCRRKRoscs4M0JyW4OnuSTNin3F3H+b3aZa5W953u5tvu9k27L572Njf7vcN6edGvQkpTfDlenn//np6WhaWC/PCSLOkZCURYVrI020yc4w4pF2Rc5q05jPGbfc5iKR19cy8z+qKWoP7FjHDYw5TTYClc1iKkpub7UzalxVB2pbWJdNtjrb09byE2dvrG3AL9+31FRmN4OFfvvxosdnM3sVSMhAZbV3L5s3EPpOlZ6QHhKi3NoePbXgM8wmkDSOl1vj8/ORoen2+vPumrU+IBkgyZ+W+5R+HQKW3P+atRTxGiaY9S/ycIKGWkipAFCswKUnQTn3cb2PuiTDf/uYv/ktvhOsS6RY7ErvT26DTmimI8Dmzre1mc5r1Rfebl+xo7Pdw1CA3Egh4uGh9GxZAhp3W07IspcyTJkRkXjAfbk1ISJmZIR0eTl1c4MwsLMKNOOHbmDfE6+6kLZC97yTo2pRoXRZKX1VPKpdFmXIRbYxVREECWnShnohINT3lHJtExjrDZthdr5H7G+fI8UK5x3ihuLtvKbuF59iFhCp9F1FTi5qQuHsw0jPBlFSObi/nJhXFnrlRRAgLgec0IeaaTot4poiIaI2yAYQZwNXNF3cB+XCQJgx19MHDGMhKICdWgdk8bl2LQtozl+slD3Ncggh96XNOr6xWlmMYVTs2lkAIEo9gyHrGzCtlDKp9zFlnJVcwfZRVKcueVlNBAqrbiER1+yCYWZPOwuaOzLSkRwCDpdEhk3nI548pDAqUGREex05FqyuJckhXRvkRVErVGgMIP4aJNcFn5rGP9bSUFvPgcDCIajYFfrBwDpuxHO5iIph5a1pK2N5bZMVYAwFhbqLDZhZ6hgBKcFIcI7bW1T2OHAakHx50gVAio5gbwsNmIQ9FpaLdQBA61ug1kxJlIQmiGkY9nMVIJCdByMdUlQS5GQkpuDBMdXlK6/lo6uhYsYCFWamYruY2PVmZTlfu5+X54/L8Sfpzv76XdublxLrWYqgmCACainnCYQF3zERSzMRmvluM6ZbY59z2GBH3zW8+X163bQ6bBtZ9t+22b2NIhniYh9uOhI837pkLJxMvy/L83KEy43Razr0t6Z11bSnwLgcJkFXZGUu3MWzOeX8Lt7m92r7f3+7wrAdDWuurBMjGvm87s4gSM7XWItP2OYYR4T62fduYMlUQRpSZNvZxWldCMtP97W42VRoTunZ9d0mzz7/9Iadd3z3db29La+v5HJ7TttP1moS+rmNEgG039+hrlxQzo6Tnd08i+pvf/KGvCwjLad3Hdnq+mMVyPbf1en3//eXdN7pcxm6iUmPZwpIUVKvmevTQED+0aImsLLgMP9YQoHSHzRnhJQoUBhdCXfjDh+f/3f/4L3784Q9/9+u/+91vfkQmS0u0z297BK6n7EJEvN12JjktLRN87plk4URCkSREIrXIKWVSuK/rgghuLcDTnJlWUY/cx2xNivTETUuupgphXN6dtKuo9uvKSvWUvltaGG3DZ2KbO6nebHOEamdmvd2JoSTXpV+aMmeXpsyX3iXzrLoylLKJEHGjpNY9PNNFM908ZsgTEHSeFhN2p9jYb7m/wd5i/0qwsLuNgTQiKngwBMmZnohkltr5cQmbAjYnqSBMWwuGlio9OSlBnGYpKDylWZl4g+jwWFFKxRWrNnPPKG9Xus+l98CxqSDWjDSUmI1ByAwRApPPjExlmeFMRCCPEOZpg0Ti4ViuKLYHJZPgFbILEGVmkTizJhaO4XsV7ER8bAgPlgYyEsS7zapFPdLNW2seRaGPxspM7o6Iio0iUGT4zBqXFAql1MuFFAPIf25TiJglzLXgrsIUB2vzZxVQxQ6BDhSl1P/qZlBZlgZURDClh08LxqF4KQMlKnStLHnEQkxcRo+6JiKSNSkPH6EwJ3KmiVbaO2XFqpV6yUJV5zyS1WrRS4BKS8ShUSo+XaaIUDnCM4k53VsvAi1JkeBYi6mUQJHzSJj0gVgCi2ctjo40sepgskzaLMevI26jtY6MUg26ZxH8zVmf3i3Xb5Z3n9rTRz0/9fNVT2dCp9aIeyTXdMiTbKaIWsAN5jnMjXifMQMz7HXM1/u4jXi7b2P6/XbbNt+Hj91n4H6/j/uNMlpf0tNHDLd1XaRcG2gUvpAsGT2EVDuvejfdttPldCFm9yayKnUOipR6BlmCJElZV4oZ0vv1+fb6k5G4Z0Tc37aurdCmc5Sbh/rSmMhm5XCDSRFGTLbvom3tLdxqItqXLgJGVMy3m7NQE1nWxcc0G0y5j12QENnu27ou6/ky9u389NRPjVgget/G5d03+8yx3/f7vtn+/YdPO3jMfWyTRK9PlzEcAlF6d71m0txnW2NZl/Pz5fJ0EabeOxEhEIGstRBAKghUnklN+epqL/VBNZPapGrGCLfwasVVGoVvvhcZhTgv759+SX+vLyfp/fOPb/f7Pt0R3Khv+xSEnJoqLadlDN/uxqJElYGec84m3R8ahCwDbEJYPcBgB+ToRxCUAFSF5PAdqUokls7rSfva1vO6nFdqHcRpSRonpUVJM7jx1xkWEek2bE+bae5OpI5QaV9EGS5Ky+mkzF3bAj5pOzdZmBaVJliaKKUShKgL7cFMvZ+XcHMbbi7LxcfWmpMMhed4S980bnb/SfLmcxNM8zFsSNWLwqLkswCRhzmLm6IG6G6l5XR30ZaJiCQRd5fWsiSWkeUjziNZ5ChUx5y1k/Ao1v8DZ6Diflig87j4EVGGW09LkHCJKZlZxEbZBIIf2sdqGIuPAJa6Y6o2jvBqPDKzIuszc/rkUqOGk/CBrHBnIiiHOw5rLTIdkURkbsQiLNOTGVHsy4fnqpadwmyWpVcKxLGwrJltzYU8HiodhoRWcGU5g2tLc8ih8o8xaURQ1ekTGa33+j4yI8xIuHxglcQGRFM1n6xaN1KpZTJBTVAjImIgChHaeweQ6WAuvzsYWsQVOnhsVYXVkU1MAIc9ZFvMh0+rbr0CFgEk7DZUe2YEUSKru0wQuBRTXPyGaXbceYiIVFZiQQbSmUXkMRUDcYO7CwWEfQR3btrdnUTGcCcEIMtl+fBuuXxz+fSLtn5cnt5BV1lO1BuTgsgjoRoTkbnPSJJUNUtPGiMs87bnFnXo+7bb19v2tu0vb9v95T7vc399C4NPQgizTN8UE7G3tsOJ3ZVIFYv2GSLBGd6hp0brslw+Xr795tP76/W7T9fnVZ+bnlmuTRuykTMxYig3JkolhM7tBdT7svqM1s+qqixL703vYcbgCNhwd2tLmzMjzC1AiEa2v9qcY8yuCnJhfn73RIQmIk2KbjDH7uSJXC8nItrvd0RurzeElQe3SLTE6mZggKGtzzkB0uv19OkXFz7Hr39r/of9/tPf/ebXyA7RtjTffekraczXjVnAHdq5rWh933Ybm+27Xt6xEJIK4Zh5+MhxwPQPaPYxdvRMlBiBK6Dcj8c4ax2XHkxpnpSYw/ZtmsED08I8trd5vT7N3TIORLOeVsvhgI+pLIR8unYvzuecq7Ks3dwpiEkC0bXhoWpnEqQXFsLMUV0kI4AajoMSGapMRNpbPy26rNxXkuaEJsSETjAfz11UU5FE8WLGRO4hPimB9G0MJzOQIZIc+ibES186SRPVxNLktLTzqTfmxnRdWhcopSp35jGSIcglYCBka2MOkbMjaXkCRdo95GPExnGb8yXtLecraaaPzJyzNHUR7pQELgI3PI2JRNSzWK2TRMviG46gZBBYalnIYD8YyVkHzhHxFA8BSx0IWfx5NvMHAYRqE+wlAADKgqtNkDmHCbOIHIBTr+D1JKBCxIQphcKdC6IbVJPDOETnlYXJzDTnrCDfI4VIJB+k6EJl+CxUMDtymnVWc0PxqUC1RTgQSISiUlDpVn2i1lqRqswsAX/sR4HEnBNILf5quGvjzBBiCCOjPF8lgMuDAcdZ2g9UrkKIcFOZcxwhWZnMfER0RrBKepR0tgbqcrxSBwS1eoIyChw6GlAGW0alrIGTk1gIkuwPlilQGxWWI2+zwilBcDdtmnHkrh3NDnN4lKKpELvjELBGslDUkxB8TOGAtAr1JgSLFtfJM1rr5NSYKWGUiByImIjOIe/06f3p3cfLx0/t+l6X5+VyVTnJ2pNbMhPznO4BS/iUfZg5duMtMinv06fj9bZtw76+vL7u4+X19va6+xj31/v9Zffpfh+ciGFJTbgxCVTgzprkc+ncz4vvOabL3JeVu9DNhoQ/X9o3ny7v312//cX7d9fzta/vTu3EeH9qK4uEK5FSJW82ZGY4mmbFS+gKbERtTpmWFjHNx5hzTLcwi3pM+2LaZN/3/e0mqsJoXc+Xk4gsS1t637Yt05mJRUTSZ9gc2oQINn1ut8yk4MvlxOtqYx9vb0f7KBCVtijLEgEwB9gyluRtzMtl/fbv//3X//VHe6XbfWts2x66Ls9PT9Pcwef37+TVPn9968995nI9PcHVQ5OaBzLqAI+iLjJzDfeZUWqEo2A74BBciLGykmZmKXQAiiC3iIh5MI1JpIn4y+vrv/k3//43f/N3n3/6fH+5cdNa7TlFMHdpu/uqko7WVFkk4GCKA/vS27qPMS0SBBgf2SxxBNsmKNGUM7FvQ5VVhRnFnAEnUa7XRTq30yLrEixEPEatl2L6EFFpgbCOVEcDp9sFksRhIzL0yDUyB/ZwIkNiysQhkiEWaY1718Z8av286Kq89nbubVVaWBjeibqKT1dRUZk2CanaZwa4Rz8xHHHXPtPfBHefL2J3ty1tAyJyEoSVYo62tIgUpkJxEHEtXOtVd5/ErMJmFhZ9XdzTy6CLuhOBSu6t1U5hXVD7lWR+cOWQIsXlP9LQIh4hBCJuyQIVjVnpbAzicK/mw+HpSSA/5BrsxcqvHwCMgwidERnhzP3Y2CLNjUXSPEGVSgIAWRrW0jtSq1Qvt8pvC8ukYogVjYaJk0BRlW5dk+7mlslEsxAXmSD+mcWUGkfkfPm2EJn4mcwjVIYpj8oJAInkg4Om0jJizqlNqWyrylRwVKrIaK/9qpuLSg12aglcwbmlAKAsKH4JdcpODCYmoaJDHVJ2PgK/at9KQOVLHAU68FhTVx8SlQUqWvsHivBSGlRpAM/WtZYfREhP6pXQS6wECxLxTHcrEidVEA+QTDbMKZMQ0rGcl49/ev70J/3ysV2urZ90vYgu2lpCylXhnm65G43Imbybj+DbbrcR92F74PU+b7t9/fJ6e7nfXm+3t9vt7T7fBnn6PnOkQHJGKqk2yzR4U0pE+J5EETZ2b537Kumeu2XfScdV0Bd+d+nfvbv8ybdPn96dzqueGl0XOmt7WlQy2Ykpe60Lj0mrmU3EpDC3e8a0uc1xe/npp/v99vbl9ccfvmTQvvl+HyXMOJ2W1ogFrUvrrTc+ndfedDkvlCEEaScml0aUcLPMJNEy7rWlF/dcRbb73caYt1uGMQOU2rqoaO9upl3nNGl9n/7ly48vn+/vP84Pv/yzj9/86esPnyP4ddvNiZ36amahy3p6+iALrJ1PHz6iXXn5xMs5ZOVlFVXCQw+HYJZalWVmLdXooIAwE8wLx5rFHC6U03D3iDFtTnP3fd/dbLpFZlt0GImqAZ9/2veNbEonEQAc4ABjuiEZ5Sr3YFR/SalZKXJl1FGu9/lo4UWFSTK9hOoBovTWyl18LKUOO30Tgvf1JE2Su7GmLgmaSRzke2pLzLkHBpCJzuLOyRwgMKVDVPd9MklGNBIgwzPMqrKaVf0qM4ORvbV1kc7SVd6fz4vKuTWlvK5tFV4adwJ5wNBUMhETys2JGiOoBVvQquzgd23ZY76J3yzvaa9kO+CkahnwJFIAGRDlqAT1CBZBTWXdAUiTOQfRkZfLqmGWkRXgQcReoeegAuMkUdm/wZSJ6QYQMxNVJC9FJKn+7Mid038m0OE4csimsQhznecFpIEhRVSq0sUhbXHz2sUXGiYfZq/MQ4Babal7KIu7FTtOWMy8tda0zekkqKTfw7x2DP/ocCEQuUcexXFm/Txlp2gFGPYaaGoN7hElY+PqTR5jj0N2wz83xbWSCGcRP6RBRc0Hq5hN7o2OZWq6pQixcgWksEhlrB+DIKB2EeVBKKlnKW0J5JG12DjcaUw/xw+VWqkGXtXF1zVx+N6I3FOlTM9KBHdnJhL1CEZqa1QmNwtUnigSDJtTRInBQh4ApXIDeSlpu/bImZnmEcQhl/bpF5dv/uz07rvT88d+upAspI1VQYIkS0rQPgPkwzGTBui2xd38bfPN8/U+vt73z59f75u9vt7ut/3+5c3ebvO++b7BACNhFWgGMQJIMiScAilI87EbaT5/9/75+Xm8fb7tr1sQbLITmT4/nT98er6u1/fv33334ekX787v3l2eFr107cKc1EU4CRmMg0FzUHFSmSOnDkDByRwijXVdTumZJ8yLv369UwSCw7ytnUX7Ik/vzsSuSufzsixq0xK+dJ1zgGnOctRnEyWi8BmJTGOVOaqGwP622z6frk9m27zdqbca4y7Xs+87hGFEqtjzy2+/gMYPv7k9/eHz0/VyuXwc48fL6XJ/ve/Df//bHz99982ynoP6+d3TL/6bf9afPk7vaKcg/viLP1+Xa9eVUBEoXHF2ABeYrHIdApkeRIhyvSFQy4BqK0WFYve573PYyAf3KQEPK0li6/27b7798dsvX3//0rxPuwtTRPRVmiQTU9K2z9606ummYKaFembOlhmsRLdtLsIzExBPh1mksRIFRLkJE0GFw0bRUFrTiKIIZFu0X3o7r7Quod2Sp1WmYM4E2YHYT9BMOEexSDczQCwtLOqToQwm9hkUgcCwXbnVsbJvmzAN35s0JTQRzvhpWZrQ87urcF7W5SR6WdulNUVeets2W5ogYUkqfZJHJKty6zNc5DTSqA23jeLWsGO85Hyz+aqxF5yfWPlgpXFEtr6MMZBpcAJHGAtVvpJPY5Ywq6l3Ye8T5TNFjfEOdAERhBu3GTHnFOE5jaMsSMxCkRlgTlRUQPjEsbnNMBPhmhYcI4pjbnlU3BbGzEVwHua1Hw6LGjvadFENpNm2tKuH1QaCmMecbWlUsfEeTZUJ7nFI7WtTfRzMnOGFbDvsvMSZMJvFViaQ+UHcqzK9WB0aGRTQQzhVfy8B6W4Mop9J0Yext3A9UoUGjikLRSYimzZUT4VKqzxWKMXX/tlNR4f5vog+qPgeSooIba38I6KluG+RkW4kh4cKRKriZsJcN1hkFDupeigSKH6GkASkfHOI6az1WOThQ66tWgVhpVfeAhHCHYicoEYimuzEPObGHQ5Ff37681+1659dvvslt2tfTiztwAkxJUtYJjDMHOKQab4bbTO2wG2fL1t8/bq93LbXt+3L6+3rjy/319v2ss3b8LeNpnEl0YsgiJVIjulDZPLhZ0amt9b1JBam0j48Xdd3yw8vP/3w8vK63bBbi2dZ+f2H0/cfP3z6+PHPv//4zeWswElIMyUPNzyzRCRXb5yB5Egwa6Y7tK0XVop08bler5lwT595vjqFUN6JeM5Y1tZ7O19O1+tp2956VyKEW+sijH3fiRDmwhI2iah1FW0IgNGXBkoVWtcmQTlmV7Gxh1tTJtHSzpvNvjbtfd9LxhySOiyn+csfvtrd1qV9/PbPVFPaV//yEyvzqaMztXa6Pj99+n5594vAEtwieTm9J+iBXsch8iE97JpEAElmMAAVCsTjBSOKCpJCRCLKJbNv+5jmZp6R7DZngpKw3bfLZf3zf/CLsW3/8fY3Cn756d4XZlZWoDwdLCqahfalBDgpar3DjdLL3RiRCMjuaQ94GTdiBkultDLC11NHJlO6TVKOjGXtrTNYeFm8LabLZDU0Dw5Iko6AjcpMT2KyQCFyhbT+SiJBxnSjoo8CmRCSJgsStXtWbpzomTCO9Pvchej+asL4/OXOEuvptDZ+Pp/PvZ2Xdu39tKiO6MpNiNOZggMSpKqAUEq4NO4sJ+InS8t2E75Juyle5/Y17QUZiFDKOsJs2mHoBIhSVNMzGUxUB0gkEFHCT21azQAq9jgy4SKtbLHTjfjAybFKffERyY6Sn/iBEoisOKm+uk1RdQ9lHPBO5jjmPUHgGgpVXgKxsFNahKaoHjzRphHBLMytOKORodxj2oHdq0UuAxU9Rkd2hTA9ok0q/bF6i3qMD6tXQeXc5mFTr5Uqs/AReaI1+oxK9YyDg3agUQqBUAMZJpRm9n+jOgJAWv89C0seR0kl0LuIsFD4UTSJcFixtDgr/6tSi4+f97BzZbFP6/LIqFzish0TMTFKcpsE1WOZU/saEQaR+7G/jai9yiGgO2itx/gyEyiI2yHhjOBWA9YgIWWh+lXDI4yEoP303Z/Ih18t7/70/Olb1TP1JdFSKEmmexO1zDTMGdPTIMNzz7gPf9vjtttti6+vt8+v++cfv95f77evL7ev2/b1NfZBM3K4sPChd2YCExhWdRpSFJUVF0mNCGTh1+tz5P729vX1RT98t37TP3oXSd9100WQNG7bF/q8iuDDRZcuLAeQHQetD5QiIE8AngGC9o6E7xbaMiNy8nKOcc9Qbqq95RE3FqpC+6gEwUS62cvX19Z53/d9j6Vr66ry6A8J99vtejlF4r7twO7hFn5VaqoE3u4jR+zbHPvOPte1e4Iy+tIX1YgEUT8tkLy97W1Zl97HBnNjJ5ugyxkqM4eeTr94WvexO4Ux936i5Up6Wi7vSS/JyizaFyXOOIq1KuKIBD9LeiNIyLOCYamWcj8z/ogZhIjY7XB6mNsYw90THuSRERStq1m2pqd1eX63vMQNruF7IunIR8rafXFi2Dwtbc6JGQO59k6g1hoJRJZtTmIiI5BMDxZRlcxSwbG79d4J6WEJCZ+903Lu4ORKGSMZ4GAx6bvTTDJPcBqxJR2um5kqPDcPpIMrn8Pc991qpry7qzYm9nAgHWBQUBSvQJKQnsGc7JEMHrvJjBnz/joR8fn01pqe12Xpej2vS9PT2k5NlLAon5uyu7gIEwVTimVycm9rhoesws+c+4yN+lfV19h/WmS3ecMczFq3Uy0l6DhkuA66DK+vjBJEkmHmFsdOliplHpBAEsg9WDkzM9MjVYTAc+zLukR91cU0yzyGz5k2JitFVHowHYpMQtgkCKhO+VlUtFq11vycDvjz4XpCPowozDGHaptjiHQ/skzhZiJaewEQJUJY/OA/H2IEKnhnHcHhOJagdHgbCQVdrtiLQ80spBTHbVl7ZDNnrrM1auBe5zMeA9NKcy7Ifp2jzEJAUh5gCybi5OIjZggzGEdwCj1KmgwiYuUoi3ZZkR0FU63tB7FGOgKqCiohDoVb1WwZEY6siJyIcsGIcnqUsuq4tBJUoTRxyDqE2LJU21R+OiJq0ogCYeWfBjIJHuzp3Pv66fvzd39v/fTny/vvSS+iGk6ZRMLhSQpm2YZZkGea033EDJqJ12FfbvvLfXx9G18+316+3r5+fn37/DZf3uxti33mMI6stDOiYwfFIkeSNFcEIFC4JCQpx1G02pcvf5BO43bfvnxe6PtP33/8/uM316Zz3E+NPz2dP5x7E7+//PC7v/N4fXd9erquq/RFRIQhXEdZopbwfHi/QcKtiUQOF9EQXtbVOHzf2iLESRTI8HlES485Idgt++kMcNc1Y1JSzhi7nS5LiTVYZZ/mkZTUuxLR9XwWgm33mM5Ma1v4tGT4tDltIqPLEpm69kyTpu5Zzz4hxhyvL7t5J8W0+9u+ffr2vWq7nN/JKi3m2+3GyxOvT3J67pcPrV8gq+hC4DrrH9sjFKc3C/cYxymfWQVbhkdYpMfhDeNKv4kx5jQbNqbPzfaxb0nZCi4UPrzoZrZoPzV9927ZX9ma1EN4WIuBpeuhEQGbuwgTk1vMsCZ9+sykSGeCZfTePRIEaa1QxmbT3bgSw8OZaNv306JEJMrL+aSXc2oLWRxtOO+Uzm1ALeGJzKDDelKqHzjIM2dNnTPNkpklxcmbUHpUBK4XXx3IDIuUw9nhxKnoETsRhCQtcsLSmfLldiOhl34jxmld26KXy+nU5dzaeWmnRZvQuoIBJWrM7NmF9z0ppe478sXoBFkXfcf6fuYb8Qvm54TZvBdiRygws1BmGemHYB9aOQTkAUceK8Qj2PwQ3SOzaGDI8N57zpGRkQd3+gGKKCI/eeai3eClEIvMCjypdTSkCs2jZCjl6DH8kAoZycw4pL3Iklk+jMYIJB/FcJSQX5ra2Kpw8wgRFtXiQEQEg0q6xMCR+XHAjIOloi4gXO4ERJqwugcSIpweCqolcLGvjoSTRBKRMFVI2yO7PEgKnENHgDaSAg5vJdiKZIGAkQhksf+o9otAhh/hcHJ4ygpIeay788DfH+5T1DQ/mGWOURnQeMyQcLDnmMjdjTJZlVOES6AdYCah8Ex3BgtTCTotPBFHY4AEAxbMkm6YTpmcZBat6T6d16f1wy8uf/oPrp/+9PTxG27XBO+7Re2gkTEmEQ/PZLLATLzep6XsM9+28XobX972n76+vrzdv/70+vbldXt5vX95tdugaZwkVk9VPR01psvyfJIkiNInmJAUkpXHXfFbIpGTtJM2guL++vmv/nJ3zl/+6pd/8s2vCEN9X1Q+Pp1aE0yLiDHG69fbSk16KpEQi/wR5kQEKTcsYJ4JsKg2be0kmJLzPrdlWef9drme5jbwjjxCmtxuOwlRYr+PvvRs0ruKtnTj1vq5Ia1Iadt9I+T5dNVGItyUmGH3vS9KzDbnfbvF9N5bTBOlDAehNXY3Vk6urGMwwbddmyBy3HaSDSz2Oj9882HMTM6n61Nb2zcf5PU+M1fVC1G7362teNh7g4mOsVeVLKhNDwXC3SvugpRsekaGR/pDjRABQgJeAUteYRt22+4sxLIWmiUd7hGIt5ev56fTN99+/PzD56+fDxuxT5POfWm19hJij1AWIgiTNnabAAEybFqECLvlhIWFZ7BqIufc02sqWCNQjkxVBRML+rr0pxOvK/ppQCdkh2yhk2CZkWyRheUVquhpWFqAbHoSecFjQUwyfYZngI8XnOAR7s4gET68ngGA0tKxRySXkckChuL/MnGYz809bMgO4PPSVXC9npZF1/OyCF+vp6Vpb3JaGiPWzgJqQjLRGyiEEizk1IUX8Ysuz6IfOe99ufv9C9GYc1eFmzEXUOBhrAqHsM9BzEeaDao1TWQ2Xb0C2t1LSuIHbx5wRGRTTsYcxkzJQhlN2zY3YU07RMPS1OPR0oEr8BLgQPBj7E5MP5tey1NarQYRWJijhNxOYJvWes/I+i0yovyqc5qKesEXIikrzhSVJ1O9jkAyE0lHGi6ph1WADDNTHnOaAJqo2VRUVhqRW9QlchheHsFYooXWjrJ94ZHensVGBgFp5iKsIglnKY/cIfmP9KhWkwgJVYnKFUivSPtyomXi54nNEayYwWBPryXyEYkLKhaHm9Wgv5T6R1ZnBgunW/n0Sh6Qh0vLKYuTBxIt7dgcsxERoneBGal4ZiCmLP2bP/vwD/7x8vzL5f2n5IbWBzjnTJExB4syawp5Yp8ZRPfhW/J90OttvL7tX17H68v9x5++fv388vb17fbj1/n6RtNiGCNhqPjjav7csi6+IngQ4rAcMeFIpqdqLRMp0lgpfI773k7L6elE880sf/+HL+vT9dv33/7Jh499Ico4db0ubSHNwKm1JnI5Lb11VeXj24jqOktBHJ5AStHoE0lKadq6T+1taQJOEyA97n0Pt9ttP0HHcPcQybkPQbdpzEmZGRhj+txFmDiFcT5fmipRNuZ9H6oBx9xnP3XhLtPfbpNV+vmkFHOadDHPvpC0nh6WZtNKdXd9/7y5hO5jzlPvyPyb//r7D99+2ynjdayxXL/54GRvb7Zv+9cvr7LiDGkzqC1lOTzwKmBiqGp4RGad5wkeGNgzK/Tt0SyYm5tVLnwgzH23IQtj0IjRSCPBxBZmcwIw9750Jvn65aWkdizClK2otAklUpVwl4rHyLBiPTIn85wRRNMyKCFq08qJNIcRcwRIxNPnsL70uU/tRfLKvi5H4kdfclmT+068A1tksFhUcHTlVxODPMk83K0WIBGV64HiWxSb4CgUasSQyTV3MO+kw62SjqQOK4vqDrIk2qCYTsSoUzXIN2PiebuPmPtPb9xUl3Y+6/Xpsi59Xft6WnrXpUdjaUznRV/2qUATdOkEUnBDCz5T3jjOQZP7VbCRv5jfMrfMApq1DBMVD48ZxFIuUXcnYcDcjFjdvXCYpcWJOOYmGTVzpv8/VX/WLElypQliZ1M1c/e7R0RGrkBiaRSqqncOp6eH5AiFFL7wT/Bv8pkiI8KWfiCFPVPNrq5qIAHkEsvdr7ubqZ6FD0c9spgviUBE3LyLmeo539p1HN+BGOZUeF0XlpLJNoFea12XpdTJNNKBbD7iBTFv2VLSUpBZWCJ1ANsYeXznSC5c8qxkltaalDocwRGI5ImmQMJFQEQ8zsnUFo3ygFxTmEgyqw4tIlwNmWGchCM6U7sDkQiRR3gYj33UMyUtKzIgws3dTQp7eFbUJmw+MjUgkChGOXIuiU4YASMV7kSLn9KrLftoDBA8lJBb64ggUkf9TG5DSeMUAVMWaWsjIpbhRs7Y54yP9jARObmWh5QTMlicKb37kUV6AR7OSOn0Y5aa14mZpQGd2GBz9vW32y9+u33z1XR1ATgFEaE4Ani0sAAAEmTRwObQLQ4desRhjcfj+vB0fH5qT48v97cv+6en/e3T8vTsy+pLQwsCFB+8OgaOgHIAJAQKQgCHkWmd9fOZcjIaodOrioGeVE84Pz4e3ry9uriYH++f7j58YGg7Om74y5urM2F27cZMFbbb7W6edrXOpZTCGPhpl0KCMMsTjlgCPcwQyTwtst5WDQAUsXUJJhTAyvrcnD0opLIO0RuGRkddwKdZplojoi+NKLgwuM3TRAhu2tdVmUqZECEFD9BiqpOUCufkTVNIJ6WUqYZqrfO0KelSZ1YENsOl+Zs312Wj6wI//fDQHG6++GK/10Nb4cmmXdOgs5vraVfvb+8+3O83u5vXn8dmd9VtKZOUWnGUjAN4An7Q3VrrFhYA1n1dFnWbyzRNNTx67xjR+tKbScE6FYosC6dSpc4VNLKPMDy6eVrtzy4uXh6eB4yZpLIDWMxTFcIAHfnqgIBOEcjISD3cUvpNMm04kTMAbM1z6HH/1EBC01Ry9mciESiFSRBLkWnjMjXHxmjMHdiQe4AFWoy2anRY3BBRUxweqG4QoJpxXjmJUCqjwAGDAHM2g65KSNlG6WZggGmIgwjPMIWgbMXygDDISKVcFzAgnIDiaNa8vSzrA+2n/TRteaJpO09bmTbzdrPdVJpmEYgquGU3hiICSA6gGoU2letqjahSLCRzkSX44HZ0O1jvBMEQ7iG1hLkHmhrKyNGkzLkJz6KL5HgjABwKicWIhU9gNJWQAeFmwkxZTkAw9DI8zmI0S5OtR4Qpi0SAdWXOZOnkK1M14yTkWUplhoiBXVhM1c2ZSp5RWf3obsIVETITNO0mjpD5+cMu7CpczFQ4z+pM3gxC8vDcbyJcpGTog4cXIslLw9RQSCNqKa7GJ0stEEGWv5kRJ5QElH5ITr9cbgwjOpSJiPD0GViyB5TfcUZKyg0IACaujtZaK0UwK5fCU1A0SFDPUBZS1VKLZzO7RwREeOqIkIiQE8wxc47M8QrKIIdssMF8UcFSA6uK2WsFgRFB3k0RnOYtv/rl2dt/dv7Fr6bLV4jViQIoTD0aRICIO38KDmw9GuBz8/0ST/v++Lx8vD88PO6fPj7u756f7x71Ze/LEXqnQPJIGh2JAQIztRGRuAIoALgDkAehhed9GZG+OQKA1KTnGMbI7mgOUaqFtnZ88/XN7mI6HPfC+vH9u8vLgmDnZ/PZ2fai1rPt7mJ7NhepRExMkL7XNDmlq04IwgPDAgZEbrVMht06swhSCW8GSHWSeTPttOwPVfuy9OOyRkAEConUihh1mrZnlTHr7AvX1Kd5EkQeLoXb0j1IDIkkwNyorSZTqdPcdAFwtbXOJcLKppR58qGE9TBbu4Kw7pfDoWujs4uLzWP/+Jd7nw6Xr16vq6nZ5Gp8uOFtmbeL8bt378/P1Jw//3pz+/ExhC4uL+fdZnO2IbeuyqZVSvdYVVXVwA6P+3Vdapms2X7/AqMR3lvriMhYylwkQN3cAIFFpgBXDcIwdxFhJAQSyFK6IPSLV2e67NEC2AAsM/FFWBiEJCLqxK3b8HbC6JzKkj1Ty0pizPJZt9OyiL13EQk364pF0uQjtWCdQoQ2WzPGWkLJAQO5D9kEqDkDuTsFZ9ydAIUbIROBm2s3/JSeOESTqM0iIrOPzM00TiLyYEeNSAdDvqWUWpScN5Jzo+ySDPAIc/CI7hBmCOuhNTzwxMftzJPwVLdn27Pdts48TzwJ9pnnyqw+MbLbpkqAqlmBikQcTFCBdgEb4QV4T3FAb+4K7B4e4QB0snq4MAWgp6bTDQDCDQBYSvjJlgUjzMYsKcRhGCQgGyxqAKDUHK5DRHrTOtdmjZCQ2XonZkJw82ypoiy5yng5NSRGNBFBQjfvrUdGtmVtpSqXwkiBBEjq3bQLl/BAwXCHFDhRaslKehtGA1uCQp8Ac8w0ZU8clJjcLMIzDRREOBlH08zbCRrtWmTdh5I/XB0okRRmogFjZThoIjAZt+BmGQr06fnBRDuG18YhcO0LMpZSzFwkrcBea40A7SolG+8iW3tT8wDgRABAYRAQVDivwhSSs1D2PrpHmEVEoDENmUOm+Eby3oxgDuhqXcNk3m4+/8XZl/+s7L7avvocsVKpag6BwojA3j27FalIuK/d1uYr0NPiHx+X52e9f9jf377c3z0+fXw83j3o/sWPC0XwaCygRHPyWxXDexCBZNET9cqmWQggBndXCBIJcAwPyCIhTo9Ianw9HADM5el2//nb89/99bdtbbquAn3/fHzXPsTr6wuZN2/Pry4vCoggDNomfEB8NNhIOFkBUSSLRQjJrYd2CCKq4Ebi847cj9aVjmuZZ1n7vLW0U/TFXN1Qa+HCLEHLsnRtZZJCJRwgb2CAMAsAYqp1sy5L2j+IGUFaMzAzdxHane+QgphKFes9N/TN2QaBzZsjbM7P98d9X3V/fClTnXcXTw/o1evuQtHbAZdojY/zlkqdkTZPDweA++Mxno4HYOqm07rbmdXKy2GRWue6AYTn58PhsK8bfv/xbredOzZYjm6eAwkXEaIylTrVKqJmItRMkajW6bC+tKWFAxU8tcfgcVleXvbLstZ5OqyH5J5RkJEJsCBZ7w7U3QPc3IhRV6PTcQCI6gbIlpKW5DajE2VEoxHhVIupFiZhmArXiaa5UilGFMDdwZmOqzdkBWjhDhwWACNQ3kdgNZp5gCOQu4dDasYhwnRUZOa2jQCIBO4FS7eWenzycPc1GoQnUpn6Fg8Ls5NOGwceMHw7EZbGrqwvh8gjdY31+aVsKgrpdnNgkU3ZXOymDT/PZbudJsG58IbBIVbyCtgo2HGSiYxaMCMpToRThV3onnB1PXCYa1p/MUZJQPqEEDDnL8g8sZMRNVk+z5hSJk41DgC6g3onpABI41RvyqkHIZTCunaWknlnSDTqblJsFlljrvn9D3UmjE+c5Mlq1VsXYQSkwec6AiZGx1yGJzCrsYanKhMpTDNalQkis6bHx4SAnKSJuKtmh/q4tBJEJ0QbkilEzvyfUcg79ghm86giHpoNrhGAp56aAFRXIkyVIcDIjQAPyUhOwMFUY7oY8teZty+jLzcgmYpSimqXImZmYTl/QUB2eCL4QLs8KRmvtQbkdmoRziLmlpmm+aMMd1djdkIWYoeMMepeN/PbX179+vfl4uv54rXH5MQe1BYNiDJTIGqAISXP7gG9e3N6OPrjqvdP/cPd4e794+PHx8cPz4f7h/b4GOuR3NgCCdAxsniSRqNI4mH55ORcBXlWQMRwYgMRBQIlwhiOeflG9hu7aiCXCAwNwNJwevfu4Yvr3dvPrmcRNnfrU8HXry7OzzYcTt24ECOnijfSNpkDWdLZNNRyQEDMuQsiUqnnpg2AuQhqQZXl4CzbzZmzMEuZN/vv98tyXHV1bT5PpRdmguUQgDHPdS51noppdw9G3NSdUeu9ucDajHn2sFJEW1PoYB6WF7bUUsFcqqAgMfS2kouuOs0TBNm+I3GZdq09BWKzIue72NOh717u84nE/ceXxz1uN027IVKA7/dO0+Pz/oU2Uwe8uHl1+/TEjPO8mabp4/GOiyxNHz7eX746X7tJMwDoh2M39XZkkZvX15tpmjbzvKmZO0JEJIWKOZKaPj8/A+B2N2+2uyrSsK+tYZF5N715++rp9kGAmD0smAjAw6xODJF0shs4A3Hl3szDPEgj1RmEFGbKI87FPSN7IRgR3AqjFBQKYpgmmXaTbGarFaR0h46oHmuoE6/JBCLlgAmB7jEiYgAdMfuUIqmwCDdP40wq1jFlHxloYZ0QteuQcCRVkDSBw4k1iNRuZJdLZjWOcPeTKiZck8MDCwQHCA/ruoSbPh/KXKjyctjLLLvtfDjfTFW203Q2y2rMpBMzg29YGoSEhMNEhaITFIMZoRTpzJuwvUwCYeE+ybz245CAhbtHleJu7oYaIpxRH2CBQKZOxFmsR2OBcATIXE/wID55sk4p0CwMkGRD1roYMZkqi1ARj3AwxuJuKKxm+XdTgm+evHR1M6QotfTehdldAQJ8uFVIyB2IyEARSdWqVEOjAEK2bohIyB6GmNqhwOwyi2BmtYZB6XBL/xcAAlEy3wA2Iv6TWWYmMw0AYsxz31xLET85hd2DhTycCJEJ3AmZCCO8lBI+IjwRwJtyHbHXXCT9bWmsqKV6GASYKRKVIjmcjPqX/FmZZQp/yn4zhW58TpFVpMjIyY/jqQwTAIRFJsFRH++tdeRy8fWvtl/+zXzzlVxcO2zMibgCZ2DbyAbhyr0pUKZvcbN4Vng89vf3x/tH/fj++fb94+NPH9eH5/Xh2Y9HtI5qlPhcZHRdTr6OkuGDnyzWFBCRKSQcJ4XY0C1ktGFAXmoNkd2Da4HBSAYjJpWDZfPhw+3/Gn+ggr//1a9ene2EYhbcFLja7YQQXKM7iAAxBBFiuCMMbiFOCmLEvGISrXbIpTIwEDQQKEhCSvepeXTtK7F4jKn8uF/OzwtiUAABzNO0u9i0dVXtxyWjdjsRme039bxrb+oRjq5mqhMjYSVGIIM4Hpa29u35bt5VLkzsZUZEODzuQdG6S62C2EIsoHc+Gt4u/txwjanfGjJ264hEhQ4/PGu/n2tFYWKQcqBa9u0w7XZUNw/7ZV0PgvDq9Wfztjz+9Hhxc+PMH28fOjoiexz7XgVQvQvyZp7KVLbn81ynImSmAFClING6tFnKHjkTJHMqJ0I0DPPkD+/u79wNQiOc0AnQc9jCDBsScbfRMh+lsjkCEBoQlNY1LAqX3hYiBggmBHcIIyFzLVNBMBKEMK6FikCpXKcGFMhAQigVedWoSD0iwDOmJmlgN4dAc4ewIZuxyNgADPQI13F2uDoAmrtbcD6HzGYa6Y3L1dZH9E4OvQgU+SsHPHlZ0uz5qd/jlIEQiBjmCAgaBOCHth5WZIj7Z9nWZa5lN5d5Pr88f5p4dzZLwc1ME0crIN02DAXJAyhoI9vmKwVZNMJSaDLfox3CV+tNSAAN3FmYLLoqIRBw+odgvLoMgRFmXYGAhRDANHeCsCSLeehWAIKJIyyTPikTQCFY2FXBIyuwcgWioMLT6geMQMLICO/0EBBCoKkGgHVFM2bpbUVOY2J2bY0wHg+nE7OdoUBIDESQNljw7M8QkjQ1ZfwQmOYImOewZMQgQCCyZ2E6QHhI4fA0PkZkAg8gZvj2yL8YseOpIwo1jFynHE5du8ORG2PBYR5FkkwUGoCRDcsYbK7JdJvrxLOawimnRYR7b0QkIqZdWPLzjIiR3GDZyJxufggLREYUAEMCSl8CUmgomLrxxeX1t3979c1fTddfdp+CJILdlAV660yEyM20sGjTwoSOxrR0f1zi44v/dLt/9/7p/uP+/qeH5w+Py909ri/QOpoSeYoJYLQZoCugUP78YoAwAY4oPJRWibmCw0DNMAg9UgubigQiAEnBX+pFwwGYHLhkgs3Z8+PD8/N+OR7ibN5N21fn2w1zZSAANecAkFzXMvchs4UjIo8nHq8rIfjoUY2AAApkJ0YXIkbgWqo1KVIbSam1TvP2bOsG2rytzbp57+pT2fB+f4iIUogDp2kmnq1beOyX/bL25vFyt0emeWIzPL88mzYbItBlDbX9y+H2/d1nX342bSdwhQgutcz2/PgifUsFhXclzu9e3v/wAI/aHpbSCfcvi3mZJulqSB4IaN766v0ZGOepXJyfc+0KSuy37+76D7eH/b33/tXXj9M03d89/rO/rVjr/nlxgGnaPNzfnl3NU6278808T3Uqu3kz10oEWXhbhE3du3NEIZiklFI0TGrhIgAAQkcdsiEFJ3IUjG4DBwTHMAwCsFJqTz0+hqppAFJZmwYQmJoB4NjfEaAIexhBECOA18LhChSIPG0qIiJXktIcXdiJukEH6hYRHFmsBQ4JYLtjEnzqlGB3VwAMTwIYEDDMCcmy+TbAzPLJBg9C9LARcAOYhduII2XP42R5CwdAC80+kTEDwUmCDODoGa+UBp8I8MzoTfrEwtbVW1swZLuRzWbZr9vz6bBu57ksG5slWrW5YEzMAIVxw+XQegEUmcNQqARN6KXWndsBaW3rAc1ZMOWizIIR5mPxyY0cyHNDSs+TmyOB1NJbS1cT5ixFghjuod6BEAncPAJNE4TwvLMJ2YemM4ilR4fIrkZPGCdBAIJRr53HPbO4a7ZamYdZEu8AANqNCBg5IhCSrqWkoSMjsAGQ6KRVdkRg4XBA5vyY2ZMsda4envuCSHK/J088DeSuFImU8nv6vCDTN92MGIHRw7JpCCKKCCB8KqTO+KGRcQoAjlzIM1QrZTnEAZAdDoRANKla/JMiTe1dZBTZUJZuAObhBYGOAYBmgYAjDTXnfDeAmEptrUuEZ2U00tlX/+z1b/7V2dtvt2c3KFOsfmymXbuqAzIVhSCCWisBpnZt7a6Ed8/93WP7/uP+px9uP/54//Lx4Xj3oC8HOB4yPhIwCHNmSsdSAusAiBEKWCAUAIEpOzUjF6iR6ZHhboj5UShHodycMhEgAFxEUoXNggCUigWiKWL37s/PW/rz/u7hcrf9xRevf/HZq+12rqXWUsowWOAAGYlgwP4EmYRMmNywpyrt0yua51MYuIGrW08Cuc51XQqMYBbS8G7Qu1epELwcNDYyb2TaTAEdiRBxmktv2paDI/a1Hde2HJbPPr/abiYW4BLCwqVOm4II2vXh4yMz1JmpuQhttmfo+PSkFP7SD7e3+7+8Wz60eG6y71NTax3X41F6ySKt5uraWBzMA3x/aIfVNttpmiuKtX7s1h9vH5mx8n13rbxpHXRt7366k8epVP7Ld3/8/OvPPv/ys3m3ccTNvKlTCYAIRxZg0NbXZX05do1wDGSZNvMMUetcajE3a8BFVtOlHetcgQHchTEcIKKWkoEKAKCmLITM4VCrgEYg01CnhZsDiUN3G+e+MA//KIQwMzGTz1OZNrVsJiDuDs1RgxTZEM1cnTq6uVPwIGYdPC+VyIMpFSaj0oRgmOpTRx9ukZcBcYRBxiD74C4g6TpAsNTJuEfk5RFj0NQTIYzulieMm2GqDC0QPBO6cqdHJHdHyMjV3EMN0aKZvhz8+VkfpvXyvG7m85sL306u1gv2HlVgYjCBKdCYSL1wCYhuKEgORjS5L8xFqJkdtXcmSOFZAHCGGZ9sp4kfpho+Xx/IukMAM69FHCBPfIBcqiGbtsJDOIezcAMzN+9A4w9FwBhJPdI8xjzk/IAQpgBMCJlk5wFE6ZAdSFQmczCzcEZBEBH21jJs17yPwt1MkDFDkbxIcqaESG5JwZ2Ih89+/DwoNfgBCKZKTGbOkrGIaaKBkzjJINHsQV94/sWU/xOma9eRCLLpGMaAGwCmlgc6DU443F2YmNnUAKNOpbfGwpHgSdaGECFjuKUvjIEygCgGoDiA0mwSCwi0QAS3liKXpq3sdm++/dc3v/k38+VnXHYNxA6teZAQAx67lkDzYCIkFKLwjoAWeGhxv+j7+/aX988//OXu41/e79/ftce7aA16I29p2IvcpseUb5AWqaRsgfLRGa6NZE5ShJMXmAcy5S/cI73AydNm6iLE8CkhohRxC0aLDsjMVBG2h2d998MT6qLLFtf9bFrevjl7M3OyOjkjIGWWUDL8qRgghGzbjuGNz7Byjgg1devWlohV+7H3xbSrWWsaCGpGzA5hGg4opdZSWTAcI+P8mALrML6rpaCYheftZtOs997bymXTmhZRnhED6oYv31w+3z8fjke5havXZzSRd1fzwvXyqrzs28Pt87t7/vAU71+w0+b5GG3pobyuCi3WpuoIjOEGZIAOoBuigxkf1lqn3QG4cIRZo812un3/QpVqLX/+409PL4f/+g/fXVxeO8Xt+3dWyvbiCvmlliJSMhuyTEiM2l21HXvPjEIhKVLOtmdqxqW6uQOGhYOvbZm29fDkwpwNQwAeBOaKYZi7VqqnA8BxLAgeEaQDBiQPd/MihTACLHlWZiJEDydAYpBKJExFgpK4gtX8GN4QW+DqYMBhIBim4eEaUYBdXUA0PM90t2wdHsr0yJfTAoC8KxOFaQ5bp102cmrHoQYEdxjSa4ihEQSIQAegLF4F8LC0IYSPrz+RotHlMjbQnFbAPSKCkcI0FTzW9nY49qVNuwtdoJ/P89m02xbbTp0d5wlAMwa6CDsCmQmwhzuYUC00aRfADlAYj8zuuqKga/eMoR0YTpY7OgITUkL/nplPQkUEcgNSJWRzAwspkhprKaLqplpqDTBmFimqnZlV43QLRESQUPbDm1li7LmdeyBYABimQwHIzUTYs/NKKBVikfgPZOYPjtPYAVKlGsAiAOCRodYZdxp5gTFhuEu4EbFqR8KIwdlCxElFFJE5qoEQUaRkOy4SRSii5NSa4T95441PGDAPkQF6n6Tn4cOLEeOJAKS88I1SkQCQ2X65JGa5ATOr9knmnuHXnrkS6UMIFsnEJ6BBRyFirSXUwrqiO7hcfPb2X/6PV1//lczXzBMi6drXtQczCRDhdq4WweBCApiOf1CAfbP3z+2nu+P3P9y///H+w5/fLx/u7OkRtaE7mANSckGOw5WZI/vPJuchswHAkxc1f/oWwRhw+rllOCxhAOdykMAqBkI6hSLCI/OYhDjMIxVlHCDVwhzs8vrqq9eXry+uducXNM2rAkMwRUQQi0fqjfLHQhkSCD5segNtIqA0mZs7QLpgAwFAkKtMGEDWlbmXaT4eGpHUeV7XAyM3U1/BjKK4qZPENBcuEIDIiI6yKdvtZul+/dnN+nx4eXkhIgS0iGbKBKoGzOc354+3D6qL9mmez6ZNYbV+PKrFsfnjwf/ww/6uXZpsH5/peHAM8g6AosFduwGDhkYE6mE5bmbxEr72sOBynA8NCcy91lLXPjOzEMt6t39+fn55/Ph89/4BwB8eHmQ717r57LPrm6vLbgBORmHOEaAWa4SFlzJb63m5shAxnW0nANTWwry3lQmNE38LdwgzZiAmAmfiMdwNSRZZeJiTsQNjxpFoTmhYagnXROoJOWMqkFDSN4ZZ8odERKV6nQCom3eMtUeD0BiwvkKAAQRJ7gHmLTTCwDAoAMKb5mRg5pRxXWqYBlfzJNjcPMCZCdys+3Dn+MAxwoMypTEwANz01M81Lp80ISQ8kE8/5mydvxMxyLCTXSVHzHTsIxG4Y1C8LMej6rH1/WY+m5eL7XoRZ7tioJViEpwK9fDVoAZtC1GYOSCVCBYuDRrJhLYutgd3is48GMW8kZDQWidO/LkQISKBqUhJLDYyPR4JRkPocPwCoqoiMJOYRtIiZhYAFopp8hmLAAAgMoLHqMBVIymmxpiCeAKITKADwG7KJIHQu/I4Bi0gMCgcPM1MMADkPHLzuM8ga1PjUvL0YcQstJHTNApJLLlZ7v/5EYjZTJko/3v5SackH4MY0In+iUg9zEJyC1AHQmIKh8LsA/lO+7hjIDGBGzIxk6rRaHTj8CAgyAUi0R53j+C8QjPWKYJE3HTo4XzkoyJwpNI2Irw7YoQ3wt3bX73+zb+9+vqv5u01S+1uy2FpCvumm4l1JWEIDCJnJDMtQN1xdbpf/cNz//HD4fu/PHz8y8eHnz4c3n/E4z4O+2zuQJY0w6VYMyPIT/hmPkb0SYxF405IjAoBgiALaQYcOsKvwVOdlcd7SjKIRmweIXrOYIQc4AngAFkIy+zO0/nl2y+/riXKZg6ONZSBhHH0J0M2nwQgpnDtU6wHAuRLi6MyCUUmIiBE9xJlW71bXwkPTKVIjQAEsQ59jXXVtnTIZH1hJCTBgFBVfV7ddJoqC8skUojVbVUkVkXHOm8lUjecvhi3Ok1nlzvrh67LcgDErQdBmbX35tEifBKIC+QNdyFdvRsIrOvSA146eAybLlVUFFd/3O9z3kGEwoeAsPCpTjJENuARKNxa74e2HJa6mRT6u5/uqG460hdftrdrHJa2mYt2c/euPSy2uy0TrWtDZhDEoKlUQl57M+svL8/r8bA929KTvn37pt0/74/7U5KxJRzOSZ2naI+wyNTW7oZrV4OIACYIA/RARCFGQDVnBGRG18pcBZlju51qFc6UeSAIYKDdPC+tm1s31shQYM4d1d0566s8zC0wJKi3Dm4IZGoIkAhPmIG7R6blAWahlQMCateRZu8emSU+QAICDAxyM/xEKAGGeXZLJQcBkALIAYXiwAOGeugkH0dwj0ythkDKgEg2VXZ2tObRlmNfz9a1t6atbc8uNpuNGEQQkGMtqbMGIanCiyuDGDAGI4jJFmKap63rY+gjkEqV6JnPk/QYsDARmfUc1LK2JzxCR+s4S+EIi5FOCoiukW82EjAzOJjZ6PnlDF2P7ANwdeTkfiHzGzIPLdOq3UIKu2pmAxBJRv3wSOyHU4YtZPctMasCmJOQdhVhIvZQiJ9PmUR6AFCbkZCoakaiQ7Z1ERJBWP4gIdCZmUa/LnqGuCGOXiTw0UfGyMyhI4cIiYhRRNLl1roSYQZdsRAx56uYuSoBkMh+GgXdTErJxL6BOAlnCCUzBWSAROJOOI5+8xRNE6J7OvvVzQzdyK+++s3N7/6Hy7e/2Z3dgPPx2LvZunrzmKcNIalpD/SweSMEZN0NYFF7tPj+fvnh/eOP39+9+8P7/U937eEBlhe0RmT083SPGQCbX1eMw5wGsIo/R+5AZAZJJrWeZBNIEIjEgxqE084XeEqUTQVdnOyIkIlmaI6CiMgsgO4oj/vluz//uC5PLy/3b652X716c3N5NkvFcKHtJIIEYCk/BUKEjKZKFwwOv+NY7QEyjBAhuAKqAIX3Fk5louxO26zNzqwf1TtoN2+aJJ6a+WJeo86k4PM81d02wh1Q1Z5fVg9CjceH9sOfHl59fvaWzhgDm1vrgATmwrw924ZVhB4Aa2tSK9a5WEVcok4osfa6ODd348kVzJvi/LxfTK5ld7adC60vh+W5718YNNCsr9pXZkBSgDC147qSE4BRemUIs9qdSbyZhT68+9BdF/evv3h7eb1rurx9fbObyvHlGN7nTZlYVL2IeCgxSdBmWwt4W12bWdNJ6no4FCrPz/frSyd3rsgIYSgyXjPVjkgobODHduyGyDzS2rLkMIIQ3TVgVCoVZkhNAMZUuEzEDKUICEegqXf1YKCI63lHTfXQu4YzmXkAYyCoC1Vwc8MAD/WUcGY2GsWQ8LsDmRGwqiY4NMAfOxV7GCTpGRgEkvohSKI55+TxouY5buA+/hsjvmaELAGMevAIz7MQKEXdmRRJmMdhvg6U7HQgQawteltV23o0U+29mW7X+eK8AolwoGNqnAuwgzKSIDmGoGCUwGCpzVaSUqW43pv2yHhMIGLwMCBW6wgBmXHplqIaIvQAIg53dQsAFkkMLRNcEEDVSsEs7yWsFkaIARZAnsmyJ+Q/tY40QoTG5D6KbAOQWVsnDiYeAKGPE0/VSuXckSwcCSMwHIoUc3PQCB9WXGQ3kzJgG2QONSmFMoXM1Pl0hCVGTHTCMzAI6dPlY65MWEpJtA8AKABSDGoOCMJs1k0NaFgb0iE8IA33CMia+PDQbkRoqimBRETtmsB1RoSmG5IIkUi7EgcgpmGNAF0NETJb2jQJgw7MAa6AV9/+9Zvf/bvzz349b6+R+djs6KZmPZF/R0cPMEAqTIwcagB0aHF3tB8fj3/68eHHv3y8/cuHp7/c2uML9gPqCmER5sDhqZQcRAhgnAI2U9+T00qKKSD930mjA2JkjoYDjFhzQI8s54kTCAyR2gqgvHWJM7MPInKyCMo4VUWEILLww/H48eNxI4b2+mwuhP7q4uJst+luwgROhBDuo+U8eYhkBXJPgU9fC0E4gkR4GCHXCAPQtI9Y7+GxOzsj9IJURSYBdutr76qq3U3rNKmbEMlU67SROgO5ube1PTwudz/eff/d7d3Hl6Udb653ZVPDbTn0simM1Ltut+c843F5eTk8Q/j1689q4fPLm/1yKLcrhHlnXd166NrbGtr7oSmdXf+L/+l//PLLr8+vtg9Pxz/++Ifv//Td07sf1/29WKPlhXpnDHQsE6xr89SgpIAsH1XPLBPAwHY46o8LAP5/5umo62++fkul2tnZ/vkgTNNmzszCqXIzDXCupQp7DzU7HI6l1uenh+heuBz3WkTgbPa+RIQwq2liB1MtgKgB5CCFTL1bh8FAIjO3bKXKGmiCcLRwiiDG9DkimBRJNMEDV+1hdjgc3ZGEr+a6vdq8f1nullUVAQpHUNQ1Ay/VT+Cxh2YuDpGnviKhaHTtmQuSgGGcQOXTdB8wcokT+Y8hMTg1LGIMwX1YYPggBpIehtMj55lQlqJCGIayoVWzRD8T0YBhbc36dScGj4iu5nYIb8uxe3e48nDAebdlBRbillGLAeA+izhiJM9nBlhDJoqCLkQI8UjUAmLQMgmoJwxlqc8gPEVpZgROpi0Tkmsm4TtQBlmSiGhXZlYzJgwI7Tak3JEEZ4K9wCzh4R6jHCJ1gB6f0iKlFA9DQvfwcObRssIs1m2clh5SSu89INQtApgxj6Xk/ZFIW+ciEZ7fD1F1ZEjlPozk0jGfegQTJaGfYWSAoKoibGZ56UCACBOhhwEMq7S7ETEimqpwyWZOc2ViODHsQ/oaiV1SzqCZvEqFx4OV+v4hbxpdkmEegMxERK5OQhB5SSCzAOVl6orx6tu/ffM3/8Pu+qt5ukKnw9IOXVvXHsHI01QjLNwRBNAB0boBYAO6XY5/elj+8sP9j39+f/enj/sfb+3pgP2IpqFGDAAy4HzMFQA8nZoncHPolIYEFuM010dk1VwmuFLqjd0UiXNnIEBPU03+UeD8XuGQ9aaXFD3BUEbkMD2g++XZ7s3b6892V2eVLs+nde23Hx9dw1oX4YtpZ0hSmDg5+QHDJo4MpxQ+iBEonkVJBBEQRAxokWlp3kyPpkcka22xUGSgAlJpuysrhXQEEJk380ybuZQite4A5fnpuLbjuvbw1Lpsf/lXuzK9f/p4uxxpmnhdjhdnZ9qaIxrG0hshlc2ubOpyPGpTABOw7dnZw8PDx4/t/a1iuVyX1hY8HA6B4EUuXl2+/erNv/8f/+rbL3Bhac+//p///vu/+7vvvvvjH/X+rt/dLvfPFQwcurZa0Nrq6wq+YtjQ4YYToHkIT8CE1l9+/PgfP9wd9kd9btc3r7WhtvXibGsOrVOH3nvXpoBQC7Pg8+OSZlJ3eH58urm8ZI7Lm127+6Crj5QYxMrClO58QwREaqbRNQJEeNUISt+v5Y8sEyAiOyojIKBOBcPGw0VYt3PU4qVMsjkqOMJyWBVU2szzdHUxyzx/uHvpqq4RxB7OwYOvd3cEt0iIJswdzNXBT+7LoDBPlTpEgAMCQz6QARQYbq4te/0AAKJgrPl5+qh9hSzQcsuBEjAgsrfwdGckK3giBWB8/PEV+6AEIHImsvC0prkHh1OAP++1taOrrdZvLsAhYgLkTWVmsOGgIKP8IhCgZK94YQ4QhZl4Atto/yhwDO3MeTRT7yPpGWikzWfQZB6MrjrMAJC6pxE9726jJyqCmQDDu40AIndkDnMHYBrVlRGfpI8jW8HVAYBZWu/pKQaAfCUdnD5doKnl8wiApt3dkBgA844AgPw25yEelLsVWHdGlAx/dnMpFBGBuQmeSIksTvdI4QRgnGb5POBGqEEec0xogADoHiKknj3g6BFmihh5yidNxMKnhXLgUyn0RBjsTwC4euanMpF75uMHcyaiBWJm5gQRhgUioGTRI3WM629//+av/vuzV7+c5kuIOKz9+bA2D8DsUh/VcRGIYRBYiM1gCbpd+vcP6x9/ePzhu/d33/3U3j/o4wv1FdzAB3x2isEbtpe8p1O6mossjQc8HRkjBwXgNNNEfOK43IE4k48wu4/zKWCkT8qKhPvAwDEAgqUQgHYL6iSMZNNMwu3m6s3f/uK3qLe3dz9ud2dl2mzmzcXlBUFQyn9Gnvm4y0+fj4NHlnOe6HoEhLCh/wDicHVX04ahjAFVtHd3V9Xj4ZA1C3USdyOKWsrmfMtMVLAU3h/aYX9/eDnWudRps9nNF9vNfol18Yk3PyAdn5eri4mlHA6HeVNFyroccWkVCleqm3kuZXleMOBw/7jv02FZbx/2L0eC9bA2PS5mXUm20G3/ePf9P37/8PvP5Mu3vzibLl7NN7sZH1s5LmvZ+uVn7/DPfTkyYeu9ravLDJNaX3o/rqqUsoRw9x6O0QMQdV0U/Yc//OEs7Fe//ErfvHp1s7k4P8uSCXMyDXdgZkG0ZrXI0pqaHg+HaZqYYDdXFuze0qCYfUy1sGnziFEjxRCRTDK07hDoFhlvjojIMXB3APSQwsJI4aUIgZU6ZWq4gwcEMqKTME88r6sel6MvHQ9tPr/46ubi+WV9fFqarhGciv4IA+AcvVNkkmHQZpGYT3bApI3cHSI8VTGZlT3ySSICMIavMwKa57A/PnHP5PoUrow4RxznOf2MkUZKvH8+F/CfaIogAiPG7ZAafARAEs7MFCayZv1lVX8OQNQQIuHCyIxIAXUqYaoBDIDMnZAcpZamVkQQBYICJoQp7D37I4Ixo5omPIoRJKjWcjtCMCZR7RDOVIDIokcSHoQAToTqxsh58Fo3lmLqxFQqq3lAtriguyXmYWpDlg2gmoxxJA+hvRNxWoUH4gGQ5AwCQEA3J6E0+CZfkutLBiHlaZmO7lTQpGRAxm6V03dE6n9L4WSNaeSMc8YEZqyFpgmeKcJFOH8SQryuay0C2U4fmWENZoajrt0HzgeBSNoViThzDgC0KQmxpFzd/ZRYkDpizjNxtHqBR16kqW3KpIvRjcfMR12uv/nV29/9b3Zvvp2mS1c4HpdD165gSLMwS5HR7eGAgUiChEhLxO2yfvfu5Y8/3X//3bvb735s72/psJKuiY8HOASbRxYWAGqc5Nh52yfUAycYM4YJJrLrJUbOdoq3ZMzgqYEDH7MSYqBTFsJAOCIJAwrEyGQM8nAlZBAiJIYgUW3r8R4efur/0G9fXW5evbq+uXn99edv3ry+rhZiwYCRcg45xfIMru1ntVLm7A0XRQyHMGCktiAAqBYsW1DsS4Rpkckn00kX3U91iq0Ri4fXIqmIOBzX5WPrTeednF2eX7+6qEUS2o7o7q1O05vPr7/59lfefzp1BEGQAVPrauE048RT2ZW22Lr2WjYfb5e1t0VtWc3iZW26rEs4wHpw1358+c//4T+8/+Ef/ue//eL8+gq0ffend3fvH0O1OjHi48f3EE4U5q5q4F7LVMu5NMF2RBRUc1JO9zeCG1TiqfjjT7f/DezVf/q7f/5v/uWvf/OFA0Fh7WrWj027WSGYCocDCx/buhxWQCAhqYyC5tbA+7IwSUFvag46cBIE8wAwQgqEmowXQmGMiJoBVkzejZhSB5rSdRYSzjULkTECiLC7YQBTECJTXJ6fPfe+73rYr49P76TMu+326vL8ed+PS3Nz9MyZyTCqFColNgAEiD6KSpKSSoo34foRKp1bbSTsD6fZISIM/cTUAUWM9hIciWpDFJLof57xaS8KtySlMoNyvC0YhBSgEGPHONmOA8Bh5ISFQ4S7Qw+PozothgamQK/PplIzph6JqaQBhh0BqZiHTDx8X0UcZrBKNjlOau9NGwGyVAx3C1XNRsbMZ1TVzKN0dO+KTGGewUEeRlyIRtCLqkm6uIUBQbt6Bjh3o8qjzh6DMoMZwN2FS1roc1qjzGhD6N1ZcpQjztaTlO/x8PoEgKmxAASAOSaviGhqLBIejJTBRq4uPNjXRP4h3GuRPBtYGMAlq8gyah/C3ZkHTAQQph2zbiZMSnpKKcCZGU48RhIDyFmA4EzZPoKfDHWIUGoZc7GfMiYQmDPWH7CUrL3MMYGZM/uQZHjWhKWpokC3tvvy6y/++t+dvfltPXsVBk17Mz+sjagWIgQURHYbobvEVdgsOtBj0+9uj3/4y933P3y4/fP79eMT7I9gikOyRSO2CZPa9WRs3YeLPdldGP3J8OmZTk9bRAYwAEAwMvipOxQgTgnVuRFgZk+iQyACgSOQAWD6KJlkPPeIgQbQWIwFp6mv+7s7f4x+tjurrhv06zgsUuaplPQAp2BrvHWnPSZF5aeV4LR+IgUxAGIaBYCASwxQLqQm2sZUJ5F5sz3T9fD8/PT89NKbevjh6enu44PsprPd/Obt6/OrXZkIzJZ1jQBklHOeC9kaT7eHx4fvbm627rx/2tPEc5lkFgKMUAhcjnr16lp3+PDTB96eU5WWvi+o69rWpuDe1wW9hx1hOdvf3d3/UP/0/6ouIqJt7YRAVAoJGrS2iKRByc2MkRULCZIwZhQGgY9NjiCUEABZWw+Hp9uHn/789D/9nz+TurG+ruq2uKn1Ho7ELBFhHq2346qL9fPz3fp8V2fOpCtEIirJmbpZ10GHMpNIyRI9D3YFYe5uDA6ZXW6Onp20wSQAwZiwOzIBCxGjFMFUsjE1bbiZIgzAIbQS7YFb2Nod2nI8+DTPm02deXM8rq2ZqYUjODpYBLlZwhbowxPqJ1F4LhiRb8AYztOKMwSHMaKe848EOKTE6Z+g/XlKQ9rc88PFqXIA8yxHDDcYwS2jXPc0+Od7lO8jngAQG55VyFB4iqYB7ajPHmmiJxbgy7qpxMwaygGYQTIkEOAQZl6JCKSHSi2ulYG8dYx7BM0QZuIsQg7AU/cLk5sjcZgRExLnYB5DtMNt7SyeFELvyiKA3lWzGAMCpBYEyIa11DSaOQ5ssEspeXjGSWNi5pIZdjRkNCzkNhSEgJioUSml915KZhzFwKsBLKsi84IRCQDxCCQotVj2VQb5MNk6MftgKUdLDhON5q4MRUql4mAjUkAAoJ0Lp9BlMAoQMQKcTVg8gomRYhTlALgF0SjyBERXTcOBY4rVw8Py9BnaLExRW7iN8CZEJCFFk7PzL37/by+//t12d+NAR+2t92VVhxDEzSwISKrmFhEeKEQIqEAPa/vj/cs//PHDn//4/dNPD4ef7mRZMbe0OKlnIM96wIGVDdc7EEQEuKOMAKIstxl7LCIkTnWauoMUguykujkphHIlyJ9UpnhHKucG9gkIWSWgBoSOgWzTFr767dWry+0My3L7saidbeYNTxNxhYDeHcWAyKHMkqFswCklAiYESx/C2MZP5g9MBV4AkaMHZHU8ApmhOzlIgHPdBRWkymXqU4U6QZkebx+W55fDfr24Obt4dX3z6upsO1lf17aoekSoep0mIQwWZ0fGH79/L/Wzm6s5tmfL0Yj94urStTOmdpCeH5+IK8l07H726po396YHkTPtT2oSjhENgQDF9SAEuizWKIg1SJgQALD0PDtcTCnl7ewWBk7sXgrMiIyOmUAFTBDJ32RMuUD3iJuzm9/C5ub9+z5NLFRenpuHaQAJB3DvXVtbTQ/H49rW7UStLVKum3aMuNidRZG+HN0aQwXGDJ9e2vp8eGImDOYyuxIhFqQ6FUK2riyldyUkCGcCESH0CM2jIwClTPNuYwQ0T8YFkDuDABz62tUPTt25FHGl5dDUFj325YnLXKc6EdNyaKoKjp6px4Eemgdy5tKi5wmV419O4ZkRkLBtUG6MCVe6n9i7gWijW7rDcoKNsPQRJC+Z9wXCADwjAoeaNDeGSKxphCRkMwZiYsJEo4UWTkNueF4vAG3V3hHshRyLcwUuSFKJkGUS8iJi3TAAiFSdgAMAC4MjMgJzNnf4ESHuojUhxhhcGREBoTfFzL4kJOCft2gkYTZzdy9SciAmQCicYt80cuI48SDAkYGAPQyJYeQ2dwhwz96UyH4bPwGI43RlOjX0RLa1uRqzhKmFlVLy///EsOKQYlqq0hExECVnPktVYvhQ4vMgaQmzIciIsiwekEZgR2J5Ihxu2oMEa5WIMHBKOBkGW5VmLss4OhyatvyRhRuwIGN4uGbrB1FG0THFyPgc8CilMW/8uAEAWbJEDdzNhY3o7W9///qL386710jSmx+W1QAcYDNtiNh7L4ge4WneAoqIpcdLtz/evvzD93d/+uO7px8e2t0TLSt1Aw9ECkghmWeHT5xSeRBhXAzuGU03QBT6uac66aCfT/jISmiISAALEHJnSKlbHsQEOR8N3zmGGYy0hvwQmYPkEFErvNnxm4u6PDy//uzm87dvXr+6/uzm9eXufOYySdlspspSuHAWuiXyn1W0mDEeuXoMOXY+Lrm7YwQhIwRyAUKIjgbOFFSgzuHNtbfl6FyLVCmzkIAhu5/tZHO+2243zNh7760tbVGDwrzd7IiwHw+hxlyuP7t5ADzsD5fXu7PrC396ttZ1bWfnu1UVCJGrOey2F3qFrU+KZZ7nstk+/7Tfzt8cH75DJAhxM4/ssegCAaEpVjVTADRTN0UMKZtwjtATNU9JppFXx+bjKMlR1pCMEQ2QXBHZtf/ql28E2Bff1l1QRHAgsdHZXEAhHNuiT8d9W1us7RBW54lFentG0Hmiw7Gb6bJ0pBCpReRs2p6fX7XeDsfD88vzcnwJlalUqTUcCKBw8fBSCkmoGmIgGKITAYJRSfdZWkEQCUBgKoyEwcWE79eeVzsZTcxRpg7Qu6+9F7PleSlzFaSs/qI8A/JoRgxLRYD/7F40+DTQWa6hMQSLAGkEI3DNgYhimITzEIAR+mYp/YgwcmQoDg1G6heEjWF5THtDgYiBnptx1nkm/EADx0A4bRJAjClvC0AWgPBux+d9VOFJqHKZap2IghAlAErGfjEB5dViiiHCgRSIWM+ZydH9uCI7UiR4iYgsAhCQUQVwQrmR8l4jALMYTbsIgGDNiCn5tyxvAKKcYrVb5Dwp4AGgilnOCCxVAECtEZL5yNuAiERymIEIzcYB6x4cKCyjNyUcGV2NiTM2zdw5M8YBkcAjelcmHL4USAUP0qABLVgIEFxzbQEel62HI1OWgvmnwyJDhNI4kgn+iAiWclJjpoy0dg/kDJIDZnB3JDZz4mQtSE75qFn2EhGZhM7AKfVRNWIiQhJKK4oUjgBm6NHnz37x+T/7V9vzz5knJiJwQOoQ87xFADOFiA7WI4iFWMwUkBrz/Uv74f3jD3989/CnH+1pgf2RumKgZ8EmIJIkoB8wwrcBIJ+0PKJjYCp0Qs/zOU2s6JMTesxEnq9OhoUjA2KYhWdnTELkpxchkRChXNvCM3KTGTkQ1IIQvff28vzq+uybz9989ubV1fnVLBMZb+pcRSgykHxcRYSUBO94YeN0lw4VQw5uCCehUGqNwQ2Aw1AqB1hEd20WFqYiUwfiQAGeNnhxDa6NwFkgwpnk2NamrVvsNttpmtwckanOVeZQ/vZf/Pt//E//j2kDvbfe4/J88/R06Mt6FKnbqa/aQwvL2oLLLlY7tPby+Hx4vGtqCh3jaMvRAjA6YFMPAmcRtxbeMyUhgqTMip1JwiJUAR0YEAnAAwzUFbMpfYya7s40dW3qDl6RrLdVofzDn9796l/h9aupeS99cqYwrCJzrYu1tfeH56dmzV0l+nI4IEdvx3XZb3fT6+uv3v0j/PG/PTy/vBxXlyK1lHmW7Wae5vl6d3Pz2eun55fj87LfH3VpHmgkk9QkIBGx1lHTJMIBRhRTKakVcHSUae3KZWqmzhEYRLGbJz3a0q0dVENagAMBIRj2o6GHr4rBEXjSYCIyBWCoe4C5MWRVeoQFDcuuQYDjCPvMdtWBzLiBR4SNkRP8tPMmCTassxGa8egGnzz/8Wn1zPEqJ6iwBLIcKVVRg5AASjcADI1iHrREp+auoVaK7gbr+nR8nivPXCvXKleXVQojeLiRIAshoJsRl5RHACAhS6nQCcgJuvt3zV7YI6/Z1F7nccdcVD3bbVMBOOJ6xiaUPjvATIYzsK5mxkUg28sTIYCACAxysMIl7xltDU46X5GSrEDXBogejgC9dSLOJDfCQCLthozEyMEx4h9AW0PCEWodQSNICoGATuwiuLkIQowCgwHZuadrIBuLcuGay9y1JZ4szJE4eBgRAxJkV4tHECIjE2kGGuU3NcOFTkKoMQ6nzBHwpJYlhUi3fNaL0ujFBUSUwp5okKevOKfp6BBQ5q9+87ebsy/KdMa1Wu9uSgxFmZgZHYma6WotkMwBvTPR6vC0tnd3z+9+ur3/ywd7OGBbURshuFsEEmHacT9pfHDIMke/UNrciUd9J2W4NuTgP5D9MQzkmJCvWQIvER42Vt9wOPWGpauehM2zrzHzTSEcISgsgAGDC8XL4+EP/+3xeRfnv/+mElcoJXgj82az3U4lfd6cH9UjAAOTdMlx/3T6pwcwgdX80sZv5yeWYt0gKgaETohpSHCYyE2BCtaZ+kKM2tfL6xvXYzu8MES4np3NS8PC5wDUm1mPVY9Ls+fDuj773//9//184jebDQdaW+Fyun519f2fbs+DZrUybXRVEDzQM8PcFyco16+u57Nbejrqej/Nm/1q1hdEIzIgReLQo0cn2AQgAhIX4CIRyIU4wiVAAxQyuh4gSVo3DUJwRUSs4t4JAp3cT7/Ly/PLQz8u+6MDMIYd9m23YxRf1ffNHh9f9i/HBr2ylO32z3/+7ur6os7TzetXdnk+U7DG88sBuMh+fdovXdvLYdls2jwvm+28u9jd3Fzg9eVy7B8+fjgcjvtDs0mnMjGLFFa1hOQRg4lKGbs4lfKyHLelUK3NwZD2R1XxFmjDdgiJYnpPjCKfRcEA7QqhCJhUMBJiCts8zIFzaos82QE0Tq/BablH4MH+DkdFZL4PQKjDSB9LwH6Qt3lJZMxiDHg6r4cYHUW5fCaJ7IYn6aiDIQAmZelB4/4Zh8OYo4ccKds0JIcoOx73HxCqVKm7M99tYxIQJqnk7mCAgiySxuRkwlEoLKDsgpA9XBffrxAdMJWpkIcYfqLoY6Q7IKBIMVNgUtMiQkiu6uosnLU/pRaHAAtiGvAZortnOL8jmBsgYUIgp2reEzzLI4ctm5QCsiiYmN2NmDzcunEp4S7MQJglP0N/6AF0AhoiAjxdtaPDaEgUEysIdw+mYCTAYJZ1OdaptL4iD0H7SWwECJCHOyKQ0Ah1IuyqEKPoICJpg3TkU5rO0uqNMHrnk28CD2BqvUtKRT/JzmI0wCSLn1WcagpIhnTx9uuz66+mcoEOcVyXta1rM80h2lAAyMmTYsLIkhqGFvCwX3/86e7+h1u9f8al4yjPDMii4zCIAKJ8g8BOD/MYWDDGvZ/5SPn8Bg5taN5bJ4zFIciTPEPEyAD0pJMRT5hYgkmpc4iMSk14ZvBLHoDZPAoU0Re9e3co1/S09w93+93m7NXVzWaugiM4OiKA5DT7o2s2Ngcx8c9J3YnCYlZ6nMwveQvkuIEIGKdEIkAOI64VjVyEzEy7IJpr2Z57WNsv81bIde2tjr4t7h0eX57vP9w3pV/87tfXr8p//V+//9Pf/5FC8V9/883XN1zm9djqZnt+dbV/OoYzYJRaiHiaylQ2Rfiw8s3N1cXuDPzJ28LbiTe73gP9ZfwoEAEDgQOcuAIWoim9ORCW1XTuGWLsRGDuqjnHohkwEgBZ1wgMg5GMH6zA4Lu1bp6O+rm/Zvb1xdArkzOCmx6O6/v3H7UdasXrq6unj12wXpxf7nZnUef98/2HH366+/gRCnCVMxaUsqxNux4Puj+sm6UflmW7nba7aXu++/Lsy8eX/ePH+/VwtKUfFjrbbCYRFkZ0AmAEFi6VsUCEilS1cAtncCB1XJqvjhYYRLWUw7K4WRF2p97z2SV3I8ji7qxxAXBwV8xETh+jeQSEwRj/A8wT0Mzf8dS64dBypjMgxh2QE8Voibcx6vqgSMF9lE5l7kKEuyVjk9dLSu9SLpHwOmIMFwtR5PXigJlWSUNUFYDDLouAHujuixnS4fbxqdbN7mk742Y+n4Q5gMkLMzIQYSgk1cgiDpasvfCWiRGb+dEPHwWVmcAhoQgkBrVBmZuxSJi31jOajYEBMMyBGInMDSIo/XuuPIl1RaA0TnkEQ+5hlioSJrYU3gKamRQJdwIkEc8AfPdUD7KwZ1CrGxOHZV0EBEBywkOzc0pzEBazDHwjyTBZM3d1EgQH6xk756XwaUEJAJimCSnMXSLbChNGzwUxs4ZTWmS5Gbg5MY7LDSlieCjihHSER6JRIoSSRzm6K4tk51cCQQBgoeCQNTVI5GYQyMxZPdYdAsv1268vr76YN2fhnhL11j2AU9poAeHQMmebMPuHNOCodnv/8uHdw9PHRz00gnDTHBwQIlwTv+ThxsqgfztpkIcgLQVyMUpHP2UlQkoi8oE+TS7ZMpo9quTpOyWEwPAAHq5Md0egbJEe1AIMGzwgju5kAypY5vPz8+35tch8JnUSppfDHj0mki5SpRTmkbuCzEQZB5jThGdcyXhdcTQ4jx85JquHOOwnAAFBiODsEYZMERQIQuLM7hbmEcilYpll3tqqTiAodbP17vdPLz+++7F3f/v1VxfXV7WWWNtXX1x++PHqD//lO41iPFXi/fNxXY7Xb97UclgOzTIppbgTWAQV4o5vPrt6/ea6/PE2SOpmJrm2eOwHxHgGb2A90JEYAIAlUMwdHcJHCmPKFvJsstDcxtzJ3ZElwAmcmFBB1ZgEItbWADYwn337+c0vry++uJmWx4NhUEF3X9zR1tuH+5fjAd1YChFp7yJUhXvT2w+36/7x7vbuxx9/eHp4eXl8OTu7mudSRLq6WxyOzR2PR9Boh7WddZ3ner7ZXH67Ob60h4+3h/16ODYvFqWc7ebCgBQRwUJUiYV4szHmFtSbe0Vzeml9hfBADWjuhQUAlmbrskIQOTHw8PK6BiATWWimCIyAE0QAtGQCLAlZDwf3IMquokx884AAG2qeU3pkQADxQIDg07qb/mEPj3RdjJ9IuONIJjQYG0WqLjLPFcb8CMMLMSZZABhhtpRejMRrT//JQCYioQjT3l6Wx7vnzflmt13Oz+a5gAPUIhCQEe35vmJGwiFJZVTPBCb0G+pfra17PHhvXApDIrfgjp+mUoThEWaRiOCK1u0k6u+lFst47SwMCPNxsDgSuaq5kZD2npl+gAHJykdkMDMNWfbgRwGAWSwDSomyJcVUMZcYTMUweXiRkn0SMeoWLG3GQS4wSIz0OGeoIHAmzIBDuJRqbql0zHEIBQsVPXlDApyJTJUIpZTIUzucCpta5hYhIrOoKidqA2DmaQ5ApNZWRCQiFtHeciEYjjN1Fky9fGr28+tP+sE0oAAiU9lcvvlqszmzcAA8tLYYZOAWByGBqXWLBkEQwiOqA4mX1m9vn17uXtrjApqsUqbqBQ2n+BjGEcHd8aREHv9gHuafxAhDYDnEPwgIoxgAhvMl8UsAoPAIimGxjiCm1EgncJFo0ydjbkqMCNFP4jcgpFq359ubV2UzNccZZQqiZrp0tXCoQCDCBEhDoGHGxHkRjI/io50+R+dwTN9OypuGoGJ8gkMHkn/RMwwg0MxMDQNEKs3ROqLrin3VTjRH+Or+4d27d+9vv/rl15fXb6ZJAPTp48PyvDy9LKr95eX4fP/ycjGrwPnuygOpy9nlNfOLxRquiJUIHIzI5/Pps5j+6m+//fuf1j/+/Z8gogqdX9wsFdYDE7z4ukYsjDMgmzmR2jgtI4IQLOO3cwfOGzUiclX2UKQIddfgMHVpaW2JAhBytrm+vrjY1XkSnRl6b9bC4eFpT3ywaLuL7f5hjyQOpq15aKDvXw4W7gG77dkXX3yN+pd+6M8vh2mamaRO4gEk4gHubmvvi1o/HuSYFPru/PyLb75+etqvj4d2WLr24wI4yyQilSOjilOXVwrQrFH3gMgFGdvqx/V4XMOTIi0TFWIAW1vgKErHIEAKxwgm1BH3mLkBnjQRQiTcjwCjMyI8kskdCpXwE/QOOcoPSwHAyREzllEcIlug4BPZDqeHKlUPY3kOM/7UN5tes5NA7p+gGWNiyTrDcEeRMXFRJiWO1w494tjW58Pd7dP5+ebQ2rbjbrcRhujOgOEhTCgpC8o8fpNJ0IGCMa4iVG3Rx2dBDjM4sYMRdCq9Zu2aUpveO1G2rg8Mh4IScWXhzBEys3RYp/KiFEbIuB3J6dg9RAaUn1TcEPzkcpqtLO4B2JuSEAH2rimUqqVktSIiUAwfBgAIFzXN2VYqg4e4B6fbtrC7Z1CNu0thTP4HMi4YTJWFSBgxurUMYU7FYH5h2WoS4GqeJmQRHpk2ERFpec0RwxOCUDVkZOL8LVNF5Gz4SsSFmSwc1Ik5A0mAcbCsnik6Fhhcd1LOPQiCmCGIkaEQgYIGmIYHkNAMaOFpexGWfddj08NRD08r9Kw/O43YAR4ZhgGQNpYAzJYbt9xR4eSozfcLTpcA/v/9jxgStRxpBhWM+aJl2lvG9SdLNrYKAAQMO4m9M3YUYKDxQMyCU5EqEWVZuOL047t1M63MfHOGtuWLzRmVuWw2dS5lsABByCyjDQ7yJsvrwQNH9gR8sqOPF+20y8Aw5eTbiph7toNpCJETuXepWwigjUuhWjfLy/PL/un9++8L13/3f/jf1830/Phsbq03jfACNMn26nxRfXx8Pi6X26u62c0G9vz0kZb5/OpsPZpZjx5h3n1dDnup14x0vdt+cXP9fv7p5fEBpuX11V/ZxTfPT/+L6y6CrO3DDSm8m1mnoaOlzGWMdGDlVJKeJiAfQDmGO1GEQ3NsqlC89T1AAFC5IuWHw3r/tJQfP9xVCShB2FsEm8h89uri7E423pf9fv14+2CJpmOc7TbPbbl72lNwmc52FyTd27G5w1FbmVgmbq1XqdrpeIwG0Hp0X0SgWew209XlpZ+dHR6f9w9PLTp3JAGhgpmNRqnGpoiotRyVzGCa6gS2dCcJXwwcAUxXQyV0bKuJBKGktgADPTxtPmZGKYGMnGxwKNsjSfJME00wcqjzMYVqI60hU94sBw74pC+AgAhKYwAEUaKZ+UqcWNPTPQDh2Sl7EiiMKyiGEjRGt1guAw4ABiyDdkN0CM5nFZNzdgBkCOjt+PjycL+9uamvLrZBbKpzLRHoEcyEgFRo7MaImO3cxAAQccn9Deo9tDvoCwuZBiIFB4BrNy4AQ9mnUmpbF6kyBC9Enpdm3qCQCpAkTV3VOWdJhHDHoMRqADF1uanAJMrGYBAWC2PijCoiAC6Cp3Y2IgoHVUVEBDQ1opzmAgLVdHSZpYbFXGqRdPx7ZJtK5HSZ1xQAuGVsUOBo7GZtHQndrGSwHAAgMGYtMKiqlDKKWSIggpiHUzACGHKEN1cg5MKAKSkYlRhDroIYKc4KRyBkjATE89aFMHOCEVyh3qe5rh1bD9BVLZqDW+QqVABAWAMdwwHDyQEKYgAWkbbXdd901TDHAA0nFABF9Jy8Qw2z2i5h4+GTHCg9nKRw49kFQ0jlyWngz3El83IxrWGJt8PwMo4WgdSdByKNxl4Ytc0p4EnzDHN6lplFUJiJw3zZqx8b4OFw/+HD6+mvfvvL6RffvH61K5uJiiSQmbEbhCevcvw8UH3CfPC0dsOQm0Z6ynPtTOAIs+AOAAhMI4KQMwGCzMIRSMpUdqyMgA/3H+8fnr748jdvvvoC0PbPz+Fmbq7qGFRl3snl66tf/vpNEKVX/bAepg1rrMfHF2ScdxtbgngKJe0ByBAWdvj6i6t/8ftvvvvHPzze/mTL/bvlPaoAIxIDzO4tPG17tftihhmBndm0P3/jc6fMb8Mp6TYCsZNhhLGgqC0ACLyB8/Jv/+Z3f/PFV+dlfr59Oj7fX7y6QomZ5GU6mC7cXIjnAoem9x8fe+8kIpVZmCM283TYL3/4L98d9uu69nk3b8oWEStb1x7WpmlK69BULtXa2pou1tHM1rYc+7adXZyff3Y1n8+Hpxc7HFd1brDZ1jJVrqIA1hTq5AECZOZL60Sy2+3w0Jrb/uWoLTN4CYEKB5j6mMcpo/tT14PBGMNolsnt/ok5zg7RwIiOp0U3tTlIo0p0DEaIpjoY3YjxRjg4aMayQcDJRJOMmp7GDqdP8SSJReWpn58SDaXQoDCBIT03SZAyII6j23OVB7JAHP7HFZv1p9g/ne0PG/UbYpmnIuaBEM6D7CREQ6IRcBfJ3QmBVt7dRH+t7Ykp3BWpmGUhIZRaAWE1zYvJeiulWqQEOWiUVqG1jEXgFK8HOGFIETOlXHNGzXK4OYsAuJlBNv+4xSnKO91U7oNCQCKzHmP4Pc2XEKaanfUAmRIaACEs2TUb7swkdmqLJ2Q3q9nGnlFlGfDplrGdWQnAnjdBRISZERNAXtQhwr33UqupCXPSF5BQQjjF0BCnTDVPc3NPvWlkFhJAzjLdespXEyCDCFenQoCjFQsJvBlW8gizqGdnirJfVZgCsPchbeKSPorAAAwUBGTMGHZvJkBT4eiG4aWQqhJAuMNILDHEANQwd0sO7HSAeyL/Q481DmigPEUHvDLGo08H7RihkRh+fgnyFaCTEhoDAgMzoCmGx3LsEhmfixms3jJHN0AduRs0x0W79niez6ary7PNtjBccXiBOZCHkBBPdmUHxGEHG+/dqGsbb/Snm2F83hl7C4FBKJn8nkrUyMwTDCt109sKKCjEGE/ff3f38fZ3//yfT/NZ7027q4YThqM1LdMMaoh4cxHf/urrH//8U6oT1mWhUre76djbYf9EwHV7Fe7Hg0UAyBQRx+V53ky//9ub90///P7+p5e+LMdnUCyFu5oHgksAyDQBSOoBKXvu3HGw22MfSzmMuxEIIlIecAwVazMIdIACMsHry3/73/3m//i//e0Xb189vBy66er94eW4/+nl6kae9AVAUeDhsd29+6ht3T8/68vy+tvPp1ohtLLQdvfFV998/92Hl4O2dV3787pVYbq+2O2m7X59WZaFC3mLs3mHJIChxq1rX8yZiFYPm6ft1eVus5mOT4/L4Xm1LkDdOyJN27Olx8vh2AspbwgYMFpXdZIi4bzZ0HJsbo4G6EEZIJAZD1hsqPB9IDpABODew2HcBEDgDnCyJSYbAMOZBe4RlAp4GE7+cUSklCBcR/xDjnR0YoaHrywAIdJslOJAHMAPgkaMFwXp9NbgeBXTz5RgyPg7abJBACIfOASNKlmcESkU15fl5XlZmwoXRgjolNcGIAIykBRM+JOZaDRpO222HUx2n+vyAdoh3MO8FEncBsKtaxHOPGe37G1lc2MiU89EfWFOqh0HeDByMJk4DYeEkumpLIKA6iOrNVmVvEXc3MGllH4yRVlqXVI9mAd9AGRfXHg6tmsRtVHTMth1iIiQE4UJbmnWdVeTWghDajXrTOLgZdDCxcMyls40mWh1M4QCEKqW82pCV8ldQICrkTAimAaAlyIpFgXP4cKT6sxgHyLsqmksMHNhTo1v5ZppH8Mx68hVgCEiBIJEkElKaataoDkRkyN080RAu0dlhgAPIsquCBOPXSmbImytMDiaZlsCESZban2YWMKAC4+I2Fw/T9vcp1xlGEz4KekbPw04nx7QlMEhYKAGIAw7oH36YyfWIE5zjoPT0GVBWG9JWVWRAEdXAsfoGitPiATb7fbm8nwqpURKnKVwZWBKOT+Okx7oBIGcLt3UXsGQ5f0TOGrgQPmn89r3caVbQIrK1TzQArhM7hawfvz47nl//Jt//e/rZlrXFgTqC0oBM/eOwpkqVktpM1/fXKoZVljWlXCRiaRsri6vD099XTtV6NZ7b+6xvZprpTefv2o44W737TefffbFN7d/958T4waDgAyv0nAOVZB81OmE+Hxadz4JtzC/IfkvD9Bu0EADPZ83KTCV33776//b//X/9Ne//w3SZo99AVnmDbgITtupBFrdQFN9fnenLVRVbaXN5uzqTAozC4VZ7yH+5bdfbbfz89nL3f299iUaf3TbbGqduBYxa1Pl1dYiIozoBQDAOczXPejabAfreri6uT67vJy2Uzu8OMTSGyqHtlK3xeC529PL09GqyraHPL8cgBlCkFlqIQtbdEhsDAIMABxagORhjonQq41JO7urxivgGOkWSBzQPzUfESK4RoSHJQ0A6JCzUcCJSRrCA4RU95/cAzndYybGaQaBAQUkfH+SJ+OJTAiPEcmcEhiCREzcghj8RPUjp1WIsm9jNK8GYlA79MO+rYqOwoSlMkv0buInpMpchHLQIiZkdANilnnb+jnPr7Xfsi6Z7JIIsKubBQkigGoHIERsrWVHCTOnEXrsR4REqGtjSgSexj6OgKnuzxeTEDNaX3tKyAEgzIgEELT301M8aAHOhFci9UHjDZjdVKjkSRsQqsqMiVVk+QpHOIs4eGJ2pVYId3dFZUaPYCQzJ8YsQ8jrvxQxMyKsU4XEZIgI0VyZRdWYKNLyAGFqSMBFAEBV3SN7o4okvpaSIUIIAoQTi82EHh4WRTh/+syUS1dkU7ZqMLbeowcCr8cGWEsVCTQPoowzxbDYMQJiABqEeTgEExWRy218fnPxl+10f/+AOCJ40wf1CdZBgghDFyQkIB/m2dMZPfCSBBZwIJPDI/Cz/gFPoYenp2wcSZ8QUvykHwI4McdDHRTgjh5IPMu0k7Ozzfn5NE1CHCI+z1Arvnq1+/LV2e++evPFzc3F9nyaZillKpOg5OEfHjkpjXlqAFSpwM6p/5T3lVvJqILOQzPynstkDnOPIEJRWxFCyuzuYcoCBO3Dn/74dPv+l7/7G6m7vloAEDuX6toRCbhw6R7UuppFX5bLq/PV21/+8Q9ffPHZ1UV5fjqcnZVSttNcu/nS9lMVqVsCQZqPhzYJ7A/PZbf5l//mFx8f/vv379/99OMPVKur8SQQpL5ykQBhLKlbQwh3hRSlDZWiJ8eRmy5GZgwXCgcm7WrawAWmcvFq+3/53/317/76q8rXVKM5VLFazwDoeXGg5qpddVl12T+txxcpXjfz2y8u5k11FMHopncPTz/++NP9w908y+WXr67PN9+/e79f1q7hx5V4Kyjo3kJLYdUeEEzFOUM4goBNdW1qAg+3d3h5udnUzebabNHoQXRcOtrC9awgx9LWpodlMZJCtatrX1QhU/TDMY/1tI+GOhAhdsgsoYDEQu0k5z9htzYUwBEDqgY45QeOCQkBhtggPCsM8myHGIaAoW2ISCgpb5K8VDC13adZ5OeZaSzSJ7LgNJH4KKaC5OqQ4oR1IEQ6kQ1QMjEzT36wrOmWMFJ1B2RhJiC1SBCEEM0KEUumGRJXjAC1LoV708KVpwurryBmJBOm3jQxgYjgwgSBkWAUhvtIXqNTuA6O+nhCDgspJdUurTVmgUxPCj8VEYO7hYejJbfu7lJkpHF7WsEpwkc0mTsxe2S+DgSiqgmjZhbQAF8go50jws1FmEVk0DgErh4etQgxYhCSBPoJUfIAYCCHMMsyM3dyknGpmTsTAWIuAQ6eYxeNYLzURY1pAmnUXyXQkceQFMlwIfeTT+qkvifkgHAzCDDMj8YACGBxwlXIMXrHGTJjkFgCA4XYsXdHIhIAB3MrxHRibxlgU+TNm/PLy/PbH36CcGJnEvUOAADs2JIuyR8zxEnWlhc/UriO4QgIMtI3Lw/3cRHAiUYb/6KcsN3TMkbDrpcTD2Y1NqajnE6eY0TMehlmLnWuu/ni1fn1zXa7rSKwnWM7y/Xl5s3l9nw3m5CSmS4bgMqCpY79IRUB44WK8UmNL+cUT+1xuoLGajC02yncJjgpEmTIMjEAw8MBuRZxPdx++PHu7vbzr38r9UwN1Cy8aygCMHI3ZxaCsrRGCF07s0yVb17dPL378fhyOJsuwvuBlioUxOY+z9PVzVXdzrNc/PTTw+3T49QcsBxePuy+mH7z+6//5l/9qx8+frTuhRipqjlRZakRGKnPYwxH5KzjCyQAIIzTtTfSURnJMUKZ3RoIgwcIgvjFFzt4LX85wuPtn8+29XDYv361670W8efj0yT9cHyiAj/++fb5x3fm/eyyvP3yi1//7hd333/wtZng4enl/U8fnh/3d7f3hbkgXp5ffPmLL9+9/7h/3K+rP/khZtls5ohFm27naVkXBUXmysUNw7xQsbVJUCA8PT71Pm/Py2a72dQdYLiiAz8/HWA+vzi76BjHfddmrR9LmZjmDgu4964UBA4BFCcuathvKAI1LJ+QfLZTv38q9soKl5QPhI+5/hPIA3krBCAFOOZwPig9SOXjaUt2SLH1sJokDTMeu5/3M4A8TuEUK5OTSsKYOHQx4J6jSh67p+zNTDBjQC4+Pl+gZOHcrIV3TStMZSDHjNcytILk4bPMiERkAWHqwgUQaxUI5zKV3St7fmW6op1IjpwVEbUbYJBkMw6GWqlTgCZpicRImGkCZsZMFhZqIiUxZAREIlXLe5cZRETdco6FgN51MCBIuWSpqkgBCBaxLIYKSOkRM+WYSohM2LO6mQgiY5gZMMJdAtzUkIgRqIqagQFAEIz88TQAM7GHnfLNXITCYyiTzNNDRASlFgwAHqtf7h3MlPNXhlkz5z0d+dvpD8hzf0hcI70PzoIRYeCc+a3C7iooqlZEzG08CyyHw7osC0wNOdSCCpTCqB6ElZCE3MOTa44QGRgfYOxm+eLN+dffvPrxuz8d7QA6SonCwMERhcjDO6SQ85QGmshxnObo9J3D6Z6HEzeWhEE+xKd1AeA0bY/7JGcbHCwAImS2b1g6htPQBREuQqZ22C8WquvL061s5jrNNE9W2cskhelyM2+KfPnZ689f3fzmF19fTtvkLnKmj9PUEHCCrmLQUzHCnz1vnIg07PhQMNEJkEIEYAAHShW05OnBQgDr8/37d3/5w9tvfjHvbhIhILTuTbi6WRDPu8v18OQgpdQQeHp4d36z285nm2VdfvXLd3/+0xu6Ii7TZnO2O5d5670vfWEk7/7h4b07IVWZz3cX14/P+sM//nE5bP76b377v/znv7+7f7CX1ZQIC0k+mAQBRJIZBZhfySh3giFtxfQxRSbBEJEgKE8M0RP43u5wt/nj+/v4j//l9mm55PL88Hx9/mr/rG9fz52W8wuEOS5uLpbWpmlqWnbb3eXF2fr41NoBELZ1Du22NmuLrtZBo/vhpb15ffnVF5+9l9vHh6em0Hvb1Ol89+p4fDgc12mq6qrWAnyuVbu7eeFZtbOgqdeNHo4mTNvzHTGRwaETVVqadu+VpvPt5Pt1be14OLJUAgIEprCuuQsTMnnxaIMU92yFdByRn/HpyR5c1mkkgLDx7p/UcgPZHyZ5PbG29ml8gNMucOq7M4AsR8hfw+mvJPiZPoTUleZrN/7+WEuH6wwGhgoQBGkOzTslc3giJXwA7pQx84hIPDk7U9nIXIExvAivY+ViBBIW9SiC7hT5BHtEQCEGJ6QauJH5zbrcWXsUEswuF8QwA0RiBICuvZbJDc26e3IegWDM0l0jnBjNjFmCKUYplgCEmuIwRmcam0U4CFOgB2KaDggAKUnTIgUgwxRGTF4gMHEeTpkPGh5qlu8wIQTRKaUyIlwgolQhRLVAgUqFENfW8lAeQpoiqirMDmmxybbIPO7IXQGx1DIEWW4ESMwWURjdycykirsRpnvthC2kqQEZidwNI0YXPEAQCHN+bWY2/jhSIi8M7OoAUUo1MoAAKATMiFIqI4kwDxUXQsnqYIqgvhhAuCUYEOFWK392ufvNr7/46U/v/+t/+i8IwIh9uA5GYVuunT4w8jg9YAMk+gSTjD/oMSqec2pCDIOsCjjJKcc/Q3A5QKMTIBOBp7pGYhrMgWoQmI0tuPfl8LiWQtNGrq7Ov/zy/OLNzc312cXZ2ZdXu9fn56+vrq7PtufbTUnNfypOAcIicLDWpy0GPoGPQ5QNp68IP7EBATGqDSJGEC4EZGU2AgkjwLp/efrzH//rzavX5xfX7uwAYD0ChWfwZiBAlcilTN5Cva/Levnqerc5n0FeeNmcXU+b94/7588//2J3viWUuU43l7/7//7x//mH//aHr7/5xpqb8WZ3dn75RubzjR3nbXv4u//Sj/LlZ69f7veb128Ox2OPHsOJ7hEomBgpIeBA4XCo2gaOQUnPEHIFR4C8AgGQASpszsk38djmM/3Nze76fDpcz+oyI766OutUvvjyknas4hFG64oGYQprfHj4cPd89/UXbz10WbIGUrab3bK0Y1v6UUPvzq7Ov/jic5nk4fap9+XpaOrtfHd2ONphfzjbbsuEvXe1dbvZHlujcCYKxDqX5dguZLOsqzzR2dX57mxHCv3ZmwaXeT06eczzJqAe9sd1OUZgIAkXZFQzTEm1e1gqxN2RIYKggPdRBGRJFRKERr42cdoUISIjWU4JsgkPjQ34NNZERB6I5gYeKDgytz0gPMIz2CHh/RiDbWqxImKUlOPYO4dwbfznxgmJAD8fl4jkasQM+WMEdM3zakiTRsltxFSnbSmFCMN0cNMYAOZBUDxCkJsPWaI7iLAGZNY/yzbk0r1CoKvCIMwiwonFHQBCSExdmDN+OcVz5m5uQ5EXjqmqIHK1AEAmVYV86wHdPBXwwgIEZh7unBn/J6NSHjGZzokWTCdOAoAIe+9AjDy+s0BBSKpaSlHrA/kElHSRqCoRggOLNG1TLQHAQz6OifVbWKnF1fBUTg8AbpbKwCwocB8W6oDITFQiRGTrzsKAuf2FMFOmUxJlMNE4A0+3PQSQkHs6f3+WzcSJviZEdDJ3i9CAq8/fXlycn11fIEl4Mid5bbg5FKJuxsw08bq07BZARiengLNt+faL17e/++X7H99/+MsHR0KkyMq5POxw9GLGqaogCbExKZ+gkrDAzMY42WhPcf/5ccbyNo5YzA7VT3ALwkkXNEImUlmUDhciIMRSpEhwSMWLy+t5pmnmL7+8+fab6zc3uy8/u/jyzc2b3fa8zpUKo3CebTHUEe7plh+z75Ba5C9yu4c4Mdww7rdUpCKMQIj8XmS2qysguAMwA/TQ9d0P/213tXv99suIGsEQGpBickKqXHhZEDBU1groq2+3m6luMKZDXw+tbXa76eLqx+/+9PabXwNNL88vCvrh/cfttHl8fL67v7u6fn18ts1mCxFtWZigzuX6yzc//t0/fP25/MPf3z8dF8ILdAEIRPPonDBmVmDnBDiiCwAgkMhsDLwURBxJb2EoOgFVKOXt5c2//v2v//rXv/js+mYxrPMmZDnf1efL426WZ7W9tS1SrLZlwIkZqe19/7z/7s9/3m2FMHrrqmrqoFCmqt2lTD26hzw/H1luP3/zmjCeH17MUcM97Pzi4vFRD+tBSql1MtOlL6VK1qCnvo4L75e1AkvjqfdNwG6aHbBHv3vcu7J39agTF97umHhprfW2dmWuxCghZp7LLgZaRLgBkvuaBYFp1z+54j/JwjKdMQY4DzEe9BNBi0OdGZAZyIQRNuYNgjDzk6AZInL+O/XVQpzi5eHTwpxDSYI/yeDBmP1pxAZldN2YSMAhm8TzhU3GGhGRSgCBMACgVGK4uLo8326FyTTAnIsUkoiopQCSqS5h3VGIPTzAVV2YkLjIBB5Wz0E2vjoVCQ9iMnAC8ghhzpbLpEyYKJjdLBDMnJHHt2y0dGWakyPwqo2Awx3Rc/RS1azxAIsixXHEOCcYnuezqSIwnZp7Mrw5VT4sghHgI0fZ3DJ92DKNkcCzhjrCzQIiSBgCAlyGdxfcQzhXBwwzTLtv/pCTIIpg4Rz881jMgy7tpblZM5eAsEy1cMtPq3WtWDwswmup2pSZWFhVCcjChdjNA0JGjbC7hXdFAu29lIoIxGnbY5zk6u3bV28/K3WKQFNjCC9j2zAIhhBmVUNinkqeBWPqMKuIb282//yvf/Hx/uE/Pr0sTy3yoQzFvFORww3QADDHe0JysBjn69gEkBBxgOwJ4eNI/cR/OlgDnJizhJAiANDBCUbyXz4WQ5MZETiCvCHCPKjS5avLz96ev3q7283lYsdffX395mr3ajNflmmOwkrMnNfzOOjxpNiGAQf9PEedgnjHApPTRHIEY5rAAVkRZySvuwMEiQQGBSKHdb396c/Ly+OXv/wt0WQKYcqMxKJuCBQG4VHLpvW1TudGh+oOiOpK6EfrgHR+uftl/Xa5e7j/cMtxAwEvL3td1t5o3syHtZ+pnV2ct4iXlz1g25yfTdv59eev/4b447v9Zqb/8P/+w+EQy7I5HA4ILiRuRhAGiXVnrglBkky5SNPPRtFMwwT3ADOWejZ9+c0X//pfffPv/7vf0ebMgszNGZtYbKeCvjuf51rQ1n5ozy9Ph6e7y/Od1K219u72nfW22521w3FZjvvHlw8/fbj/eD/Xs93ZeVseqIqFc9D9xwcEvLm+buty2DcNPKx0LvP59dn+6RnctLfCFRgsxhqtoVLYXbmIqh0Pi4g0jctXn23q5vVFwZB2e4y1H1+WYIFaShGkuZbtshzB0C0UTmFSnq8tEBCoE7GFQVqrxrhtI38go3sG2oOf6F93/3mEP20AKRMKPCnN0imZAn9whGxyzQbgGJIKHG6AQVqOaKcY4KUPTA8HAD7wIXAAQqLssHIYLYnpxsEIIpmsOyIUKQ6+2np+vXv9+WeXl2eEDgRUSwBEmEyTmROC4Px4PG7nQkVmpnVdM3YsgWgqlWTD9Rz6xm1xG/xERun5ie5TNRYEH5XmRFhqgYgBs6YKJo/Qks3MQQwoAgQ4+r9zLid3J3ckdMs2LfwZME+ByuhQA4DxZeYN5KerdPyBvO8zB9XBLARBOHEGRMCs3h0V1QAQCB7h3aRKljUikZkS/Vwvmf3QhPT/o+pPmi25rjRRbHXb3c85t48bLSIQ6BuSYJNZmVWl0tMz00SaaaB/KZNppoEk03vPXlrVy6xsKpnJJAmCBAEEAtHe/nTuezUarH0uShzAAsHAjYhz3fde62tT/+sWpeOEObJ0Rc2gpcyDsGQcUN+zhzERAJm3HlEPuzWiYbrgAJGxdRpQ0txWSlruMvM11Hzv6O7BwfFQSh602cPmBk5UTQu1K6wUUfe855DQzM1c3R1iIHj/8eH47z+eVle//oc/btYazASSpugG2LSw6+QRsuIYG3ayG1s8u57T8t6eVYydSxPyn0hxyw7n7JJqX/wxfgea7A09YMeNR/NdWxknXS63RaCczCbkyzfLw67rDg4IOBz81naCO/E//nj9RLQKjsRusQ3/2BRM7tS+/W1tzF26bSuJUAECoaujUHB43U7r66urt/tHh4Vnjn2QYtRkBW+zrSnVeDYyd45Welxv14TdMBwBwUjrgbnv6MG7j9fLGyDggLrZEvlsf2Zelku9uJ5O7nUdd4A4TXZ1dQldJ6W88+79zz7Ze/byYXe0///6f//tZrrm0knGq4c3x0Nrj6UwzRk1c/UQOatridgtIJTBnHj/YO/0yaOHT+6ePtiDjq4vz2Byg4Jd9cm3wxjol5vV+mp5Z7+/Wa/Y42Bvbyb9ar19c36mtrl//2Q29NM4bTebN5eXL1+/XV9sjg5gj2XvaG99c+XGfV/GjV2cX/aLcv/+vR+e/5BvrGrMhx734ObmykIZWVBKV9yt74qpFqKgjthYWNVvNtsSYPi2nx0CdbOBD/eHdR27rq42G7fqyMFcZJj3g1VzhmmcPCKTwogEACgwCMwsZ4WshsU24zdENA98BISwnQAHMM/3+BEFolstmTvsUBcIS0kRA0YY7bbrW9x/l66YTUSwA1YAGcJ3Tg6gPG0C2iUBGHmdRP4f1LSpSIRMCKS1MgqgAHbuarZ9+Pju6enxrBME9d06088GJ3JDZ3aD/flMhDN9WgqXQsyYqWRI2M0Xa1lMFZhQOq5jJaFAyO7UhBapGaGCI0SasMcimioHya0VZKqpiCCxRwijqiKl8TSYGRFYiptDRBF2D9cWlY+YuvQsjkezAAS1ikRuvmv53qGdhMRo1rzc4V5EwFxy6k8lfsucA0cErSbCAKn9d0BS8+RMEpzLuvOIKCKq5m7MlJ0wSOjeWiHVnAnTNGDhNk3MlOlDTu20y7ym8F0QToSZEhIz5yqnakREJG4JCgICaR2pF7U4PjnePzjMTZFarVyUQhDYS8nlAZAs0iXhAYgePaEDO2ZfD3QMnzw9lf/jv+u6/p/+6++XZzf5KYdlZCkgCoRiDjyeXqoWd5IJRXH7R8uFNUekBhH67p+tO/7HgRx3RS07YiSXuByNGqmUqTzEoVCX9Wx1tnwVZ/vw5rB7ePdovN6bM/YO9aAezoeCcjCfD31XmJmIby+UvBdit/pAwt/cKpywJT7nYt2oucYTA4Bl0XUOy+iBzBEaruNmeX35RoosDo6oiLtVU4yMKWnmPoIgLh6jlA6RYCCtLKpmdbl5q14LEwFstxN1s/X45s/f/PGdu++cnCzUVVXne3vm3TTieuOLAw70m+VS5rP5YiFDP242V1erO8d3fvXRh9/+4fu/f/37QHMcAsCsMncWwUAeTlaT7tyxINlpjgGUbeZAYEbzg8M7Tx7eeXz3wTv35gf9tFzudTg/mXE367v98+tNV/r1yiOm2WzouuIjPvv+zf0H8/35gFsrhMF8evcgatzcXKGXWT903K18c3F1bSCldKXvLBGZCA5+8fz1h5988uTJu999+32tm67wdqNcaJgN2+0GybVOyIIEgE6UszmIECA5grtb1fVyvR2d+rmUvf39YYw+eARf3azHsW4cisokpRNhFB5krtVNwsYRPbIdG1wh244880ny6PAmZwPfQe05tEeqlHOJyKEizSbt3zyI0F13jkffoYsNqt7tltDg/aRpwPMmcGjxagDQGk6Qc1WIFBSm8xKatC7tmcB4KzdFwJRtEIoHmlnV7Z13Dj/95Ok7dw878bAKiGpeShdAZlAJBLF0yBAIXgIinAm7rsEpUthItlMv/f4EEjGqambUI7RYsPDgrrgHOiRqgjsXDkKQEAWmKoqZ1NQd3A0gCHL+JnB3iojoulKrYio4iXMhQCIWyo2VsCHonnRLC2eyvH7oFnzId1kYc2OCyA8TESWSjggPDS4ZK4oBISKevQ3SMl+JgIXNnW6/kwgsZRxHFiKmpv2H8IZetRoEYp7q1HGHAW6Znp9ZxNBeQqA861KjwS28LIEIJ2IgCghVzV9PIAHBJRt5YtbNhJiA00AB+S4TAEL6qhFYRMZJU9Gk7m4h1LwVFgTu7nY4k598dG9v+OXegv/2b3795odLFEFOzzRbJgpl2BTQbSv2DmhpspJbijVn8Nv6mLYNQ+wK3/OnoAHtuIOHfAekNn4WAMDDiZiYibugICGwrY3hWzIDJr5ZLm/m3cA8EJTZLFBVoTTrHBIEZjv1LamBkIqmJufIhaNBqQ0TiltSOi+qtMY30yQCaOi2ThsA7YrQ/mFXhgiwJBjb38UDglnc3U1bVxEAAFkESVe1at2GecUwlKre90Pp5pdvzr/X7w9PPpzP56vl2Jfh/vuPnr++Ngsd8WYzzhb93sHeZtz2Q8Gu/P6PX755df7Ou+//7KMP/u3fvluvNwAYwFI6aL1NhruZNBOrEoXmDGOCiF2hxWL/8OF7T48f3D0+vXNytJh1s5PTg/n+AFAceb9ISPf1ny7Pvn3z+N29B3dPyBTACgFssQQc7A3bayfHl89ejJuxblaHx6f7e8PhfPFarwJgs1Wvtph32ElU14rU9eDTN19/+7OffX733ubN63N37crCvPZdQYCqtS+JYUUpCCyeyjoz6YWERMQsDGOsW6zuMUGZd7SYd2D7exbkVLdT1O3Wp3FERilSenAsyKUr4WFMVi0A3GsezBicGHojc80zphYDPF8waDjw7SmeV+qOXPJMO4TdMJ/HEDZZM0DkCHUrl2iHwU5wlLyuQ6vvzDcOmxE/m/lyy/BgESQmoAhEzO6oAEBvK0wqkgwwZvvDT3/2yXvvnB7NGG2yQBEmykI0qu6AWApTRJ20E+IijMGpWw/vZgUDxgjpB+xmiVthND85M7lbLh9Z2uVhiaECQiDoNBJJblEs5B5VJyISkfz7IpOmV2YHyY7TRC3uH8EdkTIiohWBccI2rFXDQUrCM5xsFxMDADNrrciYaQ7EOwSpVeq6SBHHAMPsKyDmcAVEs8qMyTlIKRiRre7MBBGmTpy0p3EhRvIIETb3dEC4B4Ln2aK1CpO7q5mUQpHBzpbfasKs40FTZWZ1o91RxUS1KmFe98gibpZXHyJIRMKMqlHVu4y6JCSGyFrFiLAQIkdAgFkn5p48cn6V7OdkD3BTBas6MH3w7p3Z8Ku9g9nf/C///MOfXgBQZreGO7G42U5Fn8BmA3Ia1pltSvDfWcNyC8ZddmHCo4A7pUQSXPlF8phuh29qUTzV1u1XWfiU5tV+rzs86B49uvPk8cnjh4cP7x7cO1wc9P3x3mwopQgXEvfAlPc2pKrRyzshNSBgskaQmljc4YQEO+NDXmeYP+vhaI4IHqY22nblPjEEEutoEUSERVgzkFYt3KmlimHVSoxhEOERLtRhiTqhVXNXZgwixgIsh3fu6bi+Obt4/er89M6+GV7f3ARcd2X+3fOX3WxzfO+49LzZrF6/OgcZ3vvo4//D//l//M3f/+bt27UT/cUvfvoP//Lr5cWSRQIPPRRRIBwia5bB3TC7VMMVAsxKKSBhYHfuHD9854ksZuNodb355svzDz56II/vdt6Z81QN9uBg6Au8HBb88NHpwXzwuiX2hw9PbL2+ud4EBgHodrq+PJ8s6nq8unp+cLLfDd1s1q+2XidArIxYOg7Cbpit1yMzsPnXX//5008+PT+/2kxbLtILh0NfepGMcApmMq3cUYcSYfmkMbM342IUFiQeDabtdjIdJ95sDLjI0AkadX0dt6Gu27XBCEAsjMiAYA4MaI3IzWDAhuzfgoc7lUZDOdq/Ie6eq7a7x65Tor0C+bLuBIkAkdFXbbVs8hy4NaXc0jOwA0Pb7+xx+8M2oSDirp8i13RidsCIIGHzQCCWwQMBi+M038fPf/np5z97//7JrANjIQjOZoSkswNh6DoE1Dp1PffMnVAhUlXMukkMJuZZr9sRuaOuB10BoJkB5OnfJiYh1loR0dyz0JEJhYVZqmrWQ4Yn6J6Czlw1SnppW/87toy8QAYMTNw1of9MtnJARlWDnb8AALOsjCgQiQhTrJEfl1vKGlN8b8IcCGJVUZCYE+lLDiDN0Il2UHp/PRvakQi1qohgRGRPpAdKS8vcYclBhcwMUqxaRM0gQ1sjI4ICPCt2W1xqNNdXCFFAECIy1ToRZ9tySxaMcCQJdZHSlj301c31dr06ODih5lkIIAyzaGVYTUZAiEhs7swURGYW2XcEAeEiJNyZex/x9OHJ7H/42enR7H/9X3/zh998M64noGIeApRFBQAGBDvGpz35gOQeucfcQpTA9KNAwlPCFQgZmZhgNLaAk1RctWO4UbDNVkAUZklpezWS2Ds5LkN/eLx/7/6d+3cP7hwv7hzs7Q39TLhrMA4jZWVC4O41a+tE/EivNaYZk7HLv0sScDtTJeRdhJBPGIG7mY7qo4UWKUIY6xsiIpYIdK85BpoaEQCWCIjQRKM0PNTBQ4RRpaN+pA4iqmromJFhe/t7cffBtJ6uL1ZPnj40hRcvz96+3Q4H98fJvn/x3cO6ffLR01HtN7//6pPPflLK7PXZ+evl8u/+8Tdg9f69hz/59OO/+9t/QuJAhUBGQRCDGugQDgSIjgQ2GTNBAbexmu7fOe33j2gY7j86+fqbl9N3lzzAzfX+V39+2XWXm9Vmtje/Odqv0zRt6/5cBmLfrDbLlU8bARh1vPrhJURst8vD2ezugztXV6vX19vl5dsse+p6WY81g/w2m1r6ru+lqg8RUYMEl1ebs7dnTz94/PWfvq2qXeFCHSEgOoZXqxhIjBSBGI442dXgJ9Fs6R7hZhoWXBazWVewZ5Otr1fXm3F0M0SZkQizlA5t1DC3aqGKjBZgQBEGQOAGEQ4VAAgp3BpW37aBFvzVhgiIyKywnB1uk1puH7hce3e75e5Mv3UAxO6RbCtzrhVtQMHb3MXcUxtzBrdiCUAkDgRE9vQVJHzhyEQAki2Rqjosho+/+PCnv/ro3sM9YlN3DDJARxRmZowAYWCMwjj0UgoLEagiYSdUCieo1TEFQJcBEZh4Cbg6MXpeQoRhUN2yNyb/lKWULED0cGEGgGgxopnWSeHGwqZjqkhUNdVzmL+UqE5T6QplqS+ym7OUDHluGsoU3EEggpSSlywCmjcQu3XIhO4yF9rbLlLEw101g6oRMCMZEFvOeLg3o29AeExqLOnBC0QgJsrSFUJTJ0ISDnf0IEIicjVPxj8SkAgWMrVbtWHbEsORJVEPJnbT1NjmQJDJqwjQ7G2MgBbmAVAIt1fnN+dvDo/vEknaJDj9DhHIkAxkWKY7AxMCAQuZIQQauGk4ZMoBJG/hbg+PDvb/+mf37z/4u6d/+Lv/8s9XLy9K4eoeFmg73XBeXHgb6xZJRzUJArm5IiIQg0dWW+5GmPYf7sCXNu/Hj6d+qo1SfA/gmqyCQ4RZP3SLeXd854A41pv1xXVYXU43VyeHBwfDbG82mw2FA8EJuVXK7HbKnNWibd67Rb79OOsAmweo0WqBu3EPA8LcXG00n7zWUoa+FNN1tQ2VEghhRojAkqFUAOKuSByIhOKeLYwR4RJkHlJK183GbRVkpahTnc/3PPTozoO9+f7N2Q8RpdZRp9jUrfXj/vFRdUacL9fw5nxzs7Hzs+vL8+tXL96+Ojv//vXr/a5sN9/dOTn5T3/1119+8/16va1TlV5q1YZ5QkYfB7VkC0Qp0mGP8vjp45ur5frmZmv7fYH9g1K4QzccwXTy0WKwqHVW+qefvrNdjeP6gmlm03i6d3B1dW7TenV9c3b2dihdfwol2Ou0ud788Pz87Or60YMHBwez9Xoax7UbdSKbsbIQuDPjqKYeaPHs+x9++sVnB/sHm+XWIdQdyftO3GsnXYRlZ6GQeEx9Ocwg94iUAAYLjZMRKzh3paseh4dHU6wrrHxt03YDVMIVELgwEpKzg4IDmkMECiXB2MAcgFaH5xBh6RomaHYRbPqfPOJ3oBDYjg+AHd6Iu4gT2FGXsaOMox0AiDvmaXcN/PietLhnYG5frWVKAzgEesM08hEWDrh1raJ5EHiF6Ib5x3/16S/+6qOnD/Y7MgMXIUCx3ZZSKJPKtAAOhKV0FM6IKFyIkKIIm/kOfXIAV/dp1IEIKRglIiGX1D3l4tTyZREpK88ym9NdG1MCwcIUKRJBsxR3ZT4rJv1CnF3rVkqXsicm1lqJuNaJhU01cQIKnFw5g/UZa3V3NNU8w1mKm0ZKi9IY4akXAUmFULPDpxbFPSXSzLm5NQl1xoJ2pSR6I52kB9iT9yCSjjNPgkXclJDNjIggazPNmAjh1pcUZi7CbaagPN8gAFyNmKRwBKpWESZJWym5W0QwslmgA2T20M31y69/d3xybxhmgJi/Y4PPIZGv9vAxcsIZEI6ZE28hhd3Z2wkMQCAiZn4w459++M7J8d7D+0d/87/8w59+/62NgEwk4IbQCu4bmNOeWsSgtrymvy/CMQJ2x3BAWwAywjAgAKi9LbArFk30xXflvdESGTMmtBtmgPDqh8ubm83r1+X5i+7JO4cfvnOnP70z9GWv782qK2FHhE3eGLKDlfxHjRFk0TEEZONA+kBxZ6rJMr5EhgiigfgWphBGiFSKlC7cwS3MmVGa3remYhja3c2IQCQRKZQ2IXZAZGJTtWZ7SX5lNpP5MFuu1h4xOzy5uXi72jqErDc2TXF4yv1s7+jO8PVXz2aHpzaW9TW++uHy4OD5sDgYNzattTs6Kc4vn/9w/+T+L7/46W+//PLy4gbJzEehEmCIQ9goPdk4iZB0XMNkNjs4PA3zIkKEmzONIODu5NG9Z9+fffX1q7/46y+evfj+CZ389c8/VKiB0/LtmW7XuulODvoo8ezbi7Ozl4B6ejJzBwxlV9Cpqm22sLGR4c3+3v6s77xuTREH2U7WDd7PCCffrJVIAtirv3j2/MHdO9+snm3W6729mblWFWJmgdgVu042MWIQmFcGASYWQmQ17YfBwMft5uZ8OcKw3HIFYWZmVEPXHIPCTU2DESGMQHJS9YyRiSYsSX4oz8isrmgzQ0YF7LqsUxyc+EETgjfri6e6BwlbMNwO/W/57wn37zCd3cEJO5FWXi15HFJEpG43dmhUamkiAqkgkhC7G4J4gFkahMkC9o6Pf/pXP/nlf/j4wf1+CDVHLqJNxYKMXCg6wTAbOppJiTBB6LqSGdPZPMWQ6QnsDhpR6zSOY1gERabfAwASmjuoZ6aRZeBdhHBRqDrVJttnhAhruVVNN4VIBGSTU2H8MWQATXeJ2YjZc0jZu0KISGbWilWQIiKbgdQ0fda7/Skisqs5GoAP6NFITWQSCJCS3QJNu5Le5cKtPTWj7Mxa6ERWD6sphWcTcf5mqTFK3t5Us3wyadJ07ULG2SNWrRlLLxkvYQGSnxUyi+U9gWju4MAkGYgECFjEdRIqyaMGITN5WI9x+fLPb549HGZ7h6f3iBk9CDBbIcIavZRRHImtCHdqGmAonNXGDliniqnn5axHw4Ht6b3jxX/8ycN7p3/7X3/73/7ut2fPz5wJgAIwoyxSw0NBqYMNZE/1f0NyOAIwALP0oamhm5wCd3IIjBRK307r7f9vUl9Eay5wAoSpBgGsVxuP9SAz0AUhSJFMV2XkpLZy4SAAyI9/t5E3lKklVUDb3aHdT/l2A0DsaLbUdLurmVLuCYpSCuSkQqY2CfRNNdTMbEF5AWHLDPcIb99QCAc1tYiEobthMFNCZ6JJN+gKRI6K3bAat4MULsUnA8Nv//Ti5N6HRuXmanrzZn18+nCrVbH/9b98xdIvDo+mtd394N3n//zqxdvXH3/20w8/+uD3v//TannNhYghnJghmMwr951NCmpAdHB8fLh/GAAvzt98cHhwc3O1vL5adA+Pjw9vzlc35zevfnjdCx/1ewKVxC+vlx1WG1dfff3VbCYEcXV1rtubO3cP753Mby6206Zulturs5txu5WONODt+TVANwyL7ag2xWarHdE0mgi6wWxvGEcVlgi7uVrdf/BwMV9MdZN0qpqigweVwuEarWfJMWLoewAIIrMgCkYiIgQJxk746nq7WfkUsqlQnYXnDugRmnx8DtEOGrexrmmFACR2V2zGx93M7tZgfNjNCa7tNHZrEbaNm20OLEBMeDrPoFteCXf810780H4yg72w2Qrg9nUICHSMBCGatgIBI4iRyKHFeJsTIfN80FEVKADuvHvys7/64oufP71/0hV0ESjBEwQi90UQAdwZEKrOeiEAQUcEgiCAXhgCyUMtkBJRRYDYbsbtajWtzoSNAICaIGfnowdmdvPMq4MAVQ0IYTHXvptZqJl2XZnUgJFZTDUVtpnknF1gEMC5SSMSkbtmt4yQ1JgwsWsHZkYB1/xl7QrnnVFXmKx1fIG5A5CqSRGINIaje0iKMlnYLbCd+ESEiJQ1x4lnE3JaQPMOYGEIZ4SAxtKn2SGtz8wZRtGyw1kocypUa2IsOffl5JtNZJnWYWpIaG4ExPmU32IVBLlD1aosgSzhqKYESIi6uvrz73/d79/p5/PFwVFrtklskTBxmciMoeRB3JjQfJdNAsCEULg1GQDk9cjMBHH/cHH40yf3Tvfff//+3/6Xf/3yt1+vLycECuJWpOuBREQRkPtTiaB227T02vb7RI4KuDvtI0VszdZyK6XGjFgLQ0LfvXs5hwNAN8xkRos93juUJ4+Onjy+9/D+6d17d/b72SBCwu6o5gIE4Mitiy51/4C7oLqdXGMnNsLAQEeglHe3Cc1Ns6wwc7QptaIdtmC48FoNiaQfkCjCdmMcpBjZIZFATK0NEVm0Eu0IACIKLNx7F9UqoNVxM9btbL5AhL3jRZju7R88eGf+8uXlyzcXf/zu8mNcAC/+7m9/c3rv4XIdAfT8+fnV2/UYHFFWm3F1s9bAi7dnLD0KR0zHh/sg3Zs3Lw+OTrphb3l9xhb3n3ywXW9dR63jwfERguwdHe1fXWMZjvcPt+PSJ3rz/PLicr1YzC7fXjFM9O6jL7/8c5FAJpum66vL7fJCaCCE0GncbGE7o1js7/WrwKvr5eVyu1yP6gDUqcXFxfL07p2uG6a6Dkfq+jA0JUYHNAZwDek6VXv96u2de6fffvd1bGDoxdVLx4Rg1bNAKR8pQgQPEqmqRQqIMLIFSCdBtEf9hLreXE/LVQSHF6u5sAIB5WXsEeATYgEgBItMac+K011gVUp5GjzTWCtPGjblNYTc5oWc+nMQ8Midm3Z8b6LzcMt87R74dplAaxDYUVBtTEHAQG/1ZNSoh9QWohOhAJIHQHpCsSBwOCuyFH74/v1f/aeffvzpg+N9AtsmZF+dnHmvF2YaClmtHYAEd4yFiQHDjRk73hWdMBZBAthMNZTU6Xq1vbo8t+VbiTGD3pA4TCFfDyA3Q8wsNshj3TSIgFk0amb3G0RTw98qfgAhJeAACMhMqSyKCPecohiAax0pw/Yh0/vNR5ciprvGcMQAd3CdlInyozMzbAJ0crNANLVE+sQDWsVuBEAwMRGaelAAoqkBJo5M5mrVpMjt6lZVpQG+TS5AIq6a6wYhSOnMFeBWX57fPla1nFKr7hJFgtytaeIJETFbzQAwC6fAgph2c3GEaUB0Qq4KDmB28/q77778hzLM7sH7i/m+MCMEIwWDm4M7iRBhWOTuBvkda+xUW7tYJNE77gZzpzS+Osz77oN37929c/Dk6f3/9s9//Mf/+ttnf3xu2wlEwt08WitOq3Q0AAhkRMriNgCAIAgFwMzDgGAI812HDFKrTMqLAbxVKeyWBAaAsCCmrp/LUIZ5v3fADx/uP358+vjh3ceP750sBtboSISydRl3hNtOysNNA9pUdwHgvkP/og381KgNbF03/iNohVm41L73HmCuDODmhExA0EICMGmopm6NyIqPcDM1agmFbWIS4pCOkLwH2yZDbNL14zgyEXeFoURXunnZP6ablxfr1du/+Z//P3t33vnyj6/+d//hnc3oVdfLP242y/Ht+TKgU6Zn371yw1XFV69fvXnzmpH39u8EOcqD+d6puW+3N6q+f7DnHkT9qxc/IPFyfUPbHkQ2FabLm5s1LIrKMH/y9J3NuHz+7YtP3ns8k2Fv3q1vrrjIZrXZXF/N+25vMdQ6zaRcTsBgV5c3FnF1OV1erJarStxD6DS6V9qqr1cjixTpqofqpDUCue8ip2xzBINqdnV5vTiYHSwOV+sbhA4BECgBXxaq1SLSf2rGKBBEZO45frAQIkonXmUx7+6cFsdyfnmtmw2jAbCn7TcQvMkuwms2g0XcYpIR4Tl85DyXnr48zqMlgO7Gdq/Nx7nTcCZam09UWgoCDFocehv7cEdD5a93CEzSYUcYpBoNmhImGjSNHICmTiwABMjuQFSUIBSRS6CMk3WH809+8fHP//Kjd58c7Q3gNhIYE0NIV4Sy49EswHuEHkEQ5l1hAg4ATD19+4NKoVp9cgtCR15tp6ub5eryBU4X5FMqPrRWZk61q6ohZu4iBBIS1lqZJQ2z4NZJp2puzixI4WH5skekqoYQMYW2mrlGEEAgyOYtKj/fVibRWoEgZdbwo4QKrSoRZ0ftrsMbiNGrIXGaJrLdi4iEEF0NgXbipFQzZpc8qWW/s5FEBHR9yVk+Q6shQM0SQXJzYgwPKRLtgIWcuVPqToxW3cARQYoQULWaRxQxmyoR//ea32ShivRu2aWAjKRheUEAAJFYreldKAABdvb9H7h0Uvry6D2eLxilyY0QkDnljkgIBOZABARIggjoCsgNr9pd2poSRkTyzM0IODqY/fLzx48eHH7yyTv/8I+//+e//9cfvn0NjkjiiAwAHsyYkXk/atyovS2AjG0JwZ3RPdlgCLf8u++e+/wR7iBRgAAShvC6WWqV8PFgMVsMxwezbr+fFQNfT0VKJ5QOGSbOnbxNFzn1J+yadut8H9vtFQAI5sgU4a0TFdAhAhyZs6SsLePoeKtScoeAUgohISEXdlVEZurUJyRAYMvTBCIDXCmxWkMRrnVLGeVOXKTbbtSNpHApZdyugZC7Uq2OoyHjwcHe5599+p//v/947WeI+rs//cukUUofAJurSbrZzdXNerO+s7/3ejkS929eXd9cL8ca621Vq6ON0m2ABBim9eSm69WyzHoSAoiqdb1ZlkJ9V0rh0mG3KHv7i/V2qTd+djVRmX315x9OrrqLN29ubm4WA9y/tzg5mu/t9aayjAoPZh5wdbG6XNl6a+uNr7cxbRSgoGt+3OM07XUzKaxjVQUiCsMINNBuyEnCiIu5X7w535t3o4hbdJ2Eu1dDCJs0oXBChAAKcg3pyDD7xDtiAka36GaCUYL7rcZyO03TNtQ8AjHdQBgY4XXnnoLw1hQVbm1yCG/mpdj9DERklHZTgnpmP4R7ixlIZVlja2mX92wNdWzPcRtIcgmANDzuxNO36pS8IwDIvUXggaeeApu+oEUWcSEqhJqPJ8PDd+79xf/wy48+vn96MnRdhE3E2JUhggJjYGRmARPCDr0gzRiLSLgS866eJNCjIWKTEhEgV/fJfHmzvr54ZTcvJTZIoW4IyBlWaoEiRGFahUpE03DnR4ABRBhEFu4Ibo7oZlak/UW4Y29VS+7hQgWpFbTkTEaEbs1clqsZMbl7jgXI6Ikkad6rQEhmjuApSoSIBGYKdeq1wTkREjtlkk3KwmkuwABG0qrZ5IUte6kpjhBQTYnyzvDMyaRb53FAWBBR2kiw4QGJAOyGzwhHF6Ycj9sUiQ07DjNCyegydQUAt2CmqjX70XaEUfpRAQBIoAvUur5+9rvnw4wB7jz6YG+xV5ghrIXCQ7gF5eNPiQmB1xbNgNRa5lMi0oZ/g4C80gA8zL1Deuf0+Gh/8e7je5/99IO//8ff/9s//fb8+RkqYtcFgLkjEgqGOwQ5YoQFJZGeB3KqofJuoPQ/Y6sE2PGzmHwapGkOscltI8wjQMfZAvf2Tvdm83k3FAJdb7VnMlvWSaTr+44JJNXirZx59wJjJIHit0HQyTbf0kaNZctT+1bL0Vj0HQbg7kHEEUrCAUDMEUhtZgPVioDNkt4EIWm9yq/hLViPiIEcnAXZRbpC0XsouCUMFea11ss35xb9/tGdUhbvffre3snRz/Dk1//6b5vlskK5vl5LWQzz/uL8fDbwOG37UtT8+++fBSBQd3V1Y74hoTevX80W++DGRKvVtW5vwKehl8vzN0i8ubr2yVdwOV8c9tS52/mbt8vrM8f+sw+fDN1svbp69vWrmzeXV2+v7j+ZP743HC760uFGK8Ykpdus9Pxqe3kzrja2HafVajQjdzQPQoaIaaw2H1iE01NI4ok+BllVZgJC9zDzqWo/HOn1JTFiiKdnh3IlQwBXDwhnThLfWAiZoTV4G7EAVCn9AOX0ftk6Vj/bribQXFBbjzUiQVgmp+3EDJBm0YYT7oDsnIh9t0FmDgsA3NbBtsO9QTftC2X8bA6gkTaXaEGJTUG9OyB2u+Nu9k/ktsWpYFJtxAJU3A1SQC4dRICwulmEES7u7H/0k09/9Vefv/PwcNZDV4LCUFCKjOYWsehK4Vycgt0W80KGQkjgnD5L2enrBYWoToZMtYYTOchmtb15ezGePeN6LrQlApuImJrOCUFVEUJKSRg3va6e6f+UWWQYnlZn8oiudKmsgV3EGwBQet/CduKuJEzDrck0UqlVTQEgW1g8KT0EBDBtq5u7E6UEI43HgQDINNnUfHQIESFZAxlmpVDGzqhaKSXMs3clIpg5i4Il7bstAQgws9gImNDMEunLDdDMADDIPULScFudmfPxjYgiUuvEzE0Q1oS/aNVYdo24yBZpAWtioeR/3AyJhGF3LiMSMpME1c3qxVf/sl2NTys8ePze/sFhL8xEAd4uZEQg4DZqpG19V5PStPEYbsLs2HIjzAyREUEKmzoDHQ2zvYfd3eO9j5/e+/KLj/7xH3732//25dmrywiWIkGyS1pDoC5iQnC3pKySNcnOl52DzC1rCVsjV+YDQoPCmLhhiUxUBAp1Q3f/3dOjO/tDP4zVLi5vupPFbL7AFnOUd4xlHEp2/cZt/kSuI2np9cxxalc+ZJF3c20CErb+zHBoqHEmpRIAMDpGOHKYCwsyezXkdoO1lHakcMXdc42pZXJLkgsJMASyOLqVgTJJrwoRNefMHJ6IYLvd6rQN6B8+vnfn7umjh58dncz/4R9+Y+Evv39tONnFqhRw0KublalstmsdR7dADgtDDAmsttZxglAzffP9C9Q63Wz6Ybg4u+5mQ+kGsHj19s18cffg8OTy5dXmag0x7R/sH56eXLx6s6qvF3O6/+783oOubisVQnfQcNXNJrbbqN6P2+nifHtxcQ3M4ZJRVyIMhhasppvtNMx6Ea7mFuZbY4YieR+HqYp0iDBNZu4H+4ermxsUKlHMqgPlgibMgEGIHlkshR7QMWEhC+DCxDSqax3HzbRZKbp1RbYweiBYtr+0UTtSzRmAO48XIEYoxO3wuouAxhYIlQd9RLS67Yb+Au7Exth8j7fKg/zJlnW4QyE9n+8A322qbRXIcaEJhQBa7BeSB+bgRkgAzNJFnQBZhcq8vPv+k5//8qcfffzk5LRHtHDXar0gggSgEM2LzIUEAiMKQmESj8KMCEIohXGHUjkSCVHi5gHIoo6r9Xh1cX3z5nu7/q73K/fRBYXR3TSpcmJkAAszEy55bZoqErcizQjwICQnDLcig9qUF2s2CSfmwcwQqG6I2BUxcyI2q3lHZghb3rsiklUu+V/arvLaVEspEJEpBu4U4S2OrFE6YZMyMwCImbEQIGfaGSEWKW4WEQU5U33yu4uIptZGb0RAypx92AmMPJLZ8KSnITDAh643VwBIaOh2jzA1yFy61BcFpDc8qVDzQAAmTA+BVgVKH1qWZgEgZmMaE0sBRAE0c8cw31y+/e43DlGr3n3nycnxnVnfEQATRYu3BRJGRrcINQcHh4TzCNEdECQgulI0zC2BGbDqyMSZ7cdUkO/MF8dPZ4/v3f3Jh09/86uf/bd/+uPv/vUPF6/Owqx0gzkYOOoEmN/4VNI3+iynhoTIdj6aCAhkSotySuUAwNwBg0iQmEuRedcPZbvZvPh+NS3P1/cP7f7BfNbNB+8Fh67vhTHADZzDzMtund9pq9vvu+N+k2iDAACz9DRwljpFBEB+I9KaF8zRhCFASB7mYYGIxG5GIhFGzFprJsqZ1pbrEo4YwATqEdaaosOBcouXIvn7axB6TCnR01ormrAcP7hTrsftuI3JPvrw0fJmM9Llpz97P2bdxflavfvy375ZHB1e35xdnF9MARLzcTshmFlFcENnFHNCCK8jEmFgHatEYKBuJgLQ9bqulkxEWFbnb7erVVgtQymdnJ+9ODxfMHR3Hsz37h3evTtj91A4WMynCptq59f2/G0twDfr9fnlar3cjuvKs0TsUiqGDARAUw0e63w+kxJTHc2JAiKIGG1ydXNHczdwBru4vFnMOxap1UiIWByh5qIQFBGOKAW4CDFQYRYOCiFmwcAY+rKtwQDgWjfrUJNuAAybLCZvGW+uu5EoUQK/pSTb5hcBSB7a8Pks5IBojSCxk2sCAjg0TSMEJcSDsJM97HinTFvdLQnU8II8W3LuyDEMM1EmbZWZ+IOUBAYlrosYoBO62fr03Qc/+/e//PzT9588PBwGcatqFc3AgrueEAYRJiiEnQBrIPrQUWFCgNIJE4RHurPM0khFWq2qqUMAT4HLjV5dXF+9+W775sveLwFGInJVAGyy88jgo1BzRDDQXJaEGQgNsBWqm1lTQ6GDIUEGb7u7cFFQADSzcGDJ8DsACDMFByRCQndDRkTk1geZ0hBDQibOPshSuggXSUUluxsgqGrX9REBarGDZohQsOEL6frbeTwQmFsRFSTWhkCFIhM6CVNzCohq3hIGIlCoJnSO0GowA9VqDvjS+kPAIRip6iSZVkHUThNzYkbCTBUgFtWaoCQxRgA42A64aLqAzKfyQM6aAQJG1QDbXHz/b9txM67/Gt7/+M7de/OhZ0ImDldhMrNQb2o0xAx/AnMjigBh8ohwJyJiNHcyJ+ZAau2aAMgU6kh4vJgdfji88+j488/e/fIPP/mXX3/12998+ebl2+1YSzcAC5NYtV0gMUXz/rq7MWWvffuSEAkOcgAwcwRmnAMyApKb2gRmY0wELrIvRN18Pj86OBIRESEWYkGADBFsNTgZvBcRqblzh9w5qeXapVkhd582rmEzZqbCK1ezAID2PXVogjMMwOoByMQZcgIe2cjBbhUhO6jczSLVWoy3AnBE8jBgbtkkzKV0AA5grsFh7Apu6l4Ws8XR4uwP39599OT0bndzc3N1db7avHrw7uP9O75aWeHZGLD88kqEptV2CkuIKRgIDd2CItcSAyOHAAzn6hNjsWpcyM0BTG1LNIRv62Y51Slu/OjeyWzeE+nQ8wcf33/8+FjYChk66aY+f35+U3WK8v0PW2FBirFG9QgIrcER6g4RGECcF32napvtctb3wlBtiujGyUQIkYhNRKzCsJhN09q98XuT1oLdpMqtYDUbHJEwGIoDslDmnwIEcZoHXXUbUSJQBPtBblYbq2qaCVno5gSMbSRx3BkRI2IXRN5AnPBUlac8QCF2z4I5EVMW0ga0IPTbRyinnEBouCemNzCLPFIz2pAE2BHKt8c/ULrXW0ZAuxkcQokRwFBw8koI+/f3P/niZ3/5H//infuH0jmxhVZBJAjuiKnrWUQCw1mjEEnA0AkXFrBw7IYS4IFMGEBYqwagcCYuMDCqQzgvt/H2/Obi5Xc3r37X+2vyJVGmLWc2KZg6M5k77FLO1ExIPLyaCghgs1ipWxawWJhpy3AORxSqphAp+3GgUFUWUdcG1kE7ZS2zu5EjwsPwNuEHMCJEpIkd0+ZIiba32Ax3JyTkzPBJvb4LMyFBmEd6gAndFBFdTXpJH1Y7ARqqhVq1CASCqjEzkngCNS0WuhX2MHECvpk0k5UFESHCaQpLGRMSM1O4QWu0IcB0qkARyX93j2w9bkJDBK2G6UOm1BgFECdJwIUY0GK7ffXH77abaf12/PAXdx++s7fYH4SIeg8jiAgDh2z0kSKeOUFmrcuCKRuvACEqUuFo4Z+BBG7WVMhp8wtfFPns6d3H945/+umTL//d57//3be///1XL79/sbkaEYOFduEoHk55BBPybc9q26zbO5fuubYCY47tEQDkrizgqqXM7p6efPD+4yfv3Ll7sjjaH7pOwsy0hkg0CVLW9TpLabN/tsInjJ/RtbskL2gZHr5To+60d3kx5KWerC9AApdpKWAkkIKAYQ4QxARO4EYIhpFleUC7NgMHQsmppXEdCBFAXBAsINyVS1GrVLAE6rQ292LoHg+ePJbSnZ9dT6YyTrEd63L15ME7+onbZMend//i333x//i//d///Ns/QxFzMycIRGHJNF0wAC/UOThUQ6wekN4KmyYAQiYINssYSkMP6YZ7D09PjvYWe7PFwPfvLO7dOTCtBGbV35zX73+4uVht+/mslNnVxRUK1QnGKSZ1Nh3VuJsRYZ0qESX+WHXaTlK6knYhJA7DqcKsZ/Vk9spmuxWm9WY9dIxB4RAGYDCNlUsqyisj8SDSCzGWvnhYzx2WAsyIJZB9ilD3gHGc6jTVzQROiOjR8Ez4MaYwFTsUoPijQSWFfHTrRc+0zgb6t9teI6wF3LbNIPJZbWpEuDUDNywnCcndcb/zFDRl6Q4H3iFK7oEk0MijpqKYrE51e3y6/8lffPbLX/38w/ffXSw6MCUJDp9Uh1JIqB/EkYlCANBiVrgDooKdkJpSG2AjMs2LUC3UnAsbeDCZhxWZJt+s/e3Z1cWrb29e/DaW3yBcAplFQHViCXNofmJ3d+bipoBE6MwMlg6sFgNcp9rtrFRIFOaAyZMHutNOo2dqyCRcVGtXukkrEQEGI5vXDIp2cGExhwAQZg3LaXIX1xGI6GYejsS7XDgIS77fPSKVnwYqZkaR+CuZm2umFiAKV52KcHNyQNpKgwQRJaWijEndYgQggaomqlNY2iQZDgEikoxEYcxlJNcTM0vVr5rCToAIYSziFlw4PIsHlJr4Nx8LYxFmAgACTI1QEqal66pmhhKieUHdnH3z7fpyeXO9Wf3i3jvvnRweDrOZMEsGTbsJh6q2hjVCbNIXgNbLzNkP52EQwAjmUE07YQ9XNaQgRBYh94A43euP97p3Huz99OP73/zVR3/8+vlXv//6mz8/O397vV07S2ERcASg3KyQ83TNTDmwgAyDwkwEByYOBE4GGEAdlYOHxfzOg9Pjk/392TCQFCABIiAhpHAAT3Sv8e9A4S3qMsKxQW2EuItxdI/MiE0ZNzgSUaKfgBnfnjAjIWajExNFHp/YWjJSQh2BqVQLAEJwjayzSNo/7Yv5gHqkEyAis6eytYU5wgk5gCkgUJB68IqBXSkIgcibcWKK0uPBcLherwnLuF7+/Fcffv7F5989e/EXf/WrN28ua/XtasOlI8zKJHRoEqZkjdwB2KuGlCAuQGbmTIJAARwIs4OD/b7MF8NHnz99ePdACCiiCNlU67ixChp0flXPr7cvX10ujq0vd8bR1pc36Rst0rtaOFmdmFioRe8ykFNvWiGChQCaId8VeL+Q0WazTrua1UCBaial3GxHswoerdwtLKfq3Ki4ExQU6pCRiUBEpKsGs0UhIxDGwaCbKcn523WMWbjqOxGaNZCmDeM5mCfVHxlm0LAdSKoskwLzG5pivGbWzf8Q23zx/7cGQAN8qJ3jzQ6GgQQerXMOdzYCbJ0ZsKPHkIQwWHgynXzZzeWzn332F//7X3724Xv7h/ssCGqEIeGAKH3fd4IUtSqEzWbSM0snhYI8SJAxqDQDbB6VWhWQ+l5QWFUhaL0NYtlu8fraL94uL14+v3nzu278VuJCqKYqiZg8cuTnXIib1p1SzENVNUdVYdnqxJShrQYRyNmlh+5OqdZMmxVz1YrMhGhmCFSrpnpFq/YdZWS2B7i5UsUMswV3s650gGCZwMjsntHcTYyaxQYpe1fNcxZ0MkSShHqsKkiuKegAzbpFvMPlGkuD1GKBdxoScIhouZ4AAIwkzNHMTZYyEldj5oQZTTW/YHMGEGJTzmCLuyOyqrsMy3DTxs9CZOY1QUHAAIMAxxAWM2sxUHmRAmTsJIL1FLo+f/PV/7a5enF59sX9J5/dffD4YG9v6AQDGQXIC6OD12oI0PWdRUQWRUR6mMEDRCRBUaIQ5kAnQ+RiHgFh5hDALEnBHw7Dwbvzdx7d++LT957/1c++e/7mj18///NX3z1/9sPy8nraOEmHLGGOTYufQiu8deMGNi8BQLRKNAwC6PqOOp8NCLYKLWevXrJv9rr7sy56cqAeidzVFIMQUTADRolap0ceHLHT9CFG6hOp0bbeIpHatdQgwR11FNZObWikXAuoyyMl8u1FAsAAi0znghSGRQ4BCIhMpoHQejywte4RC4Uqs1it2AQqyFyEipsVAXQDxs3NjSEfHuxFtzj/ww+r7Q0yvvfBQwATiOPj/f/L//X/9Ozl+W9/+9X5q2uv2aLnRFC6oh6ekbEM5iBSuLDqFsksdBgGqNAtFvtH+6ePDmeL3rZwfLz3k599XATOXrxYb7aXV5tpu7k8uwpZXK/X1W3c1npxw1zD3dQcXceaEF/k68cYGmGegQEBNI222mznQyelTFMIomrU6hEuUqbJsozJzLbbun8wq8v1+mbqexAWUeRCItyVZPU8wJpegKm6ddQDIQVWD1XbrMflUm9uJp0smpQfoyUyAsDuhgaAZizy5P9bCkiqmOPHXoiGFeS736JLGrgPDSaCnR0A2xbbpGU/aoWAMjvTd0GQKQEFIMEAAHQP9ABmhACiTR21rof97uNffvDv/v3Pv/j8w7un+x1yENepAkTfIZMkyocEhDQTLsxDody0GBEwGAHCMpwKwidVREZmBFQDg/AgDTTk7UgXF+P5m5vLN8/XP/wLT98KXRBq21sQzZyFUcJ2hzgTVzUidnC8hbcQ0sO1W5EhcxZybAJ1RKzTyCyGmiZMADKI5r3YeUIR0MwDHJh3q9auPh0yQMFbYQdQRNPia829zUU6VYWIqVkWKNylsFWVdPAiQlYYEyMFmTntCiGI2CzCrXQFEFLQlsTCrhcMkMjdswRYkFhIq6a2A4CkFJ1qRoOJpKkkmEjdXI0YRURVGYU7NrMkQqPqrQ/qFl5UdSkl0l4ECE7ZX5fFjw5BASgM5iRAiIHUgUZsr5//fnV1dnH2evnRLx8+enp693TRdUhYgnxnWbRG06PmUoeY9jGOnM0ZMNOUwBWFWQEoDIgj2F0NgoQQMz/R54VmJ/M7x8OH7x7/4qdPfnj9+bPvX3/11Tff/Onb75+9XN4sdVSRIlIigHN2R0QmJAkEM2uXM0Te5cKMUF3Hcesvn6/qdtm//2iyqqHCVEphQRQMcyJgQkoFGWS3MLIgeOyirHdygObMDPCEZzMHqCXCNPCWKVNjAAEAAElEQVQmbwtrKqxcFgIhlQIAgcTW2rGptfIBYoBFFoJlYFSbEWOXINZq7RJUk+JuAegBAEhSfKpSBiLQcfQADCgik7oUnA3zsHF7bXfuHdm0Pjkauk62q+nF8xf3Tw4++fkn569X7z568N2fX3737bOL8+vl8kYdc3oa9vY3Nysk7uc9EUpXttebveM5ISzms3v37h/dObxzcrB30C32Fgjh0wi6Odw/XM/7589fLCv1PHv7cr0dbwBx2lpE6FpDtpy132HoRoiBnBQ6qBEVq9pIkQhwHkcdSsfExECEFDiNLgKEJCXAsspcarUI7IqM28mBzGoZOi5IBSxROE7NDxp4R0RcPHYbnfDAXSA6j2vHda3cEVb12uKaE/xx153MJ5uIMAO5MJ1cyQwkLYgE4Le1Lrs5P5+W/8660r52ejZTc5YtXc2e2Ezg7RiJAKcMaQEw1czEZRHD8JjqNBHB3un845///C//4xc/+ey9w8NZn2UFHlC1Y6IiRAAUZubqDEiEIhJqZsGBXSctADybTMIBcTInYmZRc0Z0A2T2gM3WJ4Ory/Hs4vri5bebt78d7Jt5dwnTioqAAzOnpdEysxNQreUXpLIy51YuBbAZ9sABmdwtK9NDPdsVcnjlIhTpkAeErNKShpzvVFTM0oShAEgkTC2LkxIhdCklx7UkgdKFUIpAc/63dazWEaJHxoiwWglRSpEIA8yP1AQ5839o1zpWpypFWMo0aSkS6FOdihS3SAw3j478g4oIQDTQBhEgsqUly8WEKB+yHI9ymbLUdIoEgJmaecclJTpMBBCqxiQZgY25wEekRy4rqiGglE7DMgHCw8MCAANDChkxESwwxs3bs6/+bn3+4vLpF/effH7v/r2T45NhKMk8sBRwDzPIWKXCHmEtHiO16wBZzAKBxIGRTmsPt2r5wrQoFWwpqhDAEYddt3+3f3hn/5P37/7lLz784e3Ft9+9/sMfv//mT89fP3893axDA4oAEyKnxCo8mApm0HcYpMKeOEo3O5jP9ru9vf5wb+i77mh/f38x258PfS8QICgyELdjGz2AEIgJI0JbAUMzm+1EeXgrAGnxcxAW0Ao4bnG87LN0jFu9bCASExk0ByMIu5q0/hXPRaPRP25935kGpJIBwAwJ0TLgkFmtEnJ2Col0lrw+QbiRMQKGmqpmOcR8xqubyz99+ere0w+YSx3j1Q8vD49OIODeO3fv3z/d64/OL97OhnJy7/DN25vXb5eb7QRRN6sbEVr1w7jx/cO56fj4vQ9Al4i+d7C33YxP33/y9L2HwqY2PXn3YQF49cOLl89fbJbXq7GuR/zh+7P9vePtSM+/eeWI7mQmtdo+deAq6WbFplZssTgeHTGSQzhh54AY5LWaO5FgWK1bRIKJsqZCClOL/jdkHKex70vVwgLsWJjQHZ1KVxCBkIUYAQuVAGISlM6AiNgcxjq5Fyllsbe/HmO1cliNQODV0ojV6p1TnB4tkqQJtHbakLYApqwnlYw7dirRffyR+kVKJBcsdnqBQG5aoBR3wo7VhRxskADcArzl2qCQmVatGtPssH/0zjuffvHRFz/56MMPH50czwthQIAFtUQD7Ipk9K9CuLoQ9kJcJCACHIMQqSlbCCIbYiCz8tkDqnsYsPCkTkDbCldLX11vz96e35x9szn7Xanf93yFNiJlSW2IpBUS3CNlioC5vzfHctK1WYUrVGqtzGSmplWkS61jfgZq6mHgO3KOMSGErNPLZGtV7UqvqojALJkBh4wsZOoEaOHMjBjeIpxZqzl6moxTiTNtp9IVrdbJ4AG39zoAiKkig7Co15Tz7wIaM+QvpHBAqFopkg7hoe8R0NBvvUKRIQIeJGCaGZ+JCVK+/wgAhKpK0iIpIkKYIEBKsVqDODCIsIi4elZRWrYIMLtatG5EoPCGkSES77wLNiGxm0IAEpAQEXlA9s0QslN06ISxev3nby8vrt6eXT7+6P6T9x88uLt/sN8LIwATZyGyVssDlAQhqwgQEBxJTHU3KWOq3TiICuX8EowtbjGCWmZJRAQHdEKLvdm9veHp3f2fPb3/H37+/vOXl8+ev/3mT99/9/WzFz+cr1ZjVaJSStfDpIQsLOFTkqUkYu6LeX/n/v5iTw4P+vunhw9Pj+4cHSyGviulUNeLlNJqIAnbRo6tHBooUwYDsiIhWit3A3awSXt33QFNBJiPa672DQfK1zwlu+aG7RNHRJTSJceVz5bXXKWCWRwceGcHy/iUANqxLcyCCDpNeYIwcNf1iO41nFnNI8BqDdUeqRANUu7dO+opyGK72Rzd2V8v1xevLu89PDk8mgmNewf8/t69h4/vPvv+/OTuErmbpvXrFy8uzq8evHNv3g+q9eX3bw6O+Be/+MvlzY1It75a7h3uf/TRoxjHZ8/+jONK+oJhVX21tcubcbX164vp+uL10A/E3fJ6Xfq5cAmF7Xbse0EU0CmxXY8IysagmMYlCSPSZFMTnFmM22kYiBinWolYncSQCCPCwsGh9AXBIVAESyFTm+11EMHC0jFlPKUIEkknzIWkIJFDSBF3ZCnVqtWxjqFTFOb5fLZdTaNugSh2ZVlpj8TUVuap3BzAmQ6exrPciKM9HtC8mE25kIB+Avmxg33aylgAOdeHxB+jJSTnDxiRQL0IOSMyT9MUVanE4en+k08effLpk/c/fPz0yf2jwwW4M0BCWEJMBCTgQAGgpoiEAUWgY2EiwDAPFoHw7J3Nx5KZvSqzMHGdvDAHkIHXCQ3Keunna7+42Fy8fbN688fY/MsevunKTegU1AQCzBhmLAwelOyFJthAakrIuf0TtfjoalPfz6a6DQBhIU7MClgkEN3qLvENAFGnmihcFnOrW1MbeUUmN8tAtFwzskklNfSq5t5i9yyMmIlQM2cGMOM1vRrhTtVNEAbuXvpOsgrmVrybj2ATO0ZkWB0yNxjA3M2hMKATYWZGlVLMFbNjzD1DjnbF0Pl4YRLZUFhYxjp1zFWriKBAeGSwMAISktYprXSWfa2ZB0sEgSRIxG5q7tIJZLQDU3hkJFF6HBApO8+IM5NZa62lE3BE1xm71fPLb/9uef6nt28+PHv6k3sPn969f38+mxUERiZAKdkfAB5hEcwkRbRaGtC4sKkDoBsguCuAACGWTiwcs2V4lxBEgMRkFmAI4UIopSwOu7tHex8/ebD6ol5d/uLF68tnz17/8Zvn33z95++fvRinS0e2aSqzfQ0TJmAPm4ijJxfDOc7vnRwdHc/6oaCgum/G2nOHhC0OJHPZ0mqTcrvYSXyzQykSjUzWDhKTiXQPph3YIzNHCMh3SrycnjJKCTK+ok2BqWansIjM5nN3rSlSRgRibqlCEG7mCTPlwquWZkCzCRAs2SoWBEeCsU4sHBha1cOQIMQZYb7oF9vRo4LrwaI3n85eXzJz1/e1uvl273B29/Re3dRO+NPPngbCzdX1y7v7z56/PDo+/Ozz922qf/jdn/uj2UcfPBamsdr6+ioQT+4sXOX8cji7Ws76/sWbq9dvLobZ/qR1tZwgYHm1moaMVIT1ctn1hUMnCxQUzNcrcPeBi0g2ruUnSCx1VC5iXuvkUjwDxAIoLGq1YVYQ3MEn1QAgCETrun5vPozTCIRCCDnZcJNXmoMgASNQAN1WwhZiGvoePMY6jsvrm8vN6mr0au7gVhGKB2JYtNTG1HrmH7SpgxxaKVBmlBBi5O/ptzRvro4J7WHTN6dyjJAyDSV7sCMDoVsZQwOOMkkzvKqpufS8OJ7ff3r/0Xv3PvzknaeP798/PTg8WBQEYnRHjOBA8zAkKtR1PFm4RbiKQ2FhQqFmNCChwPBUGUe4gQGaWsdiEaABSOZoAR6yWtt69Jt1vbq8Onvx7ebiD7z+0/7sQnxJYAGU9dmIkAl64QoWkPp96cIsCCLAs/EdQZiDIwxQuGoFQGb09lIQthKxHLQI0E2NhfOdJG51KUyUip1ogDRigHkgAgu28CWgWmuLaYkQLhnlkvoOc89qd3VjYhEO04ZDIrBwrZrdcoAIJGkZSO7VRdjd3Swdt+2cJU6cizDVHS2RLkuDc+jM/nfYEYOlE88PJpyJqykiqisxBYCbJ05AhMQM4SKMiNOkpRT3kCLW0HiHILfqbkyUdd4QbRghQk0lFiFCIKerwSOcAkrXQQRSsHBUZA6BqqsXF1+fLc+/P3/9k7Onnz14+MHx3mJ/3kubZsRdmdLe4aEGCFxEq06TEQZljRAgd+1MTdiiY1Jz1CDmFvofDSvL2O58kgiwcMwW/fG8e3x/8bNP7y/Xn55dXj//4dWf/vzNs29ffPunV6vLs9U4Tsttv78PbovZvGfsnPfm+4N4cesJOsLChAiTToMRUDNhpsmSACjF14nEejut2zGFTZGDtGuegeTjECCzvcJco8W8YIRBOAm7KnGq0SzCUJFK2WEG8aMnKBOmaOd0T9WjarMcIETuuZgaX3JzIs7vLJGYVxQW4tiCMQcAC9bJ1uNa+g5Yp5utEzmU7U1dbtd7h/NJ9e2b89Xq6sMPng59f/769QcfnALJzWotMuuH+1RgvujvPbhzdNDdPV28ur6WHt85PZnG6bqj9XqNVbseh37++tWba9TNCqcVnr94raZI7Gqm02Zd9+d7XSe1riiwFKxuEApEiASYQQmB4ATejkRzEHY3KeRqyCXAIQiBGDkABQkDEVhVU2GcCTt18nGL80UZhpJ0IncC7gYowh4hRKrmgJ0UzPQqJiY0NQbuC3d9t1jMNlOs1waTIxBynyPfbf5CZmTFLrorp4Q8y9KTRUlrJ63JkMQztNzY20TZhiwgUgBaonht+ocdFaTEaG7qGm5ILgIH947vvfv4yXuP333v4f37x3fu7B8f7jEABaGZBs6C57NZhBOAQbi6O6o6MhMGRi9MzBymbhBIEE4UyGQWBmCekVfRUTGNqYYIIHIoKfBybRdX42plV2dvV5df2c2/LuLVMCwBtonmuDlDIILs2v2YJKHnQpIfWJ7CxBRhhKhqQJA3ROrdrMH5PzamMbNbSuNNCrsakbQrhIiIdFIWJoRs7nXQhD0AMBwIOWNyoFmsIwAtbfbuRMxCbt53xcKT2KlqECFdqXUSJnVDB2nKHzciRKJ0AxOxmiFmvnxSCioik05FCiC4Gwm77dh9xFtcnnadjhk5rWaZHsClJFXYsAgAEfIAN0tNJ0Kopk00SinNI9fyASMvEgCU1JgGQASLuCsCTToJC+b+yoREVk1KCXNAzvXWLNy1DJ1VFyJWFR59+ezsq/OrH758c//T4/sfPn784cnJ0WwxCAFQMXBGRtKkTyOChUt+4AjTxhEdCASxESGOFgaBUjIQm1IgRKk1SAQF0U2b9okw3DF8r8jiiE+PhvefnPzlLz86v7p5++r6xbM3Zy8un794eXnx9vrycj0udQnny/OZ1EUfA1LMOCapI25BOYZrrwOX2TAElEKMAJj9QoAOnulH+UqDWzo2YNcFjHnxJ0MQDqbuFbxGeOhEBJFhoEwxQThYOudsaoQHgGnOfuGThrvlcySCkfUIRExe0yEZJMwQBm7mFEjMau7hyEhBTJxuLcQhQLm4b4BKz4KBVfpipoC+2rwVCwWmYd8NVsvNxb9c/XDntB9wb3EwbUK6eYfjZL63Nw+Kk5Pjg8PF1fXVwUJOjvePFgN9F9vNyuCw9HS9ur65uJ7q9s7d/Qiso11dvD17czXVuHhzGRnlgRQe42YM81LmiFGnseOO0b2Opexp5O3IiE6GrkbMSKym7O05zywHM5+m2ncdE1d3QK51C6N1kk/vpI79vC8lVzoGMCJmYXcVZhFCogaoEqc7U4ogCyCbQ1JXqqmuYcjUzGkLRACtYRyIIXbXcZBH5u+mTC/9OADguHMD5D8beRSxiyuEiKYga5kNHkSEmYfsO41bWACoWbgH+XAwnNy/8+DJ3Xfeffjo8b27947vnh4dHcwQsStok+lUD/ZmnbBOzpkAxihC4THWSijEIiIIHpbvF3hgWCDtgm09wmEKN4tCTMyqoEAOYM4YuFzrcj1ultvri7PV5YvNxZ9x9eVBf06whjC3kDK4ebgFRlAEABGBe6of2xBcFZkwkJCsViQ2N+lKOgAQ8nR1AnI3Yamq6dp187ZcIIIHi5gacXE3yjaUDNRBRCQ1hRSVUlHX8NCUy+fN5hHNUMWYkczWQBDE4EAgMdOspLdaibCqtS+YoaDhkbn5eR7kHyLcW6cuAhJDE8o3CU7OD2bOOwVh6vpT7p1xakDo5sxoatFy7CKfD0CqNV1mkBqmaVLOQvcAc8/rxJv4hMwCIJi4Wu2kqGmW2gDu9qNEFVC0GhfgIslsIpFbsgFAXNwgk1eDiAkkvE43vl29vXzz5s//+uLOBw+e/uTBk89Pjg/392d9xwbA1CG5IIJbku+Zr82F3QwxPLUTiIiBwODhmsBeDtdh4aDN84IeLIwA4BmXybZzwRB4x2XRy+n+7INHp/Xzp9uxXl9P16v1xcXV2/PL6+Xl5dnZul7U9fmVLQe653Ws2/3FbAaH+9AP1AFAuCoNvYjkB96CmwIwQf+m1NmFtrcy62b/3Yk+ANzADXTr0w2GB3IyhUDEIrnyIBKU0kwECIBhkyYTzq2erniiSagAmqYQM2UpXmsazIgle2hJKG9IQA5XRgZkq4rEyMTCXMiz4cErBWL0Oo2B3fn1G5FhdannN9M///rPP/3VJ0f750Vk0tUwyNHdo64U081ib3Z8MPv+2aYXI10TxqyX5396OStycnLkAM+fvw6Dpx883m7r9cX64u3m5fNzAESUGjCudG//gMsU4zhN2vel62fTZqVkTGERU11L6WoFchcuFhoAkWD0LvEmwkQ4EBglPK3pYe5jjAw4biy6SPsjC5tGSFdtUnPuWvxDbpQGxigkxIWRsRRhoVKEu26yKMiEUqdwiP1OKpXRYFOXi8nXyy16OJFrBnYzRha5IAKlfDgdILdPQ3KouxWxcX7Yfhpg5yODAER2A2Z2dZTUeYO5m1lYpQLDcXfn0d3H7z98/O7Dh4/u3T89Pj7ZX8yGvi89c5hXVTHqSs/SMRAZuLkZdETkOGol4mEYEkACBKsO4ejNM4y9uFsAY+A4ThoQKFYtSIhZJ4yQOvmkMG62m62trq5vLp9PV1/78qsB3s5nS9A1CgMKNtg0WAohsVA4uBoBJmLxI4STLgaiaMr4Bl8TEzQWJQN/KJvgI7KeJRVv5G4YmEk+EIqZVJ0y2mRYozW1YIBRCtOdiBufx5CKvcx1Jk67Rkb/Z9hBy/uFcMrasixjIgxzaZN1IAAIU3LrmfK0C51te114EJGaMQFz5k40pbC6paXNA1Jy4pZ5n1JKV7UChKkjAhdO1tzdgTAgiCm5HWJh4TpVJCZODSM1AMydhdxCw1rTFSY04yJpSeU0WbBIAlAYYdWAMFS7UjJxydwJRRi0QU+BgWBTRyXg2nRz8/2bix/+8J//p/+8OP35T3/62fsfPrhzuj9bsBQmCCZCNPRgBA1Dj64rqWsiEoRQy6hFZKE0NtealRENWEdETOkz4C6NHYpIolWaoqMARGBknvFiMZzcwXDQCKu23k7r1Wa53Ww3y/XyJkC9aqjTpJFtsIi9JDplAOxqtGunj4y2S99GhmJja/GmJoRFb0NeABdmRtdgYRGyrauqTzatwl1TLh5Bw0Jon6gDogBsvaMISBwexCWLUtUrNGwyAom5QAAyclBrEGkZQ5mtCJZadEImDLcwLaVDInMrvbhVM2fiYein0StUXa1H3y4vbKpwc3l9c37x/bOXJHx9fbl/NPu4HxYLOLu66oYFsqzr5u3V+XrbIdLby+X1cvPtNy+mMa6vN+B8eX4Z9nzabC5eX799vdzcrFCEiaZpWm8mR2CMIjRupm2/lk7qiJRLk2sAgk/EGIEp4rNqwdQxBJBOihDIu5xEDzDnVCu4q1kvBYmmrdHAsYsx38A09DRN0AuKcJa4mDmymNm2wrzvGAWEAmC9GdmBpNSpArhwR0TX58vL8+3N+bi8HjU4qDhUCABg95rYYBPXQRP6/Pg/bzdyOj+geboBdkFVLbKtjY8AJIDhAOoYNXIi7Pe7w7uH9x8eP356792nDx49Onlw9+Rgfz4fSmFBTplOUKAG9H3PRAgY4XVSL9x3pXHI7jpG6T2CEiAnDBREYMyEG0ICD6YIMNUgAQA3AiqbMZjIHDejT5s6Lsf15Zvt9nz99hvbPOP6csFXBdfoYWlSAgwLtZpTtGOgBQJRDqPViNBB0/qlbkSku3pegHCzAMccrJst1VPanypSD5Aipurg7sCJLrmHR8n+3hyqWhd9MHNYRLibA2WkWBiEuWV7F+xEW2YeYYjcOjdyLsUoLOpWa82geERgITUTU4OMoIPI5LmIRgAggasjIzN7yvMjipRp2rIgEjNjmJsbEWYaBgK5R6I0iAQOCprZQU1P6EGEt/1p41S7bJhBNLeEfVqJoIVkAww17rcdCwBTnVpKRHBAmFrXZ+sxMKCDhXlbkCMAMlov1SdobmCezTPpPiMc3CwCBCri2IdfrZf/8//zq7/5n959/7PP/uKXP/3080eP3zs6PBiGjpgJ0JCYUuSBqEAe2eOGEUGFw8IC8oIWSR9XpKoPAQLRLBfp5ADJ3LJCr7AAtUqEiFuGFoGDA4BkbxA7mOXap6rmYWFeFU3BHPG2fsvNkd2yliC/BuEu9JSkUXjRpos8fZv4j2hH7BIyI4nQnutIVqWONFSsU5iGjWpTuFGq/81QmFgAwJ3cjFhY+oyuIeLY/R1zpowIaJYSyKcaHKWUiAx8SuGze7gjRyB3JUaQMqhqnRSpA5yGWR8+jeMkFONmTYCzUk72gqfN9dkrg/Lm7E131p/evb9cr7795vu9vQOd4vxqc7Mce5ZwurjcvHm9ejG+3V5Pm2l8/uzNxduL6/3lYl7W2+WkW0DebqdhPiAVZvVqKYkG8e126mfIImpGLKBhnsdgMqitUAScMnSVqA/PZ5kh0EIRUauXnpjI1C2CIkTQwQkwMJAzuY7NgbFM0wQS0gkWcQgAKn0PwtXDJwNkQLGqFIhIY522oyKU07snNFctNze+mi42LJKxc7mwQmh7MzgVWwEQDbVACgZ0z+0we6PBMcApMO+F1IRmTrmaQR0djHscDrvDuycPnzx4+Pj+O++c3rt3fP/u4Z3Dvfm8FxHhzAmPACAFZLZqDjhN1mWkFUQFMMBpskGAmZlIHYdZQXRC4MywNicCJByrAmIghfqo2smAWKq5BbozKGuNm61vltvJaLy52d68rFe/1/Wf+rhayAptJT15BUSBCHJycAxAYp1U5kWQI4gALFEmJCRgAFWnfGU8gymxlJI3gZo3AgSaYDoHsCzKJSbTSYpYtTT9UXpfESczbOkRIMERQQTTNLFwvrl5HXs4tJEUk8jnIurVTUvpUmkRHiICANWq5LHfDj4ws1oRkAUCgNDDMduywAESzMHINDFvyr2dL9RESppV3cJMhQQAreUbp1TfMrHPmwevLRGUKVRI7Sj0yNkckWqthMQkbpoWgbZtMHtYk5IxEJObdVJg5zQGQKHi7hlLbSkhlZJZgznkmoaU4jtAzdyFOJWLzREjkvMMMUD44Xxa4NXXz86+++6rf/wvf//Bxx998dOffPqTxx9//vDkwWIx60oJTr4hnFq5GKRmkpmCU0uZlD3izjOQWmRNFUwrB04HTkRq6pLTIdxl9WBkzXI66hCtKjEhSSHI8gqPaESzR+qwomoir9Ok0kHH6X3xbBmAW2Ffk3w5taiPRgEmHwCRmXQRScOUGZYZ9ybuYIoRoFuzqVoNolCnjlHYVQHYIZiLlGLqTZismXVOKS0EjAhjkay6zMBVltISqgNuxxGIll7MSNiRiExIOilKcQg1jKow+nZ94xMFlJlQL1YopnFcjuvtpjrQ5eVyO95cnl29enGuqmevrxCJjczqtNWby83y8u3maurn/c31+Pbi5upieXx3b7sex0lX2zpWg86lFJxw2k6IxoVFRWtYnQih76TqRAJpG0kRmnCpqA4O5AGYNLyF5dpuGbpPZG5UkdJ1gy5cpKDrNOzNw7WZ+d3HbRBBPy9mVatTCZGCIhEUQNIXFiKmABQpIdxJN1sIAmw2cHZ5vVxCrU4o0NbB5O9b4KubN/WONzIv0WFrGY+JdEDmXSJzyoNcw7IKhI0Yyqw7Oto7vHv84Mm9ew+PHz06uXvv+PRof7EY9uZDJ1IkLTrGAGiGgarOwmrWIyFhnbQrhYhUKxIRU9dJrgLZU5tbeDi0PjJr2kX3XbEqhAUIFyQaa0wjgvRhuLyp69U0bab15ZluLrZX38bmG9FXPVx1VDEMiEwTyUiLIgRg+n5kNkCQRwB6IFKGOSK5KskOHodwCCYyU8CUBpqwJOiXrRjEnHLMAKQIArAdtpafOQKaGyJIkWlUIhJu07lnsOpOfNW8Rjn257cK8gidAHNySujRS1e0VgRCwNqy59BaxiXn5yVpJ8sXD9o+18KjaYdCqapIMVdKF1xiFxiRwZOEkbYIzsME0msuRCLckn9UEdHR8xTMiTv3qfQmMVORTrWmMX3outRH51llpumRy986GYKEM1K3Gg4sYqpSOgjK7MzE6ZCA0hGLiIJRrRTx8DDnZqhDJDJzQEQp5Fg6fHBK355d1fV4Vdf//Hc//PHf/vDovaefffHF+x/df//Te48f7d85WcwHKUjEjuCI7O4FdzgLgEdwmkccU0WXHAa1ijGHwNQYBYBZpHiJCVOrm8wZNAecuwdjcCeYhDNBbrztqgkwD8o6PTZIU4KrR0yjIgFBIBlhWuWaWifjTrMrJgJ2mpA2GUIKwvNhox03wADYASLIXHwKq9U2kChyNSACVOZCQO5ALCTi08RAntKCVhnlAmkHyYkkF6GkLR0plUtIRagaBQhzuDJntqCVTsZx4tKTk6+3BrEex2mLNBtw4OV6a4BqdL3c1krm/vqHt1eX52dvXu3tHY/b+vbNJXjxyadx7RZXF+uLVzfLuR4dHy+vV8uL7TD03cY2q201RGSvqtWocMYbmVakQsKhkxuae5kLmQe4anpTMAADdvnA7uhAxJmg4t78tSl+reqIzggJqwKGqwKiu82HGaasF9xc1YKqzmY9CjKQMCVz0wgeJEBGYo1Ax6lqVHdAiHJ4dDiRdtNqMUeb/GJTiSjIgTzhujRkhTsCowVy8V2KPXKeg+AB4OGqQRZh0kt3OBwcHu+fHp/evXNy7+j0zsHJyeLoaO/kzt7h/mxv3vVFCNxVu47DHGqyZyElNfJQpGS1mZsBYd9zCpZtMgwnFiZi4fCok1IS4Yheg5iz5yolZpNHQcrQQQ8MwNWI7lKd1te1bqbNdSwvL8b1S1h/qzd/YH/T0bpwDQLdqnSFWLyqQzCxBzMXB+ccCSNoN9cGI1gAph+azE2Y1D2LycycOcM489MK3L26gC1ao8l4EC08IMwCoPUEmpuHE6JWzW3BzDyZHkQDzV0LgUy163pVK7vT9ZYHBcjAHEhc2lyxHY+7dzt1TRGJShGAJHeRB2u2weDu+AlPHTwgkmfEWNpKIck7AHMgnMYqndCuZBLAmcTV1SwQWuEMZo5Q4r0NgIgdh8As5uoQEY7E4V7NiBEgIICIklTJYFqHbJjJCuum0AWEqkqZ1ZCuVnNk2QkNd0SKBhF71lNks0SewG7ITSKv7kR0dFhmA5mF+5pRN6uvvv7dN9/88e/ndx49ee/e08+efPzRBx98+PDdh/dPjhZ9JxhGQN5G+1aaGBFMlGgG7CIP3RAoJCluh1aekTVvjuoeWdLSMhiabzLzG6GluIXV1FNh9neiIOpOw02MiEIULinFjXD1YGSPiJr3IhAzIaq16xwA8oQCbmmgKWtsCL0hMbo2qgaAgAFQOKqPoTElUuxmLF1gRkQ5SMnBSd0BUUoJcHXN1tWdIyCYmFgCwsKDWzOMuTOL5kIXUUrPRFprAAdFNwyudVJVU1MLj3Hc7i0OhW21GlfbcfK4uFyPk80Xw9u31y+evV5fL4/vr1bLq4s3l8IHNvnl2dvSz3Ty7ehex/1FdMSzrotAGycKmM9nGBWthOsgMxrKOG1jqtRR15Xw4q5RJzckRjQiAUv2q3F6bqmMRq51JOoiwKsxcIYyWShxZ24cWKQAOhIhOgGCual2Q4FwliJAEE6QogFkBJHs3PFM2yYhByYi5gwcDDPwADNdj7Ydo1qMm61WFWYDzQRQyIj/21hQDMPA8KwtdwsCQALpSzeb97N+72C2f3pycPfg9P7ByZ2j/cODvf35/mK2N+sWQzcUEkIRKggCEO5qUYDIERWIqe+H9PRVc+4KM7lZrTrVqSsFkIGhFBHkcVtBwQwIQYS7ubhbNQNCZqqqkcKF7NwgqupALCwKNJlsJpoU1st1vbne3rzdXl1ur7/x5bdiL3tekm+FOT3ypfQRGAYAFB6RLjJkBkIIImTIE5ncAy3ym9XOE8hofS/SR7SUHnfL8gcISONV4iI7XxRmm4hVk0QCDYkj9Swikps4ITp4IDByAEzThqUT5iwbYOJaK3OGH5N5dowEs7QA0ToliAeGCBkdymoqXNRbUqS5hxkKt7LDVjC/E6h4BEGomzCnJScbxjKZ1ZMkaEh2SFciEs5uhxojG1kzOah5qkLNA0GEm5is7Q2OSGYVWaZp4iQzcgsBQsZGa2RJW1fcjfFHA0HiX4nkECKTuNuONGYzJUBNk8UO70ly2y2AoerExDll5PsU7sTEHMMcpSAgBEpIEQY0t3p19eL86jX89l+Gf3ryweMPP/rJp59/8N6jD95/dP/e/t68MAdCMBK4c07pLQ0NHFLqHpnOBhhumNkJ3KMjmAYSmFp2JVG7BzIbAmGXo27mBCSl82zXQTB1tEZnQKv/CkQmRkABawI+RAhzJEhZpmcis4WZNnlyhpCFQ0SzCERqOAMjYqKEAAiNKLIiPpxZegwKt3AX6QA5AIAyCg1CM+6BhBDQzYIk098ghQMMJdPDMsGGpWCzCJH55JboUFe6PjzcJ2JmBFOdzTp1ZSBAHmaDTzhfDMI4zGeO3fnVajPattqwR+eXy9VqGsd4+eJqGper6y1z5zUuLlb7dwZ2rjWkh2ANnLCoEOt2NPV+NpeCFbVOtlnfdIVY2IzdoY6V5SDqtUWs11U6Dko6KrgDBI7sdSFy4MnG9moAAIpZIGg+FyBGRAEUQBhQR+87YiFXt+KujkweMXR9fgetIhGRkClIXyIIkAIIkblwU/PCrqs1oHQ9u6mu6rRlQTOtY40INUN0NSVy1U1EaN0iI6CXoZsdzWZHB4cnhyd3j+/cP7lzcrR/fLA3n+/tD8N8mC36xSBDJx1LJ4URC7aYylENwxmpCwKiAtKVAhhSoJp6hHQFzElaZ7UD4oBdV0RYJ/VAJgqAYcHpMhUhKRQWgWReBymcbCdDnRRRHCCQKlA1jBEmk8sNrLe6vllvL17h9gdd/slvvl10N9xf23bNjGYBILFLeSGgTHIACgxnKhCU8c4OQZJbCiJAWFAgFTa13JlShRORfwo0NRaJMGZ2z8MtHZAp1I+IQCSzPC1FXSOSHTMCDKQMt0nLpjdzBhYZGm3WRnwXKRGOQK5OkjNhFlJSVU1lEYnctu8gEbYoe8w7AwFJBD0E86231g4YjTUGiLR97U58FkxuABEQtSozddJVnQKjTehtpuDqFdIOFxmiBqUUtdpGf48IZEZP0W60Y0xYAIEoiHiaJuAARSkc4VLEM1QP83N0AswMVYtg4UzaY2ZXc3MpAhFhDsws5BGMLR2DCCCNCeBInLi0hoVjhDETCEFA6VOeC1w6AE5tVDAISKDGdv3yT//63R9/++v/7W/vPn73o/d/8f6H73zwwaPHj4/u3j082Je+FCanZMuiqcUQslG7WdgIQ/qSWBBTRkKDczAgMmnVrGNJSEYKB2THC7rHTtEPYc1/l2AgpyTPIdqeCsFMEFlJDSKuFYAT1wl3YIi8MgEy2dx36re8FTwsqiJjamwRVbqCGkx5JTqiZX51YCBJrnVARumFdsBAkY7AgixNMeCoakxMmTrJCTI4AgXmMwPuLWqUhaV0wmy1agQxIjIhMiqBc5hwsNDicO/u3aPL6+X9h/cODk/OrsYAXC5Xe3tzpNnovlxOJ9XXG728Wu3vLQjCAbrSzfuDm+6GygjgVLh0nXSiG3PwAJwNvY4bmJxdwdBs6xhaYzVa128ErDnds3orEvqzzKPK0Ae3qQXnIWBQZLkmNNWGTlr6Eh5qJkQiQhJAIEUQ0NwLEwWYGRdmYRZyD1OSTtyRigQwkHigaiBHVixZUAhh4La6evSzwWxt0xjTFNM2zMEmEpovsMw66Rd7h4uD48XRyf7x6cnJ6cHJ6eHe/uxgbzHMy3w+9CzDrCBy6YQJkWKAIAByIEJuVpJ2nxBiKcWqmwMilY5qdatVhCww1CFAiLKodjItXZczSykFEAuTcYsqJGEI0GoJ+gsTMYZDzcI46hzRAi1k4z5VshGWq+nl29WL5y/fPvvToby9O3vbw5surmFcOzkyRWBr8XUPooDg0sLqMREfCEIPd0ZqoApERCBlAHFqJSD9sxBBiKqaGTnQwj7JUktn7uGZAuIGRARpBtKMlmEAK0UsPGVUHp5TASB4NS5pp3UE1IheOtUa0EgmInIwyAwgCGIKMyQyVWyBvmEezIGAWjMX1zNhIWddZApzycHS1JkxcaukGoSLmgLm4ZQCI4MdU5nG5alOwEgIgmJuiXEL0zRNpetr1YDIXKCqtUVTQjNHIGIplOqZ0vaJcHMmUdMiRb1mNTECqCkhmRm4pSQ83E0DEZkoO+gDYJq2QCEdp5UlJT5a01Sc6Ynpo/PmW0R0DwLLjFzK68iNQbqum806utLMywJELmzp50MiEGSMsa7fvvj6zctv/+0P8+N7733w+ZPHjz5479333j999707d+/O9/e7rhBGZOEYQYqlOTOgM840Aff8axJzUkINgmtaKYfALMAJy27JzEttUdIoTbuHuPuaDOiIQhGtk6Vl9UNAyrPbM82YOEUqjiIonwHztmPlrlSaXTyNP3WcmgMAAzyYIHdBIuaCrU5Hsg4bAxCZ06tD7O7sXsOVOeOvAUID3bOxoGHZ+adxCIKAvp8DEAVsbCvSMbMATHW7WS03q1WdNqbVXe/cv+tuCHhycnyzWnbDICtjHonLbH7IctHN/eDoZD1uRKSfdYWQC3BnXZdadZi0OoCBCcnoXqvSdjMb9pCk2tYnpUBHdyYH4pi83rRiUU8+1RERyAlJ3QAjdRV5fABxWKTaKTCBfSSWQPcgAwQHYULEiKz5duaitTJjUqPC0vw4hUgQiB0okHjoA9FJcn33gAy2hF7CAImk4KzHDz49tAmmrUE4Iw2zMp/PZr3MZ8NsMQyzYTbv5sMwLLr50EkvFN4XRvBSuNWpBjAHUpB711C+iIoY0Q79wpYzi8HQdxaw2mxAYSiiEOttnc8GEtqOikAEFACz0iESCU2TMScqEJDXJSFAUKEst84knGkKB6pBEayAprjexnby9dqW683l+fji29fPvv7umz/+2q5+95/+8nT+Dvd8Y7AhxHBK7XxAeE15Qpvfk/jMTJ5Qh/YWhkdwFmMQhTf9PAd1xBpuqWvm1Hs3ABUQ3Azb3IaE0vqsIADJzUTYCNy92giEZgpImSanqimOx0ASaTM+upoKyVS3ccvPm7fnqjE0TV7ELEitMt3di0i69cyUWcxMhHe/NqJaLqQtzznfaEJKkaWbeniYJaS4G0XTU+tIFBiwS/+D8G53yAJi13VJ3mY3YbMmCRNhVp1Fxs4QpaNEU/fpTVKWjJlgycsXA1AImzVBAIMQJ3MW9girNVOfE7lmZDdHQjPtuIuIIsXU/n9U/UuzZFeWHoh967GP+31EBAJIIN+ZxWI3W62RpIGsZxror0smmcSByKaaTXYZq8h6IBMJIADE414/Z6/1afDtc5EqM9KqgEBcv+7H917re45ta2WhUU0WtDB2ozFBAzJTxSlFmNs28vHqqGrXoGClJGiQbU23tsi7GBczO44P7//8w7/709/9z/n4+vVvv/rt7//2X//xD3/7y3/9P/zh93/8/KtfvHrzuF1GpLiB00ojv4SZySJn1Wiu4Yjtw5oqaOueEuYWTfCxk1I/oFfalZmZHIsAtdGKGop0I1bIz0qGILgcPcDLlU+24J5VWelDueHKGeluoMtcw/nUCzOEpJpm1gftIFDk9IGlKJ1mXT5iQV4NEO4JX3ozhoFl8PSs43DPZoEFOpB5uWvaMB5zmuXYBoxV+ySfn48PT89F3/L6+Cp+/8ff/i//6Z/N4/e//e3//Hf/CMTnv3g8qsa4G5eNMIvhsZmnxcgB1gHOsttTv5t8un16ip8Y4O3pVrBPt/142md/2A8eR+0ddTSOMo+yBts858HntmojnLugW1Rj9hTN3kXE6iw1BGg9iz493OCN9cDPSbCQthsu2zZi89RVbzEuHtHmoFmkliRh/Qr2dkRjeIZvdzliXC4+tsztcr2zkRYBjNwuaRvN7i9XT79erka7XEZGpmMbm4FiAGGG7ku4WQcREg83Qr2tDhckQRueFpj6qOYUtE2iZ12ul25vsrruLhfrPo4D9FcPd9IUbZEfn3dT8ID7cRzRHpkRUMVKyFbZNavYPmFT3zj3owyZnz7x48Hj1s+3+enD/tP75+++/v5f/vHr//p3X3/7T//t/Xdf9+0vX7754THuB4bZ9DJdxfPW45LmtICChLtoqIxUxWm4r3ALUIeSIp+X1sg8FLxh3Whp321xVNu+H9U93Kt7G5t8IdL7hS+9D2HHMZeztSvjUjy69/SsSYPLF1kKkDA75nRP1Tu5OdgZMfepG9FfioY8BMuXkNgx5J+ttZJZ6N0+4WTVxUREHZWRy2ygBoi1i4G3/ZZjZG7NNl8Nxa5M4FipzvI9e7iFcCU025QUWCUx+2qDibBlkAvphPTQmJ8ZUrMj3HJ1b8FAWM0jPIrlE3BkRnWzWOjMnMrfcDStayockeTwbLU2+hlw5itndd1GysyjdSMj5NNT3gXdODsuY8t4fX/N/DRlNXRnlZuSL93DmrDI7nI4nNcrAc7n26d3//C/ffv3f/+fXt999uYPf/s//vr3v/3bP/7uD3/z5e9//4tffXX/9tXlcvHh8OiqDotmhyXByBDlK+jQXQIPhwMpz+FZn7sSZx0qFD85dcF3kvTo/zdF/5N0W/r0Ve3Nk2NfudwmSyEpHrjFHCwI0fUfmTvotM4xGpSvoJdIhJCEWiNJ0xyYZnCWbJBGC9JW9m/RIpuihAe6lFs9SUdUz2LTc9bBpaNweMyeZpg9P9zmT8/z+XnP+4dkfvm7X3644fsfP/73/90fPbHvT3PG3eP9w/3ltj97zTpusKp5HPs+Z1W1dTV67j3xabKa/PDx5uQ8UOiebpbPn25szvajumf1ZGzoroZxmtXLiczGzSD2pCOTaIMffbiF5wCNBbeEMEz4LNJTHsBaK6mTnj6OBqdFpjtsONMqjO7cMkYg/fLqMS/jcn/ZHq7X++v961djbNeHhzHy/nrNzOvdNUe6mYePyIxQQtA2gkRY7HtFON0xyxNWgFEtV2kx3NC9LCtoc6THrAm2Dx1RA7SJZiEzT3TTDIyxzUl2EWWeEVGkx+giG54mw8xljGNF4nPbtja6Qenr4GpAikx1BrA4GbNhvj0Xnj/U+/fHT5/mux/fv3v3w7f//O2f/9s//vN/+S8f3n27Px/1vF8uM6/7Z6+368VG9tGTrZ+g/BEFrgWM5pZnEXp4UKXwgNNnlcENISduzVIfbbM9rGumrzezOEEcxzTHlpviOQmUYok85j494GHHPiOHWSihq2BcPRWSSh9AiQ2OzBK379r1BQU7aft+SKYtHbk05GS569oAXrYCnh7eKqq5xBaQgFXMCRKpM8LUgJi+QiCEvfqAtWLVRM7gtIDLUlA1PXQ3LO2nHA8GHMfMDI+UocxO6FOROL1+n1ZynO7GqS7i6jG2rhmZhCttlAQgY6GkOuawMS5gRcZxFEAZziNzr8nZAI45bQmZoXg6fSSoSTiIEal8ypTtCPDu3LLryLi8ur+kM5wIM5ROS6DhXjB13WhvtO0qPUZeSM4Bzv7u/Z//+T/85T/+h397d3//5Re/+uPv/vC3v/3tF7//3a/+8MevfvObz3/1q4dXD5ccyHAYVUQpPadhGfHdQZSbRboLYmZIMqtqBEJIpvT9vVr3BOJ7SHQkzsSAeglrNKh/Q8piqWAX3dyqEGsgYFR8UC+K3Nk0h8WQZmJl2zWQWiiwHoROW18cwoypBxTVfVTBNgTZNbvJrBooauZDdxW7y2wr9uwmt9pv+ozGdi3u+/5Ue3967o9PTb+7e/32uJHbq/cff8rLY95/9sO7Dx+f59On2fbp6dNTjHj34/fH7Rbb9vHD837r5+f++HSwnj491fZx76rbreZzH/tTjlHTMA8Vds99dgM5tG7zmKtaAW2x0eI4dvdKD7LZsEiDQiB9lpL9U2138FD6sWfSw3UkeIaH5+ZRYTWf92r3Ybm5XWLcXV5/9vrx1SU3vzze3z/eb5dx/3g/rtdtu9zdX7brdnd/vbtet7FFjnC7RHQj0jM8Mvb9sNaWbhFDCSVARZFVHmT3FjFrGnC5yHwxh6eFtfQCxciAMSwsg/rd4HQ2nSHzsMG9J5uc3RE+ux1w560PEJkOdyXAHbM0SA/3cBeqcswWCOHuc7Jmz0INn0QZno55O/h0m+8/PT993D/8+Omn795//fWf/+Wf/+Wbf/mHn775hrfnnjPHuNuy07fhPu3156/uLpfiTUq8WR22gqEjkt2c9LFkht1c3HIDYUQr379KYZd0D3YXOz1YdQZlsFEyWhLNqow8uqToJ3ndrk/7HqfhP8KbyguGIFBznyoYnocRuaVAlKoJunTPt+M2cqtZsS28RF/acGuuGFFVP0rCn2NUVTc9VoODubHaMnTmv0ztc58gcx4lD1oIrar2oZ3AbPOuqtme3gsDPrFpgo3w6NmmVchV6xlpcdQxtqFGaUr7rCqcZs0S9tZT91sY4OFzTvlNtsuoKgPmPKSF6bPpOMTeeLR+P4GQxzTDyKFi4XXMS0fz0nVkcSJOED/jphp6YLnpJHB0LpIaHny8XraxfXxu8zPLrFZnqfxj58Urdl7GY9QBApbY6Jacz7en7//hv337X//xf/1/Xx9fvfrsN7//4//45Re/+Vd/85s//s1Xv/jl45e/vv/ss8v9Ne8u5mB4GBmrb8+wWBk57k+QdOUpm7kVlpNLPb9N+RBecP71C6kNAGZCsaraw+HG6tNKAEV0SBBKcMGjHhlRLQvxcpCJQdBSbMP6pJUyHCqSNaxQUsnqPGvOsV08lVFldRwBzjk9IZiSYO0TgT7aEt3dNZs1KxttzulWFhP2PCfi9eV1XsGDIPs5R9zd/f7f/M0PHz78/T/804/fvz8w2u+O7g8/frr7mAavwoefbrenowv7Mdk8jnns9fz+uQ6DX+fxfr99irzQDEIghx0N53CDY/fL1VBYIiuRJtPMI5KFcpgPNieaxcytgEayDja6vH0QSbuL4dv1TkyJNcf9NRN395fLFp+9vX98c73e55svHsYYr9483l3G3f1dZl4fruF+vWwefr1sADJzjOGw4akH3UCkzWpvw8FrDE8ne84OxSTSwB4WYxuuKjLqkI8x0ow1g9UrW6A7M1cesXlrEAhUHQJ+WSCq6qhG79XzQAYs/v4f/2Wz/jf/3d+6K+MEZqYYuRRrQimll+osFI8zqz1Ip8dBzN0/7fy4148/3n56/+nduw/vvv3x26+/ffenP/357//pw0/f7M8f0zo9DGnXB8CrO8eAVRjffvbqen9JL7OBKuFNpWwcJyQFbK5mDKzuaxrnZKhpS1pENSEroarJnrmlUj2koiZ4ybHPyaaC3pqLmz16biOrimBNwNvDVbdZ1UpJn12J8IxjHipEK0UyG4Be0j5ijNj33d0U9+tt9HALGybPU5dyKsTU0sznMTPT3WvOyKimwvO7VCBjbW2G1GjP2R1Gdm5DzVCZoT5eXyQwW5nvfXaCiuWI9c44ULNyxPPtFulyvpE1/ppkT1Mf9TIHk929+iDDRc9O9Sa7p4U6d0jWIbWTSwKkxsuVRtiEre1BHP2IBLpqQoKn8LX0USw91GDu4bqTKCO82MeTYTfrbfjdZfz0LEsU1u+9tqUm3dx41l6yS6CX8i9JszEMNa7XrAmw6mn/8PWf3/23b/7x37s/PL75w+e/+PWrt7/51a9/9ft//eY3v3389W8//+rLt2/f3n92v91dVp7PWcAh07BSj/QKSOpJk6rfDO0r09u7K8x6hFrJaEsMpndLyizqnpMcqnuJgiCOftWLmimjHCp0qJJ21JdUwU/pKeTmA2EeFsjVHGg0WDsAeqQCswwNN9/SCQulpNmsabCI0WRulBshurtnjosArXlMcqSPCy6+7b0d1UftN/L5eefOOBD/8s33Hd6Z17HNvj19+un28TkeHz58+sQmGcc+j9v+4af3bti2i9W9O4/bkys402PkpbsAp0Wrs4zbUbu1sft6ucyaNZ/hxLLexDGtGdUOG92kR3exN2Q0ksbx8LBdrtvdJSwyzQPXLXLw/nFc77aHN48Pj9dXrx8fHy+f/+L1/cN2vW7biPDIbRs5RobDc6iann2UmRk5PAOB1mfaI4T25ha2wrCkH/YYnhaBYJdnOotbaom0CC93A2u2xDYxovapHVEB0A087bfnT88//PSRidun2+X++vh4Z+ZPP93effjBnP/u3/+HMcb/9H/6Pz6++iytPx01j76/pl9sVinpTNUjt9thFt2wMPlBSWu3WZhlt1s9N96/v/3w09NPH56//e79X77+yzd//ufvvvnL91//8+39jzZvKI6x3Y1hEdaABz3mUXRvm5zHm/vtszcPl4HqOauNHTn6mOGqtDpmVXhCYmagq2cBZHgo8vmYc4xtjGGA0WU4Fnlfc4HbRYpGPmqam1mE2zHVxXjpbrZNlIx8JFbyriROamXtSoXpdhnRNFSpDNGjYe2my7FZCI8mlSQI2LEfMeST5exatcCS/3bHCGuf1cElkGn5j6i+oO5ZTbohl1CJiggUQ8jwxVAJIGpSEpBjVoSp4/KEIHq1z4KeOWvqNc95iDI+Dh3uOOqIWCk0dawCgQiXLaKrLazRWilBwBghySOkrq1SGZd1tcZ86ejNrAk0fKxNTe9xLDXMKtsTCR7LmS33+KoqY59jsC8tlyevd365Bn64wRwIcDW3S32lQFNJiSQkUEwglgnQDGC7mfeqyvORd+Ou5+0I3z98979+/OE/z8K/95EPl4fPPnv95Re/+8P//te//uPf/P7Xf/ztq1//6u1Xn1/evNoul/BAxpq4u+UtEP5YYSZr2TijfR3mkRB5bgqc0H9YvRo+DCzK57mCGRo8SyINJ4tgthzCPE8Tt/QlR8ZaRgH7OVSYOjEbLkUxWg9OyLSscB91ghhDYbFkN8vOhhM0y3z1mEUMIqeSAKxiu9i8YJvY98Nu5s3x3PH0/ONPT8e8vz786vd/fLrt/vrTu+9+2g+8ouXd2O7GBY/zmLf+dKvbAWDvsJ7tP336qXsw76sn0PvxPHqrPhSFquBYTSRANnnraFbzAqaW0UPLTycRwGYZ5iPS47L5GLFdCYz7qw9e7sdwXB/G9SG++OL+7ed3j68un33x+v46Xr+6H2OLiPScs9To6RlmplENBivEcNBwGamvm4DYLUKw8rKDLmG14j1a5iZ3B7aR0xkjnFbVAtgb5hlOzpYtz1D0CPcIZ3fP5vv9+f/xf/+3f/ef//PzPq+PD85+98Ptf/c//PF3f/tHzPnjTz/+7d/+/v/6f/mf0vLh4XHk5dW//mNEDs9YQzeHO6mgSQ/faNZzmkXD6Hab+PTcH2/zhx+e3/3w8fsfP3z//bs//cu/vPvm3bd//uaHP/1p//iDO6KYI83Sh5kNKRlpqo3qHN6kyJlXj/54DfR0w5a537q4TiEl5kckqMmSxAmNWJh5cxrMw81wHLsZwk2jZkg2wsXHVZHNtp5zKvn2+fl5bFv4xm6FRW/buN2ewrPjrPm0CHV/Zsy5ehWqJTExwbsWDqjLxS3SCPVZhvk8WhrIyKyqMfI4jhFJFfzZC1u8NCBENFj7EZEw1GygFAEQ7mRnZjQIN2t7UXk2l3Sw1QyzNCOeSaM1esSYdZBMbfWE7pajOyP2fd+2IRdvq/JgzjEyPGrOaqhNl6wmOQ/Fo5sAxdORRTi7YdHd4TBzNi2tZm+XS7OkPqzm+res08NKBxZTLdjE6AQ9upkvsi15cNXd6Ppit3t6WLMN3IY/Pm5j40H3lCRm5SYgjbXcHxLOVGNVaPMFWDy3MssG4ReyQIvLBegtSR6DZD3ZfHr65i8f//Sfvv6P/6+Oy93l7fX1F1999W9++7s//Kvf/+EPf/zqq19+9sVX22dvt7u7uF59DMvhNBqDIPqU1KhqVASPLHWaFiTidzlQzJQ7U3yhDEpXm5nid5TQiZfmUJqCQltJiT9XzasDF3B3t9N0vBSltsgneDjPAlrCPMIo3bM2JYeyaQC5GAKuTyZc+i8ko1jmDocj0p2w+/C95rhsPjaz8fjZZ0+fPiVt2y5vr3d2d6mJ1199dtv3Yx7x2dPtaec86tY/vvtobfX+6e469n3vjio6Rx+9baM9aV238A0dzkXQwTLNB5JWDpjHNrZLu1UdYR45mhZj5JaIzYPjcrHEds0c3K523bbr4+Xx1cObLx5fvd6++Pzx1eNdbHm5XGxSZVY1q60BGzFIu+SFYNqAFLVOa+YIUZfoTtfnzggLfQHa2Z2KQIkUaBkjAes599tuHkYdoS5/pdKfV2ilG9Z2v0IgaGbOfT/efPHZL//wu5p8uHv43W9//en909svXn/xiy+29MeHK9BjG3CX31Ll5Naxsiqq3LPNJmnunVaNCn+a/f7j7cefnr7/8ekv37//5pt3f/nzX/78z1//5Ztvfnr3zfHpvZNBC/iAD0+FmZvr0AdbUjeDAUXJrTw8en72+vHhEta3xix1sQhJtuIaGaNZaGi5jxAZTKn4DWCz2GuwFUbgeNFeyoK/PPrdI8cxDwJj25rdc66vk9ucuy831QrhcjcCgZWm7K4uSW+njpfZTauRw1fxCbvRXZEJ2W2aVRUeEQEK9J/K26xaKQfiD0aoKd7cPSOOKncX7arxfx5HqtJeaSwqYyoyaC3cmVazz9fNbmY4ymYdtt5/KlGnScwaY1RXZnZ1DGU7m5lt29B1QiAjqspDMV+uAGGlznmooR7kCsWO6IVtHa2NTDlFWLU5qCbh7BJDsOhtW9V0riAKdkSQnZHqOlZqB2hwqrcLZEa8oPuoumzb4zVyeB0GcyIpXb3RBKByBW3WhIeTQLCrI6PWwoUXwAyLNBHsqQJPOaIBMjJT18e+z9uff/zhmx//6b/+5//P3f9tvN3uPvviV3948/az3/+rX/3iq1df/frzL756/PKLh1+8vX91d3m8+Mi8DDd0BqkGzRXxTy3yqxMAzBFSN8sOw1ZNn2UGyC61U9Ne4uE0CMifvGQhL98HM1t9kYoTNzfCFPGtpwjm1bJAcSkWqtVnLXtLd3fBwruW9B/n9akhxmEZXk1rxmU0alo0ekR8fFY8Bi9b9d02j6Ovdx+fjzn3Efn27avjYJPHvr//+BTb9uH98+Wy0XD3w/veuz7dMuKnHz48PbePyaNZD2aWFsSr+cDmTAoM9OP2bOGRIySVLMAzx6VQMneNy7WrY4sYoRMqL+Zed3cVUY9vttev8u0XD1999cU183J/eXy4z7ERfrERl2FAjs0uFu77nNeROTZtYSPTzcOXqHqFcQWaUCxyug1XKTPdGT48zM2O2e42IjyzuzwuyqHkoZxBbW/rql6x8mwYMpyn4AGwzeM3b9/+7v/8Fv1/mKqGI1rlRo6wMzJekbK02UWq5GM2oom268E8aB+f++nTfP/h9uHD8w8/PH337sdv/vz913/+p2+/+cu33/zp07sfen/qYw83d7t6ugfaFIYImc8lHS70GQlv5uxScKE7wHbMh/sxsoBZNQWGQRlNvfAQkW0CRty9e4YZVU61AnZ0dKLZZh6rTNFhy8VZR2NN0OPYd9UXHvsxtuHpK/YnvKG+93a343ZERKEkKTT3nk3AzOtUyQvGWeebRrBWbOAKUBLhqpgAUAU79AwCc5YiOgmr3iNCDjU9SPsx7fzPz1x2j5E5RlbV3I+xpXSgcRrJIrNtBQOoSUCSVF9AuZFk2DyO7bLVXmZ2HLsigzTGRzjVLt0U+zFSugg0bdXN74qMdqfCksAF2ffZplqByBHV+h18HocusZFxitdh5i8x5bbArLUUA25wcoJt5h4x5x6Sm3VHYHa7qZdm8Cw1G16P1xXSSjOPNN3l1qIwrKk1O4cX24w94RnVhRV7DHfrpey0E0+HdhBYarPSkRzmRNu4GudF/DmeUJ+ef/jnf/zhP/5927//fya2a1xe3f/iq88++/LXv/vjb7786ne//uLLt29++eXrt5/dv/ksXz3m3dUu2/BoV1QYQBI62ZdfjEQvMzJIrkhbjTuzOsLNbM5SIjHV7w17cbxYoRXSA5gJamOX5KYyzlhENDQEopt6fCNzmdIQNTsifIMBsd7ZBrTPoLuDODsFDcrEo5JaPOB2H/s8Cj33wy1y8zZ/fHzw8KfbzgYNx5zVvL+7c89tux6z5tGvP/+MrNrnvB2PW8SnI67z+HCwrw4YBBiyZ5X8a2a2hadnXNDTXO+dmYWtB60iEUhJVcfFzDo2j2Gv31weXm2ff/7w6vH+7np5/XC/xbi7u95f793TzEfGyMusdo8wd/PLZQMwLMPN0yMiPLrphMECfr0MM0jc7eYAMkwagWZHuMwzl+HSSZibedYaf9uFFJjU6xEGgyl219x1xfcsRXfJ/u0egpy2lROFhnWzSAKz9AyjrA0xy33k0dybt4lPT/X+4+2n98cP75++/dMP3/zlu2++/vO33/7pL998/fH9u+cffzRON6alW1wiOdItJIlvtIW8im2nStDc1toCVNEDLgBgTgtz8u4+Hu+3EQZDuHdR4NZZZM8+DuYmP5CY4UaHJazDXcmjVVOgPqjgGZne/Xw/l+TXPeZxADiO6e5xykzNPTLmPCJSQQkRkSOrCm1hRlpPnXI0s8jsc5pEMzKqiq3DhBm2cCrdhBJ6GGuW1meAPfvs0Vs2w7WfECKrVnevLa3eatWF5XFMGEYmyEhfYQ8r5ntZf7tPpMwWeOJwTXNsXi7X4zgitMOcixGUPONGztkRJoNbdYUHl14F7u7ZBqtZiLQzqtLcupgjYFZFS1fxQHooE0LCgkMh+2416c5q7dDuBYXn1KwcFi7R5zjTOQoAWQ4beTnq2Vdlz9YlqxIyMZwP17huvhcldSouyE47g8JNJW2GTBlBgGHZXeY4d55FCYhhlTLq/LTRJ68Bd6OSHAZ9xZSCncNAjmbjxtunev7uxx///h39v/zbzfz+cvf6/tWvvvzit7/93d9++eWb3/zmF7/68uGrX7z5/Mvr69f56pXd38e2WaSFEwLrmvp9hYqG0B6tw8QlxQ9bRtApqTvS3LzYdZRnFFope6twrdceoMUTsKU8o4UUZVXbGLr40UuxmluCMIFALhWpd1X4asdeMYeNJvfSc+GZ5gaMJh1uhX79+vW4jOfb3uZjVszDqw3o2+TyPMIdkdYwD//44QazyJGRkzhg10iLmLN7HnOno82IwbCEsqQum6ebGQtV08J9JaM0wlHdvXuGmbrZ++4+Lw9+uc/7+3z9ePns1fXV493rxzf3D/d3l+vletm2DTSQs9rM7u7ujDhmDZUNsCPCYXN2GBReMEb4SvAz58rskqRbBL77ijgXsqMFrpuOFfcqqbutJxEiEKs1Pzn0XLsBUkkA0Ci6FGmiMKUE7F6zBJULHKObB/Hx1u8/zvef3r97//Tddx+/+/79u7/88Kd/+ebdt9/95euvP37343F8Om4fwwuGjLiL8Lgsj7zMIwa6d70UeBRpFqD1aiU4mScAoYJGN0I95NXH7e4x7y7p1jWPTGdNwSymvrlmxnCY7ngLmCHhNecZSWuyBDer9rmNUdVnMe/KcxGp5sbZpfzOyJQKpr09QmejcNKqOq2vUFpfdymL0h2qUJK6cs6ycDiFzul7KREgaGp77NKfhjJBj2Pvao80g0lPXi2tSFWh2yIAOS4ICfozUbBwK1ozhY20zvdeMkfKZ1vnwE4QHDlIKoBUI2NXb9frbb+Ze9e8XK5T4zPbSvHvaNUEqaO9aWbPt9u2baVu91X34u5WBKs83WhVU++sUmiUsJoeuhsjfM4y0iK6yrWgVLsgZlOQoS4bZ6PQun8ihumtIQgrdh/PYTAPrUU4Bx6y2/r+Ie/v88OTcSXhONKUxNvsVNFpKT6H7LZ0zqPPzU3XhZmhW6Pz2lFMVq71NdSqoEYmrIVj4VKwAACqaqnAdtSma5iT/AHHu6e//P3f/yn+/v872sbl8tn9qy8/++xfffnrv/nVL7766lcPX3z18OVXd599fn37Nr94e/fqVV63yA1Gnrs+hUqiT9BfolCYGVwtNwaiHVxwg3sFF+iaKveQjtt0jayv0MJMGSKB5U0LzzSBjWtAoc6UWn+F62WZ1AQ0Gm3AdT/1LCXrhjtGRMG3YU7PjBEe9nzcRne+vuKHj1XTDU/7AWDuU9U3PmweVXWgicDlbjv2j+MxR/hxi3mbKAbX1lfsC6NmdXXX3O6GI5sSzizfNuCX6zCzMXxLi83HJcyZwy6XSPdtu3u4f7Vtl7vr3cP9ncHC3DOPfb+/XiOHtv777S4jAENBCWV3l7EO/JZszUcGZ6dkfNWhZKuTiO+mxnvCtJC1/Hzy2BsBG2M0u6o8zIg2JzrORPcXaEgH/RSX2G1u1bCMLmwj21FVt6OfZ73/dHv/6eO7Hz68e3/7+s/f/+mbH/78L9988803f/nnr+f+XLfn2p8D9O70SPdx2UKadHP0evoyVWMjEUtBL014LCT2E6ZApRWYmcqQpC6s2R5sts16fHV3Gcba2XUc1a0kf5NqQYKR7jL38CRLjlRIqyN9gnv6mLU3umaL5VPVIhWq1S8IKRihrMnZnUOFSO0eNQuEDXdKOAmdqNK415LYihcgiS7K3xsRXQxPxebbqo5YPer60x7hzpc1JcwPdlVnJpvzOLbtMudBpTvYkvtxinKdwp8JmCG72tN6ts7QbWyq29UVtSotF2HH6ho5zNGtODCby+e1MFwB3wQ8vNlzlruz2kbOWiUJ2zbIljZBITBK1zPycr2r3qUldwtkgKeCU8ilUge6INGJlEsLZeoGuys84daskN96bArsqjll+ELXyEHULMDRhIO+yrOcZAM9O5zXa7y63979qKRKR4SncwEhbAUtNFmlO5dN+MBsulm3cVULwQotxrhPijWWR01L0/Lw2eIV+pTa2DLZW6ihKPr8A2YwK4dbdLbWwGcc3/z0zdfff/2//N1/vMIexvXV/ZuvvvzlF7/+ze/evL7/8hdffPn5wy++fP327d3jY779fNzfx+OrLdwuF3PYCDOlU1A6HI2NWj5b/kJdWG6m5AMIDJwyMVInUlNxgwZrNt0cRYc3BXH2YmsMXTVGdMPOdgQDl1kRy//hsJFOYFbHWEt9as1sBHD14Zu/vh8/uIfhh/cfDtazfbpsw17ZxId9n/InHMcMM0S0/KrdVnz9+rLv85i1XUdlar7NwPPzYZMeNoaBvt9qS5oh81J1ZObs2ka4BznHyMsltjQa7x6GpWfYw931i8/evnl89eb1m1f3D69evwq3mp3uY9swLg1kpLlkJ+4WIKQai8iI0LoWm5u5ZCRjnHKv8Oqqg6HJlHA3marixJEzXTikvvNKk5dGJSCmHRSiL4SkzZZPkDAjQNo068ZBn4d92vvph/39++cfP3z64Yfbdz98+OabH7/99t23//Ttu3d//vDu+9un9zyeWTNgGZFmlhdJycXe0RquVS/hrUakVaHep7tlssiIZX+FmZt1TVtqdK6Q2sUgSsxMsDY/Xj+8vrsO2jOMx37L2JZCBoi043aEJ82Hj+rZDQ+3WJryl52p5GI3zTQGt5pHtzClsvXlldmoIvP2fFtkrAyY/qIDpNBIhx37kWMAOneWlFFdKaDiW0iuvvE5d4+MiOpK32bt8pzLUAZaN3V4tqTzbrpOAGYMOdFgmHtFWqTVLJwpAHa+BrqnbjYFFGf4nAcBmtUJW+sLv84nQ4MavWVbwMuJ4Lb6ctjmAtFc96SZdx/uXuyVUuOLM+ouGNxdY8sxd56FN4LYqqBcyAyxTSXq2VVCwrWuqjE5c6wjpnrORpjCr2lgdXgKqvLwo/YIv45tgrUf5m2GVTLTVPiVO+4v8bDFJQlPjqgIulkY0lyGMQLVPQ3m3omVMjFlsGIXFO/axpXHuqwDPMEyWzyclkusXFoqRZkr3GG9adEsIChjKVhTw3nImG0OQ1+GbyizvecT+d2n7/63//JN/92/C/h1jNeXuy/ffP7Lt5//8u3rr7746vGLL+/ffvX61Wf52efjzevrZ5/dv3m4vLr3u0tu6ea2cg25tm/RP/q2Nag0CgMZ+hquHGnhcjjRvEibx4oyB9oMNkKOlZSWecWdtv4PeVNXhMoUj8Kqvl6y55y09Gx07zPHaPrt+bhYcNY1h9/fZ/rHD5/izeun2/7hdhsZ+74/Px+3p30P34/56XnfzCvMmVUHAB+ZQU4e1Z4OdhorAMIDKMAtrulUTAQs4V6XYdtmBoTn2EaGXS7p4dt13N1dc8Sb169ev3n9+vXrz968fbi7u14ul23rqgyHKXPPR3izLSLce9ICiKWbEGLm4XqvyDZHv+h5cQ5EyqTsNiIiDA2naePUSa6AsDB1tMnIiHJKi2VY1RhhE9ZQrTmqcKv+9NzPR//4fv/hw9O3P3z85z//9O1ffvjm6z99/92fv/vTX54+/XR8+uR9i1luNtyHPrgI92FnunCjzVxSga4yazMUptACaHv20KwJJdgqa6Qban9Eu5/jkhNU9QiwdlSEtXF/uOOr+3QcDpQhfHM6jV00x5xtK+0Ht+NWNcfYmkuymbHttYsSP+YxUvtBwDj3qS5CT5cbhqrcyqxZAqDCrOFzzoxRR3kEwcgodlpUt1GViyq8ExlXIjakrOPCiu2Uw8jlahNHzY40eTyrW6oAdxctragUafeF4K3uORefYnWUu69kyAUshUa8FD07cszapXMSKPSiAVy1We7sjshWYsIs3QeRfuwwI8znPCzcPKonSZPJVnkBrcbaaDYayrDuoi85TZMWmd3TYdXSrrFZwttS+1cjMxalY9BvOHvqGM2RU563WVgppPJftDLwbE1Jajb2fe6OMOPYtq5imAzlFqkqLANH+Kv7cdnqKDIcI9tgm5t7m6ZuGtsK1bKFO482N5/sLk7AgEbzMI+1k+vbaC5Adkk2AclJoanB3OHrapVEUtobMznUJV+NHL3UqLEEoLbEWu7GdIdtOYwOszra7af9p++++ek//+kfHNyQmdeH7eHV49vP799cPvvFF6/evv7lL7786u31d1++/fKL15+/fv344A8PuV0yN4twdwwPguE/cxpQsKgicwGw3c9cCNAdXRWr3pldXMLtOIORCZgbzC3bXzZZkTnRXjrDMp1AjAE9YJPb/fX5doyR251wp7JMu1mrRDPCPj2Ny7i7u9ScHz48z1f147sP+zENVs3bMffbTcnhxz5D9jQ3NI+jjqKApoxoK9eI3GWODIMhM3KLkW5AhEfAzeTUfbi/Xu4uGXHdtoeHx/v7V2O7ju16d33YVpo8mn25bFpM0ZU53HyittVdYQaykZnV7R4KMPCT/Yz0Bt2yyTkLRpnFqqbGyIgB1Xmu/Vz3rzmccU4b4RL9zsbR+LTz/a1/fJofPu7v3+/vf3r+6ceP33333Y/vPv35X/70zTff/PDdnz9++KnmJ+63sEozt7jPcJiPoaLXapq7ege72WyjNStCmjIspd4a4gnQYXTnapyX80SQwBKz64/g1Fi3WVi4WfGARVVLCN/zdvcYj/eX6yV6FqthPrtcO/Okenhy5Dya4DauxelKqaFpisWCgW3OI3OrbrLMvKneqtJhaIBRgBU9LLc85uxZ7kGjJNdAawWqVp/XGo4Johlj2/fjBQXRWJWZrcF62XcsQuVI7ubEhFs35b1nNZZC30HM7gh2Y1o165RyS59OrAAxyzP8Y6WTai+b8xC37i5dDcxXeFq19FUwC304ZprPbD92N2v2iEFSy0VXgchIvNg9auaI6paqEKtlrU1v43pmVu8j2RF53PaxDTPLbSxh6KHKm6lkAuH1c+45RjfTo9mKnNOT7eZzNkHlSA+Z2o232006qrR02O2YkTyzd6J1jrgWpXKv+413YZ8mEWHpHohrwJ3wKlLea+uIC6t7spw4JsKjumP2UauzTKSAl8TwIEy7jtzJi/zWn1lbseKSTg6n9LbDzF0UPcrEEa2Ut7XKGeGu7AYpDLppNAwQHLGBZQD57GDt3x03fvd9fWv8r24dI8edx+V+e3P3+Mu3r/7mzZvrl19+9uaLx7dfPLx5+/jq/v7Vq8v9w9iu4/7h8nAXl0tkIBPbprGi0SJMYFjZHKrd0LMI5wL/TUSa9qIXClCFZob4GejQ956AkmgMjAjCquZQgh6wZeyzrHm9XunRR8/o1w9+9Lw/jtvtSNg+extj3+v+7mm/Hbfb/JTjeN7nMVuKVs6h2Fm32Z1bgAiDRi4WQfPMbUtzbJfN3TLdgBzO6rv76/XuWlUxHKuwMMe4XO7v7+7vHu4fUqGaqptiCMFnJMlAQLYvXePLtq3jEBHZgJnPYo7MS7h7mIfByKPLaO4LVHSHCldFXAnrJXDQNH3NvW9HPe3z6cb3T8e7T8e339++++nTdz88f//dh3ff/fjpxx8+ff/dp/ff3J5+nLcfWYfUmeF2URT8ZbhdCAg9MQS7oOfSxR75+STDgKDjRfHsy4GPdc2ZTAhyuS/xHgBJAfVwnMgBcB6+sOZqJZLFyc1ox+Pd3WU4WJKcHMfubhbrXBfFPffDLGu2XdXM2xJrqsyd5HHsK76sqtn6MERggpQ9pasyxuSU7K2OUqKfMCNCVbVRs1VOxamjv8WvNdHHTrKrIrNn69KXRBfQzBSAHcdUVoL5Ug/qxFYaKNg58pglaY4ZMuKYM93paWaA0daFIcBmdon894KFrxxUgmEx2QRHbgd3CSm7BEbb2vqrI7X2e9W8bBczpEB8N5gmjvaIWZUZOrtkBciIZruHnLQNcnaOrDlxphnpKSA7tySIxqw9RyrhWUBESVxkJkcfuzKWycBoBSpZyIMxoubUdLnvu5lFxhhS0aGqisg8Wxdc0R8vo6syTPrxPi/bngUM8OoMtzT3YFiSs8qHo0FiHmYOS60y6H3CAzZbVJqTXWBg3ZQ0xAKRjGrLBBcWi+6fFZAAlk9XsKwkN4qggB4pEjCXSXHtPYB7dqvoxmQuJVukgjQcDYf7FlE1Tfqgo7yeuj498fuPf/nH7+zfsRljMHKM9O36+PB2jOvrz9+Ou7vHN28f7u5fv3r1+Hh589nrV6/urw/Xh4e8u/jDNV+9vr+7y/tLbBd3mLBoLb9tpLUrYNwXybxQRnMPZ1Phd64TzSRGYq9aLW8gHQr/Po45JBOYtWUeNWP2NTMertXz6fn5AIfhGnEcfHrej2vf52Xf92q+f7o9f3za53F7eiZRxzTzeRxzj2VnEUhSlWEg3eibZ4abjcswwsPMMUYYEJk5Rk2O4dv1chnb61evRo5h/nj/cLlsQRsRllLkShiN4bDrBiwFnztVEeqLWEOEeXgvqzYnAdhtQgyBsWrm5TKOMnPskwSP2fvkMXs/6umpjls/feoPH/ePH49Pn55//PHTux9/+vH903fvfvrp/ft3P3z/8f1P+/6hb0+oT9yfMyuKbkRYKqc2ttwG1BCnXHHzNc+ZxLnLQQIxWIpuNEKuHTNWWbq5NQ3VpmGVBEwCQp0vpz1BZ67sISdTJu30i34QNPUary6XGZyvXl0frgP9qSFOkEE3D7LEafUsuWFgdkzZ0DnGVjXxIgJxqy4Pq9mZCgEtANVqbWyYruSu2e5hjfa1oKgXOjOryyDRfKwVpwlYjOxaBdfmthLyI2VrJU8SHnbMYwwR5jr9NC+auF/DIrRv+y5Xk6CadWPC2JxdwGpkImrEOGoa2NXuLuVerrgurIub5H7c5BKWX0kCIl1rOit7dm62bZdj32VNVKEwV9sizDozzCDgDLTbfmQGtNy5Lf7AOPcZMn64odFG3UnuvkAbI0jJdU27nBRqp4OMvdI5VPC7ZC0Wbj6PU6/ZLQOqTltdoUKWhVm7e9f0zDoOfRg5wgmGbcGHu7zS6+J98SYiQ/U9TB/bIGWuZmYeR8Ni7uXtFWk1MQOzvcg5u5tVAFiFRWVr0gdsQDivAqqiAYPShbrMvFFYk5zJuoOVT4IXeYPYqtNLJfreaA5fhAsXJe1qrzB3MGfDbNBJRRmTniBrGMEbHODOaV2Yn/jdu38i8fU/cLJpbjbAsMxxuY+8xPYwrte76+Xy8PjqzS8eHl+9fvXwxS/fvL6/vHl99/rx+ub14/XueneJbZiHbVvk5iMjA1vC0BkeQbOIoMRnHuY0j1ywXxerzRSc1+Gw2N1YVUMZpF7+OIp1m5Ne9uFSNZ8+3WL0teC+W8b1bn96eq7Zl/vpX9jHjx9rzqran/cxYs7iLKIhY6xZzYpwVd+EXm5jbFmzczjJsGWxUaXlSB+X7bpdhm9Xu9xfXgMX1mjzotwnAAxt7n7MijY4EN4lITh7UoZ+EtWMHLOsq2/7PFAfn/Zxvc7u56dnVJH28JDf/uX56Wn++MP+6dP+4f3tpx+f3n/4+P79p59++PHp6aePHz58+vjT86ePNZ/n8cTegQM9lQWVHndaMJVwMSQYDS6Bha2YWIlWNMkrnkryQU36BtWAoalBuMkw1+npmrSqYQ68LMTCAHg+t2o8soV04efoSc18yst5UVPTOvJSddDI2h8v9up+pN/mfCL2qsMtWGwrGBmmyeuoRnfmNo8jMgzsKjdvzVvqqgZLyc/oF/mNjFfp3l1m0V1jZFXDvGZ7eKazMWtWdbO3GM2C2+04wjVMYN93N6s+DN6sEK5qPPMxw08/q+6DOtvRazJGCIGnwrXOXMualelkqy5Y+gtdRYCFR3X17PKbFIim5Rs+a09z7y6S4WZmc87LdTuOw4UBCZqgNEyFCKDHZSNJtodVlfZ4kX5u8BHNhf0tV4LbOOVGgIpwV8Vjq7fLbM4Oj0WW2vIk61dis4UJmpzlNDU1h4X7rJIT5IS96J4aHzKyUapEWGdiV3hWtcF6dbOo/dm7fZktBXg1Zs1wXq95f1fbYcewTm+jp8OWQxWRVQVzV8DFyFltPnqCBgtHEHGgrCO8u48yAFEEFZQc555vaWAvvs5yKXGM1tk9LR09zQSBOnQ7CvEiEbC1fgsq0FUgoRTl5FMKG+TZBalBdCm+JQI2WwXx1KtfHz5JWPMs0oGZ0Vk6j4nmcev5sapp1t3fS7jnYlPMwmOkwa6Pd9eH+4zL5Xq5u159XF9//pmN7e7+8ZLjzeP1GvHmzePj/d31en+5ZF5yDNzdZbrf391dL+OyrQxTlaApv0+xtzAnYrbUzziac2bBIh679+vdpWehOC5s4131cdTT8/OxT9A/f/O2q/WtzoSbzaOP41Csrlxs0p5QY9FJRGJAiarmK0oGBrd0xPFsvYdXsC/2zifjskUfx5yTy+Uuo/jSBUrlth96Sufz817EPOYxe9Yxxrbvdtz29x8/Vd3+9P3345JPz89PH5+///O36J632/sf97nvT58+dO01j2O/wdh1sMv9FBs43GwzEIwwult4NyJy2fIB2ApZWFmKEi+DvoIEtYRJlqD0EzPgzJQ4lQyS/Jp3FZYgRndHCH2WtPScVqTlAQ0KJjcxhFiE9QKjF66iH0ILzbBFI7ow94dX+XCNbTSas1oZPjKvmynaCSBcVFX30kqZE33c9sxBctnU3bqIsD7qPMVgbiy2Li8rECs/SVGfrXw3wrCccewpeGOJhULab+W1ZQba5Hbtg66KVLdZZWocaa/ZACSQjqGDGvLwr80IXIuX2TwqEu5hiiklV8KHaNj1S5BFC6tZsIYvM+pCjkCMMdBLQdCzIl2JbMVeV0VTwPq+z5HRVOPKyrt4vu05UtzNNnI/di4pIQi44G03qZe09EbGcRyZqb+hJ+W+rWP6iGK5u9o4qxYrsoQoRM0y84xx1O5aI4CaE2YUsUaXDKmqPP0klUFrTnWEdrgtHqbExkwfA90Wye6web9hJGzE4bZtgw7CMkd1Fenpcy/PCLNb0Wg+3NyhEL1q862nalwsRqt9rmqKQEArlYpQd0u1m1allgoIoflIA6loJEeYtK3nArFanQGz5WKQkFOkN7RC21rhg7LEyMyjU0GntySYoEF8jxlOBvGEqdZsh1xYmdEctFbd3goRIWBN3gzoorUR/PT87ul7lYKR1mQjrJep0DLc2iMyxp3h3jwjrwjevRru293lC3c8XMflPj1x//DoaXf3D5x2d/dm27jdMdO2SziUkTMv26MnxtXcO9xGUC75NnT3fjueP77/9LRXGbpZXbOIiIxjP/a9np6fuznnDtqcZW6sQ3IyrXAAZM6v4pzPtO6acHXpmEWgsY2H7f6zV/e/uFwejv14ftqfn/f9qFnHvu9odPE4PinokObzuFU3+bwfz0SYec2u48nMj6OMnHWDHcWJKoUAlk666kCsiUtQCWFmEUCGiEfzF1BRysp1Fsjtq0JOss2W2ACrlEhkjf49CKL1iEKB83qOVmYOlo5TcAfX89QeQTkQ/VQmyf4oSNPcJFuQtl4ZpH6+XDHGNForD9V01WqtBd08vBz16n7cX8I4G8rvM2UKmGtpcXMKqajurs5t1DzYR4zUKj+2cdxukbGiMGGKmogYmrSE0LmHSlIbUt24eZhRTV4kGmXmPP9VV9k68VjFyFigE9Yvp2y0nmVAN9HTM8z44iBTXpM8p3q3uxZcxl4FITmG9PFVZTDVaoZZ1dQEr4dsKUvc6piZkV20NN2UFjZnX0YsZNwgab2Fr3DB6hEBN6qdnKtJuJcujWNszdJVth+7u7mlPvM+5ZCgVUsWYl0Qf1A1t3GRWlRu7LENrhp6oXwC/f00DSB9VO3mUSwSNWduqYd3jO12exbaDljXFK8r7yu7/ZQJoZWLbafyB4T3LF8Wm86M68DdJW4X9y0oJMkMVpGyXVpcR9NQvIykd7Ux3NyqG7OZ6MmuZjWnAbQ2Y3SzDzd614xhZDvBaHVGQ3kdeiAWSSCNWLFagDpA+UzWuX9GOpuSqrFcDYs+of5RrEdZlCu4/iNRal085Qdr3DurYwTTsbtP65qF2NzWc2hL0Gq2+OpFbC/kFrQl/1+XDru7K1UHZAwEYH2w5o39LDB2zuPpnc863KLmXFd1umJtLIXTpOUGpY6Gvg9BIrctXNgrWD02I+kRixJsmvV+60jvOuZeMKuyHFHHAUPVYpgivI6CK8+OZBtQpEeyab7eM6zL2Ju1NFAGj7Hv3LarMUj2PDyMPYHJLkn/ZPFiSRNBd6PGXAuPceIdWg4DbpaWCHKV3Cpdo1GRyW6PpL3gyPBVBG3GNleSQZAyrJi5Sw+unVBDe88XEl8H75o0ZRRYFXUwnBW4ErnzRNDZcHNaG4BAd4eFEkmWcNNOxQNOKfSLetgMBkWT8YwhIHAqPrW/mik7xCgJkBuAHjHvrw+ZIOecRwu1G6m/tquNsBFAT2lnQ5oXQOCwgd232y08qnqMnFWzplFOKtJoy71v3aWUoRyb1DjsUtiJzo2uHtsmGKNVh0aYi8/XfFZsgq1KesHU1R2AST3V3XNW9IrY0hoOkhiRc+5S+p3MK4ycLAVpKnPTY+mwYUbWnA0iR7RC3c08nWDCwOLSCYEZXqdaPcKrqJwGneyZ0WxrJ+lKS8cpRDndFvq3xLk2OWaVr3jZKYgQXJnJkactyM8yMrZHzppjJLsyU2IQM4R7iR6geOa50lCM1vCIY5+ZERE1Z+oatzUWa5ZhE+jwAVtaKjuN8rMrclTPcLWYGSRINV43u16jN7ctuMK1SbcuxEgl6xSMYYCNcCdqchKRYeEwn0eh2giUseY8imAQPhINK222YLd1+xgNbQYALGA1dzNgTtipvJhK92kIqgENS+stmsZOYm2NTy6/Ls+jGexeSkO4rYOZCEjxr23WhjItAqsQSKiUehNOXd4y7rIXF4ElbxXXsBIGnaetxNOhjK0w915/j4HhaDoN669wLhmx/Di0DHNzRY5xpdXayrUPKE6ipZIkGqWGQayHsw75KidOIANqVNY9baoI6nloLiab6AUBCR5cqRk6ppeGRUBcC1U4/VZUzrb20W1juHXNUGRwWhfcEyxlZGNpX5blAyvFSn9hyoRKaIPXF16vf91nyvDwcgugg45ASrLiIFYtkt6HddrCYBHVBVfa0gqB16Am09Y6jB1+4tFm9jJhLDoQSkQQp2ELnwEabQsEojtoDfgLmmhmHoEudyfUHAEYDCa4XZeJr0RC+xmAJ06MSXfxS0fIJI8teH9/yaGaF5euVK5LM1clRs2qWZEpOKiKoIWPYx4SLaX7nIe57/th5mzqD1cVgEhX7lmXvZitenZVR1gOBU1qhhRKw3n0GW+8viELwFnfQizzFgAiIxqQs2zOCSF3Wly1wFHH6dSl+DJMmft+281BC1bHMJxLHImu0mcq8bHp/l5/LZJc3yj13BJkKzubx16R2QKmFeSQZh7CyPQj5OWXPH/OWqZ/wJbMa/UyuvmctTgFQ1Vl5jkIQKFIyvE3i6rDPdRrfG6WEBHnZplxHNJmwczmPsdlmPvseb1e5iH0nZokYM1qiyjV1GWAXBmiNDNGJFkkw4ILy5LKUNOPGTg2pDPdaEC4MrBIZgZgOXRN+qG5ygGLBrdQJqJom7Ay0FDdlTF8vdc10V6lIaZJUv7hPlir67GLHhfpgkyha6CSG8x6HfduhFk0Z0M9Bd3mg1UIfXFoGSvsQc2apxTVlspUX1D1phEEJeqL1A0Nk1AAwFlKLKABttiFPg1t3rB186BNnUEvXKKuB4vU3IElfQH0YZlJ8aQKM0vxXWFs0M3trw8Mebhhp0z8PEOJVrEfXshEQMOIAGgzX6pTI9xUEGuO9sU5rdvFKVDcU8IKocPrSH3h3rmuq/NekaXXGmeyqgc8qRdsoRRbcgXl4Rx+sU46/Y1LarNwcRDrt14dRE4qIk1KKcGDuusDvmTtBNjlHlrA12e3gD+elXlBtsuEu+D8l+xCdRlyMUMyXC0I53wD+vwMz2N6dQnppuzWkgSsxCt9rdDSO4jwXb9xr7GiXy45XVeKXl5/6/kaiTPZ2AdI1vHwKh/vxlAuNoGymmXKHp9NYJLuFhlrfDzmGAn1I8qi3B2RIiyqOcbSB+p1jJEALcechW7P1c9jbtex7fte1eYmUFqjwNzFoHh3aR4nFzWq+GSdqzhjuKe46CJYrqYt2CwNwe3mVS2DhasQGABZ1U7kGNq/2znnkUv8jfPmwLqQJyPUkgKARsttG4fKWwxGegarxW9kjj4jbRY1Kvxk4TByGLa715xuPkbC4LDZFR5C3ARGAlCJJcmUCW1dEzLF0LGAaBjD3N3mrG0bx7GrkDNCyjhOIa29gg+3TSsCjViVbD09kj1P6NM0wnXLimdsiXblXZU2SKaYxZQQ5Jw+LubW7LHF9d53Z7s1OlyuO4nxCXiEmflEh0XC9+rQb9/0kYRV95wF+pwzgpgdFl307YK2o+biFbuNDrq1DPoCA3x2g3Q2GtooHYEuGNkrmleXkuW6tBT1xxRjuaLd17KVCa6DB33WN8YprMLK3I2FaSwz5/rurxx0BYxIHGKggcZFOgMIjZtLbNqymurUadB5HsuegyqFOhXfC8BePKt2c6ELLpn5YhSB09l3Ri2tcQdmsPVi7Dw3287h10xoJ82httwFW0tHrbPNX64MQdCaWlYAMRYr8nIevUyp64et3910TPPlXF/bCMRUyTRncsBK2nguuitkYOFnSxNCwqFPAcDa0HP9xVif+Xr3lolnmf7xguAJsOsX7pCAWZ4/UWvyGgKCeg+hx+dkGglIIL/y4xS+SwK2ugJ1RtAWY7cI18X3nsP8wuNVqevnWPFyya1YmXMJaIZ6s3VTqsI33GNpZsPDqu4vfh3ofp77QZuzKkyJKW5gVWdGd1cX3Fg1xpjVEdYtzoGZoyTeADITaBXaSP+y8hu63V1GTFCmpZizpGThykPUvM9YrYidY8xjmiPCZOsDqX40wNglTxZ1hOuqBI45fVGJbYbjOGwR99bdPdvEG2srPoF6g2WmYVXphkcvHdNiarsb6KKiV5i32y1HyHOifDcALE2C7eGzpsNmLVBFQOg5X7S5wbF8GU0C1Mfm6KMAoWJeXSCls5EfgboSCiQVFuxm2zaqmkBVh0dViZrXowmz6mZzZJRJaOwI67WyuAFLI+go1R6dE5Wb5Yjj2DNCZJagP+i8QBPtFs02C3RHDC09GZnGDTWSHZgLLrF29KSHt1Ens7KvjTYUI+t+zNk0tIWbbUkCkWRnOxs1T6tvjW72UZYJtDEwAbjyz5rgLAM0N6HDiPDo2kW1neJfZ08d8ewW0buUr1xKI4OBqxluHWPNJhZcyNJGyIXCLFxIb8XqxkAvD7f+AhfSIHngy0nXp0DIDGbe4vpIg9YT4Dwf2ywhS5zh9IlCmQEAJU7mXwtFtL7qr9DEK8cTl8bpZXXjOQVD/+Vy2S12cB2+546p+BZTipG5iBODdU8Jpap7nbWEUupWafPSvwP6pnLlqsLxV3SJzmi9JTI26U2GRazGiPWLGWAIO4U2a/PRn12bO5eX38z041agg+VyTMi3D8MyggJmcOWLcMFx4et9rVVYsV4m1rvDtVbZOeovwc6iLcx6xQadGYIvI4WmTXtZ3c8bVuCQLGA41zVbb4xwMN0xy7cIrJCKsLUpmS1lsHuGoEUayT6c83p3vWzuPsmCuj2ouBKRz2I74B5qfOpauSYsCkoCKEZ9+XhJM+gU6l4wNWzBAzUrI9R4kRnHftTRscWxH5FZ1eiOCI3IVUVDzyId5t0lIyQBdRxI2qEHups+BD7HunLCq1sBtA2u7iw3kgJdHN49+yjxjmkxWeFOct1VWKDQWkIMZqY/kJnRxa7KsXJ9lWJh5iqJFPIemeKgff1seaDdwlfpiTu78vxvq2cq19uV8k8F1Nm6Ra1quo/M0cLXIqpqHpPaNmxN4zU7RszjEIik6uDZZYofEAByfqsaimvzY65iYYGJY/Oq7po5Un5D32LfnzNHzTJXBRraen3S5gq/b2t2jcy70U/WU3odswbCPZLmXob0y46abMJJCDqDWViaRP+0hE2lNcLD4jgOHw6SVUvxORIgjU14X3u2IjRn93ZJzmmezQ4Ym04EjUSTKS+jckuWDoE9y0PeAqsu65WXx5XSceIYhNXqsYSFvRzVynRa89+pH4TGTGu5NNbqv8burlUvQ1EvkWhb2SPr8UtWAS/HhMZGLm3SQtXXVmE/T7s6LGTtPE/9pSY3W9vsOfqjpGYgIDRPczXgC+V5mTK5VluzBVfqwJRicald1r0EPV8L1taNYdK+n2j4X4X34bxUDIC2r1gQydo4aSeXee6fOuGwgG4KwD+BM54EwVrbdQVo4dMK0rR42c7WK1rLB1fw1AqT1PUqLdPJOf88fZ+IVCsYzs5X9LI0vNyBWOC/zvH1bYXce5oMl8LaEevQ0QW8KiTWYqndAg61bdlaIWQsOH8BCvpyWi2BEtFkZM6jdEneXfzhboysmjdpc7SUwLqqx0trLEmXdFB47xDVuSAvWzDLoqXkqcSqwBOoMltSH7iFGFA25yzCdMdkbt0VkVSk82JXzYGCdZM9ZSVZB1ef+VoSMxtMtp2Xicn92I81QRgyk13dSzgA81kNtai6sUWUymOxTKWCgLgmxRPmO+fDXJlc4aelTi4to2IhWsa2NYZnRFdpu2msHfzck2Eexz4tPDPE4SsjXm/TMY8MP2alhysTslqoT4h0W4CCrU4CUJ0JyjonOXL1UMqDo5Mu0kBjdY7Bl7CklSPS+nQJK7ZDp6SDXrNGjNX65taghuAzko+QbGWWZ5j15jVQzpZB3DPd17mmE5boRXU0L9cxqy2WXqJ6jsiwANvOS3SMUbMNhjAkCWfRDGqm7ObYNjZ7dtLdHZegxKLCnBuucJoufbQ95Q/06sYaRNHVbCgnSGotHuHuaNpLEby3/LjiEsycasEtX14zQsJUmCzkeJlnXc4D6ncPrv74UxPiJ6xgWN6nCPkfyZJrT6JDtEpHCjCLF5hDMr+2cGLRTevoWSc5T9wfsGC3MbCG1gVXmhlljzuRDmDhYTjhES0Z0iif45IGWVtDcUMo/wn+6CF5+WMvl4K1DAIic7G26vOnLvJW6P6ab1++kArgJFaahy1c6uRVYFiM8Tk6cl1lOuhORmKN+y+o1JLQL9xKvRrnn9Wv3xCcp5n7xOF0say94DSKLliJ65fRRLyOufVa1q91EgUC9EhYK+rHTviJoG70NSI5u8MQ53FcJXfnooL1LXZD4J7xbIBy8NlFzoy+bjGGVlyYexcxyxwKbdVCYg2b8HCjFUzZZcK0u3obYz92/Y7u1t3VhK284f04zA3mpZAGLnmkuZUSi4tmfhw7zNBlFmG+pmSzQ+VcZgghtASsSVa3QdFv1NSwlqQlKOBLOOYKcy1RPku5rpuVq8Fx6XYJgShuVlUZYe6s0hMdFs1yCWpgmSO7irLWQQoPmKHZDqvZkf7CyEO/KkgywiTMB6gC6+VuoIaSBnDdLrMP8dckuijrVkg+wUV/SespCEVwjYfuMbl2KzzAFl0jfkIXYI7ttj9v20BDutq1v5vLhJzuQJkhYXIgj0vWLDaPnhERGfPYI91O+EvDrzAOJYVl+vD2eURQm4WZV5Pg7Ikc9DYiw9dTDmwZIhln0zd3uIPZRljmiuCqkEqn52zzwLA6jpFqLJgkDJJhaxXzKumq2cUqOGHs9KShiuhcKCSMBaINWnWVXQijhb7w3RrU2KWgLtDQ0n9292rjldClWLaeTAU3Yt32TRbXabTuG7o0BaIc+mepKcCVgQFInAJhEes/t3UNnLzfGqVZWAb1ddbazyoUOydzrPI3mp8xW4shs3NiPaewtSS0KAOHmIVzihYTd87P67RePTnAyTT3wp4WxNHugT55JJj5kgmx+0xCAhZa8jJTnrfAuUIsrEzguS41rHP0ZJgpwP0seINpseAS4gG90pYMOlnlN1Jgw9qGu1eri+Qo55tOPfQvv5b8IUsb+XIL6iXz3HYMeOm8eLniubYYeb9O3FZSvDVaLR2ZHqB19cCVLmMajiIC6Dl1RYDwhQd106ztWf7QKpiVsZz1cH8ZQ4v8i67M2LVCpJVBCLpSBsKrp3vWnN6Ndf74fux6E+rgGAlgoSM/s+Kasu1ntxcBWM3GsDn3yCEny6xD+DMByKwqpzGXzGRNMvBlAuw+L1RIWxQ5sOgnowI6z7CyOnq7jlIkrDm73YLQgAsdkuGLGRVgBT126kcCDLQwbUo5jxnpS39FVWgF10JWOZwiHsGFxS7PVEMsQi3mQBEu5iCYnqQ1+1A2ofW2Xaurq8J8u1zkTdD5XvPIkearlEc9CTLLiYEIt7PDq0dscx4wRyMym5WeNTsypCK9jHHb+2V5xZm+j5UdNI7jULiXrVyp9pB0jCJnzJ011XtigNIM030LZtiUXVZpa+xLXiarIHk1R+aC5CXdJ4HOEXK3eVs2zZzVEcv1yEaMEGM2xtYkQc+hug8u3YDDkGaHtMbVNjszjaxiG7bLy4AsKela97UcqLFeNptVhlNVs8OGw446hL7XURo7WSsTqUm3EMLIOhESLZKzLfS1b3Yvv4nWzZXrInp2nSwLWz6H4QXyLrh8xS6cItV1CJvlz+APyQCLFonzr8UJ4gNSXzW6sVKDS9OAxvXzvFWWORfGuGb9NdqICbTzx0saL/zdbMHJZlj9JGK71pjMxXFDeXd9DuS9RmK+lO0Qp9pz+SxIxR1AvC5xvmZbALo4BuC82OycxIvLvWEvANL5/2QelbpEZenLLPLy0/08zRfTewqmXy5KMXZrPdKT9FeIPnCuLzivgBPqwwuCptwG00RqJ7/zM2K2kLL1KxgcZtazEBA8qPfphHx0SVqj3X3W4b7pGhvR16tf7wbQ7FIRo6SffVKJUD2tEc65r8zgMAeUFH0iaQ4SErur0FAMcEQaZJ4quaDsr1LUtjGOntvYZjXDj2NHeKTifSCZzFGHwRuY++5umhvgobePZI7sLvRL4tkqx1UwlBZgjWJjZLeSldcnaEAXxxilbymgLqyFI5kKXBlh8rPDMI+pmMUU4q9shvXdIEShnMc0SKgaDMTInFXSj9ds0eV2IifaH9Y5ZZIyqGCDVdPhRXIeMKuu85o9Y16UM2Sl+zZCUkVqROzucJ89CRjbIjgrRljYPEoPh7s/P9/MTYnWuoHZgPW68axXMsRpOt+fj3EJPw8SWd7dvU8emGx4bJuN57Zuh5eeleVemamd0XyKF7HoUzvm5jEWsK58L1an58ShI88jkdHVGRouCJmJarqnxZqhpt5nR3YXWcWkFmhsiEN1KiCADG/gOGr9PuZszH36aqPnrAmzPmptxGD0kCg2uwDvml3tMB7VcFqHZfWxBH8k4KyyQYV5LTtZVXC1l+gZ0+9yHr/olr9cEW8JHlzVEYCuSuHXK4qiF+Ik67aD5wm47hD3poT3PDkK4P/vWI81fuqvXS/cz6NpkczniUy+bCDntCs42xR5dqLtf8Xe6v5anliqochsbTYn2GqrTudnCY2WAzsD+0HaGssAE24rN9ma/gQAmZlCM4ULL/75vDvwQhvgBMTOdWTBLdJBLJHPOnfXLqGz107Bkr3wDUofk7LZz93L9BmZoB+YJr+T1HkxA52NF3qdWop8gfN2DtQELRaR4B5dk8tIoQ8IYStC3Gg8jbxariLCLZoH6xjed9u2DZiXCWYszjpyrAldUDbM5lGZYUaHR/rt9hyRqiM89n2MrRT/qdvdVzZBdYPt5nr9LJV5lWx08pGE537sI8a5F63oOr3h8yj3mFUZQVe1vcgLSufSzZpqq+3QcNkdI1VH0N1LbFnsLhVLVhdUciT5LFBdrS6WOUWpaqnSh6ZsUYAsiVLQTQeS3dq1NBEoYeOY020do4LAZOays0/x5WMWdEQsWYK5q/TVQ0YV7wUmSB7tJCKsqTigXrJc6aJXTmyPGFVTMv7qJtsyI+LYbyPHGKNZ5tYN1eJkZrPCo2pmjqqjDljYOkCFb6uiodozYRwxGrPIy2Xr7kZ76CGzkzcPtZ7NbrCNMExYyyBcTkxdvAWnmckwJRunrdgYE8EAWK/62962uzlvGUnv9EE2LMxK87KlrEWBtG56RHjOWhsSSFgeNd14dkoQhBU8F35CMgxK6KtS8RDHdqmq8Gy2t5HgFkKXWT2bTjiMpUY1n/KsXVLEAUulGzr32I2esVbTKnfvo2wkazoQZhpDjMtCuM7kzqUuqYbu9cbyGmDdvUKl3O2s8oUhYM0q81hRuvqttU5oBDK6tOStN0JfW8lbT60oz30dP3PS+CtwQ1cQlhAUajaSZdHWMlCaYE8KDQvtXjv1uYgY0S9dJeCSZuq8bZMA9gxmFfAC6J+coNMLoiKpDxpYHSZaJsxtBWsTUD4TxClwjeIr8PXlZFgi4JcfsW6jc4o3OlG2iC/G6S7Ryz4FGThBEHv5xKixWnYZAXG6iE/i2i1guqt0676whgIDdQULE5LKAD3huYS4L4kvRvPz+vJYlCnULOS9Je8umefSNLnAA8BEBphBaZLSa8PRwDwOfT+h4FL3EqJ9jrxSryjYTudSt7KsQS5+1N2bZNG6PEIn8gLoXuBMl1WWI0dVjcijZnVf8rLPoj6GUMPluUUatOWsiEuYQbRpgWYRx1GE+nqVn+tN06HtDgWazWOekstWP2N4GrACH92bhUaa0W3Ny+Zex4wx1k60hqC1zlepDdi5PBxuAVUEk5S9SOh/uFsYivs83DxzHMeMCA/rYgkv4hIaAaiq3LLmDIuJeRx7jgE2rd0h+3Q3x9jCcs6KEbenW2bMboNJY1tWaDDY5EjVLOjRUhdKuJlFVM0In/PoKkUuL6aulSN9eOSCizW3usYWeu85OJ1q+0wfpWEpZHwymol4MISaHA1w0j0ncPTMCEdlDr3sjGB7S3Z1qiOqJlEriwPI9J4htI0yClhktkyex6QHhkpJYA2bNZXYoOhwEpnO6suWEnhVOQUKrTYem82jCkW0N0ljELJ2sZV2ZWz2rdy8OUnDHVhtXPgPrgHanEgPLJGxdCaew9c82zQEe0pZYzIQ94lpt7T75giDt7UpNnWRwIkFUBIGavV2HYVKpJQd1vhC8wrNkOa+11kIFoCzY9NWXL1S7UwqmlrCwhf15KJ9T3DFFwJ22qOEUPQ6X80M3i/dyvx5DDzZWSycZYVFONC+XKNrZdDdouw+Lqr57D48OXVfBA0WVSz/3Ikv8YU4fpn1uX6yyBbJInupQiX3wosq8yQ4sA4etVJD1hcuLlWDte72daXZX8tvTNqq1SwCnOG+ZrakDLZeqrIwVapnpmpMHUVKg1hUDMsaPPtEnVRbPFn7dsElLY1gz5ruq15mssNCh6nubzaOWZ5OtLuxOGv6QvNJUNoeNXhP+cjdAUXdiE00rZRVCsAHmh5RfQQDhn0/0qPYDnf3RqFqXX/WBrUielUfKIARqa1G5w+XZYKtsmIshybMjuPQN/p228Nj5Iq1133PJdTW8utNhAfBtDy6nCBX74iZW4hjJ8zSzJqFpoV3V45UwGcXM21Ro2tNU4UTIwa4DFXaCXIMOY/czIf7iqPrHKPmFDPj5jWnHrSehGkQXPetHm9ZtHOEO+bRI7LWDcGROauLRXZNbltGuCDi43YYLUda8Dxrlk5W6S3K2esqosXgA50j2dQNZ4CpRcBDBUArdl94Q3o0M+F9uKfyCWbXtm2H4pUUZWVN+frk53DAMYtmK2AHsCZzBLt7SlpOmJMWI0iGJwKcvXQgAMHMcI95zNh87p1uZoliZPhY4vQcAbDI4EVTdoOwMOVmx0qFazbcLJwO39ZePGjeS4Svz0GVPoB1TRdRXcSWgZhz13lCITizYEo3xOAF8tYLqWkNx44TjmXBMDS8LkX4qUhh0Wnr8tA8GDKLUBumqAEEwPYY63wVwaCrQgfuQk50m2jopwW4Dv1YZ/3Cegi0RRIy0y18fPXOLGbXhWjLMXviSzinac2Lvu4bvETW6DbTp8Pz1hCn4DScVUDr/pJQ7+f/UPonW8csQQk0dI4vrhsn1n7iUX8NyXO9eFu8Sp+uNOhY1x84tULCCRarvhKYycVStFaJl++qne8xfqYTfr4R10O0IKaXGwU84aP1BlGBbliI3YsSWC+HlNr91BCdcN66ZQXNsZrWybltuLtEbq6k1UXBmfcxbXi4VcmQvNZxyWMiYrqi1bBFHpMGqFKw62dVpf6JNMBtPqsjfLmquXLm9SxUN893Q+/MnDMzqiWbTIEWhzw9Oiy6x7aVQFezOWtsG0gaujgym5yl6LOq2ZfLaKPZ0MpyHIfpC2amDU5I6qyGIcM1K0ckYN1LhU5Q2T8SoWY3I1dWxjKKG91DmRWs5aIUNwvDKc5Zz2LXpGHOGeE1p4+h5FyAbtHVHjF7bts25yFbmp/FBXqLSQgIYrWPHOk1D3PLDEnBpGqbc3qm4C0NRtXl5lXMkQaZz2jmmUkJOpdhONitzABFXADoqcwpa3JF3c0aY1SVuqC1GEYOpcG4+2XYoN1OumKtsaEHXekLdGPDJntVbq5vlbn75ZIkujlZmaOtJOry8CHHsgWBsHCVTsDk0cmM6r7cXaoqRxJAI1Ix9BqpFt+b6TV7zh7pYVbVHgkSTLLdUWUUNTO9jlKQEUFOgaooZRwdGi2dzAZr9vIVN4ObQEfOYncP00pu7t3NvcxtzlKxh8HZpemmyxfUIj1BG1muWkyiE2YOlXZo9K2CEoItrQ6p3Qy29Fnq+kiRpnNB9ytLClgQeUMhGapMFVdwYoInFn4iPyeK3T3NBxfmAcDQOom4fK/6uv+cr7cgkBU7oa71ZUFzEjyt0NBUKwL4pdTXX9Q1L8cjVphfL+DklOu8XF3roVov5nxJ+jO2wJGfLynCltoSRq4MqDMYXAe2vWBE63XYySTojDPTmQiYfD+AVP0nvqZDHeT5vX65ZLTfvdwXa68wV/TSSuPAC4pGWyHO67Vg2bN/vtkV2lOz0t3NknU3fAwXkSCgX+9KigxYT570FCguLuGYU0YBkM/PzzkyPDU0CsETLu/KZ16dpwAwj1qbl/6nUVybQUOvAXM/chuSWpqZR1R1WPSLEGsxO+iaygOnmdHVSyERY3VFhLWafG1sY9+PzCxOGFS7C4CN6lrV7j9PAajqjKzumtMXFgQSPZUYprbRI+UPspUI5vI6L6wf7RlGaAxvsrtyxSarLsUsnTSC5hiXocyGOSszuipzVM30VHCbgpr7tFd0USjcggfdWS0lt2bDVQ4Vkqy6fq7oF8DGGHNOPa+SWC0pkYkhI8glgcDywejrBErfqZaYJpcZBOemok8pYnC9TQ1jWnsfsKu5TcUuUjol/XAQVk1zaI2dzWZHjqUSJMKzrckOxSUaYwQlnBfIbgrOMbZZeEIdEWvioKlLeTFv0u7PoyKcjtAo6IzLpphJ2SDCUU208UxgNSO2iFToo3PW5erqXEvtsNcBvuzeMEzffJ1H5Jxt4Z1nFPWa9XDcDr9LTZb8WfsTK+++Wog1iZ49dySMHfYzFuE9zejSzzm9zaphROdAncr29rXhaajHuaTCqHyshVwScHj3IqKaSo2Uvu30XJoPGTHNTYY9j9EtR9s5zq9BdeGWa/Luck+wV4zpiRfpyLL1A0pM2XqVixzWNKszWFSRnaeuCz/BYhyw7GD2cjnYefj7EilBY4fuhiUY1YPEBTuJGf6roVzE+aT5cv6hS3HNOPeIFZ0v3trNlmsBBLW7C6zS26m3pcl1TZmRcFu7iy7DFaB0IlKn6ZVdWC2uXGegi5E+SXA/Ky7g9LNzIMwRMLTx2BJ3d3kZbord6tlL8UgPW2lDYmsj3I1lZOuAiojaO8fQ6541z1tnReCdYpCXnPmKjHV2w9xtTtNZ0dUEI9QPg7Ft+t17aT3o+lcwV6Rh9zJ/rQue5hBRLASf1O8zzc0iSFbVGNuxHzmiISbcS7YsNosOa/bIUdbdzBE4jWa6rhTho9mxieOY4GkfXzwNW1gQQQ+vo3ka6FQNofSUU3nNfc4IN1O9gAGHMnzkqUUAqEif+8yR3Z1jkyDOtS54nLmbCznspsowm8sWVpzz+dium8MoSmxNOlg3J6nVx4jF3xv6mIJq9L3JSCVlo5d6zn3Z9uVwFgh76LwZtvqj44WOrEjP6qgyawNH5lymSFE41kAsi6QRHJ7EXFej0AyzifIMFmltBp2kWybMqujqoY2UHNbMZ/W4XMAeY8x5CDNGwzPM17OTI92surftejueaevxOqq2MTQwehgdLMb6YmSzY2xVc58T4HAPi2bDbGwbwD5qnXfdGKH90jJf6p3JNPNqueTRVWNLNLs6t4B5pHd1AZhmqjZowiV+Y27mJGArInBJvo1tLsu+OWlW3U2no+HMZuvCZjeqw6Rl7qBI3DQu+SokQdG0vLRVFNerkCbMsnhRZIMQvkTxhwa8GJzME4TMrcvJqn9oMA8WhdtINbhklWt+VXjRSpQ7z40FHAtuwYtW3l8mcs3sSyKsF6cl1c/z/QR51nmlgB451RZzcB7E69JYN8SS7a/NlWBRrYVd/DlIgwAROVhlvuix9fK1Sr2EDoEESkTmMjnzxICWaNzWd3ZN/vrC6gVxMVsU2eYh1aO1rR9q65ZfGlL91qZWC4OxAxzZW1o4yUn2St6v8hHmYWyYDu6VEy1PUobNWcQ0s1lTVmopJnXHVtHjNLHAumiOzKGVUaKpWSeYYZosg5QHba6/SPJtX4N1N3Vdty1JrgNTQpiiHFfCbJudniUacZn8zQyzd48oVWma78fMCHaHK3OiMkaxzRARx6yMKPUJdseJqbYY9dUXwlRubZjrm6wYYWc0OzOKNSKPWYApyUfILqhQurV5bpdN040S32z4rEqPmm0uvKLdzZw1G1jmn4WlEAQjohXvrM1YokkwPC53lwVM0eY8zENpUDVLmw2KEbqJjBLzDPHSqz24umyBgFIKpfyl1a06SQFW7lasOqaHwpHaIByMdKLK+xhBeNzEKzq17+qSVKSEBQ0o1np2zAwoItyKYvhjP2obEW7dmNWeAVuWHhh8BBoNajjiOS8GLUfWXBckBDAbLDzdqifcBjw8juIlt4DDVp2AGdwH6/BTtOoGzzyTreBp4slE5CCjjurucAtzM/RUOHCwV4+YmUUbDTVLjaMSNMq0CUOkBSAz2Eo/1RG6Img1Gfpx7OGhgEhHxPDVugVa5ywdpSk4Qn93zYNloT7uKoexAbXIdS3IQFc+22Ay0RlAmwRWSMcKsZExTAQsJYFZ8/bS+Agtl5gn/AUcWRMyF8XqGthP+/HiOZ2cAji6lP+lzxh//T+S65xCgJeDWk3ZmjNMWhSwzVxuR432Czrp1Zalz+U8Zs9F7Gfj0s8QkvROp/seC0Jel8WZ+MuXr/tKHV//pYbydUtRV7KtzRs4lyYsbvw0CfD8t+c/XzzFCrqFndo0Ybz63zTGSZ7lCq86fzprzwuu1y3TuopdIOs4JPNHAiepJLCviimBzZyZ45jHNvL5drteLvqBETGPo4seWZO2WmxBQ83O9KoyD+WVLeeI8n2EEvbacNYZx3L94YaHS0jFVr9T5dhanTBkK1uhSsM0YXsdQnbNXOuRGqsE4JiRXWHmcJkz56wMV4iAuyl4/zimu9QPXFXqcNDmrAiNU8wxfN/neUavBudZZYY2uDgTNzfrnuHLv+JLwOvhywyOv9qRASoXOuDFkr2b5H7bTxeDA+BsM1OUtMGwACiTkpqxwvyWswPW3Zmj5YvTgNYG49i2Zpf2oPDzpJEcrTOyStL+Iih3lZvNOT0Chkgd98sQGJkSjvQslYfklgDHyEvjYzd5xNjOvoqlk4lMAGM4lwKiYRbuHs6l1nAPn/K7Ky4WEJ0BIDJs3YktZmLBOHbWm4yUGSbGqJpLhGumT93ocAyk9NSeGRFdTcLStcnMWT5Sw0fra2dmFqpK1f+YB2yFk7jDLMzd3bphAqKMnOwlLu4Yq9Hy4lcDWBN6K8gGQk1u64BpdRPXnOYmJQjMHIjNja2Q87SortkMX+7/2HtWWRFw9yx9KL1+B1axHQpdb6XYijpsHjpi5Ph1o2ItZOoqIBcYg+YqZSszLhTUSKDObsSFqliwlyNJjDGFrS9zb6xjzkSZykVs7kN6VYt1/C1vv5/H3zmh2ymNkzGs5ZDS14HnZXVuzASVkrteyjpYdXJqQNSU3efsfSJUctYB1a3a1/VnFMG0tgA9UefffIL56w9oze0XVEmH1FpwFBKnP9klNI0ervEIL2uGnencK5rQjGiU5IXn7UCCLBPgeS797j6aR3Nm12XzbZihhBWZYpRJN7jZMadB2e8wg3sUiTlFTl8ul/32fL3cHfuRIw02ZwGeaUu4U23AcRwR6ebSheuBdzcFWHa3ObvbM2suFqpn61sjBTkMtWT0lI6fZ9ewGRR1oyFdqaVsVE9zR7O7zf4aLRSsbSU0Xg8WaGa3254jNeFFpk756mlNd3fPOWezw5Ywr2enpvuxZR1rE9eN0S+PpZvkqwvO0/1DACqjoDiDOaefqRralnNE1TTzsNj343LZumqMESHwgicVZH2STgIQPUKH0XEcI0M5j0Df9go3z1g4L+hhI/M49haIsxZ/jW6I8G6G2awJoniYq0Jr9UnJfOAWC/MlIsLgkB7Uaq3hsJoH6BbpNl2Leq+RpCfd1bLVgLsENTRiGTrW9yqsjeyCWROOWGIDyS7WN/+c2dw81+wWZrN6OesBM7yYG11DkAqwHGS7JkZzO3V/W6R+faEKMN5frp/mc8C7LSLBakxHwOEzzM3Dj32uAT9DR9IIOxxrr4/KGBbGojmqa/lXWTYu5Bqpw9DdaRbXIbWoKAPz4QbfvEhA8ufBrkhAjYHdOlmthA94MEg4g2RtVtrCaU7rcqd1McyriqXH8rQEg9ZgNTkNAbZc2wbh0Pg5HJkSQgnEKwXBSeTCrkW+isWFqfVq4Xu9eMqlBdU9ATPXUs1zgwPsrFbHGawnLEV/OxeQsh5h2OrBWU+0PltByHpazs/5dDuvXWBh/n81gtsay9f/8bIUamPmetkvFjcAFqFBQdDlz3Tsz+AToZvP1+XFU0bm65t8gmUn8qVbFEv8rXglJSAB/vLvLUK/CsKDXKFmkP9JaTENdhnaUI6621Q4XXMeygLoBslIpTTjpXWHgpdMbb3lISmnz3nEUG4zKR6RIDjGJjVURGTmcRxsWkZ1byOn2mTgETiO3c11i4DWPRW2mJGQa89NpdM4YYEmUQ10Ri4zF3wenRlVjfMQE3kZ3hbixiSZbZWpuMecc+RolhhjAelVVLKAyN3q7u5MGEwvaRFDgoDWaXKWMDjOrfClhIHyB+xbbt2lzcxoKmzxMMomB5i57kCxLkt1ZbaNAZ3I1ZKm1GzNT4r1INhnJBMIlVV6RESc6leOXLYqUdmyh8+ay3PYmmc9VsS/VU3pf9YDev63cx4hmY0GIeEFBODdnekAZk0za7L6yHExW1jkCIZaOHRzcCG6aLQ3zDSitS1oQCScKrEMfjZU04zHLALVjAwRjF3yiNrPGg7ALTQXOEy2g8s1j9scY1utBL04lTkFeBAwN8sIoQo5krW+zc0+uhQibvRVhVERI6orL6M5ARvbkF53LbThZpbErAoPnuYJW7F9HsP7mLltBoT57TgkHZf+YnISzBGkdRurPayr1MzoCASU1CeU6BJXJf2VW8+OtDrUztjuccy2QnjwYPjo8rm30tA8rRtdjQNSngPkJMONjjKyILAeC7AW+dlVWEOwIGzTJAk1GkScChftcietasZacSEGE2NEGQFhAMyGe3FtXKAKvHA+NCdne37HbK0DC4xah+eLBECBK2JW3ZxcY/7qTl8jsy2oXK6tJk+S42QLdAAt1TVsnQXr0mlQosJznj+lNwunAEC381hfZ5lUwMoIWZRvGl7eSbafM9syRb6ARbL8LlmCaAtfkJ3+8kAfLZ7AsYAvW1Z+d+KaeLiO68XZN/3waiqndTnPzd2jj0PehXB0FWDyUcr8BWC/7ZnhcIW7mds8SlW1Xb2NrWo57+ac7imhUXgSZNEt9Fh0cdahbSUiX970WVPUHVkZqXXOzYrWrSJuJwm36qkPzWyFWqbHnFOvlt3aJOYsA6onuLgFcWY9G7mM4u6h643okUMzetromrbwcyepucug1gUuQGpksOkex3GYFdmRYaCHK8lam6HBY9Eg55PrsSKwuA7NGAJPSbKrc8Sx75fLtXrqq9FrY3pZgirHSPfqnkeNMaTWR9MzqmbPim1tCe567bTwjNz3fTUlUjjG0rJ4uMFrHjGyNX6sluRp5pJJ+fINlBg4wNKXwwJuMLojAe/62alxGj4zBtymmXmU4tjMFmjS9HTSussiCES6mDk2M8PM5qwCPM185X5IRF9k1R6Zlt6kexA8qpA2qwZiLrMbapaMj/TWMTDlyws00av8wLwDET2LKxvZCeYljznDAwa0Iayr4ToCITRDS28A3WoxZR+T0O1FAMOvMKuek+KyVHxh2nbdygFaGGg5CJPfompGpAUURdOrVLsXoQgPd4DbZdQ+2wyoHE6bkQlXirO7iks1qM6Cm0WiqlthKc3ZhjD3mg1Lc2KWG3uu3gKDODQIwliwzkJEy1rASmsfhBksF4C29D+KNX1B180UPGLn+LtUoecfOr889nKOrtHctMtrx8Bp2tLd0I2X2X9RBfpZ8YKtn1P3kpb+TGzAFuK/bm5bJEVLa796jL0XnC3Fz2m/0JZ6IqsGdK86aK4tYwGaXAwCuwTkGk7akTiBM1133efotCB+8PSpELZItTKKBjOzkEEFgstTcZuVUds4NVCKx6k15ndNj0Sz0OarBewEqBeHAYKrkMfdU8KBmm0ApMZTeSVrZQ8DaEasZa6axuWXXjywe9BlyJpzygi9cAUXnzGqZWzqlU68FLNwt5pFBbv6ur7dQ5LqFsIIpTMApIWriKZYGT5nmzlhxzFjKU0YZiPGrBmR1Y02KubtKKmMjDJvTnS39ZoLDOiu8AAwRkio1FJg1Bxjq+oQv+w2q0BUz8xkLVwHAGqle67pgwyPvITBYuScU+pPd+vuDPeIYx7NCmMIr9cRJvONHkGWm/tY6n43m0dpDx4Zx5xmUH71SoXpEhA/FTM30gwxRs3q7qqb55K06jvVDaUC7fueWxqj2XMeIwdFpdXBnmcHrHnmMYvg0Xv6NuecdliMxYW4+H+vBRqGRxZrSd/MPYhwNMJDb/zIqHmQ7hGOZT1aduWS8XWFoijkQteVkAE3ZsQ+T7RQX0HDUbUS+jK9/ejpq7+wXXmlx4xIKf3GdiGqqlyRpSdM7h51HGbu7sqAyG0D9VV3gE6rJQ5xQ6c7q9DIjD50drbRfFs9BN3KLo9wd7fbsQMWFtbonltkoedsADU7Iu1iXROqPI1Ru4LQuG05myvzg6B1HaZvHBTlEO3RPApkjJRiTMOUbPxQoY8eAnPWBMmW6+AF3imFKwimB1f235m+E8t5pROeJ9u6RGEraAdnHPv6dECuzJw1Rsu6QhJnGcDKVxBCLsd4Keq8z3+84n6gHppYwxcgHmLpb07mACf3eu4ofiL+orDPcwen/x/uYd4Qne6S2+lXeJncUbTwlXunYhNFkffpFLB16Msvob3HVrPYihFb940v2b/phP/ZWFanllY5wiImjhHcRgJ93A4QVdWkCmTcQ27WFRgCSRXaVinDmVNiQdDc55ynGLLNVEFqcJW1YtbMHOzeLpvKEJsdGboYxDm7O0uYYkcMW9dJVTXBzIElQiSXCmPlIOgrNhVOpyeB8gbWrPYIR5Pcj8Mc2xhsQe984fludURk1TSLkVt3VXcfnGYwRPinT5/GGCTmcSgo1Iwjt2O/JZsab7WDaOzFIr7ILpKZqUQBi3Q3hZOcs2q7eUakZ3Ga2WJi7a8wQbALDHaVe/AFXJSwXS31NIUz95wCRYU84EwiF1IPg4cFvWq6e6SzaxtDMBxMsRj9/6PqXbfkaG4jwAgAWUN53/9VV5xOXPZHoJpa+Rxblkh+nO6qTCCuVXXOwUAjQEQon03jNo2LFL0P5xQsIH64u895pOoxt59wHZE0O8ZghVLxCU45UUvkGZnhsYGTg+kJN9neR+6jKZLxqAB5NPM+57l5XfK6KsDfRX0i/PWj04/fzmmoUFOC5/BQ2q1BaVATYdWtIkEAknLNAAM3L7TTnd5d6tj93OsROkc8ojq72t2/sexT3YPK1Eqrh/eJM9NqN5ZodWY4VB051WFpjHAO3DWm+swQNnONDp+ces4zXSDOzw8wdQtQBt/Q3OT7PYbRuMCBcrQY/4m6FfQSksexUcyUuXrS2ao8qLwER+0YtZVPOuiV86tePfTbF2YO2Qa1hOwKia9Z46vzfBFuABsRwTXFiptZSc2O8Bwz163whdJffYeWndiRWAe1+6xSfO8hys4U2ieMVFT4JiTxjcpZjpqik3w9vaplfq8pvuoyqY8ktZhpey+unWfXprVv+qvUeP8XuYilbdCbFBN6sXWGmGFGWWn2D2sa+XgASeAXGefMoF5XtdzScj4T9BiVQZl1T/d1llX95z/Pn8cNNRhzsjE5bMJUjLRtIrPM+AqL8q6h8l1NcE68jiIQJg/TzBhcx31EaBfKrHe7tbylT0o5NwN2N0CzUImY0b4d450FJYbp2xBAAnDUowMJFMX8vAXdo0abFYxsM2XPC9Atw2R0xjTcw8I/f3+BMTczk2BUxLKZlfabWB/Mb94BY/SKIjBjHpm6NmDb90stVhbRaNJu3ogz1aCQXNdWKBm3UQnPTRchKVPJ9tcYzcyy8kTsRtW5z61Uso3B/Pf373HvgRvA6S4Ln2mQUy3ZgIcvqhORmR6xOV7cWIz7uREOQ027sWCz9lIVy3xOPLfu9MLTejn2zQBeT5zP9hP4zTSLsPY3v2UG7qFv+lMfow6+7drWDm/E16U0wM0+x1L7vFEuPpIz/RMHw+wrFv5TaT3u1osGpHGNrtLgPq4j1enWlf85f37vVWCLhouIY903NcXw3jKD0ZtdbLVJxPHMMna8QdatLJEejzOc7AHm+XnUmPH7SQ97fRca6wzGiKjKzn00Pc69abRbaRFT9cTZIBA/x+xTZR5VpZIfAjU9VCjuDNrCGoWWlNvGyK6uHnh2ewQVoCSG/FZnW3DIqUE4hia5tMvV2WMBG7eaBo1tM4lxUzanCC05+1DTlQKDprYw7k2VMBdVSLyRr9vpSsOeyPjX0rjTqkAlheFsWYFwGABv3aluhWVdXwvWd4EmB1MYZ4CzNXYG0NTVsxp2d46ibNAjuaG9bJTuEm08X+EQZsbCMWP0neF3KsdXTYc3UmJNawsLDcxbUHIJ1shl4JdnXwjMOJTzccmKbS3tgdn7A7xu6HddGr4NOaT3G2w5M9Vz3J0WmJ9jj1OtSJnaovn9PKeRk7ro4oT+Q+ni8ybeL8CMN+8swtZxPG/S1OY0MgAL5JGtDDZogVheayzYE+wr750VhM7MqMTw5TxmAD2Bap30cAxbZnVSY5BHvPPudI36PbXpK25pjdn6xMbMXTIL6So7p3RO1MRzRtUlI8GOVzdtVZEOxjk2M24uYERTTHiIKJednaTcxj1vR4MTgHsQ05V+zr2/zwnVhmgWUKmLHmp3w6C7q+7xg1f8JB2CoPDuFXMe903d65luKWTdzYg2BxDh09OdHjHdEVElpGzMxk2hSJsyYLSsdVLoCetuNO/cAUIubY89+dlioiCerYsu+LjM3NDB7vyYR40MR2VETho9PHoq1fB5bHm1t5RDwRI0GbgHG5ixUKSbZZe8J53t5j/Pn3mVINoJd82k0HhhP+YH3WMWf+/nnCNgJ7uNVr10pMEac47SV6zqvgQeZ+b5OQbrkocStrmM1j3VZWFVTdDDayTu6gFq5pxnoKaN4Zgz6O3qwgXPOYsgk4SeH86wMm2oDSMEMCpR/MItVKpTlSNB5KOSS7V8GHvG6UoE05kUlp80G3+8bkV4oTiNYKc+mXGPNIW6jobVmQXWhHdIEbEaU2UHYUNwLALddOzWrWNDL7MgowHd16C43V/vcatQna8U5p3RuZDO/O8aMXtIKP5R1k3Jf16wvSmKGVApMTRoL9ou4EiYUzi6bVaTzT3B32Xl34yDVx4zL0/xGpj61UyQRkp4b/Yqhd7oNHA8DFwTX3c1ao/+EX71SjBm3p8XmBUgkQud/88/lLIl8WXI3xsCVRV7NDkwdT8/0T/P4z5d2ZueD4QOU3aXW8wLQ73AC/XvFwybwUiIYTkJaeSvLpsi7ZYi6zWd6gpEZ8exqe7KlyNXwIkMeUiln8n/0phB3dyfxb2zyKhSALBsClpRWt+vGY241QSy0k1k8spqNEaEe2WKM6yaniv1fAtN8cWHzS1v7n3oG5RrZInlnZqaN08c7UHfzEzR7qCZbS0K7AVheroqsVumDrPpSjevauWAyUrZXR7m4au1mOXsumt5kxeR1BdMY8SRBWzjH+TzWh2zSTODvVvbXSGEHIy7KbZLeSBSjCxVANCo8fPrtZFyNNwl+NmUlFopHGl6pAAJ1+jh6HbibNRnAbM9rDdtAOK3/lowwsWIiWTX+yZGqWW0JjCSloye5rCgnr4ZcTfgQpmDNtf33QPuddKT96OnvKc9zGjCtafbw8O8s/mmNj1PWBjI6jFj453SxmZgbu6EKSOB58QTQaOZPc8x2n+eHwKk/b23p5/nGBkm8B8k3N0wmVmdsPEwIRYCDwH4/itI/Dw/uyW4+TmqSUh5NTFV3dNm7hEu5lrMAzZO/O3V8ziPOc3djzwp/fPzRxDztnw57ZiFjwNBC7fnsWP+OIx09xM8xjAetyeG0ptbz9CD4TSD+ZjBDt3pDvU70kBv5YDLlqPON3fohTGXtXmxEjmEzWFBOs1B0/8ecOhDm7Gh0XwXgPe1HHDU8KVAAulQ6fqV7m4eCoh/kX0qWUMnhUk6Z3yTG7CiIDGruj1My5x+NwF91ntjlXK7WhsvVrnzEgrU+T/oqTeifBntmdcAo8VHg8AKoN5/NeT4fd878iVFuyQUneyr8ik7j5AvTrPyT/A/j3Nufn4xkBmps6snPwVN6+CoznOmsrJKrBUF1+z1w/upDS6zZdQH1Njc3eq80uVd2V3z+9/fz81766sl3J/7vSR8K+mHtNWymk8js6pHmRW6AIUa7nD6anrqpc0F92dmd49iN8N1JQ+JbWJA3prGzfs+OSr4Uw7Eti9kJmkjN43Hzerq+ZYuTSkBAt8BZ3o2AfBdCUMEAC0zN78HjWnZnUrbRyngt5VnW5kR8TJeAkk5BiPGt05BKAdJN+tObkPbiqBJ5a5Yd0lm6uH/3CLECihfb0FVhsfse7JZ2l19TgzgAh9HmtTap3+6pAAxalnZmI4Z1/oZMVAqTbE+xtpWAExluyuNTJwMmjs+ddfAZtri0Vcb26E1f35+PplSEJo5uk/ETA8RNElhPveqEmd6zBDh3bowMMQTf2goIbruF5m/CTf0VP2a6fDszvGI2wXp1YnM4hhRZtg1s3k7swdmJ1wuPMzAMYPukpiuZn6e55hL0Uz6TgXgyCET7gyAWQXSj1lbz8Y7NTDTHvau6oiI1oFk9vPzM2/elLsBqFuxqFFjoBVZv+Wl2mjYGhPZXgA1+ZXykpk9hipMz3msSvnn1lP+GAaoEhePmlEa7hRGPU0zcJCKa8Z6m33PvBly9Rp6zaDO25X/7tMsKFbJafIAyyaCly+VVZFvGsQyyAMBSrOBDlwG2DZKSLLgL9e6yI7YWz16cuSoFUBM8ov6mPNdQhQngO+1YSE4/kWmvnCPjmbbJWUDmck3DELBPpu0rd+456G9f9C8Nuk9+fVSYgSHcb3Tm09nbzYpvtLbDSKsWoRlus5h+IRB2b6VraLxrkKPGwcKJAbpmQVCE6qFzZuWuw+iPtuBmWfWpnu+q6Ei1YSiEAi3rG28FCtTNYK4dfUqbrq6VTkp4qxrpsudBjZZtRLM11Y2pII8hZSMiLQZwelrpDWzc/5kfgD1xkRLaj99PHTZaFDeR0j+wd7NdHp4DCV32swmqk6Q3+9sEwHlm6Vx3lgemWYYVqUsITNaZsUx1YxM9zmnS9r/kaL/BSumssxp5rNAmA2hbuXuFionUTnkklVQlv5m0tD4BjMNJ7ONxlhZdGbt6zIDwM1TeK7AvmxIHTHQh15ZPG6C4Kecpr54UbLQ9bM54DI5oyo1FHjQDU6on8CMDE9Vx5kD1hyFJOgmh+PE0fyCKm0SQlcGPU1fOYoJYdsoKLc3fRPiRmg84YBl3XOOwbnyHpMBqqv//Hmmxw4rG+8fK4qws7tTcj0Z/83oWjEExs7Cx9TQoziKmZ6y92XYgcIsuyKeeQtDtB66KTK3bSynwFEQ7NQVT0ixi93qGwbaw6o37dzce+huvGLNGQcwDMyG6/FDFxtgT1mYc9psqjVOmxl6SpUdhr5gvGLnsMk8blL7y5E7PWR0V31uxJnOm4rYe612PZ14oWfQCQFTOqRHBffSprybsI48caUzEv6Q9gU/5C77ug1eQF2/nqMrglwQSeepkgOcoMH6n61p4Zyd9AHA4OZSh/Zgk1rEgJraTvtduNvoe1AswC3YXdjp/r2+K7p9Y+X035QQhhbVtHj/FnfRXoJ3USC8Lz1aqrgvqMU3ygSbBWTdRUXc2jeMdfTUmb8rCTFTjvv//N/z83jnpxUzkNMo2oh06doYVtmEJHAw80p5N9aKtVYuQL7rHsUTiJ4cNX9h4xQb4YqAwCqmpP7Y0hytUCSnuzE1l5TQSNDL5FXuG06cnhSuvouUg/0FIsZd0bmU1Iewmcx77ZHtn2Yr+tDFoYBwIxXca76xSrr1N2QCdj+XZE2N4ns5nAlze99tZLaBytWZBcrGBf2vOeV1DJDqLJmdEUzBX+CYsboJuZ8I3Q1ruim8HhbXfmFwszUcyGnZM+I6Df2WSOhN3tC1f3ICdObKe3sifGa6y2ggBCl0yROY4d5VPZtXUbXBfgroWC7XTB3KXWW+ClHJY0/8ZNd0uw3rGv8YCaNOnNaOWfecB+aLO769wQ1OtltonhmKxakTP70ZC005fnsEAlJnoiiZ2TkNaOXKPj+eWXnrxFIXJ0I2KmnwG0MFQcOIptF5VvsxI3mDjCEWPp0oOMgNbVu3ESRCAGuaZuHOrtt39q0c1xEmiH8KaDElcSK7fNsjKM3SZkaubqLfsgWaW7hV9aiP2uV07UqgERF5L8E4hvF7r4c/Jj4YnP6J81HeC6DZ4Zj/fj6SbJLsUuuAomQx4DC4nQdu0+c5qBkynoDKY1+Jjh0byYzMp5vu0wpnx7f+SDwuRuaydfhs5lz3v+OUi6y+IMiCIzOvO/C7fL9D+Lwgi25x+VG0wht3B9KvlRUGX2FeXfc3tcLXjIntYZD2X22xXwhW6qzee+d98dXZoM9wAfuXFh6AJjGxzvr58hgvuIGZkanin7Ji0F1UAMUuO9DRKR/Z9wPANzFUTQa6kGYGnL427YZj3HG5SuFgNFYWwmSmpaGrGGdmqhJr9DNA8LJVlUuaca/LVymP7n6yqxiWcl/mKfNpcKq1xKj15JwYlbuPYOaGviZQWWqq0LVNb0er7UADGdAzddv9zTbcC6ynNbO2vkOPp6oIQkUFU8qMIalq3m5dx5zayFvFRvTr99agpFN1JQKg+l5mZuqWBmNbEO+dWRwWJihSHAJaEnu4aNwXZFQW1ggzVJSKSUamvTU1bEr6rUFAkG51RYT2U2WPVZXRNuCNppbnkO5lGHGMjAia+WrybQsZ3KSGNrPuxZTPW2agT3nmPWR3eMGoxWxzYgpg3etu3ZeQJ6uExJ5jj4E9rIYSpDHo5HSYs2ry05VdOW+S2h4oHCXB6kGMCHDiOIhQpL6ee9jnJkgL/PxE97SEKAuvGYDK7mz38BN0gqgSRGH3N2vEFpTMZHTzsHg8fG/kwVSXjNbAgBjDc0JfC2nTrSpHc9fkFe5ZV8yVh9NgTje6m4LIAcQ553kUrwTCws29CwDjhH6xsoUiLEK/TJ6PUZ6B+xmMGcxVH2fo1kWrBJHnHDczUN5L6nA0affMN3Cr4sglAhAMPZljYastIAabRL8/RTAi6IRzjHCzcEHZ8O0us3CGPjET/Lo8qE6znZGdMqObY98LOViN5gPATEKQTXQgaA6jcgqXYBD4S7o7lcfksdeFoGRJhUz+gu+bqnSa9YXT3cJf9kScLUcTuEZpW+gGCxqIdIWsCf8OZ3Nti3wNwJpg7M2ykN0EoEgI/kOQ1u6Ld5PXw2fvb9DhgpU9LQ+xl0m3onKWNHiVt4KXzSwCfwKPj7F37/foHCX1C7IXwWCuThHL7CnOIDPvvXJQayarUkXXdE2XQvWtqt0c/RZy7aL2jvxg5VQ3XoWobZbGDCY8+MbDYZjVMvTrUdmZECvsfS9Bq6xqvbWsWk9k93TjZuq+kR5CLUxCPOQj0U/RNaXqRrWD7Te4zTs3r17+2hrdox/J1DdCThzvGhn0KRSOQ0JK2BlJ7kjO8+d0t4I/dV13lptyehzA8Tjhmk9JHf2pBU5tLd0jXk/5GCS78h1ApPyJV/I65gAx6oQxo1l1ZlZ1ChcCZSNc3kwwYtU15/PzaNFdPOmN29YOK+0QMFVVnY039XqHx9JpGHG6Cs2uCgP7QzQMXT01ugXRCdkpuzB9zI7yV7uIOQo46jagqkTGiVRTPy4NGOE//HkOic66WeZk2LQcOTTOQjeAAZU5Nb6XKaqbHuBywmrr5KKBk7cq10DnRxbuuvczPRIODTYJvKplwcWMubtT0tITru2wqnzoFiOikW6zj+Q3XIfD7j4nRKFKJgPyeY5Oiec8BlGUkvgY1OH31nPZrlDapfjPWBWuV066ABr9hLmRY273/pKjc9+fEKxsIeZvzI0hgB41Kcp2gGGb3tLvGRcmnaXFdioBgHO7pi3o/laKgrTt3IDNBoQb1Qn31oRpkjUzs/XYkRxj49/JSXeYvk+bPXntPSIELYpa3QYb3XMDiLKyUH6FLiTqGtMnLLpyuV038c8uRyIpYb7AgH3SdFDPbi+iCnbd1we0fxrXt6pnmF/R5K4LI9vC2ux0NchnhB02X1J41wjlMWg3oaZhkR/4IkCceg7+8xMmALPR2A4IfQV5V7FVOVVK0DNJURSQIAhIHi0KvZOaE3ipR+VF4x0iZ3plWjMwk1R7FvPruTcHIxnjzNC8lzxvqZy6p0vuAt2RS1HophV8oh/za+/aG1FCgEatUnm6BGBqu12QR1dpmGV2KHxzm1TWCOIeFjH7rHleNZVNVPVR/nCXu93KE0cJX0ZW6y9UQhQjvDK76px43cLD7uc5ijoQg3HnqxUVga6bg62kJFmkV6OyNS+aTfQhaF5xj+6ysM5yNcibz8yJqL5VrxbIPLsk6RHp26/AFIOe7CoiGnCSbt+i46+gSpjIvBo43WhAk95d5t5iAgnQ7uc3+LAb1q4xclqweWbC58Sf5rhboilV2YPzHBSqm2Huph38cytpwMSJz2dVbtUVJyZh9Oyhobv/78+fe69RDmG7mX5CpgaQ4f7pNLfO6q6qJjV0u4Gfzz3PERDsuwcqHZA3E6vLmroVx7rS3Faz+wYudY+HEWZUjWV7uI9lpru34jy6M8fNzWxkf3+XQyH47l41plwUIG91jxa4FAPtWgIMG/lb8DCzRot7bWfdC1BostMBMMValX5X9fz8+eP0exO0mibZ6Agv66kJRn+umZeL5JWzGiqI7mk7epdWxrdSEQLVrUx0G4c1Z+q18X9nV2LRswVn7MurCSjDIsh76Epco4CMl57FzsK6L7EI0uiCBIYyDZFYi9MeiwOYFNiO9TzayJqv8fvtwpvZP5y2Zcb9nkkCqF6ntKA+vAe0qOCV+e95bYRO0P0f5VhIRreBRPOSc8pO5n6aoxQKXWaDMVK77HQT7/jRPQ2PkKFGZy2m2TfYxzH5mamswh0Ve+jhNxJwnX1mrj6SnKpKrU1FQUB9FGcLdNY5p6ewgcyYGdUnaTBtJdiTn98rTtFWVgdpTIOrPs8qo/ihoZk2BckZRfzez6URQ7Xb4b0OxfremyR9cx2uH3/JGJIYtodXSTWnLiMlsElRRnf5v2DqIwMVkeAeeRNrrffuctdR6VbZ9gWoBP5hSEaEuw/HIwbjET0bRBfur94RjalMktPthuccGfzNGOGurIavkLYBUBJP3brnPNo/5Fqc7ggn0V2htpbNZoRkxVkXyq8Hultz7tfpWCp1MUmJIGRwoLiF14kBqBUaEu1iaDhx+L5tEgi1+oeVYUnBpB1m7HJT2M83HZt01WiyFZf8osk/56DRWcZx4+f3Tk+nvItWlcQmo5UkntV9CwqUxhio0FepR3WcaTCozWoZEftGhlt1/ZwT7hi4WU17aBFZU2lXGQ09NxMQEgAj/jxHKA02mI8EzzkCJfUaa9qicdD0N58SpCGOh4ftI0Q372kSlQniZvWMSzo6BPA8j5kdWmfPbKCNQVTvt3qXQoPMbDjVdZ4Id7Ntf4BMTCBtF4vnhA4ODyOHHGpMH/VVEZzzOG2c9HA/ZodGuIdHmMMV1W2mFDP7ivLttdMq/0DJRtxMA/dDSjO6kz6Ge66aDSHUVTpOmAbwXRzMfAV4UjDrJ3cbrJ9K39JyCW+cQg9Wb6oh37UX2tLS9hK4Qwunon+k7rRXK7K3C3f+fi8c/M//IwQJ31lfkia9Djs4c1mN7+/d//veijvGznuN7O9dTGkw0q3vNTdclz7ktDAj0KLI9QxiytE/saEQWSXcWlET1Vj8RJy3BDMnJAHFK012Cw4izv1UNyobsOzqnlvZr31Bf3NphDIrqyTTwCygHh4z1JBeJdc86ypFSuuMtexMuiWmM6//T2y+/mPhb6BgLnaNRjlXIUGvwDJvkSaddF45kVqz1L0pDKDe9tau3qfL1oM1jbqpBbS7F8Ja4mgdHOIVJswFG3mE5GWSJ7h73vRw5UaR9qmPfMZrtTie9yVmAYWsRjggU8yIXFUHwADhnn2Xalsgdb9te8X4Kgc+T7TQuikAcYKNqSGo81RAUK3cVTnpK0BTpc6JM9O16k9JXPk112bfqlaotYxm7q5Uyq4ZtFvcTMZxG8oLyqGpRgaS+qx9BsxMPo9gw2NREAnHn+dAVKTxiVBhTneb7+kile6nPnowQMgq+RVA6HLCXlZiVJuDvOkREYcGFELUPTAzN3OkCNQT2enHWYg4IuZ07hMIj6m6nVltwBsX+m2c1pDrr3JjLDS6OjHVBbI+GlC7RGH1enjxwg/hcfOG2dIW4P0g3MyVy9o0xVC3m+Vkd5kHzWIQ5BCfKxHBEKxuVbw1Jk5s/Ea3rzECPWlhzpN5Z6y6MENwDOg2wYtuo0xQZ+Apu9M1Fl2ahsE4da9chxI0VZUToHafkQjD1jw4MLxRPJv8M2oU3u2fq7x5iVWA3NQzk+hFv0tWi65ZlGaas1+9Lbc6WMpaasmlUl8tu0YXF+ZBNffp4Om2sBUOuA+a2EISUWJGH+slewVQDAQTaZhbIEuM1vsw/ltY3gObpBwqWlN0EWjb61pPDKoVtqGltpA6T3ZFQnN1tINuD/yYHWtM3Xtlt64EDc/x6r6fPI93aWCHR9yqsOju46empFrMW/EjKRcaMCja3abHLaT4CHduOQoJQyOnQ4xII68a07gQ7LC73HFOKFejqnJVQOxZX1FPB6W7aTmh9RkogkU/rPtbb9G6BU1cMaFKdm2o6JrBRKxgSS03tLmZThfpsUNzlvuYhZQR0hwSmO6YLgzoPOfcvP6EoBMMZZKiKZS/uypvmzMrxZ5XXjVMktaTNN7fGyfeh3r9XP46mMMVLYf1rc2YcxL+PFj6G9PT0+GBaW4ELs9ztIXezNWJF4Dx8H6fLXer6XcvoYX1zFSe56ixM/OqWWI/c6GutKmKOJk3wkhTjYeSi85zqmtKBWQ3jks7glv0aSHQImK4gN1Kd8y60mKp8x4O4fvOSOeeTcXKm4fLiK8181PXSHNXsinJzATJ4VXyj/E9IBRAMD27pXHwuTfslKwNQFc5vb0BvsYRm1YYagHokrBs8ub0tfATIXDkc9PcPGJ0kXvcTEkb5aSrSkU0ulllPc8z8RpHX483AE69poOpKa3JvYsG3XnOxmldiXp1mrw3opnNlFI90Aj3rBJo0F0+RthIuyn/b3c8walzrEdy7eGohQaAnLQSs2kqH8LZY0DjgkNbiDLOm1oaNsoel7JJxXluqDY/mmo1fOM1BqH3aMA01BLzKpJnkyF0gw9fMyhIj1il2F4etFj7lf2zFo+FCV/ubq56ZF7beaHVT9lfvuGf9HQ4GJMv1DYChWN70OgwH66UZbnN19e3bmpAEhN+B2XN9fPOJPNCqfjeHHor12DdmyTfIvnVQN6L3nQpss04/x6DdwGGTYf1c8INHvx8BtkWB4N7S7vgKL4lbDle5SeTn3vpnMJgfv78+fv318PnZSuEG1fVjFpJQFpNTw8NUx3nWGN2G1mao1Q3o/2OqE55E+peuq1awVjZGwbe1tXuykqDetGl2a/azVtvpYW3gDggP+nhda/5iunNfLPLqt08N62LucgwprFNxebn7Bm4cBPNzKraSIvjdBLMe80MlRqs3O3//vxZMfF22y9r85zACBiBUhyqM45pdtMDIUWGDlspTy3UWlzmXp0e7k6hVNIJfz63q7CG966uz/3AQJvp5syJ+Hke6PvatzSBCbcIF24rWgImjW13T967U013VylXRM+ZWAQzZqWZNrv1mkN5TF1G83BRwSDcaWx3UFKiJWWGRjj0c40RnD/PQ9jKrWudlCQxLWEiOHlrQMWIk68bpHuGrwUcVQnQPcxtejjTWXlTioJbyghabamZnTjgEMy8guAlqH2Om5uHqcIhP3dFZm7naOKOOOE0JSKMSQ4TEWbhBKvrhIOMVWfRjY8fp83Mz88PTLX1Jk2neD8tDOZOUwXN0mgRR0IUUe6KLF2F8zozFm+Y1xLl7gpZDjdfWasmakYc88VJ3Kyq5T0L2VkraWDQgiYMScoiwMPdnBw4KN2NgQ4PNwO9zczDPHjikJLkywL8VfjYYNx+TDSoxD9cnppmkKzbuCixXP9ucrybOwxwNlrQudLeYC8QtLDKatWNm8//8lJiV8csfF86mofwT1Mtgs6UWX3AXlf//Lx66jRS6Jb6H3J5LTPvMazXW9hFY97DXkTEvF4H7IWol1W0vjg20/mDN+DaI7SYEtSV/OoQXxgKMgk1AHc65xwzm66+mq9p3SPgnuLJwcEsEDQQJgmgqoUZTEKjZM9IqyL3c5ZCGiW4mapGzfLv8hDop1pB7dR6Cc3MQ4l3ivjPDD9TUm11pSpiFM9KnQcD3qtaQSrHVKf50jQzlaWB8ivDoVnPuLtRGVYYTFWJTK1Kzfv2OgeNZvSepSE0qb8XgXjQjs42p+5kN9awp8L5/tFwtx6LDXezUUuZmxHq1ZRQT1S7dhl9f18tAI3mvJ/rxzU6YViqIz8Hg7yXZj9P6BuLc2aSbmyNUxxO3VRFpb3xXhamhy9rQSczpryjr01D3S+yNZxwsfwKvHvVNPvoCmY3Y36SbgoXxKC74gTHBkNV9RoOZlXuQOe4e96i+tkz7ZwZy8yB3UxV7wp3e/zMwBw1XfCfH6VIsAefz3teD+wlqwHQ4K7qAgJwd+0BXbVqpW46qxJkTcYqBTs8qjueEGF1b0EZK8YB6Za5RSuZ7SbVZvWMcuF62uiEyhqlzlAEUwNd02YqY2+Pc+81WE8rgJDk//3ff+69NHw+qeOu3zFUwaJ6V7vbwCtLMDHdiDAagvUOocpOx9L76Oqa8Yht/RKfpo69UXl3KNIs7+fEk9wqcA50jRkxTvlpF7FpkWZNN9cDKZyl1ZahOBRNFOMCK/g/3Sb0mVw+ZId/hXnvBCxB5xfUU3m8hnSdqkIezGxQX7eRQHXdiZr8zShy74uzLzTE7WK04+xRKZuGUpUWvDgM940S06w/AQtObcDRKxvlnjqQlQA9G2z6+sIIvD6wr3BU/2cHV8qKKJR1sOF45EyLlOCMilrB3WR1mOyMR4eldJMeMZWDAq6mZ3P2KyQFtqXj3pTmu3sEJUfY9GSVKYpnphXeKca78Pu50uNL7INh3Zow0sT/rcZobWtAz4noaUl93myb17QkpwWZKgTuErYDvOGdFM4uAxeBUaw/icqKE6lCAjKzhFpT7eECDgdvbcZkiayV3XUwzElBCgveaq3sVzBai9POTH1a+axhzsokqDlX7r6aca9zQk0RylJ6ay1BDVld927Hb2eZLtIesVPoiROvmr6rEREg7JWy6e8nyP4VoJOKZuxU8MPzPIIRO9PjqA92VgAnyFJlpEZCnKq7GRVcKu5SBQY13eOafjRGQWt+nPPq6xeRPG84qKk3ZxURAzCeU42Ysf51/l9PSeM7CmsdeYYM5EiS2Ms232qL4xEzVeL71hIsbYSmuV2rdfp3SmahWSYhOpSQ002eDHH9wjrDzs2axm/lsjIzRhtMz5hF9T0eILK7agjEo1B740x3Z5WHgW/EgDHce0phq5C2UL0WYFhkJ0c516XNNG8C8oLyc69md4+ovOquyUxB4HFsRzajfNefm+5m4VlJi9Eo7CRoUoyk/D4BW514KSkkG0u1acwBzWoqkX/+nz/33k61ykIcuA6q3TQExfc2TqmXZkoBmTC3ltMSOmfajkkbzjCQbjFV28iMjc2n2xKD2ZJXUsOvYSCk35QRL5BBUhzd5TOjg00aocEodZFYcT4BZa6JzM1FV8zoM7UlNEai3VwWH71ZL75PjKkvc174pSvRgEaRWbfqe1Po7MNePtPYtIavukk3BDXc6gLWU8HlYae7NVWzqH5EwRBLBQM0B/7Fxr03x7ycs6Q44pznuD1e7si8UyUFEQYTuov3HO9pG5r753PdzcxvXr3gEUEya1nWKeSUwv3Ndes3RP2pJ8J0yyrPBAAzq3oDcu7njjgVN8w8P2cS+slXP20+uimB2Sg6EU+eVVxQbwwID1E4+l4Y68fcB7V3pVqLL7g0wPTPz5/fz6+QnJJBDJzpW8vB7OzOFZJCqQCG7gp3Tiu/AaR/fq+HaqzVLk/JP0gazc2ri25ZZWZuMivrQwIGOS0X4n9+/vP3/r9UsZ6ZhmvlD5mtCUjrG1aMr/HWAFR3eACsqy5wevhUP8+pri/uSBK9SFl9yo7tKGL/Az0Oq8pJC68s6GT0wczUmOyyBAYGvZb9TXTFII53t3AKYg8FZvs05oLm5kMZ3TUysgozba4iZT/nfIUElQlzEPfzOT8/w5ZqjW4KYRDKUdUM4+B54t7CbnMKxbUmMtPFW6K9t0EjkVqBH4/hyB55frxuAaApck6xLRPK1OuuLHOTS8VmpvVAjvTs6g5bQnEwPVq2jLZKODUCNQbIavcws7+/H0lyq+ZTn+c8mn+7ipv3Pp2FRleHWXdlXg+SrNz2xKrshoT2pnQmpcC+OUCDAfr4KXRWYmQqRNgzaA/rsXvzk/k8DzCje29TlhlkVs5sqY4NzE9PsRFxfu9fqYG0n003xzACtXZZySyg6XRHSzgo3+wUaUArZpl7LuFd6ehuw429N3KFNbMQy2AsrKsIB0YBUHSzrhnQbapQ0lNCrW2Y6UnVTq3iRgCK1qNemoGQ5Depqmepo2peq6KmSNS0fvnsvUUMpjHcBuDZyDNJ6mZLbF7IR5SmBKR8zdHbI64tbFF9vDCERFIiMWB6HcSfY2ylguCMuznGUWHYgDIjVt3GytnFCYqJtBko9rl6bn4kCBRqL4Ls+VmqjGBmHY95dQ5VL1ZTbZBEt0etvC29Jd3YNeoX4Yvm//7uPgGhoEaRH7pIxdAQyGweI3XZYLp5Tk2RnEqEgBk1AM9kSyHJF2nb118b2zBbfWfQJ5BVa+74Yn1ry1+6WF1eiyhWtjulx+9pjyBa9NHnc/V9C6GTeVWyd1uLuXGb/LqnxZqSOBGf/rh7hG9r6ox8AxLYyUisJ6NnzM2Dq5Xqlz1TzclAAKVe+FBWjp7dbvEh4eZHZxh1vK7AuZukm0nBGKH26JLVZan7GYVvaBo1sGd6SsGQUNHEml27MztbKLrYveV/MBHxT901kOL4hGdWZmrZrWo2AcZ57ifVvOhmJ3wqZ0aEbXg4zc3uTcnCZl0kXT2VLbz7HHeYvCUaMaqqurJSAqKjEgWs1tvDGwWIeZ3nyAroU3Pz/v79rRmS5zxxzhISBmB+np8w6yp5Cabnk/eTqVNLu5TwQ0p0f9yMlQ0g3M2iq7etiSFdVljQLcLoWGbCLI6sx+4y51pwKEWWPoRpeBht/vxEz5w4wsF7QLpW7FeTstqJH3+mpgu5AzdBuFvb0ODxP16mGc4aIDxszZo0UW3UEi3qyzloFacPSPiaz49q33xhFlmWw81pyhYFTJS+0U/ob0lXeQREJil32sJpY/KRE5Uq4FSiL8zcLTRnu34GjdW9z8J222r6e4Oy+p39oBPJKCJqbx9ZE16pEr9v3vY0agew9zLDesG07r0o0tJR+ys0RmoSWyJHqVH6+4zcALpjtJ68BDleAaFe8/0Lo8l2Kh4HVZt90LO5o9PAjNEqewbusZPyipUFX4+o9a6+NwdUL6ywQSHvO2nrJ3LN/ropdzjs7OlNGZHbxj3cVeRCQCKXHdVLttXumsmuF8ePrlFLzPup7o9eo0sF996pMbMWhKJmN0FA2lFuAdaj3IgRMD6v5jazqkvK2tlcQvl17vRU9+7r54kXPDU0/vM8GvHCLcIwIz5ghbrd4S6nvYYaoYlST1NBT4ads1QV6SbHP4AwBT5QJgJtDio5q9uzfEMd3TQ9VBmWSyT5TlIroaW2Cqz6ivvEkOd5nuN8A4T1n4vwnLesTsCjMpIMG1bjxkHtDSPjzGycrNYVWUqJsWl2USKzruqpzJE2f++UyUw1X+rheMlqcxoHPz8/M/MTYcbP584A3bpiYZjpz+fqovbQa7ETgYsouJmZMHCbJXDiPCf+/DyhHtUhCFUISZ3QlVUt4o5jWRnkc+J5nuPPz5+fE9ZVldnZ2Oj/GvD386sDAt2yl4UfA2VNBFidxik9mCXipIXkhkfW5/k5WmrNRyHEvcJ8jSesKgG44i1rajDkEPP53H9z8UiIQozU+pRBxEg/CkAdEKosBedV6EEOEFfwFFTMtJyzWEcPh7U7aAAb1aIEznMwHUGyNEKq/hAiBZxUVJfRj2IMd571kNWfjXo1CwKIXL/SuXpBfTIwnOfHwuOcc8Js7coagyyWuca3SFXjOQGWLNIWZkdPWLg5SB39eLMWtOMt5g90zYL621av4hjqmhRpv+fy+zHu9TJ7vttmlq6Yfd9OnZFvnoQb37OBFKA+G+8j1pfvtgDusCLe2LnhMmasq0SWwkwEDUmqphM0W7vvIKsHzGrJ4e9N3TM9Q9r9pII0euR0cR3BC8Eo2OeLHcxK8mfDric8piFyZeO+t0G9B0xZXfTBDqRNBsUZvewU8N5bWESLrtGWZpnrCjN6ZVeNu3whQnq9ZQsgBuxataEIzlLiTfWJx9ZV8XL172UrvloS4pl2oRczUVVcQNCn579/f2Hj4S3uHgA2oReCqtf4JoXZxgz1JgAT6JUujwwyXvWCNs3qyszzaEIsyYWPunpntBG7SfA7EYG1NhQb+ozWtVAYwEJSbPZYdUX4rHwOOhPd/f0A9e5JKjPxWLWkmww7zVwC+TbezwVgVRpD5+05T1Vi9R72nDhpn55Gu4VA3OoyI8O7UdV+7HYP+ef/zqdSQ03PJ35+2FN13b07RSKdOArLrGmaD3HO2YXupoWLEKg1dzD4tDz1InW7P91mVAdDXsnTCVIBs0aiERaDCdvGO4ZXVyhNqOt4OGJLjjDT/ZxHOXqZpao83cFa2IM2xapWJhAa1Wjkc55qBbKb+0rZCMOgSjdxyfVltJLg+oRtpBqqK6uPu2JC4sTMWPX9rccPacddBX6aT0mjNTnxnLyp8zTVBKmGVbR7xBli7qexDb679epdHI58vFz2bKEnPZSdYx56PXUHVw78lcdMTxeHWD10r1AcUuDI5jMc8gTkQgBqtiBPCG8PGiXgXA8qdnB+T+xpiTIWBzBu8TFNOQgv1WevpuMlclU9ISBog0ttbPQqYzZelM5ppziAFaL5rFR/Fxq85IAAonXbrAyIxs3Q10k/fFnhWeHhKCdN2U2AEKK9jYS/A1hX1LzHF9A48ShTIJzBdpPLQvHLqVnTllzH1HplZ2D0qlJPFECZbOUWahvlD+hK6u3z4jKdRmFHnOnbDCt8viCdrb93dK/NjDrSVatFYw/IYRgl6PSYUX5uxTnTvQ5TWPc2rlMpGhw0Io5SPyW11uLrHugtll5sw+PmdXMdWU6/eWd6q1neEaBu2caKbLSt0b6iKTMyTug1a445j3SQGJpp/Pye+u6+shnIpDdGRlicML0nZsCEbY6Vlg4JyaQ+fE5wvGvClcxm6kjYQLpu4E2LU440Ge6bAubmodNZCXmzwyZQNT2jtGN59PUI7QGhvzCMwz9/ftDyQ0gt+jum02kk0BCFND1GX6m4nlxzMxl+5/hQMaJaVmQb5rJew4ljJM9xI/Pzub/XJSCdFgiDwVQRlllPHL3nvQqZnbBoo/mPoPI+N/IGLLWOKoxcDZHnTHdV5afEwLn5T4SbdbaRxz2CJKfKjBozMZ0l7Q8AZmUDPSXxSXcpEP+ETQtkSuGhfJkShztws+CQoHGqDDQhniUsrjYUZ6dHKhXSzdzseUJNlusw4ITbeo6lCic8zI9Lp3Aze5TVJSx4Kxy6SqVhgtTdXOuUuYE1PSXnWugQnKyUqGYrxohz1JyDvVcJuhbJTaNTBs+QVPDPYO2N5PE/kkxzzatq7Rg7PoSFQ87kQzpI1t3+nEWlwmA96OnKTK06sKGLNlnJsqaxPVn1aZJvf8tiyr2ZcAMV+Wrsc9IoIdaLcGCXiPVC2qYQSX5PvnTa/jlrQpby7iVs3wQBIT8rpdfNLJzoO4SKuhHnz13h5SofzJjxtW5JCLcfCzcpqqfLCJt2wDB1c58k6pGeAbunagFAaDsBMxulI/41475wSjcy5QQuKTtFEnVPZnXJVAG6zrEX0cLKWKREWHByQPBKiafyZJiIbcFfGOF1Z3aoleSKsPVz6VOVAyO7ZvBJyeKFD1FaWL5YqMAlkmIZSTYnMzGsrClJjIDXXaujmGREiCXas31GKmmSiHClGyglSnCkfhaz72AFWbSAOaFyda29NKqKS2rJ0UBqm6kyTtMdK/xIDgO0YhQWhjLyRHTX8xwZVrXpKKPN3SRaJy1CWQS7bp43hqiliwc04Bih20XhteBkX9FKbnziUaGJwThwbdmDE6GtRTctBXdNZ/7tKRqmrqFoqPps6t+0AxFRWUqlATvz/pxnBieOked4mOcnVd1JoLsdKsMbDxOQ1dV1697bDdkWZybvlQGgBD2OUD9ieM4DTFVGhGYylVwKbu5pP677lauIV3KAUpTFvhGNz73u/vM8tlem3ImWlWYWcTh27AfDKXQNSNVc+PFz4nmOXj3x2Iu3Qvfov3jO+dfSpwwlYfAYtBKkw8IjjlyY2BNJ9no6C232lfLr22dIGY2BOnBbQyLNPc45P4+bL4hRA8LDYXxOHH/0APtxqogtCCpPU3KR8TA7ARs6PSzCw400kHZe0Nw4dhHGYx7y1kDuAUxTwky3EagSTsf5eSLcaEP4Y/DpaXOP5zmPPgONAiM3f4R5kD4SnAs61rQeEWabmWFuAga/yJgSeKS3EP+hPDSBq6v12exImJIq3vw6cqPZ1IglrH+LJHb5Vq5q7xWlQ1nT/ZskseGL8w8p2jeqZzEfvrhW12hpemnxed0PwNhUcI6Lnpr81Mx0TVbD7H6uUWvo3LwY3FuVaUZl2GDQV0e/hkUFpTVlEDOT+lbjrViRUm0WEBavKBH8rmeAFEEzuJ+UZEnxsqJYu6eyB+jqBe6JHtUCEuBSNop5EI6jECoRyDCxCcrGEKqxRJbZOgca92bPZFavdmbkPtskDMVIbJSDVXXmndHfBzOwQavOgEIghH7kXWJqvUt4MSzCYL7hCrqgnKqN1b1LAOcEBnVTql41mVXXu9QBnOpc7rdnus6JE45Bd5nZ7+9foX6x/chygkxVZiaAez9LFhvDXdOMMoHImS53C7dR29Pguz5XFog4oY9bccSVJUKpWiLC7qllX2f3BWAiHjdi6hznlKNNxjH9OXqVXCZ9TMNgNy8oYqo+vx/xdWY2VXI8UUUmd/JWVe+LJdF9jVhEDXykKVVfGuk3KgDTVT156/P7ocuNBAL5ukr0yFbPp5oyB60IZHuuRXoE3QaVdwAzu5+M8K7pwb3VUzDcvuBE2Kij1uxWds8AU6OasNwQLtt9CLVAqyKOSYLaUklW5d+/n7yFNZuO3KGkveGReIsSeLPmdTbGY0Pl/WpMeSEP4ddUXJetbgXIyqF4Wq1HdPqgPMyPzYzmzK6k04KDTQJ4Ae1ZxmsG2NpLEubmR7SOhh7QuGGgsp2H4ichA7xxBBuBMON5dG5zUCfcnXToPlQYUZwAKoLwGblwUHCMic3UYrPSKSX3bCiLmbkA9mUxsYvIP+521JgiPsUNoMxTOsK1c+wuomNak/ziyoIYjBvraeSCePg6HsCXCf7HJnzHfxF4clYL7NInZEtqL99GSqcOdIUyszXF60hSOH4WhPbsvMSqsnARxbdk4Jo4T0sIZg6oBYS3yszvLZKUUaDZjW1sMxNt24Pfzwe0Wcm/mvv2kISBRJVoXn6ytP4K7g8POaWkAW9CasisMii1wl6NkDKf9foQQFXfTwqWWJZ8ZrZNlq4sS0WQQvvTVJeqI1LcwxCAGG/b985elh/2PCvPFzsZIRRnF0VdPkY7zwHHwvCVDKxxtVt8gKpgsqbn9/cXGp3cwIkT3WXOCHOVOwIK5lcukG2UkCjsufdjroYSVl6FJx/p8sDjO6cbFCAsDfKqDZRxT+fMSFZ1Iow2RekbT5wesdmBGXP+xGPrytzRprtJF065S1JmT1ffl+XM46Odsys1WdebEU0Sm2/O6RGnOgM3e1fa0md1PI67iXXQVFS9Imsq93jdUkPce1f1QVsYUZU1upvdIQesgC9MKmob7O5P5gCVrWS6qf7vf3/V9FItUrzk16NR0m8LA+ARVIEG5cDapMMTvhjXsG/nJ38/Wfe+aiy96qsY2YEZlKBgMCSOctykFWNA8BdpZj3d6JvVNufnEVAOQr0MioNuAbQYvF3H5wmFbehiwEiC3hrwf37+6MlRd+kizJwdcn3j1Px5TKo+YVCOXQiCThL9xrJ1PGFheiPk5pXLmgs8vlLrUtwd3HlCR/IqLyXuM192bZ/gLzBjanPZBEAzGtskQDLGcV1m5zm0RTz1FHm4O4SvQWl/HFNeyaxMZUWf+wG+8/leSyambc9oLJc+rebj17Cy4c+6NDUqv+OLhGESTZkpcZB70bB1wgnAfVmF7+UwsoZpPcRATkMX7kqnSFjCpX6B+DbBtL+fBBAeFHTVA0FAQA9KcaCZ93MFe2q11V+BlLO38Yp/Rv2pZlUjU7e7GMExykxKRWPpQyxRtT3VzXkJ84JeYUG6QuQ1NuRNAeAKeDf6vVXZ7006SuskzSPejWOyUhe8sKC6Nf2q+gFZjjH7TxQ6V9MEf84zPZXlvuVRmlJNuZW6yNxC24GKpeI4OObW0yopNFINCZudK4ERCcwTB5wId99gyF1gl5AekxlacKOb2CxXabjZdLfi4zk/P09XuZtw6p/nRwfTCODcwh3tIjayfnTLp4p/svGpbL6e6TjHNr1dvJlKOOxm3vowSJvYIjS8akT3E066BYmQPlMgBsdsjKU40HBzMyE/7gxfFzY5grntZdbkmL03JVPD4FZJ4Yc3TH8haVu7P205IsV8V10AcWJFVBtxTmJrOgQOPC/f+ETM0FuBRPsFkX4sHo9YoGNdvlVD2I6uYAOVGWbnOdPj5r7IN7rH3O+Va1oT91BTT6WtMNSqxmCzXRb9uZoQdlZvtR0Rw6ZJXTeA3Xt1N8j9QEKwy/Hz589/ZuZWKTx1Bu485+iF51JNk121UOtUle5Oo+fNE2FET6syZd5UfYk+0fNzfkzfIIcc44uohNEYj1mYhF3mRoP8FWKPaPpvbdA3PyQs7DxGZ/VkKWhIaRP0Exqre8YjItzcJOTtF1cWJDNLw3LEhcZDSr29evzNmVkh5nAbwRRZKKhFnrJWEom8TgJatDbhxdM2XOvd5iERHDT8YfjKDLt1oP/v6f1yCq8IZQcADPU9lGK3dcBhz/5dCLjqJBVLkoR0ZW6uNGgzhkPHYn6KtC7MjEdkdao7d/hJPWKLsVh43pquNzeMyjOurBlKJ0m6sr5bE3oD0FxlkjDcW0JxqptmwujFs2j77EbXkh6663oxNVR1iqKcuW+5bFYJGlmkqKa7JDOtj+RNVNW7vnpxKa+pPrrEIWOALsmH2KU9FgAkhKX+Nm4NfPJm5ZsCOgNcZSvQthsE6ihohJLXiBn5C8qwz7qWX2AImd8oHZe7VV2K+IJkVWI7ldG/ikxRmNo9MlXhha4SCWG09QdQgQ2luLC18FHS4A272AumNzrD3T10mw+mw226z3FR2WYr4TW9NApo7VZbUPd0VXb93t8h8g1dEPtFQ+avxKaQYdTgDkdZN0fllxwiBHMrk0CbJFhT4b6cJw01ea9b0KhZ+Va5G2xOhLAAEC+CQUxN9zmxALF7WChfaBpDiJhdgL/hctvOfD4Z5m52K0nQYzARxkF333sV9iAX6YKz6LyZVTSTvw89pUpkPfeVehIj3MjOMgt93XTGz7GtjpKCYnc+PXZSc8fbRFw3tcmqxVRT1RgANPr4Ubnc/dzP/dyblVUD7gDdRt6bAH7iaMURaqFmXd3SdLpbVz7xcJj39vQmz59YQhJTt7paY7v+ho0Gcc6xGBLP4+d4PO6Pmo+/76OE7aDUwaHMGMAwHIs4zxNxPEz/rSlRxgjgzUQRXLE3fXb3tHl4hLs9T6gRz0Il5+9IB1TfRfT3CIcwT3cXhwHMaPj67ljaXORQk72KgzeY839ZWePyBxpiNTC5Obrf/WH95/oL7ScP7rnT7y4rPJkSTepP1xArkf4Wagpa0h/Q06vGGdWtmJmjS68sKyk0S6N9dedUKQDZABfgcbPYFJE723DuXUO4zvTfz8XQXTWHwDD/JyRZx/rLo9ru37O+380qH+kv1S+/3IfCogFq8uj1HCB18a0a1UVzZFXeIihZPEcY/PtZzrs6m1VPpmotJHOaj4wLfCtlZoXg9ZLbM1BFjK6svKXTTGSOdIz73Q0MDalfQEjeqqIPrYCt1E1ic+RXLLdpyRofZgbKdakSD7ws0CxJoMm0spQrq6Fgp2Zstp5xJ53Klq7B3aoKw3OisjrrRLhbRGDa3ZUt6q6wE+1rK8CY9xXdkV9GOAXp4916QRIRm5htgGbDMEcPm8BU3ar0Ve9bV02VWK3j49YvEy0y06bH3N/TX8QDq6TZqUHHE5tR0lPZVek0CQzWlu32VeB0dVZL5Skv2OfeVTnL9NBNWfgJmuypGa652I1buDY1UmkabGqyrjvdqdf13lx6bqDohbrl9ExNGfSIFQLS9HPIn5yZmXcGv/diKMZCV0lmGZSxBTc3mgo/K9X9LRBG6cSL28/Wydp0u3t2k2Nmz/MI6NB98Pe/v1WtzmEF1i4YTTNaVhsYHhy62fNz3CPzqodudmeFDiAh5s/PMYM6BjyCLgE0SNb0qwh8J9AngDnPEQfgIX/0rNXLqSdNgLmZqaWmbtJ4/pzzc/yEe1QWzSxI8k0hpNhjsGkQHy46AWyGA2NOj9jkcClj5H/tiXO4TB533nk9Fst990zX17X1Dqr04+HOJYdtBVBvIKsZdIkK5vpO68sH4/1HvDUyOwEoJ3FHOrXdi9WkcWskdj/gCoj+h9ja5gWuEEkJ+0ngGOMtBqgudNMpGoR86zBhBFtdMm9+gaxe914MSvcKvarvLQBK39IhTdq8RVUEJZE3M7cQoE+yu3umem7WIu+gMcxMbkSSLds/6QupGwarriazakUz5lndGGXVvOuI0ayy9S2IVJIFrKpFdZw4n49SVfjeE69TjCZEXRnsM7MVY5IwdS8QZEu8LDIXx6mTgj6N2vSctncmMDk7NL1L8L5zBGkK7qe/ZXJmbDHAg9eIq2t8NH1QDd4EAT8qrFjk9Hme+zfjOYRan/j79xfglqQT2NQEdNVxX8VAqwayAKw6ZPUl2PHhq34DX6zzK0cgBh7x6g82KQvTT/z8xJ/3d9I1YnWz26bDJmKBWrcxxTd3q+AFcrFRgVkWobAvmd0hBfR8VXwyj1S9imClAyl3rCkpBbFwWTXAzXqcNrJriSATmjGTJY2EWiobPQ6L8OG8keKvEGvEU7lLHyagdmplJKvU/AF0yLq+xOGcc54TT8RPPDMDWL58sObHm3Vv9nR3ffJj5n7e+ieyqs5zbDVU073Zxb5pfToS5eWYbYZwfyLEEro7iKx3eZmWOmAGWfmcoyE1JE01qKRU3ZDcaG48Ee5RPaDUovrx3eNgxmh5SzBFV0s67SrGjIgTQ3hswhQwERErEnJtP4M+z/P8+bHFU8TEMo4Do7WVnOcJAMcDWG7/9SEqPU9fFmWSVi6njL1CpSW9hO0XKvxK8x2/z/2O9hYR5ia3tp40zXTL+gJmtkLSAZaxMC40pLSG7a7AesHw1SMQ7yox+6JqAAIR56wcG0KulnRYQhkKlWosD/Yt8Fly2zlTF1OQ7eJTPeiRBYw3JdnQac5N9yh0a2+RFxgzkBohK8mtgSSt1AxB7LTfG1deO+/jdTTP/VTV6CbrDcrj79+PsGn9KPdzZwlwZqbWpu6pW5/Pxt5oAGot1pmmomb++11Kh7yf1Iqz25c7MGH+yRS3ondyZlQuJuRgUx9mFcvdCuajGDKNBLuiKMdjsABWRBgXUdWkIS5Ld5GKKzc5c7B8u7TJZvZalsMd2Cqu46dzg/oUb6VP0kjxCvrQ+EpWw6Ornp+fSiUcDA3vw6q6auYtj+hul71CJXCG8KAZFM80MHMMuGGK+kFsemblxv96WYEhrfKq4lGjDY09LYmLMWbWZkKzWQi3DTWZEo/MjC/uzxP/jjXV8ulxz0/WLb2u6voJuoH3c0nyraXUL65p0mZmSwfkQZpd22dafldFUM3qw95wnpmNXXPPm+CoS/JzP7cy5G6YERIqVeFgbl7bBpKFiWfQM/fmf//7q0PWHBIe5K37uT1T3bdud2fl4H8yD8yMNjUaYU6E8Ba9gZL97eQIFWXa9KC3ixGA0wD+/ve6hWBqsSlaHP+cMzPhPg0OJtuMNiP+QxbTuu1mRm9tpStrkRpvZia7s26cjVjAggAzU3QMO47PjJFxdio121wQ8Z80nBM0nidE71Mh4LTBl9Z+M/teXaOe+UXbZTP3NwWTgFGdftgOcS3o/hKsGIhG5gnBbiaM+XXwqUPG3YMrWMdMbwnS22Q7X5RfaRCSCxl6mv+jwKVqx2feHiDZPEGI3BXkB6mK8D3RqTFq9sTpwWwpkL01euukEF40S0ssDtMSvNf0Or/ZeUJEWneNeVSpP0bA9VKMes+F69JMjCvG8lbmck/dqJ21WT3VHRH3XiH+EmO+wiQKgPx87jQyl5nbk37Qg8zyON0TLgxqTFZ8s3vzpdxHYM5x32rbpe0VkGUDZQhC4Zi6I80M0v+aSS5lK/JROh6rx+GZjYGHt9p+lLbZQ7AbvaPk9iqbeQM1SMlAayxiQysI0NDTjTLDCevub4OunoIIc/I8bjaiQ9+IypYdyRd20PdrM2luXf38/GC+VOdi5P6K9wlMjxt/7wczVam30U/MtABMWxOJ+IYirLJ64zCtp0eF3ktq+D67RFe/KX5vuNU6VkxMw6xXgADM94UT9Ge0m5/qS1or+rXKzXvqOA1NFIVfASm4bCvmu7ueOOQbIAOcVzxorlcRN29jpPiuavknuoscVQgAY3yPZPSf50ccgcEcrEzUW1ZdrUtCKnlzCzGB6Od53G3YmYVBRBhYld0dEQaE29TIyuZkxIrKaXApGiN0XZY6bDECqtGNvYDtaEWZJuBhWkn8qM7M3rdmpC8Rbrt0nYQfPZJszwJcUNr8CfV7mNEyr9oRCP4q3sstgiRcRjtZT8wGXVUDZLWZvaoE3Tq7pEqfFHb0W3Wrm3G6uOasOD9HwbeNdo/vACXxynmOJEO6trXS0uycMBHMBiHju9lpKtoLylrIsg7NnsqCGQ0hPeimKJKAvQugpmUz19vUk+aOV8u5ucQzeEMyJJR8izy2WXIJ7sHrHRNasuzNS8tjBm6h14nvhEoj3n8zEND9Tr8vUobBTIE2Q6fYuR3C5pXAC/dfpOfLJzgJeey0+SgJRrjQbA5Cj9xoopoEy4ycfoN7s3sy896UEOa79wPUVtPd3XNv3dTRwJslnuCLe32rbwib6hNHkzDwryZ+0WxuWuebQjotkYJQh93qF1d/4ZryNd/pamdl77VH0c6YhlDJfqsCuKIgeFiupZlZW5AFwCO655XZYabDvF91ZQ+EWc0bShHuBK1Kbk8MUKXse6x4lmuXAMbC3K0UQIHpXtcfX+2K1s/doOREH/11mvKU6lrFyC2lSANhoCRDf3thQ06F/9zPJSxvTY2+re+mOdBig2+9sMJBKkuDc/cbaryziI5cjdgLVdd7eGnw1zHkEZJtSrTui6gOOZ2XtPu5Yc6RLg22ho3VTMiQZYtsFkEPC2X1zavAqOosNwPHwAiJCP2dkFxL9cwoG07OjrypxzllAP5es2aKjuicmVGlupupQVN29+qSDz7MaHbvFSql6JfpIRFmU5M3e9pIBYV1ylZdhG4m3iyQqhjUC3ZOPMdFv08NaPeTKQdf9bx+OjNzqkgut/BgZqZNEcvVOsL2G5m2fYbW/zEtp/SZdefJs93ft5Yb3GDv+z7hrp6Ymb5ZmYXRlMIpHcjWU6uQeR21J4Lm7ud46EY/z4lz3mF/5Z7u1oKqnHRXdJG+9e4CcW/TzMPVkUeOxeq04ISNn2jMgkhG89fePk2RCjoVw2fDAKdnJZW749Nqtk9xIXgaetjjGwBJynpJ5b/KQ/rqsADMyuEoJoNYmSsmVHs0uxNhXlXPgIvD4pWZElj18LyeDLQYFFSnFJaLU8+8kitdmHveQWiCZlgpwjXrmGFKahFgauZeUWCs7HDvQm3To1BqKVZ99oLq17qAyu6aiAPQPYjN4p5B1aYo7talyCAJewapWlhlNigGymxm7s1u3KlsjVPHqNQWW2txt2RpUNeY0v9BNZTpr7cflEhEEQ+kGfu+RPKMbheqaqmaxpvZo7DmKc3UvVzhYLrRg+p2D76XzfvDzrxFYwNIaukE3JfdBUbhEoRE0latQLvqbGn14oViq4XwbACetA377/FiPhhi3D0/K7BR4+Pv7wdAZQpNstVYMDP1rUX4Ko6G0/Xn+aHATPtaiiT6nAhy5nhoTeWG2XK3ar51ZnratV1j544dqsJoJieJ8FmQQ7imeKnyjBxEHOVGOtpXgUmoJHkmzITXcSwFgnf1VE1Jtz5V1W3umVdJd5UpnG8FnaIQfXOS9ZYa7ef5yc4wypiWW6Y2lY1BTzHQXR7u4RxkJd3O8+QnM1MdHG5WlefPo4afylpBJCBYBgM0qnJKVoO+lfoMwyPgmLk3761bqRIrpczt8eQujEULohHoKTUHkAoXd7cRyvRGX4iEvJXddaveywnurrthCGEsimzURCpXi+65Eg5gi1Vy+Of5s+XAvqIxtzUMGulHA02dc9DsHKyKfUxgzFZowY/clbQwJc58S+stNBWSMvvORJw44cfN/c+fY27KoonnCCePsJ6tcHKhQ0Kpdo5jRIjGDRG50A8rOzX3rMRIqfw9jOLZuD1wzBlxjBYeZptmOl1L3y5c3dL4C8/HjifQD6KTV2f9Kui+ok1byazIJ775zgO8/J8oQ1tobmXKiBPLJm6OkOD49wbYVcDEC6y3mRx03etUrkJ3pmgKwAVM/P796JXfLRO4N7/Dgbkrux/8zpqq/Zrfz+36J2SfgZtSvCq7JFVw9+4ebDpF3sIri9KVo+dTz8AMf39/M6/OfVIe2FacHAY6piP+GT5e5N9kCutZdYouQjqn+0T0Erkib93p0H/SKqIgKVWPOi0gqV3rpFnx7TczWMgbW7HYPVaVC4k2X5XX+8KoG/al/s8JD9Lg5lX5+f1U1/RU1YlQ3zw2Lwji9Px9BDWV+2u10h0rmEEwK97gTzNERFX//Dz5uXrIzgmj3/pIK+Yuu7+5Sr2HXb0ssWvDiuVDZugc48y4mXvslNaq6PzKn1yBUHorAKrpE2B1TW0DJxQLhHYz8zEk8vIVcHnI8EnlB9ANpEmv/yI5+igyc6Y9Nnnm936MKmya1CGYmVUqmq/pmgRwM6snq3ZAE5+5ZxZ/P78iDP77+1tVDCXyv7q8wZ8jvxv9xTGN9jyP1A7dxU2BGZn/UWCPsugH8/nc6ptzq/uciKCud5XwNLuRb5ho6XE658ecPWV0AJ97NwHwDYvVHm/rZ+6qdvMwhhubna0j6TxHX7EEic85wgxnRtuVjK7CkM0UC2a9+YDqm9hYZhqnO6+uLkREdWukNsM72YAbziM4EOZn3wmNct0n/HlO9Tb9Cqn78zw7m28UDzEtVadx3oBxmBtUnuqM8MH4o0JTjWWcFVDKSMWeUqMhRsAMzcNe8OSl35IGCTf0J6z1jqsF19ImryMBAwQZ2/LEIwZivWkadXS0S+ojirjn1X8OX354Z1cKPfseYUPztUO9MI8wAw1Stmu65tn3D+0xo7mre0X6HFtDBvrme1Uhs6ona4ditMnCVaWE0CCthrWS+hVKijTWOuNukFynVp507+VellOZ5i5brocv5UuIahZ25eaZpfwNrmOfb9qNzyvB2E9PP7Rmf8NgslKye7p/Ph8uLIbeujfdwfzc9LCe1tYlpEhdcyuoM77ktlweUzNmphF2H853UOjpyga4L4M2z8VyXgpRNQ79KmBHYSMvBdqyhmPi7OEuQxK/YGiXBgGN/9MaJVeVJX8fd+hzcMSkSeok1ldy0s/nb5yA6JS8dOvXpv753FaxuPRFRNdgFDbuxk3m02pCwEE9NBIFjHjULtK0wew1Su54va/XP4/VoAvt4YIfFddlgHMwjVL+T0F0iBB7tFYrhXwI0AR70OeNecnP5+/v3+NrxapM16gnCMLOVOcnAaspXUFGdWpKNCU1IHrmOc+mWDem8fvf3x5WdmXuNIEh8MkkreoOkJ97fz/VvVFfPeg+cQx7GiyUQszMc+KYyhFsoQl3HX+dVX9Trk9t3VIc5f1gsF/EXlpHp829RadABpkN92QnZf5SxPFw3H2qjcLgxmhy27/PmxARx3upmNv0ZNYnc7UsM9ByMHPiodm/0KSb50glT6dFBNh0SRnXEQkg88pq4EEzxppiGOFAnyPZ/jOoPQ2UIBtnN2k3sT47dsxIFY2BmmYp/w7HI+jQLSWRScSjHDrd7n7iRGBhgapp/V2Eyk+PW8jTM9o2ZVOS0H7e2c7ITZfePACF/XDfAqNzk1CX6FY238pmdl4HCYH13BNh1icMaPhNPSRdhR556V8Y1l7kfwhOCXFX8uiuGpqSp4qdctvEcYCKNN/rdVbATJoi3vRe/P7+7R43v59FWozes7Voma3i5JnprMr53KswhRlINIlhVX8+abS///0VVsXlUXwPtm6aXW0YYGWtlPabwaczfP+19XyCanUhSgOf96O6ac0HOmMHuKnCN8j8Kz5Jl/IbTiPMuWeavkewtHzb6UMD4B7bXgsNMFqj1agMm5pp3XJ6wfTd7nYjjGW6XnJZtCHDfLpfOcnkvd9/LxSI5usbIl1+y3022F0WpqkTrztd0bLTk79Xbx1AoeT3fnTFnIiVymw4ywzkA0wJY6bfOCPODGpqBxkKM92yYnloZkbP4XT3pDwKAhzia8HXn7b8G/QXhmlMtvDBVqwYprvSsFUcUAtYFwbhCjXZK1DYqfZoQcQE5EozOoj1+ElLajhPaCt0i/CQUHJmOLifXNh0+lsOpfEsnmNEHPdjVRuerF7Gv3//viAHpBDV6CH1wnTtMIZ9YjBb5DTLPHR3Vfa8kwVm3M0w29ZzwszQLfWYOBVAee8A2NMRrpitqWViWjlI0pW73cy3wW6PHnXsKKzrVmepqEGxBGKTSHUyG99lCxp81uphkoTDjM/PGWx7MKZFI5nc/zPPeUjEMZf29AmpJ9HDEViqFd/e4wq6j2Sx6e5zzqCXzZKuZnUd4hv65XfH/FtPrd3IANgmvqE69503LkCHoRMG2gDIzN7zt82su8xDZ/lsRZi+wRW2TX2HS136gxl3fh9cEGhUNzXRvgCLRLoCKt7UBy7cp2iZXrGC1iCPWCWec0wYg7T6ek57oWh7n9pWzhwwU52ZOV2gwrEH02/uJGdQPalkNzBTomeobQk094fD7n6eMw1jyIsz4M3UPiK6WGduSkCx9DXpi0koiOx5jgArKgd0prEGYLxh+NNt612dTX4TZN+oXPJANLVGeMyywWaOXViXpdTEM61e0pHQJjwqawNOlud4KTDSw/QD2qsL3093BmDeBIbmG/q7qI5yzMfcOY2pVqWJghZF7/B/aq/7xfo14/cC/fPVFUwPzQhU18xItKdrXoITgDUlDq2WvH6TY/eHH6pcd6l9Sov4PD96ApWD0d0GM/PnnH0fVgwArIZb8RJOWGWDXg2A4SFlyLzbpYDOvRTeqf+7hXUXXzskIOxyrYA7vHBiC5dqusTyi1+TFqjRir3D25nTWTMopdW3CECxlzpNpqpufj55B91TJCI880OgKrNrwKy1Gq2XQkFmrSy51SPui7+2wF29MZOZoVat6epxeVJp4YIR3lReW0Ym3JRy1J1NqNJI//VkVyaNflwvwPO4hxF9jg+wKVddZniz1TrCd4N2E9cS5hgovE+4Z/doDZqez+9H+c9DPOfZBrSZUbjpzZ75fD7n5xBQ5Y5KjGkmFeb/DLgaAigxn7sN2sxgqO7XGXCeiO6aLhu6wh7cxF9QheyiGdz07FFx8Jvcu3IJHeeZdZ4fkG7cRd7ozhNnhUbupMtFpWbQ/ce9jX2KvxWLJaW8u0s5bh4jBKC7snSHKMJEdbJmFpqZMO5hHpCTf/bKx+vL+Q6HM6LTVyXy1Th5iEz7Ft2sFmMp+JcD2Ais1eZpbpEzRJoTw3qsYJRXUa/SEg6Vdw1cy5DA0KxCpgP9Eqcl0eK/jNEdLH5vyr50M39/7/3kJ7MHNy83IpEABa66+ca9kU6/n3WSqTW6tzdmxFpLvdOrYG69I6Mehv3cZl772HSHx0s+a8RcuWu//iyaSS2iMeJ+LvHOb6sRB2HVq5b6qt5LwUvQfCnS7hWGgnfhfqHELwIEani17Yl7Fe27NqJhM5tyJTdVvpjc/gOI2cQi4wb+2EYRSX7Q0zD7fD50J/DEU0IY33/p1n0jJJqK6SCpMsUIfZf6PGcU9SOiydTaurueWZyQAyEzRfJqq/1urMsETHcPX07SjI0xc2gnAdevQHN3zP7502uZ4Iyi7PSDq3gPSyzT3KaKmNhZeoR+gCX5jT58A7u0Ys0aJwCgPSLr0+jfv38zq0cInbKlePycE1Rk4DAzzznqYeZGf8ybGzokjrqotqdCH8vWtJq5EHDpnVcQMsNNi4QEUm/GJSrb3bFisvWsU8/38PNJ7iKqQZzketkj3M3u75VtTUOO5IaZdW8BSp2DGF2F2t8sRVkoR2Tk1h5Ieag54DzHNTl3Vd3nPJLbiAd6s6dc3zyI7nl+HhDulr01ZYYJX+GJkVPy8b/KloYSTbYiAZytTNqeptlW58GMIueM9pwwoxIP50Vapf+lm7s15pwA+igudBbg/lI3pmTmYITbO2VLcznYhGriTYF4xT+DPie2PTR8eszdw2dAh0cMeoFNo9QxXG2J2evcXEJXlADpAiSm7RVQLBm2QTd6bjWiDVZSsYQw/kcC1y+3LEL5k1dySQ+b/zmQBtN4v695k8lkX99tuwmc0Ncrpe8AuLdexhh5K2/rdazu+7k6rAXReATN0VunscsrdMOpz2tIyxThTY9QMUBvthrM1Jpind23tQSIl1o8oCrO5s7STPXaEuFlpm5RXfPdveQLWUuotNwDNXPzYnW3kltBiI1ooQivaomGAZp5hLNR1eYSrbzbIQZqxobNGxc6m/gCqezw0u1ixWZgmtZ1tmInd0IwlgE9VT0LIuscU7qFchfUPbQjVuy5adU7jwxakjgFQ5BDQEiDngV7maLZJg2YLRoOQGw+BkZOFxelpHySs4ULir5b3acYpM7q1t6ttbGrFYFCzQw0Fvq9EgiMW4jR0BiucFTpfad3Y5VUurqryxzO4VyijaSqY4SSGR2cmuo2gywqi5Ju5VtKbLMEin4ctjS4Es/tAN89DfeoKmBkLVnuhvb5/Qi2oi8ZSIJD5HSNmVcr5VsbLme6srLqkym9rBJ0uZCAtpwNV3HjExswbFLJS+krmwPajOi5n0936TMy2wy97lbR5u/vXUIII5A35eHoolrPlJnezJv3VldZGLFFj93t4ec4OE43ml6tr5QiVOTN0U+hCBehSbNBDnbcDdbdUoLBdHGSznNiOrXK7uXvu+eu9sZMP/W9aRGryXHSWV10eriFdad4G5HS7rac8ztDuzmM5i4X99o2B60cG8kQtJrW0GxeDTMctfVPZmTEwhEwVidpfmKn2+0Rm1m9g+nd1CtvL/aoW3mpEY1dmxusX2sYvUTAm8WvOAClqHbP1L5rLxCEPRQXbVvaDBLziGPEGq01C2KRnFHup9lSCToK+HJw00lM5SUUdSZdkowLQihF7AkiN/ELItw2MoJU5Uv3lCghEkBWaVvYqXwwM+4ua+j9VH7q8yn32DAVIG9qYNB7rNkhs8StyN6YuVu4VqQFveSxELJHZVD35ze1Vej+JhhugtMGG6mir+i1F7TweWnnpKrHC+d0q42YrpmCwHAxKMA8CLqtHVI2ManwdmX8pw14cXqxVbp2zbyz0a0jBj1u5uFTLZU5BppxdPjM7M+QeV/V1G4iStOUQF97d3XR0Z2aOxalWQ0+zGULsczUIdRynHZLHCoDiDam75M9PSvT7p6ZOFLm/rMsCMrsfXJ6nWszq8KnZEVy0wkIWqhKV6evataMdA5fDTPN5M8MN30Cx1e8oYK0HzvKonOzPz8/FIAAfhfkujnd+bmyKz/Pj3iUvBluP89zntDdbFDLQsj28vv3U91ZSeOwlcWuol1VNegaM/fXqkMqMNhdM+ZmKuhd4GT9z+MOkGxMdsmVrdN5iZq3IEkUkYJ0tEIDeE6oL4Hv4HdcQtYwmDQZJyJOuM70mfu5tfmAFr5mTj0zmr3sTawUu0BAgW7uAvLfKGBh5FglTC9dtud8ZjtcB0dN+XGFJGbV7ZT1qNH7x5E/z2Oc85xGE3SRFrNcjsgPAYag/mK9BL0TmJvZU7pU1n1kr+WJqK7h0A1apqXOXIPm1DSo+8aaclyLONElAklvBMJoml75BvCFRmk2U7MJYotS6vt7oZGdzruns8y9obAd1SNX7Mbje9MILwXndcCtOqQ300dPnSawbvwDk9EgFL9BqiRYEok1BoumgPbILBNOYyv9FW8wa3MVJLsS+Oqhe97CMG93zRTUEZbdX5ZiRzqwUlH7qOqqyWzC8pNm1gNfTxZBy1t0kzNR6dMKfuLmiQKzHctuMT1ygOotkY683tUhb5J+TnT2woaz5rLhNuCCUNDZ3s+z08C9l071oMizvUNfQ31WepFlbs2dU62qSJduVcKnxWB6umGZ09Vx4kXGZ4lK2xIVKEPYQhrqWSLB7C33mNenoLkJUJ8yPCRWW9PENDJTYI6YBvPV1+tPJJGduyBPj0YMfQJ756rrgxHhy5wo+qOlo9jHUTrUgS66e1O1BIOZLudm2L1PFN9tyzKzMvWVkJD/+JyQJV07AYisXTbxugGm621PG2A+lV9bhibJmbr381GdgPCorppW/9f0a9LrqW5ZHJRYfp7Q14FCdSpKQBOfSDnVST7x4I2CVV6/LNwnHiFdJNRMH75BpKNEXGBvxNlk2s/NARqdWf/9+7ex5XwgnJa33ilTaG0pS6xnskvBh4JNXqjXPvfGIm/4Mp9VicWmvdFVpSQycZL5uZ1970dWwswyV+qyGoPTPZQ73dNVbRHnHAiOU5iBDp7NwBpgbpagErHxEa58Kv3XIgZ0wA0WoYI5jLXvscAS8D3s3vR/vt9XM8yCHl5TAIVNcaDunRCyZ2ouipdxMpBdRaOHTdVeYNOmHQI4HronOovGiKCr+0gEVMu2SnT4GclAR1WfOxaaDi+zWVSW7yu14MsA1eURWtbN0GtefjWYe0Pw/395sN8nf2bcg5ysu43TpBRf8w4KurzfPX7/6UqGX3WLBiw6Z/LeL5eQ+clUmkN/PV+g6GlvjZNDtDJi2ZpLBiDc/PO5U6PEZh3Q5OoglRIYFiJvu4vuyijUrj/D6o4TeQdDVTneLNkIsr7J+iutktj/c2/dXpV8KpVLpQVz4myU4/tDS9YhfxzMMqey7k21EeiB3G4ZWSMxMFPY+edztyls1VDrfMUehpQdBFRkvevZjQgqiJQ0d44KTzbg2+798FX02/+MFaJkYZyersEXThEKNm/YkNlLYixqiRl1ou6h8P6H4gDRExs0aMJ2sXprugWGRiUK7PYlb1stvelyK81Wv45wOqh3bzFFtm7DTDfVoIvCsi7Id+f+DJpU6e7SFpUphfUr3dOMMzQTlkJJm9Br5JkR/a9vYOV5Danf/NX/TY/Bfv9+pvroIgPClG+x/6oq2baml3yT361uD9jojUoVq2msWai6sp4IndhTffOjhIe89+anZ6qTX+D1DeDN3IoeI/RdcGBqYhh1eUInC7e6Z2NabEyfktmLcYLhBk5vAVy51NkzkiG/vDUUvtg9mvC6pJlxAOfPj5+I412lm0wllJVpNEh4sjp3rExXtKCZmZ1zpCwg6CG592yQMzbTTj4VxQJHaIK36qLZCRVAgpzbWdPVnb2h39jLb7rXKdqVtDff+MVcZsXZmwtisoFslYWA/tnsBeNzHgODHs/RDCFrG12FtAVMuMXjRuroDz8DzfRmJh2UzxRF7RjhpGKZXumH5COv2FoQ8MalcEc3HSEr1zKKLNslWB+dBML+Rt7q5HMPvTWKZnHz3RulmpfpUsU34FvpsSDwK1UQQijncc1gta+diifpzHlDDGtXOan4p7JfwQuVH0LYKGJ5NPvvvqrk5OrZjH4gq9UUp/acgXUNaKB9bglMJyyrSSulrppW9lgMR9nR3+OR8jFAihWBPAKsuqXBX+TnvSNBUDmQypZ7nrM5C6YijcXZhGDrMtC0ZHKJY2CwcH0UBpKuaXwwfz+/Qr/GWPsETne5e2UT8roA0/2cg55RfoD2+15Lp9FaIkVRKFj9nbA+hbBUd36jivWQkSone1+2mWHWdl7jbU7UVKXBMKWqrdLqrX+jK8ecMNW8lE5tKchiG8JYNyXaFGJfuY1R2sq1cUii0K9bTekLHDhtZo7L9NvbQ2nAioXku9MOO4YRfmLGsD42Tgx6mw8MfWtnM3TYlrLunkuiOvOKzGnlcDgHvXnhOujF3GZ1129eqMtXVhrALDaXqub3pinU33aC2m1MKg6djd3bZdOjs3lFVy0UduKdvpswZ6Nhy/QMkJnqLdrXn3RamKqEdF1NVxsZHuhWUj/fHNN1zw/qpjQSLcxSRW8hiw1v5jRAnOfQ1BIuO/QQsDAxpQDc7HM/touavWEQrKrllLXGgTo3tertQLqD7AKGkizPsltUiBjI/JSGhyfiuP2cJ+izZD1hFs+ZhXHwKnDeVzoUyuCSCUGM6ea6NGBZpcbXqtqVQ2iNiwABAABJREFUYjPcCmgPN3eFinPsne9nY+U3UAE99W4tJqttT0ujpVXLzzH/nr2kUeVlNKUxUbHLy79h9OT46yoYfUOqSPVVC9yP3KrUMqfPX8f4mpIAVR/glcd8NUKbj9W11Wvk963XzcxBV2nn2Bt+94NR+McMFICvA/2fYmSUWkLQ5JT7fFKdtzpJWydfj7nLotia5XUINT75+fx+atd29ExlG13REd2jCXIGn9+UZLrfLVigSnUvaTzQe0Oam7+I3PQadyQ/bczaAtRFy1XdCIwtk8vX7HvBcNO0NnOhqt/ORHdzrJ1tSFaWPnVpeY77zKhBBMP8pLReOt962uIJzX29UQQCjMdeaxjX3NPKHyawyVrETG9KonGRohXVyEbQr7ZSx7WEDHaeR8PXrGoHyvPDzHnO4khUFke/6Dzq5tQqEGY2eaWruBIa05ku59a8sgUnlc+70ztqV835qq6sp299FLWisTrzursiHHpajtyIqLeC0ZWiM80ZTqGSOqeMpZhylOjST14AugbMTAbgkbNxTX2jpHLJzCtnEcBpD3PazzlmBK1elYsYgp7Ba+I7Efq+qOQyudJmcj2pmJkphB+VL0ogaL4S1HhV3iTuzc7VC2mlcJfZcnT7TjaAIwDQ8bnartE9WanhMbNm+sSpbgmNzIkVdM9ricVkdQ0x3RXmJ5TITBRas4xKTuicuZ+cofnW3bl5eMz0cN8rku6MCPmudSN26WSw5zkkPWwowHSRHPVI/2+cLWl+XmBigB7u8SI611ZBCFAWU+K1VWpA1aE632F/j6pvbT1J51p7Flxhc7I2DuAF8EdWfH0qesn0pUhJJKiK8k5OG11zpcZDHU4rfBCf/FYzAgZQJUWCUlTLqpJUbXukvVn2ICBUxEMfi7TO2thhaonI5s7+2KMKhM4K6KN4qUBsZtL7K1+acD+/11Qm8Lu7M0VN4c091u5R6uWiKWiIVNEeqzVTeq7keq3IWZIbqHyGZqEWMLpnKpa5WsNfzrKv4MCq2zxeVahQjyUMZlC3SVcSNd3xWj5J11+ya+fFEWo8Ok68u193DSoldNTbIxEKqzo/mt1NG7MaSnWX6CKu3N5fDl/tH7qnc5YP44u6Yft3lxKT/ROw/JS4XxL6I+KcfcE0LCxCatUliobAxuFiW7oqL7l8UW9Xjtq+pFGx3kp7/bX1ldPcI46GgBcxom2KRevO0Hi2+ooIN5v6orbrXTJ3zRdvQIppwyUIUkhwbXsO5DLrzm3ensV4z/Mz/6Bh6yrQzKJ7CH7ub9bVhWxkT7+iqeG0ZKRKYdNbdjPd3iWxuyf38bYZSi5WTobZvDniC9MT0rqY29TsOKQpqDorsenTTTO1RZhbZoogERcpuuTVfToNbqrHujd/xdqp1GGq0a3KrrU7mO1Rrg3GtwDAd+IDMcj+/Vw3q8w/zwYzeKwUCaDK2Wb5c9X/Sjoygy8G3BHbvqJXVK+WREfTrOowN+dMTY+EoRwDEebEdNX0iL5SdzGATG3urR/QnVNvpFJPVwk6FvY15Ode7R/guPtRli9EIPNWSs5YnStoJ8iBbUKAvzE+y+fOxilorOermDRTOMrSDEYp9IVBCf/18LMP3saZ2XR6hDydz3ncFZgoGYzs+lrghYWWWDcxtIPeQ3bx/H7n0Opp2purSExLNaee9bUpvhqLMScd5+eYQVJ+BREM2ErOCBNzvIgYNlf4PeKVnCDVG4SG91YkvnuXDpR++1X4BnUN5t+5gf3l5osemY2oOOOAn881c31H1fPJCl+iZXR7wLrn8yuMYcu3U1lY7gAjYgp5O6uNpk1nmyMxM9rIpSqHKmXkrNLqAxjEhn7bNrsJTQDyojsU1GObCn5zpU3rgMO2enHTRGCvC1zSrJqSOsBWMz17Ytgi6vvzYsw505/PR+u7KqkAfGNbtREKmH0xfVhPRcR0G+3KSb/27a7WjztmsS6zXrFwdyubRD5AN995RDqmQWVBaOO0vxyLBmTIivYvQk46pyEQdrSu9ow2z8bcSqx9F7nCrv2wQECFYmLMpCR4z0Fz6ZNIuTFpmSlWRGeuHvtFh3ZzBTDS4PveNEbhOaUkLLjhODCp4HXOHHczxvqoJakWhsiqkrvtm1Urxt/IqlQYkXa3zcAAaCxV2kqxMzx2hMCim5jO/nyucI262Vls+uomPNy7EgOLk933lTHf30/XfD4pLcrM+JeesXVKu9lxp6HUJwwf9P3cWU6C0/3z8wOyJWHO+jKLLkP8zKs803dK91hElLPs1lojOtwr22GVndUW0lCJ9cIMukck03bnTpV8MfeKmKShcwU2EeHnaM5aNMzMwxeqCt8/rVtFnv+Tp0BgDAg7M/M8p6a2H9rNzGLL9mbmW4zO/fevWmNPtPUT6TiwTSamJsHRatvTWYKbZncqN2DiCakwuktXxc2rlVrawhmhIkKd/knOwZfrmF0bAG1Rb16YiiYlQHYO2nbF6l6j5ei/n3f5xv402lnxhk/ghI9YwNVxrnXj/UtwPwgNWt1finI95wN+yw/5XVZGyJJujfdAg5Rg5G5ptdiAHgxUvR492teb1thiyM08lh4/DmDduqZVmrh29HtryJ6W/udbMDAt9dqJOKQD1rB6U+bFgM7qnqifBC/DO4PqUQq/vrKshRmEa/n7Yy/z/NJILTTqNTnpOy2lkg6rNpZuVtIFGpVw1VhCBQP50u01qAuJ1d9NpAX5RUcB4I2Q5ci9tL6xgYcrwVzLmocTcFPjBBSHvGjmWxbXlZrsVlnRDTJO9JROOu3aWakXXAO7xNpi8HNqpgCpGtr29dy7tlVF8GJR5Eb16nR+VQbdmZvr2Su9WOSaCHPdLDdvTWV9YMS0Osz3zSJpnGrRRPdzO9fnXJUSQE3/0gCDEau9qnKzQX+zQZ6I6QnV47xCEvm6bRD0oENcogpUB4R2UgA4cQw7R9y6IoE9ZE1qM3ZW3V9SbfIaqeYczdd02CrxZrSQKs0/bPs7K1MWsJ6WdHV2WF5xp2Fu/mIQ7mF23Ds1HTSgrI5NKNK9JXGpKVRGVge6+BUVfHUqUnOvCiPvRwvWWvk0SAGNxr33ZRldw0otPNdGHuW3zXSXruqqAnnMzSazghaMUt0xzW1JEx2IABcaBv6RRu8PhdedRJWsQQrbl1PdmXTn1Y3x2SguW4309DYcKlJ8VKFh2u2MPt2V12jVqdRMklrpJXakNMyvcB/7H7yIKiFYKSKAAZq+qaWDaYhF75oCUd1Gk8Wy51snPltR6SaPnp5nXVxS/eGll0yUrwuLaFuj02j1FN3yYjvflqwFYaAUIOlTFgrAO4QOpNLugc7lzMFg2rAZw+Ih9Md1db+JzfqtMyNHatfsF4W5n+rXyaRoKwWmEEb4QCAuZhuxKFtGlqD5uTdFDOSt38/NKoUakNYF9+BXKtqYFjhJNxdzMIPqMrO3FB7dbeTa0ECj309WlrgWZbPr6bVNl5t+U+514u35/c6nbxxvtyKbqgWJH4/+4i1kZkHI3yJYC61jRulO60WcHmJURxEetYG9JicbiLy5ORczZup03aeTb7SUb1XvOpVp6yrAvmSrM3l+/ugtcY9NrB2JPylLFlYaTOW0rAU/FzBxzWKKF7elNb7IrBROM3iX4pHtDVRRBqVBHIw7n5+fpS7kFSK4Iex4hcCQXSXCzs+jn4jLy8Odx8b0jRoHFRYKaX9N62vr7ynpTzDg4PNJ3Wm/9y8N5pG3Mi+4hjLtxZjJzKprZqDqQ9GZnavZFQpJ88yPzGuAtorWCQzg/v4SY/RzQqCQ8GbN3/4KqKaxMtDu7v5coaKCgyMsnhMkbl339eMQfM6jNkSa2cDIz82u+aQWjsmqW2lg9hLLNHYNVMI5GEFVWNmY3Cx17xQgBqW1cRqBb3ysmiNkWu6ZFrQ1ipzD3//+FU4lxO9zP1pHZmaV1FvmQHWrgZgdLQnCwpQQPTPhp9UiWRJ79P5jF6eYWd4e4hukeJFhTAF6UmUQMLOsXcqp6k28bTm9imoLqy6N0PYmSsnxLG5G4LwO2ojARvErQcEEsIg21Nw3HPHnEaEPfRd7gWzmcSR/ozaMlxDSky+oZma2uUjX715pwBc00hXV83q0sDTMUo5cPYIQ4JnhG+qFlRqPrhN+mZIXmliwa6CA3JHEoBc3FuApsnSGGwEC9spZ1tvUPV24d8lhbTM0y6vEvBDJ3GraUT8PtUkbICUkQeuZz+cDsnq+qFV1XzWSVt9PkiuFN1hXSwf/whuMiLUTb4XAlxD9dym6BcDO2Z3NXdyq0fTkc6l8dr61QiujagD321TBJVSmRxxqr21utcAGUsDVvTkzypddLG8Pw43w9vA/fx4SqPlOQcSIGl4PAZA3NYXRrSo523sw2+k4OUl5fUe+jLVjCYvpLk2F2JLYjX4FTNz3/kxEqqvarLunvl5fc/f1Mb3bsnQaSu12D1BIMbhZ5Kht41XR+crUhO12N0A/UakRtbTDdkk2iseB/PibTeLke69ObCZMryTtNW2vPJyQ+B4zn3ur093OOWbUERDHNeoJshMo++f5I46uMvWHPH+OuR6mCHdz5mZpTXfXvT0qsg/NqsOW6kPFCEJlv/ec/EoRJyJ65FG4v/n5/fzW1M3Ubd3T54nB3EwM7v2M9LgYkmbsaVHvqjogMTZG/tvbnBsPjj0SSiEWGKdX1p/n57X4fp9jhVSUhAn85rHoyMCefdOdmfFEuFnQn03bXDALTVVVa8uewUx3V4+HuroICGNshdrfvno83Dai2fRqbAAySQsPp0uzJ0z9k1e7ykKdhu2EEuapeBX9geHCHt0Cg6kRCbGOXAGemXk3TFvmKW1vNz/TVfdm1sjcOqNHUdP6C2cKp5M9ZaM+zKzxTmnYMiVh5dKoDMZPuJt5qOnBZHKEsmXYXXt5K06C4svX86UvCMDiI6sOkgjLRosANicOMxryaC/4g/bFueYfY6EkghGj8e6cStuHIsSZWSMR3RKAM7M8PmZFBMqXqpmlxDHdMPNpmX6p2zPvak6rWhe5EFJuyNp7OWFlv3tnz47CQkamxtZmbDp8TIp7M7UfCg309755WZh+TTw0o3jBz/11U3DO3rKvovTlcXcDa8ya9lOlgd99cWBmebOVB7MTUfeSb6O8AX1RA9vUZeFtpfHVBm9yxeJrumRt00d18n4hI31tnSOEJzzeqJlx87ypR2y3xcFrYPF7U7NG2Nf10tMTcfQJ3vsrjaaFYelFfPJK4aHIb8xYbFbd2/XYZhbnzKBLhqnQww1N0OLZlyJe8xv1vTZoNug4Z4HOniA6Cw0zhvlRqD2Ng4XIDCqj0Vc2OlfeyDP3o3ZwDPKjOkwo72W/6dU7tpt3F4FPXj1SChzW38pAFbWEn/Pz6IqvKgwrBUSgpUGkzUxmKh/HjG7QX4XDwYRbtpJhoKTP5/Gq7J5779oGSwdnSrVZmfutVvmrPCTBwSOUpsdNYr+USEkfdVUSlBdMkTUgPHwMeZUk3LdqMDdv3V1dXugV3V1XwoAZKG+8ww0zz3nsfZ41OI/6c7htWbMe2qkqM/kfCY6IdINh2s2nO+jUr9eh3xuxpxR4TVjcpB8NjIuf6hORyM39PRZnCGYnzezYkmprqNxpWmEeVN0pRnJo2ILXJPyY8m7XkPYdAGemZynQjQUGyQgLc4Mb6SEvAkbj8Mz0pjXI0SMCX2DNjsMDrfUk5b7VZSJEbeafO0Onv630+UX5hXrbyqWky9+PdA/50bIOXXKVa+tPpbMt8KBJUa9eZWvtqNI5/n4VvTWc8m1tCTDtPVuM+7qwG9wabf3eDcbSu6sdQQZj81DD+2anK0DtVfpzuGoUfSaqpFY5mlBRkNg+bYCV9ZXoDKAvunoyW7if0KZbOTM3s3ucVtk/8UfLkHbKfSB1puRGBonP0ymnY79bIQ3Y/WZ2FBXVAXNw4HGqWzkHRqtKX364hPubgRJWhq2eYQVnAwrghIdXpW74jVXgatRNWiCY4IWq3TQjoru4IWOSuop6GhrDXXPT/lFufmKq9AQ+54c05asA1Ngb+rldeU8Lw1lYVe7G3j3VN+9g4jmYkdhjZiQu1HLQVWYkxXGVRCQzs3p2eVA1raDMRo0iNzOnBEeSQBWyOxMY361LDEjTUNOf+sDhJtqtzWxKKT2NQdY1hzsJIY/V0+c553k0GgpznUFsS5qfc2aa2JPA3ac7wqYkqHflwWH25Z+G5HF/f//WFEjJjaRXc/fHHw2ku5TvrQnVr7nZQhrvpsldzXEeZ2PIQelJr2VQ7HmObjIl3EmlQxgaqP58PgDzZpfSBnkiAHIQEaOuWhqBWzlAxNmXSfD5e39/Ph9h6G+6FrZsQYu2zJmgJBa7ragCxklTMKdnlwwiJGDbH6LRFStcKhiGK0jd1Xzm+CNd1Pef0qvESFAego1Cdg9zsxeu1PtNIu/t6Q3KBrHXVWdeJXRHuElJZQsTTPUO4t0vFj/dG/QzU905XZm1M7E2IIEwaoqunmyQih+HMI4dUwGlUAAv5YDpfR5W5bVyyRX0yG/xUtLv45+FN0BCoNMXrRalsKTiFNdeMxoUuhWLr/ziWXsXVlg6PVO7c5QSJKVWbOjoz+7qvvf+flKVitgcJGTq5Z68IikH4FprwW6MskPVD7RvN5dgWHko3p/Y8n6jfnrkSZoesKcB9otml6K8B1WlCVgflq+lzs28B4SLT1HVku5UjT7SONnK/NdsoZ/a6N2Kf3iP1n3ZEwuYzwA2BY8wV4GDcE8SUEKnwrC0GU2PuWmcxBv7UdNUpfj0P05mupcjx8bGzdIWfEHG8Pj3XAn9V86oW1X3dGbiZWJfKAbT/bIXum+LpC6MPz/PWifEVkElqzzPox1TXs1jHuco8tuMea/eESG21U3uYefugo2UEjdKqTUzCnQqA/qT7uA0OfJPySuVWTpf3KylKwbuvQJbpEgZjjD9yjvTn/uZxq00j8oM957mMD9y/6oxzZ1eNzFjLu/fwNidmfn7+3vv5/P5xYyYJZef4Ph+dBqQMDVrQDPDdLuiUt0NfNz/P6redk2aG8cRBUhFlnvv/1Z32xUSifMDVFYfzzwz3fbrqsz4oEgABP5ZP641bsl/f3+7e7/eOJNdsvtStZmLxD8/P88abCcyzi5J7+/7fFYQderd2+xXMFZO7tI5p1XOQ7Y0dj3hGAAKrV6fALEyvA5l8h/QqXPVAAlZp0/Z/oh4nscPjH3zT5UbU1ewW7ttOzj2hXE7DL8Vll15uJ82HFbp2016LqYhRJJDXXaTFkZ7FDk23UKYpYJs+i3ebzN+ceqi0HWuSrMBOL7UawfPaEnd2OTneSyaNTdGkkPbn/GGw3V7nmPLgSuzwzVTiAMVPHXBbW8MfZ1Uq8653StoT1DThoHMbKirVy5A/gWeP/jFJcqLYMOMXF7gW+R11x0AjS//VHMjwlV+zzny3Q6rHUtkGlL38dVj8UbJ+iIv0CKY8t90PrAMWARAu/C7NVZjsjQuhuHT2hHcuqYy5AUgvEt8GhatjO4IAFv2oLVw0n2Vhdpph/u+4StzuoxlnCygMgParVO6h4q+yJj7G7901e0X3NyI1YPXyS1MszV8XnTPrZ9Jq8cEwOMIAq3zVrfcf3gVwss5l1LwQgG8KH8v1HhfXTGWHSCWZ7OWHOs4p/4MXYjM2idiudP3SMXZGxLEc05L67MIPs+jCSntvXfml5gybdAkhmwgLZc0xJEe7jSe3fKaoiWM4q4Njp1xaXCqzJU5S9EtO4GA4btiZRUcHFV1Gq5in4xgYCWfJGpGMKnVMzM5Cjhj2TM5fKSkDejryQfkE49VZJ6wZtv11jLDdt1maNyY6/P56eq0MRLUu9SOV0Mw2LcFd+tpyCIBWmrdau9CwYJoyxF+nh+Vqvo9e/fv3+Q2MG6sZWMBBfmzHsK+K45g5T7HJEWu8KL5ihURjWZorWdaY0whZPB5HkZ81uOdO7eT3Yq1MpdDFLrR50iqqnzGkDzzPntX8sybc2khiq45pXsrkrFWrIw1Cs4eR4oZBnHN10gFo09Z5U0QUNV5z29GPg66gXzsedwxDe6y3tX2H4GEktMgInOtlDVR32cYYNLp0LfuCAIQ6/EqcPhh1neFyvAoNDoaMJmDHMEbgbFcWZz7elWh1bNCZfFMrmXDzufz8UCcmd8+Pa2U67bL+dd1EhdcxiRMle2yq48L3HhH+kvcUewCYK5SA6/N8UbOCdoNUVXmcwN3QWEEOv0/zgNDdJ4a+K6HPpRvtJmAglwB3/d0CWJ1CRwbBKANYQl7W54+ebxVXguYYyrodTl/h/F7+CLvvhoxHj5Wr2C/26ZeA+mcwqTvVnfvvT0zSiPF3NXgl+a0+Gc2786p/Z73Pe0Wnv4WhpIIsEbJKo/s3dp7EsR0IbuAAycAYBwavmNTY78bxlulDss6g5lhxxWSK5Y3qvbefU4wVfVdta19Iji57cF9jhkXBjPTREcu51Kq8bcIY12AXXbXs2x/lsuDMgn0Kb9os/RM/nw+mP06erdzIsCu/3jEWCbdbe/rY+6G19DQWhlc+dSxYwjdvU6L131m9aFHYtUCoaAFHsagGTFr2dRpx2e2dBCKtFcMADnlDsByJjDR6Ilf7uMh/a3t9zkY+7wCpA7G2fu+ez2WL5jDzBTl7+/vs9Z+NyOsXHyeZYoyV64fB0IJNwMAUG27utEpytXIXM9az7MSROPd/2YGwmIJv1QDxNUpqVaslpdL87/vr9DM6Kpc0dOPjByvhvjcAM57bDf+rEV4w3li4KrOZ60M9qmf52MasOtkkIvC5MHyWtb0aYBeGemJFy3PXeeMcu4mwgmEBfWzGVfXd49CuKHTQBLtI1unSvaqHF5gOkyS3oZzOwh4G/kYZzAXLAA38L27/DAAUNOxBOZOG2p0pAdKtS5A74eGGIY5CbRhInvnuqrKWSjGFjDLvbKchmEMk0tIjahEmnXfq4+yHMVe63WO+6SYjGgA7FNfr4g7z3nbHGaVXcL9deat6IGgoVudpkJ6a3iUXXPwI9zKavpGq5W8luCSC6H62lAaNzNF0t1AqP0D3JXGjBcab0RdZANiZEyX0EMIq+XFWdtSSHD1jEvwSIRUpydhbK6iq6pvVINxHE0jSTzn6K7FSqLN7EhcEMWEopUF/j9WmnVfjld8nmfFA00UqB8S83qRKemcmm1poKS9T+ZSIfNxO+KlPA5ahsxlR+i7+UXzqjL8EMM9QIjl/MP5Dp5ABzv+77+/xpUMnPqxuJKwZsSsYwS76+fzscG9YYbM2Q90H202oKu8LpVrWY65z15Pmmcd6zZZlTjWoRHGcPvO6OBMJyH7uMoAZzvw033vsx6vP6jKyiBvF7o05WcByIyuwmXITbH+PD/BzEgNjBn2FDt7T2MFkE2bXRs5DUUoA061NEg9/Vd5Pbiqji7BqKNxEobtbqx+gqAV0Y5GkTIyuao32Cvz+Sy/WwOa7/Pz8+OURBCnyoz39uEmUchnmfq7eKvs6eQ+3dCAIIcgq6O+okBbITXOrr6qyn3eTOb4tPBZPwDcRcImo6SVj8+zAHnz0Ht/ex/bqX9WguwxaoFD6J5nTTKXW4NSH1UrJk6dGYHGDDRu43pMq5azG70ZNy8IvO/qfrn6GDIMhtXrBD/PU0fnlCaFJTxRqfrszeB6VqzIZ/T2QIDcvxv25hu5SI9RRah18m7CeBdyoNPgeXeCED7rgaDTdN8Vjh60Z22dOm39ZVd3yaPVbNRfuMPATpnYn33awdVr4+55RKwZFTIhrPUQoRnBg8DzLGtQ7L1zW94B4TUHKHQBGTfCJD26uSPWHBoRkf4vdgrwMro/1BwrHiHnxzNm31URsy1ojaW1TwTpEMnR6E5HDAydHHFzGXvYBt7V6B6HwUFuJcAUur+QWZCS+VuJY7GJOFXHLaP9H+Ypsn4NLsF3Ctep83e9RvPCG7blFbM+Vqtz9hwz0zZxHpoBGlPyvNDAcXvS+vd3q2Ubomc9Gctuekb+q+UdBYAe5PfZAN+zYUOOsd0OSaWaDNwIYd61JKu0HZfi4x+Yx6JOP89PjHWz9rt/fh6CP89npL0ae3RfT6EnvlGda52zZxMBvDieG4TWGA+V77Wz632+q01YjjGkG5nqrl1evp0Ddn4cWxrnd2lFPE7DMr32R5TxigEUa6nawspzzlhYa7q7tRaC+31NCIPYtUG3gB0uaPD5gYhM5rN+dB/oYJzqjFjJoZ80NinV6EauWWYWgFZi2ckqLJtRPetRqU6fPVagfQnwAbW7Alm3q9UNcEA4VxKnDlpB9PasTzqXOLNrGLEypNpHKIS63GFpn9qn3n0MpFwo70/gL2FFELPQc04fn6Bc1YdyMiIYKNSpE3fBu6G1FsXHzCRpJOr//v7+CT84Oy1VdZyQPj23SD25KLx7t8apIq4uXF9AEzq1p1GcMVPgYPE+ZWwACUNeLmbE797eCcjxPPQ+WmTmz+cngNpHhg/ss9ZV2xJJTKsVzIyVqcKuiliZKyNb+DyPxlGHANfzWP64q9T9eZ4/TlSwxHvl+nyeJ5b5NjIj1+efj+sLI4cMC95G3utMWjkW5cZGXIZGy6RBccdJAMxcVwsXPneI2Wm/1Jr8rhlcddU16M0voarBEIJ2EBoBj0aCN1DWZeRhNsIUi67nz7Me6w9hoJEzYtQ+XSWLNey8S9wenOB4TOGP0tQdPTCQ7z16/JbbAtqt/tndAiO777kRYQq3rbttRSzaqKdmnIkbdhth0Gt5hrbRUF995DntucrKKAa7cd7TknEF981kVFlCSQjBVOEO5QIuyVpWQtu41KkZvloD+0akU/a6ZAoH+rpA2sTBwyq6neXKJz8E+3L+96/Rwwl2L+iyysoeub6h+2yfw1YczqofQ98kHn6NcJUZ1Q6DZFw8wbCMkcdJVM+xxbAjptVRGhpkvGqt08r1BOHd/e5yH2c0C+MsrfBmv1MbKVFwY+tH+rrprJ/HfcN5q0936+wN6Hl+VjrHCeFXQt94Co2baa5Wnzq/73/NcTMDlGfhCAblmmtqLh0X0dVtpbwItb0r3EPbU0zljeKMsEvoLYibFLJX5nqWuZguq8p4enz8g1yZJJN5GxfUPhCO11J6zDk8t3iBNp9lG233vGhU1XZ6n28D2rD15/FIkAz8PJ8VoRKFff4ldDdkodFCwHZmpX73qJv/3bvVXoc55oQEjZ1GqXR22W357F1VYQYexo8xG7UUgd93x7wAPuYAqat3lTNSWp0zMTBjJAamB6vr932NTdldaFn5ENM2mCVyW4NpaFG7z9nnHPe2lsY/a7WOzTJLhwE2KJz9nrNtM2OiVXOKW/Zyqo6L5akdswAxgjSrg8SuLmMl3ecPeOH0PfZD9jH07VJnJc0NWlfbDJUcn6gVpfI7awnWeXft7UOBhPfJHcCL22zzixe10lyQrqQHsBStZwzzwloEefkDzVaFZPfZiHEzg3t/0TjdHBdDE2og4hl1Gk68wTjE+BSPMHc6z5FnWHvPGPuJuwhpcHs+29jUZ7f1gwRCY4wzHIfmbVe3/PS6nLalCiVpVtgsURkix8crBpq+U1RcPZOXdsO6C38AgHvXfnf1ZNm/7wuR4jkNeP174NevsAqT4htVLfHde+WzfzeRrpdnH4J7b11S9lQZ3aqe9K892UpjehG5JIWxKnqZueurGTKn6hA7QUScq7p1H+IdtnDOV4wiChfp8yUwsWkOiKQ70fsNzXUcowR+of1We3vz+F6RDb17ZzJzefnC1IKX12bqCdumo85xb9gTtSvYjMhviJrkevL5LFD5LD/Jdar2cWcaGTVRspyS5PGjJ66SZNjJuVF7B0lVqPu8ow23bTLQXo0e0G2W89kGZNvT/LOW2Y7MJOI/z09GRMbeTrHQlPKBWbUyJAbCtlPV7d24qtOn/Ky7iXvfX+Mqy+5ppIDzdp2yxxZaJn7IgPhYbSkF47vbUtWZ8axn16lua5cFeBawn511WXbakJTI//zzE2nuSxLESYnIDKDPe+TlcCmAZ33SYt+Wyj48AbDLrtHeDOLP5zlnBPWAnpWArV5tuwCdUnndQaeOXz9DnRH58/mYpjznPTMPdVdbzWAo0ViMmbdZR4LQ44rrIUMtm8Oa0R8pcMwr5+bAbR3vHhygDF4VVrNR7xl2bux8cE51lbp/f3+rjqr37x5ljw3LOLHyPhdc8Ox/58W05XhixvN5ctwpcE7VKckhQq0/I4qB5l16pxMPu1Pwu3yYKwA5F9Q6jr4BolbuZkbVqXPKkhe5hly05wbWu0+GrulWwF/2SxBA4oTbjEdmBB3RoqEPcUvTX5c9pvVVty8eAaGheR9llkL6MbyLBG3ICOM7KUuYq7suByypS96ivzQ4z6nq9nqsl8siZtLxYLT3iVgA/cFLPWQVxHHZYV+LRmt7PDgdT+GRqh5p6Z16hGBmVQu0Wf45RdAOpiAlnhIjRxc7bBAynzkwgBZbirVOWcXCPtjv6VKAqH26RWq0/BmgZU+THHKZUl/FBml1+Redu/14nK7M8BrOKGjIz2dhjDLMKaHVlniHPbToYJPRr22boNqjNNDnfNYy8ZBrXb2UQzBGzCGPIJo2PG4/M3zQAGHtgReC2BE+JbrOAfE8nyt0g/MA/SB0l2caz4/+GHNWUZ/Pp862SWJcDKhqpNNesBTGKUythUiyzgnvTlRXV0SCqtqATu94gvCrwtoOimCbdzKpSzeJAuEl0NdGRgAD3ngi48nl17ukhvKTEP75eSwK9AGp0LOSkheM3v1+1iOpVVDvs7v6/X2rS91nl6Bc8Xkedavq1HYNzaRU3mEUGmi/Hl6qClCO9agq9ed5qnp7l42q3rkC4YxyUPx931abHakux5lWlUWHxqNPH/et6gIsn40I1q4pa7DQMiLd2ZWBNefXX6KbPqSG8Jchl8t/RgLxfAxHWTtkFVD3qXOOXXxbdY4DLG3NGLni1HbDeJ/SjIy1ricE2QN0eIddpMNwuD7PsvXcSqNbDW97xQCqY2pDr+Ya7mCE77OPDL/JxmAMtlSJkSMVGxeiNPgSGeo/3bYfKg/c8wG6DOq61XWL7mg5krm8gZ7XpBiAmIRZgammAODNA0v4vSqlbit2MB+V9CbW3fXzJcIXYeK4Os5JYGynvNznoNC7fjgG+nPBGFGaAPDMBSAY6kkc89GeTMqdhzE8K3oBRZ2yhzPAc6EW46qXIJm0tUn4c6HLiMi9j8nqc8rkkIRTcmx0t/NvaEd8N9mu79uH6nAPPBOo4LuQZKjh9MfJFWx0067p40Qt/u5So0rv72SqAbShBa5VpfGFHlwil1EIho0csJ4loHQ8rV1pqUtlg96cVkR4s9TyBrsf0fKXNtnG9SzDBR5sPWaZ1PcAcO2chwsy0ueMPZ/dX89dgFIvpwG7q+HURTLrHP/Ub0qy9/fsdE1GveW0jVzLZOkF02lgnZGNyhXV+3mWX6eugn2HvMgnVQ8HkGx6Gejr5CfpuGd37i6YnmE1K3JqL0V7eI9Eq7rUVaKj405k9D7fyw3pnGPKe940m2rnWhlrrdqVEZbV+nS0EfGAeyMh46BdaAKmDpDp5sfbudaOEd59sUICa0UiezeFyBTRNf2diY46ZXr52ETIJW8wkHEdP9vKQk5HWZKwf7fVHREsNG/gVFWh+fu7QUSEONyy20lVj9VPda5FeHXAvNmXhsQFtrrVuWK2Hf+wApDOnzL14z4F3abu59eNS5qhd1czX5/ujNmAIEfk2p56Ws/K25+462wHB7nkVTdvBJvUK5botwXdG5x1We85Dh4Ev+0jrRz4feqhxub42GvvQjEWxFhP7L9lZeIpg+a3m/6uYjrzTrrmLrrYkFocBnD+a+2judzWt5TfXFp0kGOYYU7i+6Jf9ft87r/vZtBfV0ykeTdwjRbc0Zv/uacGhmagARZ4Fapue+pXrWsUUIw4p2CDNqAbNSw3TnWPeIHDfk/79r1ZJmNh8N14V506NuYCBfZdNfAZA2I9zz1ROFSvn94Dm3Ab2++WWl8pBEEVCKrNhGts0Gz4Ocq3+DqhzkfFXXsGWtj7UKyyam5smP1uVuv3d7cU7Y3hzDYmRY2ynnCdkuRMV6N1aVFENwNeHMh7m/3CjyTfXl25Im9sy9BxsKDNF+HzeSSZFvP9YzACVi57aj2zDhpX+wUSayXF6gJo4vtURUTSytPZC59lPPxpOVpdfb4KLVyzNvjO6a5UwBV/wsK6O66xhClxC3vC7nLdoc7J+3a6CClQCvCcc+qY8D6aK6yxWhKSX7rfwhGV0LQwFMBj9epj8TtsxYPA57MwRMOkLZms23uTUVUmdexZGKT7nd93t/r3bJA/z2NXle0REOzqUjXKAEWulK6n46QJTvoC1M/6+NHss9G9Vq61LE22Evndr4PMBIVw9unuGxcqIn3kn6r1rG+hNDNpLsGmVxGxMj/rcRmHs9sUEE6dlUvQTEinz66yd7Ha25XdPZNTO3OKtsb8PA68QIBV7SOBxFrR4zoVK5efQIM/1l67epn2MCrOmdabV6HkltXgvtHzye6xyQzB2bL0JC0CZets9fv+Sjr7YGjkNdonP9RBN9MR46vsCCuL1pw0F20nfa21vtSbybZlbZMdqHURDqHtTY9LA0y1NiUTMRCWvhzMXwqCnGfY5FcMMzgBaaEqJI0oc46sAU8s8fJHveB/j9f2PQcue2xQFTEbf4PVVPc3sMxxNGHIZabAS4/cRmAwpQmz88+PmW3mWaBxlTFLMHMn7Pec1x30HFr+kd1yBGZm+sfud1f1v//93dusdlv1P1JUe4kDQ+RPuq35g/CEce5U5Ofn7LJnuC9MRvgUGTTs6P13v+dUtd2i6vR+HQDw1U/SZ+fZNikwTx7disyYCyqCVI2ZreAB1hhTudyXPaztmD8VNnWhHtuvexrIJ3VtQLo71miILUl0ugtmmxLled/g+65LASKSaC2nDXiNrWwNNqOWhMj0Kod/VqNN2e2zu3HagaHAndkioucVHhvRqhPUxRkEQ10U4RiAIPD5/Hyf0Vi51gr7C6pIQB1sumDdiRKjXq+0Kmf0U+NMd75bfKd/319DfgINrYbAiJUrV+qW5YhYK3bt7iJY+/jRPee4dncNlwB0Ru69Jex3365V3ZVrZtVk/L4bxOnOjCdzrXyeJ0jbo5vz9FttP95lG/e24jvrbKZPdvZNd1BB0rMyV8j9rn00oczIyGC2GoL9l46XYB33XV3VT/qWkhrreXMS+xSvCPXf/V9RmcsKB2nIiQhKhW4CK5YVIu4aVg64chcOyljiWlk1fyeuCQ8w5+XdV1VwbHFbgz5/devv3j1YDfx2WBrn5tTtajKJkBQWQI+gIwQbs4GwMyjWWs/nsYO7Q13cccxG2+VJLce0lZgZOE2dUVeZ0AahLvfjflmqjtAmo/53pMZtQmmaPalu70W7ULbaiFNb+Q1egBe4OiNrNKYpvzDjPbHGf7S6BVuTKtKldkxepbJFhLHcmfKm0Z/lfCNR6o5l8MR+DN2tc7a1AH68mZQH+nAzbpwsBvyRJUB+scZQ4YoWZz67Ik6vZThYaS6V29AeXjoEnvIeFqvL1MRaj/c8JKl0TiPi7wuBtnUw+0+mV9AmGxE8zops2R0azbPd7HKfrtP+XZ5OTb5O10FrTOnOuFp2zzrnXEFj3iVkAAyny8YKQv5iQY6pYJvC8rAJEC5w5Qs9vw+z0dBeCJlt2Pn0k1w644XPH78XXhGAVOfYLIuO5orRdAdxXsfh1h6SxDc1Vn7s6qfG3rvHqWqZOfdr+Xk+3sdwM+vGJMJ7qs7Tgfu+HOtpXSa8I9JhI+95e+bEidXRUHA170ydrn6eWFamxB2UYP8pxw4LdvZoouyON3/QfzQZbt45mTmxVtT7RtC+vIaQu47UAT4T1keCVeWUhYxcT7pFdehETK4ZLGwn8TzuCOcre4qaLtuKyFn18rg6jtPAmF5VFazPQD/r8fNL4vksAF0HY2E9zq9mpM8uW60J3tGdfmdMR0i78vm1iGDfh82hOu6ZMBgKVrpeh6qr3owonc9an2dVnS946NTiCw4wA63T6AgGAo21njCy+Z55FwaWAtURN0GIIrTiumsNeDeKSUag8ckH0tk7BEG/+183/N6wqe1FhLbr81xPUwjV5aiJTOMJMFni5jBou8CVz5XJTTubsVw0DZ3Y3c/HZmTGyktUlvcEz9mwI2pLf2tQ402fky6LCBG2NTSq0PTomcoZjnlR/h6NDSRpPc96lv9Mn9ncmhliZFb8my285W4VsLvzqTO055U0G6O+9a0hbd28u5/V2PQE0ygFMh9j9LZWdlM/0CdkGKSnzdIdEB3f5xUwMTi4kJ0YGnVt/YNxLpls3VYP5u5Fs6/hc0hsJzDMI2uGeVilU13b+fKXiAZl2wlQx3++fHPvhengwnTACD8nsk+Uj1VZM7nWI+sUwFHK4fL8Amlrky652mJ0zhHs6oiMlW7YusvSco72aG7G4IDQPDr/gwm4lA2So/Gj935Nd+eKgBgM0Ij8XRPwbbicuNB3C9frVOt5vE/zrGUnQvcRp3b3CXLlsqQs17Iu3i22wzPpfa4AZ+eF6s4xsWL7wNPQW6eL49qhc5WvvgsIZ7ID3qcY4qY/6z8rns/zoDspnO2x0LkNaxFSaDoRoSIQthT1sq1fg++WDdDnrPU4dCEy+xQ6k9H7VB8D1JIMnUdE7cpIN7wMymbJ+KLVE7MzUQnCfvcxDngcyi6uQMjEjVu62mXNKxjjojxAs6+S6nRL+7xpL6Hm2cfGxb/vC/XzrN5td3pB17Hf55JUM/aeo/ctu5NUj/WTe+bqMyWvrKHKXNbVwkqS51nPZ+1dVU6LXPs0SWaU2jPl7357husRdYyDfLB0uQtKRJ/OzJUJqGsWGw1rSCNwmLOB5EiqvF53PSjJ/3z+j3/Rz89PAAneDpFloYzLtHv4CGFiSq0VdAcA0ECQbGKYCbL6SHKSLKCq3nvPumYXyFxxmdv5S2om866GrjUe48/n4zbOBKUGdnfZlQnSacIGghnjdPe9BnZgWpWgH67qOlXn2J+OyeEKRsmikXZ65gbpIXVeg7GaYJAoqFy0zN7csjOsAq701+ANxNEX+Qv09f/AtQrXPa2ESFwKfYAofKGhQUjiXC3i4AU+qm/F979jRfsoHomqPqcaMCvzzXOX8+vFOu2nvY7ORHmjbnbEOfW+2zCGpF3VpYzHpO6g/LLjt3lxnVPqEaINCVGq1jmqOt+diRb8OPiAcWIMIs7pYw6pKtIr4mFJT93eYB5T/g+vNd065+ImU8YhbztT1e/e+5yIeJ5RHOrannQN3v35PFVHNdaD7vjqlKARgKfHee7p/ac7s/MUrmTM6kyfWPYzCPsCqSwAqFPdtfcbvIsqUnfDlvHGNTNt1SIgGKd8wjEjn/V4bE8mGX3qyR/OckNkfjJWlareeXWDK7lWcnyxBnGGpGud3adNsSyvlIzSDpEWLkRLn+djH+HTjhBo9BH68/nJiGetFSl17RPUqe3jzcTcft+qPn2Moduz3IU3vmnR084Xew6Drq5dp+rs3d2unuTY5c9+4oiGYyjkiNrTSQ2AIPPEilxVXfvESlsYpUVp0KkTyVatT0oG3AvoLp06K+Ofz2elLeHkhlrTQolUdZ/apfJOjqGAn38+CgXj7AN1Ms95re4AoqWMjOB5jwVGtsKuPiQi+XzW81nwZKnCGOgTVxDe1QE8KwUbN5aTgT3cciBaOmBrdgIIdBut5H0Fpr22uLnPNxjDgInpqJiYLa3IcgIqU7AM9LQ6I26rPuQRZuN/+j1gtoRIROasKHkC2Luq3/e1oWa1sylmGFW3zTcj2L094o8so9t23H9774YPkq6LuE7U3VV74wqf4vag5LeI27DBbvoSh/O8eoiJk+sq60bQ3YayphueB9jNLK57nYEmN0bnFGRryLAKbUrWhXR1lw0zQqMZ0RRxr8JhWvKBUOQq5wMnhst1FxJfIQ1ibhmrev8eC8wwqU6SxnoL4PvuC8fP+GvfiL3Pdbtz0u30rV43m8J8us1WTlZEWKoznrIXcPO1sqcpxjFbMemq88BkrHNUrX3avZ8DM2fX8XaplkK5nAVHvgZnHBO0/6x6jOYtBvNWZ1xLI9t1RHxN7ApwwcLlHmrSiGgMkVaMuPW2ZUpcg7SZ7iMMFFxtDLv6nPKdvuo0SDLvEJdxUreXIX2Z+pSPCh+zT64ZsIN7QkyGw2Dk7/739EyE7/73VGc+p3Z31ak+JW3oAJdi6o41YzhhwwGuZcN19HRQQyf1DPesqpKNSNxlqeAlDC1md9fZC7RTZp0K4rzvk08w1J3XxNEpUadOrMR4enfVSUQAuZIBc+IrRqztyQkSxa7ZeUlMQUF1nb4NUa9nebbe2x5ArG44qyDIoDMm5e7h2CGSFj53V66sc55MtAMXnUUQDZxdUq98ns8zzg3z8FTQJUsWU+73hXmzYKG7+99//1WLpo6kpJ/GQ8D2SsaySf7z858gVSPWclvZI6OGrReCsSIXA2oKz3o8VAdZp0Ik4NYbF6N3fpNnF8v6TUzw27LSTrq6K6VhlzG/I/as13CYjK+RwFp+/s8548vjlzsiHM6M7guQDpUqqWVNAWx3yllGSW/vVn8lmmV85BRalja4g8nhgRVBi5pcHNze+dhRnVwpYq1xYY8L1eP6RkA0sDzPks266xqodkOKIKo1CKtPJuPwniEmc9gfwHq2AZOd471WMDKynZnl8uHsOfI7xF5rDd5COQS3YWdO5gzdWRtGY8QACSQ56gBM5Li/0vghunZqtCRj0Ja5/lQ6zcyHMVFipi1FXpAL7z7V49bjwuPhJ/Ox0akGmlKV9nuoUM0aM0kqpLClnbFEYFb01DolCE4UevcGWa2IYX6s2DG+32XFtGgQcG4o1HX8oLs9uvL2GbPWerrLF21EkHfQUveUPz/UUp0aHl+jU/Yg0s5FYkLKTCNl57VbH6SODK96eRHffW6XNXlCyWrZXLkiiVC3X3UAjAVd8xP3LQYBMBCQMUdStGBO3WbPehK0MmKtBc0KfjDRXGtF8vNEoqmxI73NxEgKurbxjWWiQnI5kNDl2PSiSTkY75gpnYja59/333/ff624jxWfnweycoS5Hg0IlpA+nx8yvROqZh2rEOxQOK9GqxXw4pBlPJ/Ph5zt6y+TYddSSd5aNbpl5iHIyPw/nx9P9p4wgvz558elxq+lutboIGPFsnK53n7fN1baDbm6M1dXnTpVm8SyYMDaoQwzQFVld6CgUx+ayDpnSFGI5PN5PutTcvyDZWnpLPZJtUyrgfH+/mv98TkVbsCdsnTq7GNXgCcfoEq9nud5HmMv8e1IIoKYPTXGNFkaUxrrV4JxTp1zYsVaYcObyICg8pHcTOZo+K5KwUnQ6lzreZabblyx9Q2jJ+n1Re95eV6pGCG7H+1ro3YlNBnLMs3R61vWDCXDO/mM+Pn888XER2cBSMonp6RcEXhXO/VHg7dgajTHnn12TdTqwpyvQwJ4NHU1DxItjNE9CEeRo+0SikEaBMiBwSaQpnjALYIg3ICgsl8zR4D6/XMuVLOi1eZRYQDAP9gcwV1hNoU504XDTcemftdQ7lcj4GgmJ+p4n9Hv/VQwwNnCflDPsUiNls/W1fP6CQRZpXP6NQlcauE9ZVbSvPGITZkAnHjz7v3H9DLAKLU3jW1l7FPlnUBf50Faw4JYT/J67AjgRDRIUkZaoACTsjHkhOcjtfq0I79JnrNjPNRgCp6kRZXwMF9dJRpYWaFuu5XufaZnUVupYmTz7BNhHc6oOce6QLMrLgsrtyf6/FIea62urm5QUHgLAegI7nMAPevx/fPg4pI4T6c33auDoOKcbezj8zxmRM7e4tjTi80knF9aHThkr5X4js8AAKfNoH3sncsgjdwWc6knNRTzevts75DyeVoa1YEA4fd9MYmV6r0V2L3V+jyfqgNx771yuYFaK3uiqEfj7v4L6O5jYnkC7gFv3wDfFR5IskBHlDhpiKcaiCM4/Hb0P7bkNfY9ljMeJUcQaQO8WPHk0kzcej6fr34lr5C09qltruXMVDb+4+i2DNqaohWI2tvg/IpVfSbUSoAm69w+IlVb44NmABPvu020BAPoQDi9xkfge/7F5cMs5B0VCCXIgo6xO4cAVpd3WM85I5BGMyNyIsI5IdtS0IXVc9h3x2MaZNKuuHXO2af94pxKpy/BUlH1Les+m62RPvsYr2uvcwIWhvV1pwCJMdDtlrNX/bKXxWN9Jeue7A2cxA1Bmw6anpMJjVuDG5p23kp/m59vhcYMBTVEk5vnryeVzwxQEV7+OC4yHsBdH2kIacSbt6B70qmrz9WQDgin78If5o6SQXHlY+2iM6jrXHkCjEx5LLArrGZqrLb0KnKyHg0KlU0NxjTh28i3ialW2+nBzSvIvasajGw1CJvw23Opv39MIHGMfREwQq7e55z3WK/cx0OMbbHdqkdEnDrhHx6eF1t01KXOqb3Lu70qVCntHyBYVO4Jxo1XdMP9Z3XPkJ4B9crwCeOpzWIRYACuqdR24GlrvBQxdKxPP8vd3BOv5+lZpUPJBplWp06PMOKwsatERHgT2nwXgCRoQXNGn+pWrHTn1V5rBHz6YFzgEbEsl3yeFcFz2ibaZjhaPUCnQ9qCKx+09h7h/KUDw5zHdYmhbfoTyitusGe9PSmeXAFmzmru7Fp3RwS684txzk/HqX3x2QHEn+d5ctmU+Py+s48UExGj6k88wah9GAj2kw+hz/MM2AtW1ziFAcPPCIz0HLoyGWH1utqomqoLw3zO6g5Jw/Rgq/t3v47GHF2RDyRV1WmJmWRYEEEql31Vx7HnWxOGpygfqdy/v/K2vduKIXD5rCW/obpdKb5wQcxmVNdoTqYqjSqXwJq7oWBIpTH5QNXZNvCpakwI4nCPZCDq1H53V62wzTggorX3W11n7+pTqh976zuX466sXJhBokMmLLS2TTGowOSsAboDv4JiRKTHuK4629zj1MKWACe4+4RO+7BO+HgabnUz05f1/hPQTJoraXR2nNEa5DkH8jIthiNGemvBcJBR/OvVPzejqudy3yNn+p6Lsnz/n2V+/muAKcfvlNRj0AObtgb/p9DT6y+aKcAA+tVDmVC4EimCdiP+gnWmZId6B0+3QwPxdSGlkTdrh4yreXfEhDHupTPIObTBHBIz8tyMLBKi9fDf42iu+hAJ5uq2nDiW0a33NcUDD36/v+fsWWGzOT3I89aKj3W6uWz2PverWows4DSYTzWq+v0tC3r9Meo447Y8D4kA+O6q0yWF7uIFky2FhdL0RNRMdrd34Xady8Z7CJBbDEsz454gGLtXGHKFBOqriLB2c548o4ogpGctG7fY0oRf3+k2BDnqKG9Y5fKDPg2Gv6End4hfkw1jYc6Qi2VRqHMys7rr1PM8mbOvZobAJ3lkDMHA79jur91rJSFjJba0qzqQ1uLKRjtZruczVAf02BMJFsCNsns9ec5xPfNiqk/ZPh0I06S4L7MmwLolfX4+EUax4PMyYtwh5S0Yje/83nuyU8ZLTm72fVsJJvjzzyfCtiTKlSTes+OGffqHa5rVqNP//v67x2XFau6oPmR43c9H91qLcUMw/B1bguJJS5X2LkG2sL7OaOhzgGZw79OqOuecDTWB9WSdcv/oQ+h6dNepI8KJWbOuNVpuk5WU8NgbvZr2KofAnk2X7s+zAlhhewB+1mOMa61sdWZaThNpflh2wWMmyc9ayVDN6GOXUIyFrWxwZHmbWqeak9433Zb7SpvBEYaSRgHZN+VxrZUcqNN9iedsoy4RidY0K88CUbvcA2n6Lv8mY6EG8N093DZ6ZH0SVGeoSEOgo6idsq34U9q0p0/jt0NxkV/BTkzkiPU5sxtweesZjm/XD9so+aT3cyVHw3OUhXPq3YlHLdKnVwA+km+dJa2ab8lgkTvF824jLZyZFjOK/elQFaZzJupOzMCEhgQZVi8dryhdI/ovsDSHvOhzQaST5L86HIR7R9RpMAU22JffM1ZBskuZD2kEdTSmZObz2W3FhnyEdynS0b6zT6D7H8o3xQothI9UH2zvKW8+j/mgYF319Ml+iKXO2TwEGfucQEqNbk8rEjKTga42uQHLUbvBuBZyJscQ9IvnsC3PChYK+CFle1mRPL3h7Yyb5JXBwQlasgtjt2nb9UlzkgCeT/owmgdu9gw6SDGtQPdwlbmq61lP9WZEt9weYy40I1DuD3KWXMx+ePHjzhndp2yfFxl2gTrdXefmYM/EWseatnh/Dz5qv5PTQKlRz1otLONs8hti61/RaiVATXXlSo+Wz/Mcbz007zabal6krD4rPx4oBqarqiAZPz+rajsxzEQOgi9qdQI6XckVEV3ns9YgKvl4jxeJa3WwTtVayxNVdaNORPSYLEV3u8SrJVRErFyThUCevS1eB8V2gtusNw+hQnc09wiXtzqZEYWOWRril7sj+931+fl5f1/6RIQV65hXQNKp1xzIWKwcP4E1DmgZkdW7darVxJOrWhF59uE08nqe5/19GYFounX0zYEy1n0KOO0uZ3nNyEBGjDbBpyDj3Eic1lGLwZzyp1xLXXKe2J0UC90O7+bIbaY3ngIkP/nWc+cTpr4ys8c8QLg2NcMfuBhLZQE7QIFILOOCBcp7jrgYpZMDItIMnNlBp0xbt4pA21ryOjMjvijoIPhxSeA7qM0pEIS6jUvNxNBq9Vrme3HRH33lTz2pdhy3jFauFBSY+Z38ojlazzNe9DFIkh/UCHvREF6Yc6tHDPpjP4ygqj3jmKj3p450nOQI4WgZwRnkg0xAn+d5987gfmut1U1dzffKLBTAtlaCUV1oiIpknzL10+qIRMgzlwSvIkN830OiT8eTJOqtWDxVUq/nM64843vPlhpauWy+CXolmBLuOnh3kJkxeRt1O25GJlW17B2aYda5L2kemQhvH2D2/n3+TiTTMOWmEQnamUvl+zezfHVTnJAvSzomgJfBcWKSKbluRJa30oOMmAVrWG2JkQoRLblR1sj7FlQj2ALIzhXnuu61UKUqMzdhYwPvXNiTC8T+fa3/ZcQ5R2JXa4KeQWAFMpoUwStxG4FEIhKxrtd7DDjUgHMYxyKkBySYKcpaIWkmOOuUjhriP88PpOfxBpaiw63faJJMXaG9Iz1XHOMYbjdIQPvUqQbvnjPp1gNkOC1HUCMQap09BOk5x++jBUVOEjabXd0RBJHLwvrVXTajtn8+zC1LhkoAnL1lx+NWnfr554cxu9b55CkLnRvBmmbA+qgBYiF09fM8kQFwgmUENdG9YsXKXUVhFtmklekZHna2QYEsp7gwvFxad2vEsOxx4CVk5LNVa01p3GfPgotvVd3hDxb2mLXz++FHvdS4TOkoKC+qje+Y63Hqax5gMMH9O5lDtOB/hO1jIYk6U63cPuMa212AWlU1OJoBtEG8NcbXaibEMePAMJSNUUN4WQHdwnXTDO+jt61//cJdkYidL+YnDLPqazp/uXgbTMEgfrwlQ9cMeRr8HmLAP7n7byz4Cp0Fd7hekZ3rcOr4+ewe4tyf0yyrn3d8RRkGiDBsWV3rAf9AAz7+vF8cypv7asNHVpHGabx7W5Aaucw5G5cyAmNVvgCJe5eH7K4+2weovbninKOGgavWDcMBZOpnLdP8TggXQgqVIp5JEQAlndO4+COAC+GnvJ6Iac9htu2uqjMiPCB8Pj+CInKWUyR447dN23Ykx3vR0aCtPqpxNqvqXs4Uu+OeqznCn7vrDJpKpuufUd1xnohw6lBGdMvOW5m5VsIo5KAMPY6y8Ca+XM4J85Gpls2cKVQJ8vjDuuntkhMCmIMVADfEp6rMXn+ezxjOp40DD8W1VgQTij7rVndc0sMmQt2nqwiRyqFPZNmcn8h2btwQIXzWs+zdmMFAhlGI6C2Vdu0guorBDDa1ngRupDAlVAZ5DbDKFDya5Nl+jSOQPqu6HCGwLQarLqhHGa0ecaSTlrtSYWIqVn4r/x9ovvvs1/7ve78ExJGN+qu9vy/IfJLBXPl8Pr6JJNd6vEqTK08Vac52nATotfgG3YCrT52IMFEJsLsoeyh1kiRPn7RPsntI2sjIPuFYzxoOFY6oO2qJI0Pou5sduaazHy5rICKDAyNBuYJ3l4SqLodM+WSwX6jfSHOqaAcdzBnmt0QdnBTDyMcj5qmZue24FY5ZleY0NVavK7v0zNQCobFE+q7c+9TBF5+R3GGkG+z0jjgvcMQ5pfLJmWpmFmjZAstUjfmHcPXgxXhGpTPkrGvsd2CZF4su/nPESF1nrOWGsjZO4NxH0wC41N64A92WRRLq2PbyWNwyHdCQazKue78XTP/emeCmaBnLEaTxROA8NvTdc1mZPa/T3z1ejxH7mIi8dmyNKhuxRbfO6WprkLB3C7DnsWzSxeyCfZYJSvz3d6u5T3XZItT31ipP9GlvuvhUqDKFytnSdpJSzzbZaSCyTu/3yLHJVaYfSr0M2vkY6WqvkoACwxR5tyIcNSdJSHJqVjHinFpr2X07GC5JgPcGT0b2ImVvCcj3ZADNtmwy1+qj9aw6ffZ5fp5u+L2KYHWPGWUNKlrVuZKk4wnWWj5xwjQd0A7EMG8D0tuVBkyhCFxVBm1JjWBmbtVwLK6g1U+u6kYEhbXWfl+pszMimaE+5HJIqaBYQVWi++yGIp6WJzqULRkhgiqUSmMKcgUYa1W7HsfuAp0XSrtz2RhAQJ1uaj0rAvvdz7OMHcmOVE9QOvs8z2OM2GjVytx7r/UoS2WThrCe7nmyVX6m/Tq5P50mwMMHcfbv1ZuARPVBMMjq0yWbzdm8/3n+kc7Kf3iB2Vajw2vb3YfBFZGR+92WVMrTnr02QZJ7n1jxfB4IqgkavltzNqoi6dtkBnjmv8wceJRjqtlVCslrkxcM6ekNPNfRPiqWXxhZlhRPQm29gxFynwzPyuqCsM+2NsZbeCphzc+MINYCtLxePhT6pUXdSbpDtybdkEz6P/tzt9sgCblylIzCADoSGIPm2jHaP8/9BqO8/+HTpgeQ9HaC8zlmUPuO44NNEaRddwedkRHXzsweT7O7fqUZuDm+L227eD/wuEhdRnYXIjCyzcuJzuLx8Mc+RlRKIjCCN5+OamWEgxR4T+u4kHq3Aj1LcGu1Jm7TJxBpc03di04E+4zwhrOhJsrHrucQE9TVp5nun/p47TTZNQE3Hh8QoZYjrWyPdV1CQ8HR10gZOK+t3LrHLwoePtzwBWMUclIbOiaA2FVjpmtk6TRJVUeGSAv7723ySWXhL20h1dKKbLWsaBpF7CwIOv17rYgucCJLwAxRumoKj5bplTMgGCvXimjpq6Y3hehyMxt6Na1BrEeSvbGsfkSDGeBFSPz9Qa+SeY3EQipyolfotSfNz2wf6lDL45wtMGdzDbOmQAd8AcgIj9UwpOZ1p5kZjbB0Mt/3Ja9lUPiaZrX5AxsVFCOe9az11D7uauzJZ2ZtO69KRTUdUsGZvmNYDxMhWrkCV6jg4cYIJ1B9guOz4jrVxwd2gRKNP0Nt6x4mw7N/pLelUN377O7ee1dXrjj2OEMF+awH7pXg6lorPoLXgI57M2GUOX0RNQldRoQBetpFd59zns+nuwZtBdSHnupnoSUFoBFwwvO4ElWflYH2b7MXWFR3O7A1A6Jd/bq1f98cD22mQUK77LWeNeGLPZuTnG5G+v19z94DjJhB1zUg0SxzTdzMbVwhZa59TtUBDVmU4LQsOlL1dA1zSLYQT3iRzYdBS3ETgUxvTi/EVMs+nTBx6Sc/ovrMphXYuLaR3bNpg4urWLYDIzyhUjA+zwd3D3YGk2QsLkv6nWVIOJZrmu5kVbvVLUcIdZlc6EvaSjhjYX+b9hYEKy9sCukFIHcFuvT1nArfLYNL89pEiFbnjEwEwZkAkoZYB0yXOpiXyZPxell36OqlSSidWWoa8PkjHPZRtpARRrrqrQgHT8ksqLc3Rp0RHlG8cuiBLsCv5RfAnjgB/2cY6unG2d2i2i/FkKfn6DsxXAfkkJWu/BxvGMVqwKtb3agtv7+G5iHvbcB6fx/iZmVuvo07C7oHJQJz5Nk0FBL2mSfcQJM/cBci0vjH2RWk1y8xXEB58CLlJbhAI7niomCnJzinTlvBpPHJGkx/9AoYg9musr7bJABs/LkS5loY5xwg7DLoJnStBw7GzEXOTpTI8RSbPbJS9/Msku/3VXcYPbH3ueK2vVbeZQj6xtsEQ83PetbnU2qrVyFIqn1E1t1FkHqtzGD3thNyPg8glWKG9NOtDGYgQ0GvkrFmxXEczSBNREcXCalInTpG6Ry9aMFZJL9lztIsSwLaWLY3jcFW8a4+9pZasDDUkrcghHcb1cmBDt3h8Z7dvPmdaS60HHsqiQ2Cz7N4oerI8FvCwM/ncYBmn8MrUKFXNDMYOHVQui54xDiI24VAQ+E8TtCEvFrFOKfffeZVHLihP8/z2jhtYA6++3BYzVHb+ak4dVx1QK3nedbjJYBMx7/UelZf9ihXDGJvUZOkhnd5nWK/99m7ANoPYz1XITYdponTsXHHRdMNyFg/bYQ3IxlYmeAFQ8KhCKo63vZr2UIEXarTmdHt1YcYtUqEvQj9Bb39dgGqJ012Rex3txOzIZ+4kvyISqp9gxgFE88xfp++xaMavADLpHNPw+vyZhx49jwsYB67QEOp16zXUL4HF0nfYN45U/xEXbrLCFAZ/RwozxWEU66JL8M+QJYRqlFoXnzPNcfHy4xtaiNgjOGzM/O2Cvh+VN0KSX6bgda3asMMT2BOCN8Sk/oDM0gSwo52oyuFP7WtbsLoUAm/769r8Tlt62vEpAsA6GOxEsHwktc5Ldr5vM2gtAU3hlnHPNRRkfYFP5pZi/dsu/9xFpVp1afFDisyVYiIrlYVMz752bXjemHbAYIZkpJhTIaZHtPM+DCCsvqz17O8hRGRQq/IU8fw8fN8TherrGumxnTQ7L83miXtfdbzkDxnwHF/62c9cjpr3u3PYO2KjGScc9Za+93GNNUdmXUZ+ouugoTck1W/53feSAEZOhXMYsvCBiHsZ1knYz3rM+FahLo/n38mEiBSdpY+IipoRb+lBiydtsuKy6EgdZ2O9RwbBlLVPt0OIk718/nY1AURSXEeBV3yGPkkIDTPeWMFZEdyNJRpXxpJvdbTAwq3qrny/f3NZRitMoY1JcP3mkJVPZGF0Yn6aadlLd4GUhOsPhgzj4lsVVesxyRtnyOhWFHfARwEPs/n9Kk6bLx1yIJ7mAhNM+DGX1bratb6LfGOPjXNDPCN/bKEt6pQTbG27ZFjZUg9piqZrE7npvpJ9spo+SR45jcIInq7/xjI5uz9eT6MlrRy7TpuQkfxrFEMo9TdjuXZUpLVXi/F7mO2o/rcjhrPWgRb+KzPe9461YI5rRw3Ikg63VYc9dkjpfhjGrhrDxQe1on0+jyoPn00VNOyWV5kmrE/p4KjVuOV4gBsnd7tSDUDMpbATrcw0Mrsj7kFsX5pvI6CE5E0M6QuHjDQl//cd0m6bXindvxQyKtMsCPz/5wKghRJk8xeL73ngOfpaXTmXDPPMbVkkLd28F0YW6LZ6RmPQKdrS/JlMXphZbZ/okHCxeUe11vfAMb0mwiEdyeCYaJbHrlIhwjOv8Hr+eh3COPPYX+pq4/kOIRLGFcSgkKjCfUsbBmaPZqOeXRVLVoL3ohFw8hUlMewIKm9676zCaC6x6Oco0kpkGqcHj0ohIu8oQ19Vql9+vV61hmdK59MbxvymqO6b3A3NHNWsPpYX/71GZbT3wgQGXHqVJVzSOqcGRO7TUe7zvodsIt3d/tYQzBXWhXuyS7X6qqwfVsEiKqKjHHc7gH9MmJlmiRgsLuhYIZBlTs6B4PV3dVrPRFJ8NR2qz2ZAeIKPCsCiuB6PiIbtl5h3MvikpuR6lpjy9Km1DIjwLXSZn3n1H5fezkBcr5Cz1+lvjrMKkRMfy1xObTy+8Q70NWkGj7Px/4Ztg8igKb/xbriobfOsJrhF9X3oNT6rMdYoucbI8KG8nKNxN7J40GYen3WiqTTacxV0PuvppeB+VIlq/vDLshlTJRSr1zPep6VZN4H3ls8MyrsvT0SGzTOtOzhSCalpNbzLM8HEHpXzKKdq443PzJXSrIHdYaPHi8/biOq5eyj+wTqQg/w3N8gcJxLA3WXH7IIuvl3x+nfAhpN0lGRXM96nskF+3mesVOGTcv9S2/zO4tJirx8obzgLRI9jljuPzj+jYOS2CfA1VkMZqTfu56wvGXhQ3fnSOEqrJkcpqbqnDp71GnhIj08620z4TM6bvrFd/HA/ZpxHeNcIy7wMsQsAItjAjEnAefiwjuEMXLkEC6jU56VL6sA3DNphqe4+5v+p95DmkW2/h/cn0B7uRTkdUYQ3ADNiYXBmeFm15cLaqvUxo1VX5fMAZ1BX1X3mv5DYEisOcopoKUaC6m+RLkPe7uqjBPfHdJmZU+32wTDA7b5uDrNIFf4eajrdmeKvk97fAS05muVCaKocwKx6ziUwzacMW7/lblqD/JbdbybKkapnRLju1Kq4ZRa3ZXPkjQi9G6DOgP4VFn3HWRfbxBRsjHWAMRsDdVTZz8/P2fXyjx1Vj5c6JmOATD90yL3+64nu/uSZ1YUaHBYIgIZKXCfvdbn1M4I722oxxvEbGeXgIoIKjz9lTo6FWg0GxO3RiYqcUhVbWPZ7Ud3IPLiSCMQy+ug2L2f54fIqm7qk08N1DFwqfuasgYGgPg8a+Vz+pUUTYzvWLTUoxNmtxrnWevsAzJySd4F5znH+4QRwWa4J4JTkIXRJoMBx9NXdYio3th3TIdaCqFd8jAZQTSsEeHYQ47Vz/Osqqbk9iafjHn+gJFBcu8NSoXIcFZM2VxXAlQS41YTpqtIn1J3PssWXT4wMlN1Pmv99//95lremt57m6oxbOiCFDfrDcB5t9dfG0rJbIR5w0wAsAJtOlArj0r2BDXexmcZnjinbHJmX1JfCZuw91Wvtn2VLQ0wTTUdYuw6JJ3mFhGMcGa1m0zZ/RWqb8ctyAVIEqHuEUInz2R9+5T4wzd4s+zDOzZTooX24yPn53w1fq7RfazMTkG6zLm5KHgsrkYy4ITh2UTBV+2q8QS8f8cfxoI5e1IKAJPkRBGY3jQtRUB9NSQY8lz2UJkJbnSMU3wtonQo4w3mbPsXDBZl//r5SACtZhRAMZLnKDhLy3P8YihuxuA/f4SOvYQJE9xsDFvbasqf0/i2lzaNvvqC1On1RHCd2j4SR8mIy6RgDh+jrZlP9bYfVXtPdlh2mkByyLPMFp02Ki6DsTOR85zpTr4b9573OyJ3HaPI9rJwiHaQkWx1uNHGYHC6s6Qub6ULduva2kAyEu0APguxZaY+YmWuXBaoQvZCWxGxnpwTHppswhbItqN9VeSa7iNovam6b2sjEHUK8vbdJZNq5C59KmPJq8iMcoEwVhh0/XrWY7FzZHh5QCPSsOeoQ5ocgta0oyWKXaFxSBXaxk8Dd2o2gckbV0R+1qerjJIHePa5PQzlwFLP+/v15PE8i8Q5v/46HrJNtwYTiBU5iz2Z1ToqJq0ig2dq8pxqySYIAM8+Qa6ItZZ33p61ViwJGNd7fjnhXPRTQdvqmmvZB4LDO43MajTv/p92+R1r0t1+Kjx5uIMz1ekgDb+rIymGBNSuUfuAAQZokjoivfRkDx9jfSvWv7/bNrlVVedYBOz8HxGnTs/mmg/0EQVoMm/lhR2faqd67/PuM1PGBRgvuXbxVo1GxXCWJQy53PMqZ0oTxq80SWau85YvvnV8nu/cPOXKJDU5Gfbyi9su91orvEeX1m6MniQiIpaAs8u14Js1xCCs3VCHhILlVTF2sP5DvNvCZtpd/cEIC+Qkyfnya8imO5RgDFW87+P6rq+G6l4t960+1S2+tIwbmCTdr5zjavzNdcAcMvxAWo8IBhujy+zbCd9zZUbXHu99413Tl+juvc4f9mMQ43sBokrzb9zaK6LhRQqvh5g78J71lDj354y/WciXzgN4X9RB9gHDd+EJp+qcDcAmQszQKOCzu9W4O4mQULUNE+1zfBSN1nau8zAirvQrfXhPH+AoMT/l877MliSQT/pBSafqQoHIHD8vkLg6ddU49thWxld5zgNHdAKSnvXQYFk1RFvLM1Ce/TVPx97VZoYZ/sNnHwlD2Vvbs0+3lg0XJdLiyLuC5WDu9UA8+4A4Z6OVuTJSc93t0Mn9bkmZS1D5ORgPXQyJDWkW8KwBH7rIE96EoXep9aylOmh0n7kewPLuS1ltkl2m/JUmptzRQ5Zb+a3MiOvvhaamX+qeMR4C9PN8fFxbntPqlZlP+mY1NLku3faeC0fDd3fBjmrv76tBKzNmZhahSP48P9Pf5DwZ5BUZ+DRU2cuaRFXnMsLUoDJyRayVjn0n+fn5eMnyvgVSo21E6OWy6WlkXNEPg6vObPRw5m7Ldarq+XwiIybM0EtGCM7ALjVap05mWpG80oiaMnNZ2ksM8Xu2HaWqTjI/62dk7Azffr+omatnt5wAVzqMpaTW6JXsJxicMJay2H/KikbNpal18n8lp5nIoG2RAPuZH0mQo9a6ZzvQ1QsZqUK9R9I5LaK69rEe19B8mUjOXMD4+Xhxdwy3fcVt6LRWRi5DMGJAK5asoYiw3fHp6i7PVSQu4ONNprvwbhExcDWB3vo5t9sa6eE9UMCrTaCpSRN1MQTt9yx1+/J15ZTNO7u6+/RXRPC1CB3rIf7//poSHJkcar3uAtdlpQzTY3yfpNGAeOK+04NPJt7acBcYB7kBg+0DcGLo/TT+z0HkmaAsYLgK1PAaYBgLoSdF0jF6390d2FjQoMg1TwhjN3R0Y9iB0CvZkYNIt92LHYwhRsRaXo/39vNUJyNy4XNPkpqQI8DCu0v+oAL3PjYLA5gRy0Cz7TmBtjDfvRnj7G3sydm8Pio06t0mwkJSVWVGLhvhcP9uGAgrR0LPIUGAxMrlw9Cg7Rxfo59ZEeGtEV/52ZnJFFXoXKnrztaQVTLfNaWv3FQ1fIbxE7/RftWlJrFyrN7NBeXNQVvPsmFAnZPJlTJyZurJXZV1kxBuBQeEvbfXrS4cp0uJaPfZ5x24nyo1Q18PAEBB7to566lttyWpq45tJFbMEpnUlCgth161Akzr8a0laS2/oSW1Txp0q9XGHxzTtDwWZD6xJqzD45oFXVKSK/LJdGJejcol0HrysV5LwPok1FZy9JxzLizeKebzrO4JPTcKl2uJTe9Yx4Rot8ScdGxPHsH43a9fm7PPPhsj/ejZffTTwpwACnXr7P6VhnCwTZ7b7GQG+eQyEMGbjrfykQb68Jqk2WPQKYBtwqnHJfubVqK8gdHJ6Kpd59T20Ing5+dpdEaes1vts6pb7R1OiIGIqF1+VJy6Uaf23lMEZwl28CLL6p7PZ3r8b3UFS119/vv7//797/+tvdGcl97Kaa/gIzKcDhaXzhUG9cBFJ4DvHfz+9EtrOWQ0v0tiLqrfCu3FZkzXPlXTw0Eb7jdYhIsFzTLanBWti5Faty0Qs/8F5+1JA8fhzq4A5DHaZdFTji8O8EcaeyiZPZ7WAA+WO48UzW1vW7DuzzmfNu85Z+1yRnd5hwDOArOwAlYPdvfYcN3zTDdtc7hMWX7dtBm10X91w0pxH7E5iiCHtfg7+t5JMGvrEc19pmt73ehs3kYnnFAqtaVWDVEE2nVkfRLynrCr7YhkABCBEJuMQMNzru+EWJZXdStjSTq7xkFbswoL4Xk+1SfpaFPbxqH2YUSfzieZHAAAsi0qiQb67Gc97/uO4UCsUx2RQHsg+Pn5UJzFTiIi9yl1r3z8W3zl/fQFswf3uXZZmKlQUjioz10h+f6+uVbVWevxeloLgY5u9TGhQjpQ+4EA6kjP+ph8SkQDVSewbNEizIJDOpsvfM/wWbnrdDXQmXkk35ndpyUEVfj5/DgUE90hnlNO6crMzFXdJgmqq1TrearrWZ93v0n+nl96nC8UHKOBOnYg8fjjvXYamjujdOP+3Z/naXSdk4zjlCuzx/6CXgAmIyMVSFP62agux9Hx1Fng6YoVEZMS6n20CKukDwhbcZmMe57nPdvNWqnNGdw+3aUKGamJUfIMh+5GgnHXtC9Gb0dYVz0Xslb7Kaqu5X1A490tm985POctZ43Fs9Kq34xc8VS341HKWUqtuC7Z5Bi1lZCius/eP5+fc3YyMvNUqRC5is0IjvABHhRypWlJ3G8VjIwEefYeCyGrnMymEQ3sfSJCTXUz6Xif3pURaz2mLiCeXbw0vh1T/IbC0I1n5DHD8XoRLlapDBJZvT3+mLeJCA7hytutC9NrN6eD70azFS6ghm5G7eM+z6+gFQc5P54Xur/4BIdTmdOG5iRI5waivwfNoKbdXpA282WKFV/oyspHczQ+fvD9LUDwGsAMJDL4QpWckPa1h+mb0mXuGhIz1BL1rfJt72SAQVtgf39b1bXwhJLRX7ZjbJEiGdVnTJgsz+leKzXX8DvWYK7P11MPyJWoWmcr11JVncOV1OzN+uCic+7vCe6drKvowD6bpMA+J5KDCUbOVKjOlfqmXkxOdOyqh8BNFpveAZrtFX0v3CARHkS6yskhnHWwefggtip9rjdatXK9778EY4GRZ7e5vP3uz+fxsuesh/get5hkA7MK2xF56n3yxxrkmAzCu9oehEqc89YmugwiAkcrEOiV2DP7F1eePkS0/WG65piPkbXMqQyCXMvNWiNwduV6SO69G44EGre1FVQEiSRPdZDn7O4mR+JpGEdSAKUZxU6d+9AWhKqTGTYpN3ZHjNaiq1aGCO2OBHIW5bvqtwo+tMkIto63wb0WLtpeRQRjpfMJqt/oxZiWBNMRJwHvlxqBNtzpbHegfePIuFFCdzqXxvgTWjaomA0hBaPq3MoVDJx2KLytu6YB6iomGbDhXTA0jr39s57f85K2QjLr07r9aY7te9l/s9uLRc2YXhTCru0Ina6q1jgz4KLGaGgKxJN5dNwI51ouvlUnY/UsiwzVaAzBC3GqilwZUWdH5qljWTgJLwA3zTSQZO0CSUSXRrlaBXFlxj//x1VpulooE5Db6WBSUnqD7+ImEcNVTLP/hU2seOQZdcAMRdcM+Do3TOHGhCAZczCEawp6NKcyM+d1FTfcf/jQVwn6x+ACmGVA/7AbXZkByXtIk/v0Jcm/LMUsk19/Asn7vRTdOseQy/L0b26/p+iP11hm+pva/QyEyjOiOyHOmeWr3foeTgZyPGxcuEiUXwdo1rNneunZyRDJFrsUK2tX5oScC20Qtd0cy5jqECseQIh5Hhmo6nAel5dsbETJK8ZfuWxxExHMoEbMRPDUAfzGzEW/5ja4alnAdsGaxUn/0XbcowOLR8cKjWIeJFplaYbv+Hqc9BYY356ZPTFF2TwSjikbb2lLe+9c63k+k7uX8VlrRfzz8+PfUvsQ9EqwVPgugPgYjiRzxeOj1yuLJiTsS+Spy3JOj59WBMLfvc8KrQy7mKxnQXLQxUUnv6JlDeccAWnlGNtFjkpqrYDa9YvQJz8ZYWCtQ5l3S2jeBE3Wo/3mIs52chZUbd8Ig0xrPZHLr/0XuNcVbAOgFGPoVM+zML4roZLdmcyFkA5d5Wd95hqMzUivTPsnr/XEyi8Yck6ZtP+sH0p7b3V9nk+GAwXy2x6OEMouArMpD6I5jVXputcRsOm1qslJZXo+K+IGot4tIk5OiE/dQGMO4qCB8ow4fVx2ehA8Xrq4I8I7EbYBD4wsV3JUkeFSmPs3Ah7kyicRYEzd5y2LJhNKK/Ls3V4L+jI0bkPvMrz7eQPrwXBAsbHK2UNWTYvpmb2qzrbn68Dx45bjZtV28RZ/O5tevAA4vlO57UIvLXwfEjfHqFMcAQntVmKzt7nQvEaMkm4R9BFoASVwMRypuzm+lRBE+kS5J69mBojJiJe3TXpEl4bd57fdZ2boFkYA3PsMTv8Fiu9gIczyrT+LP49/4LBShDeI/Gk8/dMYgIYebBsY2wazW4Ldgo2Y5Re3x98BZrYeGtmCj1h737WPwCB90mBMJ+n08smnHHYk15oxxXQ9jfXnOW3ozqneCAK6gp8IsnyzhgfqFjrDh/qkY2v2h8bgE8A5tXKBhGhCwxUeJCPOnjT5c0539whaO0aXapkmfp4fN/ZqmFoi/Uy3hDB3NUVyiKfBmjTGIL6LudIeF8wIhsRxryFuW9/2cTuDt55TW+rz7rWWNR4muwGe6ib+hAEESHuANHBOX+hweH1m1GmD5g5w9ENXZ69U4KiP4PYBax7FcU7308MV3Q1zg5MeVxEGRocKq/5ugWExq95gWKJnfSdBm4ZCg1SvzLWyu845zo/MtM+zmXbzN74dvGvAIfRsa+sO9Zpuu8r4B4YUQqiVjD7esRzaliKlz3o+z/NEAgabxtMmI1c+gXl51LXPb/XXC+909bFcJyNzcjdj2CNI2HsDGNM5E5OBKlsMtY3W4IrfgFDnnFPjWeSX4GpVNMY1+OfnR6cpYgRdJkTE4ShHwBND2XGoKRLqZ0Wr3HOpdGpcs7pUl/z0LkJDskfhPVbJsMFcQ2s5Ngck3/d1v1zqrjK+KvzZyvvohe00jl0KtNYj0c30BaabYTcF+dW2LNW9naHtr6mGDwd638cIza2umlPq6nhakbxb9NPzDWIimUBxgxkTHYxRFcVf2cRA//OMuwg7Z5SXib0ozhwPtzfWl1WGn4H5gxdAGKDD/3hAHpB/1KvXwYbWNHPQf8YeJV0PONxKbSWCy860ehrPj2nlZsXiQiumHkfN5d8TLXXrajHlhDhjM377+qth9QE67kC+4DlI3wVuZuP3KCPdZ4x2K/P+hHb6k4RkZq6vBSmv447AZNhhM/ruL+wzQb1zRDvqDIygFWDP8zSaRK5pHiyrz2D1eT7PHOY3XvjeHI6ALSIyTm1LOZ7ngW0gHd0TkUxJ+z0Ti+6nPjjkyfyoRTKTg1CJdulxw8pAriBoi8rhjMiI6FsaZuGlKxi/+9cfzaSFkRMLdQmEmUnLQKYEIjOqjrr+8/Mfjve4Rx2cvdeTmUoUVQHZKfv3rToa+baaGbi8VldB1RdkI1nbuZHH7XAQ3dsv9znHq/Yq7d/TguNJ3SY+a/XpsUwg1G0OwS3UOcfjhxej5qlHxMDZUPf7vqWmKWxZgxTXS0enywLMARRjcpNPneZNqUX1eLKKZESeydyqqg1YKtfX+7pXPGSIPYljF7mqrswQkcEnV5LuUjPzWTewqZSxZBXL9I+EBZeDtft/XUdat80Mss4RVN35LNOfflFtC9FlKf2QAZOlbl1TYe8DUG6e2hlKDl73EpE8B1dPaES5H+rhMz3MdR8qukriW1tEyX/eTT9wNXWOs261Xym6gW/jf1DNzoqlNH6Ye6bz9rHq9y6SkenziO516MaxiNkdH/zFxd69qr4aCcZYxV2PB8hyBk7ah7F1Mt373yPzttuebqe4+ipVe3Se0vmn5MHMSZ5JNQ0ZANy43Yv5kLiJ3368faDdooyLGWJmxVFVf4Wr94vMhqlBSNn4hACs2tAsVHseHTBvCIe4miifnfe9Q3oy8w93uALcWvXMN4OF1P2997WagYZ2Jelrf/vFuHyZzmmBX5XU3tv6qJyM6+gqVwPGrPGW7ShGljtBW96ZTRu9GcRfz4rx+o97LHslRHeOayf1RKxudSvD5mtk8FmPa8ry7D8bcPRPMMjF2cm+z0yG3xnfiVz55Ao6gU331AcD1UdWyrSGTcO0Gnbl0tfKCchMdT8rAayVFqKcfTJzfdIRgN4oXs9j8Af3SO8uMPapQsux1VVShyXQueruJdgolmSuZfHSsoKkx3Drs/KTMaHg42ksY5HzGlncVg0iVpQX0O+6g7w7RogUR8yOsInC3dQELEBU9941oE0wIs7eXX1sKwxUH2Y00RjiKyNdbSOC4u+/v3tvc19SM1l91F27vDwyIU1zLnhhtTyTnu11Hq8pDmlkMxI10OhTHGGp5eISR/U74m5Dhcd5sfZT6AKC+ckVrlytdRNN9z51ypRsBBlx/JiDk7Zq0UXMCJ6RDeVaJPc5g2MLdMEdNTAgrPWICjDXGinIffdgH0D7f0CeynvY5sFsCbvdMRBOXY9LM2LQCmWud79rpdWWDiNTuxzDoBMz/XTtc+4eAMwEZq4ItsYCefhUr6lNYY28axkk7ObvCRb3i/KKoL/FGppYMdsGzCgwfNw0FEOjzzPQg/Mb1qbvzO1wr05kCse3/OhWfKjPUUljP46ZdiIuJgXrjDgO0kPJXv2o1S7JESwNOeV/oFlRnpJqQ27rOy63N6MexnJG3z76+/D4kJs/HN/9RdymXPfcp//kl++ZD4M72Xxf9XmUZvKbp2K6/u8TcpuM+dnzWJlf0+07ZxYW1np8MHojlSTGLKhn2MLAk7gs3HlrsKg5eYCw48IsYUZm8OYgG2eEBo3pVnmf2KK3tjFcETi1OT9Q/zMhogfw4nrSUA3swhbco1Ymb7jEqbOeRcIxHYPGGrpcFsayobUWoGmyfKLO71JEGnZvehCW+3Bbx0jmD1LCOdvd1jlbV9El85PDwuD64ujUuakSR1IwCTRma9G17cl+QvbVXsnJz+uaoZn3JZICeJbHvF5OStF3uCaEXDY2jerKSMd9dHcdcXSonIbapqEaTUhLXb33dp82T5Jke9Q6rpsn1+herLl0cOiyQaPwrA+AXNk2S3DpP8b3rMkcIigz75Su/f4KzBWn9owsMAxRBlrbn3KxdLq11poXhYOQZiQZTy6Cp+r0WU8WtOuIYobYg1wxVq7P82TEsvi4jqel3/fXgIl10H4p9vjouWBqv7vruOW3XzwadawBbkGRsR0HbfIurhnFcDiWF1+l9kAXdgFVZnT7QpdUw165zPkxtRixh7B79y8G8ITQmTz22otIWmfcqq6etcqLsYyZbp26b3uP5Bc9FFrbhm8Wa0eYaktOv3X+OTCairUWidYwoXUE9fcxoxH/28pb62GapKvsjVg1HUCPYFFT5QYOuCyDK7vJC8CyJWCiWOas4lDQrW/xtDYUDacp+aOhpo+G/gyTXN+/W9BD6bmL7zG2aXIsPVxj7+AxhddHUfc0Je4vvlCZLgPocUNjQNs2E6wLprkG8XtERfQAR6EvjOV2pIacmA5L+rp7Aqxvl8RozIaEN2H9vaoF2a90xL0Su401VVfTa/ggk92KOlKLZA1+pK5OJuR9SwFyTVm58lnnbL8Dn+epc7pOzPRUOXsNUPXYTrbuBk1TeD5PTwZPdLd3bSR9Pk8PfERjoCDP2QKEtlbHsFJXT1pZ0nJzBPIJg9F775hmp8lBBHsPzxmRdnB0hTf1Z10d1GIHvRjpFaTwsqu61Z1rrWed86ub2hpkBs/Z6F6Zutw92apX51faQtMuQAiMdJUUe5JXudbqKsNtgCZYCgJgWNm9577vdoRrXKftS4HWtfryPkQg08wKAdveTszkvHiRJNZKj0Tdp+4GU2su1JwEwV2vk00jQypTjp/PgmAo2OJ9Kvbe0h25Pj8502sv5idXZqwnn88yphQMtbqazEB8ng8wfz4Cfco0EoPdJ5No9enJaXGWxd7T1cIfNfr03iWVO4nnszJSsv9s/+5dFsZLXV3XT9RzDmwS6ZJMrmepzX+NYzT11SPCwoj0eItQ4y7GQ+qI0NjaTHpf5Ew2+31d5TQeiOoqNjIzMAbx8C5kd4hdYqSVze655R3R8hZN7b3PPhaSddWzZnBf+aClRqQZQjkq3T0CeGdxqWvkLiIaskkW/I9vTBkZK/MmzzRInQJgFtp6G/84TxeuGJZKRKRtCu8FnDNe/TcUALLT0hB+f7QvLiLlfas5Q8y16Ao3NUIdaVwaZ1zRcAy8ihJYfTdZedB4+PA6lV4GyK3qVywwJ2WErhHZ/xAYfacoWwzJx1WscRuUbKVFG/3KfCy9IuDJJHTnI48eHPsc1KmZaEhOyrHGJiAoohxzZhHCTIDTxgxfbYrT8Nr1M58Zay4snVw6fgymy6wbiZWR0ePSZjUFJNQ5ns5mzYERa03bhUFqdGNV/EAYxIqI6j7nMJefD3/uyRn2+jsCfmYYKx/MTxwAax4sErI6CMvb9tcTo70fnxHU86zZkbNPV9ue5dS16Koq2u6raq21nmdFcpQd5+pc2wIvK3mk/vz8PPHMOCI5QRsRUvlIOPsElMGQTaFlkaepVEGNZiAybKNEA6TeRq7qU+5GQTwrVa12dKeetRpta4Gc8jHvRgBqmNFtZ1VHrLU0qtOuqs/ncUyEcfGZ3IGzZzNFrczFGIsxR+6YE3Gz6549g7amEfB8no62ka/n4vXEepbH41MVyLXWqeo6vBhAREx3MHy+znlzRT65bsQ0DLKclrDWxxyRm0rXrBW5LOQtAXzf38js7mc9a2UkHW0WM+6bnMsVmV5xtPDUJrBr+Qkx84nLG/lSuQuOyMBsa599GrbCKvDawWBwJGAKWCCYEzbnztLT6pig318x+O/QA6lTfY7XIwA7lKpVYtt6AVdhMrKDwWinanvY7hv62BccB1ojQDDY1deJw0DWTaS4YMNFtwae9Sf5atst8FnLZD8ylttcd7eZa0JR/MEM+GbifmE4jMHbTBrnj/ijhgX035B1lyDg7QefB9YjOZBycGIrAMGcmYGDzLffPF+xuCU1MlT6qpg8+UGIWzFd7jTXsy8HCUyBbitz/Kv9ovl7aShzM4rpct+DknGGIbu7t1ry+yp5/eI7dHiyr+N5Yrwr2sLTunZPl/6cR1aXNxXQ7V/R55wzP5lkMKlq98Hed4uff1LleJpY67H+t7vr1MBtcA6qxv1G0/pJcj640Y9caXDJspDIGPMyMiwzp0WSJWmfHcPkDAtkX4FWGRKTGlLJ6oUZu0naLtLXv7qC0Y2uDjAjx3ymZ4kBkLsii1tyxVrp+mK2Y/ik7q7T3Zwq4zua63msVnUf7Q6kBkwsRjyfj3r8Ktwjr+AKLvbije+0YzDGi+/sYxdfq+ve/Z6JpvMUOQiPn5B5EKt8Dqk1NsgUyTUsMU2qW1/jDjQm/RwgM7liVR21tj3IvQUjTCNMhu3hp78uP9Z1ioqxopSnlAbn2knd1QbPcuV9pRGMVp9TEZmLp6q6yLwO/x7BRqBSXWb87znUo8ax5NmqlTpWCpkEXe4pW1Vb8omoXKu7Br08Z//uU1UW53mnqRFIsxpWjnlN7EK8bHTEXNIvGuBnr6+6Rnfs94Wt8ioUJHSVbDLsyVsEsPf2afT5PDE2xTYWtPhnhgMDEW0ecAAQzbTXAh3mfpu5IO3+51hzsI4PYHJFffvWHqjXiTFjnSsR0ReG9zwIDy6MFSv5jalEVxtHtwX6mZ8MkueUMLpDEA6vMXNrxdhay6vkrr8G1DCFr2VjKMk6CI7jCHABQCMhg7D3PXVI/rnI0RfH1X+oytkoNyZJ3d559OIuzIN/j+58sKDx7JTgtVAA33NF3pe3atF/h+OaPTu35ivc+xregXniRjh9Wn+uKGanvseQZt19OrnbqvpfYt9RZvSaY+UKrys2Js7BreBMCeP9NxCWxX4zx+hOmYhz2qFBBMLWLsg5a42VeDKT5E48I1SyIBrEWkuqzGjv7DQda5Xfou8xwX+JEtazcuU9XvF5PhqHPBDa+zCSkT615tbf+QNfZkmuaBbXtKcxt8OyUiIT3lK+lcrA2VpPMPqMhYg/HydheF3RYVgI1NMTdwCkTB2jFfTt7KrKOzCFrSwiJC9AsK7tc5ifhUwY4msLB3bpnHO6Dmz2ojpeK7co65jVWo/J9RxSd4oL6lQwdm2SfRwkZQWshy7X9+lFvsCuMd865/xuJ5lEcD2PpYBXC58xDziCrH00E0V4eWfk2UMeTZByjoMjLSXydfbLa6bdeFVLz+chJ6X2eTx26sqopsQAipUWTnd1e3lZiojnsxRo9coVzK9J1iWlJh3j+Xzi1razj7GL7v6aG4Y5mxxq2Lyr8Zme0YjBMGwKdXXFilyB4PM8xq/uznOZ7FTpnKozDhmCPuvxedxdpWLIxXXMW6xTBEhWjbNTXF7x7gWyu+BwtPrm3/iwqGn+QCAopM2SpoW3rhBDFNuBLmYGeCKnq56Gwyh6zTk3wMxQzeDoOx2jwkDmcptydtUp72aSkZHLKnXMApITHn2uWdHkd9ats9dfpuK6ORgZmOkB/CGZrtBj03SvwAVe7rHhk2CwOL8IvpjT8JEXbTEeT17yA2PrNAAD3KTYbMcfz2lr/hfu1u2834CRicGoIuR7ceVJGNZ9Bj/IBoWjTa521m/MEeGlea+fDTMTaqhkBjhXTtOsyRmwwUb1kPD7d1tC0iXj27pkw4BC0FrP7+/ubtGpQ/Vnk3lOZaSq1rMA2HKEwfUkiGetiDxGYIMw7J5RJegmHgQBeCFzBE8zkTbQzzNDQ8TdD3TwSWMY0ciJgF8mP/3kG6kTNMngmSkYR/Uay8xOM/CTEm2Z619kbKTvHhMdPDSdbFUdgme3LWn8QNZIeTG9OWenzV/nr0X1JfYImfNGPjaypAQ22qpb2ay4lcGyWdD30aTWSussM6LrnKq9d58907L906GvS3tV3YWH9tE1HZbaA6/anqY/thgEUKeTGQhU53Kc5PmeiMeZAGckfUKvlZ4V3N/1ZfY+6+G8SC35/AZC3YeE2Ca2nVxmch9A7fKJQiC/wRprnSqC3ppgRCyLUw+Jf36eqWrwmkV5bUvSObVWovFZCdUUFpLk+76OrTfihMlVvvtZ4DUxdqYN73Tu021QcmOmeSMGcdWKk+2uqZxBWw5Ou+mW321QVQ2GAAA4PuBdnMfba5rclakb3GiudvplNa60xmwQwTDZY/K/x8nAghyfydUVHAsdD9krcxZbp2v+FoURL7gxqdv76Po899waj0VyVtp86upR0nBAE9OjI/uR36Cahmw2tLxki2nlBvC5aP1AL3RtIecUcZeuewuoG2QG/M1J32Z5nuYvy2GcxRX/CmymTEA2NrC/+QWwdf+9O69M3fk7fDWYEsNCr/k5U+WvGOz6FvrTYKDH6z1nLhzzDHBoZc1U0reJnvNuivwoFK7w3wqg291r8gns9O6f55UUAC7G3W07oNl985Jjq30XDRY7mhtg38eoqp2JQaBUhmx8ZfpOxM+zfAyG5dIcyVFXmRdu54iR3UUvZM5+oC+2HyaYX53ZrFWt9Xwy0xBHPjkgWVBQBr3i4osbpOUAuJcxMyaW0h8yF8HMcKn1nCuCyZUptIFmx7Ty6xJFMOBcSbsg5Swhe6Hy6uHvxY10LmkBHRFEA2heG5hIczJ+5xeXuphcScNQCKSVRd1zX+FDzrT2NPjzrb/L4teoEtdxejtOB3jPv2aIgrHWAsE04mO9r230eE6hlcGVCe+kClVtxNwPoelldTWKgSAYeGYtlraR/nw+EXnOlvQ8T5LSKMSetSRR+KyPm2gI7Z7AUMCaw/fU+3mejDznZKQxgHsq+RN0pslJnrovt7lldV6XkcxYz7IPa3Xlk8/KUUBVuRRbKmayahvr686I5/PYB12A1X/u+ldGbRNxep6HVE1oM8yR4hrQk1ApjVCAaNapuCO5jcT9FSI47pYuTOU2ZdSupFuZzBu5pZa/rIwIkmulT0eCZNpPjc1BlUwRkpJFGLepufHd5ql4cW+OMr0QQ9vMDQrrvgRyfT4XW3D9iAH0+6Lg823m7mGSalxiLb2UeoKQR2lzT7K+ypqZDC5YTwwCfPGdIU5xj0Gz0H2++H78b0mZTzZY+xQKcqLK/sYIb6u5dVi+QEPY4H4vDStpV5QphReugCSLmCM4pvRXyTpgkzlP9/hzP+bz1umvDth1kbDx5w34vfFNuKLP+TYR4Gx3x9WtOhAyI9DoYxq0u7uP78XYf8qNcGaaq4wPxT5dPY7EMPAyI0+PANRF1lfQF8ZPUI5a2ahbRYym+Ozt/scxb4YCxvZdLemcU1WC4UUGo/vQv1dA91orL7BjpyQSfcryADo0TQwHoP8pwARMNpkhJqn9irqbqO5cScrijpXPilWn/O0g1OnneTL4jOzP6zVXMsuh7HSKYCQDnbfse/ibIbgOw2NNgag+As77XZKZEESCyfHkrH3sabFWBvGs9ax1W8u+Z6U9cmG51NljUuhuY+Xy9Od5e8b8yYIOECo9uXxDrQPJldZZEtynCJ7y2NSDU516zzY36BUoQCTOOdaMrmeZdjZlsNZqtCiQ73ndWZ1daK9NNYjaLst61iPonKPGfo8fbr/2puMc8lynq9TVsxngLGLj4demCbcPrKpzxvRtbD+B8x5j2PJmeGA5oCKWj8BJkQT8CdU6VfGYw8C7d/Wl1tP74Qeie//gYtxuk453bvNLbRaje+9tLbKRDzee96lTX+4hcuw0AHrPc0gpyyuqT1moPVpFGxsx4tR2KTUY5frI25WnnRcZEfE8/6xcAB4rymkCQF89DEDhLyqyq+AEp4x5qKZCu0gN+vE3HWOOhzv6uKNHpB9bL4171PS+tDfFRiwwg8KMoYDGlsZdf9++elC10TS76M6Z6ZbUsDvmDJ5VNQavpNPnA74d6t9hQ/wBZD0BL/5FuH4PcKVujyGYxY9BmXyA4OJQ9LrDDfWdw8z7k98BxY/EzCyBtVLDMnlvBmR8kc8e/lJ2XvCvstK1geMrk/SXFSazOuaxg6Wpjfg7TyKWZrkv3vc9e4eZA86GkZ9Uj/+QSHXb+r/pfy2CDiwGAKzPJyP9hZ7P49rqYx/2ocWAniSXET04TJHnnC69+911CJxzVj6u4Iwgxg+Z5McB9F2kIhPS8/ncx6IGufC+WMSgDivr1Jftqz4Is2TzoPj2C6ZCzqkjtduYlQ8zHCTrHEpVpyq6UaUyPgOD8f4+AyV1P+sT18mE4clBp0+PywI/n5/neSJw6vz+/rZ06mgcJLTW4mXyvNDhd8FJDJh+lNXtxAnd+VIFq9YM4ObKNl9n0wXaR1fGq5/JSmQ7kNlzP2QR8sUYhbEjpLNt65zvWEnG3rvN+tYh+azHX/dZKShj1kBqnzrjvOSh1eY8z3pwT+zxO8oMwvnDmenogydXkO+7uzvCKN80jz+fHzeKMfknqrP9GdzdmTFDIIKl8uAC6ONlCD9noykaiF2jzoRLz8gtVF4f3/akrn2jrTscYTzOQP4wUXVs/GBxfATVsFiWgA2j4M12T9hGQW39BlXbk0r73XWqZ2twPtKEc7QfGziA7+L5YKB0Tm21Tv16hcJ32UgnYShJX3LYjZfGoOJLj7dw8VIJ12aZ33/pizi5MNfxoWxCdhjji7tObo9hqO9vZXrJ13ez71xy/+lIV3yIDPIyPjejkvQaRAxmou9E4vt5OYFbqQec0FWH9Te5yOd9d4+F5z30Wi4NZ/5dwF/hokwAcFx0B6a6R5f7vFM121T9RWluV606p0sTuCT5Lsw3N9niqy2Tz8uUp2aTCZdrYTfUvI7LMTdqOCFBrZWREWb5Y6zz5c0dT8RGMNwIkMzMs4+lYcewgx9baY0CWpzbIICni+RsNoz9DsjZ28LNYFNrv9vHqnXZz2OkFSsZ5DhxgpAiUwMDhIBTZzBLH8VkVz/rCYzJ8rJ/BZgrv38oM9d6IBguHr1N61mGfFhl3LdJPM+jyTZqepPZt61OLJIdi2ke+Lo50rv31px0S1ixLnKn7q7a0yFcAcbts+iTuqzWyPz9fdUKxqkzaDHhrTqLqjnN1YhK1P3kAkDBSo+0K6Ez4XwgYJban7U85EYEvAICAoiVdIjm4AQm5INE7aNSZiRnBzXG1dW4XJv8+DzLEMKK2HtXnWdlV1mwtvcLd2eaRPizDxmJINTnJANfOx3wnF0tZpqGpfTz+U+sqKrneTJtq7Cqtst9xn3QJam946LRKNOkVIgE3/fd+0xmU/W7fzlrQT0fQO3UuamlgKqtP0yPtj2dXK5AGTrrlQlyCMBjHivOaZC50st5IxO4MSNupNwqe/+o1bsqwBXJ6V7sVaaM7B5HHgGtcnM9AnM3H7bv6tnTMZoNp1lNe45Th1LwRiFOHz9mbbjwLu+QMvA5ruDKB+IQBV/wBiAwGkVbXRqV0l3+clE2ESRD5f2NsXTb/D+JLiOwGeR8HBd0JSR08heGxB4YiOFmGt+Pp+8Hn29pSyRfmfBWzeh8fGjO55y/euAkK0OCsyHn5mMAZNkXcmQOIOhIbWup78b+HC1B2mNt+P++JDMiFzniEWPgfrtmy9irDN7AtxwIFJw2ag/Rma/cbnYpmFUds4lAOAadGXWPpWp9hyL3vwgbnIrjcDTBAF7/8UjnTs5Tpd3NZnN6SO35YoZx2LIpf2RWnUlqmuguMaL2IamqjGwpIla6u28SmZxVQg8c0srkNZ61/ZGRer9NYmcGwPfdYKhxsT+1uk45UmqtJetcjRJUfc/rYK5YGXlZqYZmyQC8mRURJJIKnZiGJWa8P7JnJAf6a1LMYODJSARqnrw+rSq/hO9+1Y6gwrt/z97//POfzBz2oSaI3MWFiYg4XXdZ35wi65wMm8TtOruwTSHg2hwOMhTWE4Yd8S9h+FfTJRkX/s9//uPDMjJjPQyOE1HLWiMMw6aMhIhLbHkFhIRBRptpj6+yOiJtgNVd3u/LDIo5/sxga8UNvQq2jr8EgKp9vOgItvqc876/ex+SaLx7S7pYO0iu9ailaucBkMEVGqE9rTKAmwYrlE/XRCmNRzqvKC6mo/L/DQB2oBTw+XlsLiXAucv2+Fu5MO+O9rvPuO7NTEw6GbsoLC4MB7NavSInaU5gGoyS8b2IgHMoTRCmKR/TlU2BlhvaVE5Qo7szlr1uXODTSYQ2IjCwcVvaW44oxXoen6kg8zrA05uGHsfbyUC3X7BodizYBha1BBaXnXUx9Yt1BVpznmAa8rFKcPvVPjENsnqCmhYe4GTx+Hzyu3wPMKuTcJGZqUscjqRwcUObr/DOGeatndIWl4/psvou4GRsDHk22pcLTgnfAQVyBYjpzjG+OFRbVzoVzOSJ/zA4ioYc59s7NQ3ZCsBMBo1xmTzSpXO6x1LQ1ftyDlFS+FHgRByMrATovCi/l6p85bzZv89Lb5qB1XXOtp7O7aGG4tG0J9fxxhfb0OH36PTyuKAIOubUHcsaKia6OjMZCrBPT/CmQIzq+aqJPMCU+/2ItBLMx1G4Q9999u46z/NAvZ40YdXD3oyfcO8zzOpwp9NG27Co1ZGprp+ffwCu5/H8MVQLU/MKgSyyfEipOwESy7x0TeCnWjrlfBsIzgKdyVb01yGQXM/zBOJ5nlyP3Qved+OikFUX2HfZhVo45zAvSUy2rXxmR5IBZSzcA9CixqqSc97/R13QUpD12qe+z7vJ+P39/XyWCn3u7vEkvtG/d8YjyJYkEXFOcfgho35DjUSM5V5E1NnobpSAOh75wYxjjycBXxX8VGFy4KAQUOc43eF9X9qFdD2SYqWMAlQBtsy9/gQ5e5s2EBXU6nxyn9OysnlfN3m5Gt4OMS3zmFSyLknDWhOR0VAgunvvM4OWBiqQ2iENuYKw2YPjF0RfDYCItX6M4/tVOvvAjzoHxqxzjHxOH2qS4Iw0qOoM+AKrjtz9q8+xLOqL3XnydtiLpPIC9EV+/AJceYXB94Fc/3xW54yjNYqGCjJG76TbZ2gC9QzafFWhLqVynfaXryueH4x6WgR4bBm68cs4exr48gGS7n6AlfX+/D1bEWXgQVOlL90MAQYAhlDxR/rr2DQmg98KBsEZAF+Qg4Nh+K7aJqdsPjgPgEJ2gNBswc18NsVa83m/HTspG3TaviETd0MlIo1Rkb7pQ0D38c0ZDtDnbdyv4N0m76ypFbm8vF6+iNbtmLswPVJ9BvmaLpjPejzVuDnwOhgzvW0ksE4J6KpdBYdSRd7haKK6g7Ct2OfzeSKJKLX1PUC6lNFK856xg6RHrS9LlrkAjljCM76NVNRrxSXSzEyYIKYtJINGsSKCaz22IPYm0QgHV3qO6Sq/0vMkkXu/JH/f/1afUztjzQA05Awbza4nwSp2qw4tNfE4NaNkV5WdWIZBklQ95DeE1sq1915rnd7Vxw49wGzGZ9jIqFYsCCsmOAFD/SOZdU5V7fMGkE8+z/Pz82Sktt59/n3/7TJFn2bMMqLR2z76kNSxwiYBc/Ejn+fH3WLtWs/KZO9jPHPlymeIxYz4/X27xeDzWedsirVbPSoXdUF9xlEgQFSdyIxIfyMP8fuceU0NWJLoThNPjIEmgpDOKSKErlM/nw+HEemqs98XZKO9L/3kU15rcENIAh5DSy1/03xWBDPS0MrtQ71wxO/bFfxWSADmnIalXLNr1kR0O/ldDeZKO14QqDrNBrDSS+wcoHxWuew/yAlgh61aNLTs/QsCGWs9a62+2ZNDLTIBG2mozWM7x4IalQiDUDABfj4rOFCHxpxetxZhdnoxrJsvm+fdnpQVv9NeumhhdOF9Z0y3ibeQzGLV3Ojpx0lQlgUZyOoe6D+IMJ8Mb0LJuvfbvbud8N9zYzxM7Kw/+NP6+rq1nX86lf/exumm8S0YXsWajsWfN+L2Ve7UAOuybgOti4oNQOvPNEw+RnWjuzA4+8eRLfi61ghV/LPEsOZwepT/2Q0ux67NuTVCT/APk9P3SQFprf/AUeYhLCaLTOs0aDi7amXi+3Wv5LZqvNuM4hnb9QlqR1z3EbfZGfDBqxPD3niFD7AeMTNWrhZ2HQ9QDDQVI5MPXbk9rQmeRAhYtSbIRbm6MMZkXefAxr1V58aGhAHZQcRmgEIrY/mQoLe67B+pw+CpA2uZQEBeTBobuyDGTmeR7HtAOor07D09DjrYUNnarVFjOxgXUiPgXOWa88NTXekMg0R+fp4ZiduHQv/uvSfKpullsaDd4t5zZCOKHIbSb8azFgPnnNrn998NKBbdpU5giDUQ1QRqnDAwjiElqBn0tkV3UXr3r1uG/b7dx+M2yUxrKLPqOJjsiTxv73NG8UU427m781mRNC/y3CfQtaDafePodwFK/Txjqfr5fPzerpUUu6c/0kDMjMyWFDY9Zq41pc2wIGcAd8FptZdrnCM0yncP1OCXncy1ZMugMYOcER9Eq/46Qp9IVi1nYprfU93e74tw4BKf55kEykHy2vXAgN7QRTOpT9PZUmTYO8/6JzeVMwCp/V1GXEhbhZWLoeGOuFJB0kzgiDD2fk1it7yQL5I1CE67LXXFmD75sgPTRfLuMYXLn4UkzcC1ozDVJQJtEvuaZWKq9+3274msey0NHkzvFFdxdBGdbmHy+GA8YO7OvR1fqbp74csWfDEfUaP4/B5Jo4lxujowoA2G1uZVbfiTuopelF8xy3z8m8hinI+/6qDvX5N/Xv2t3R5r5t01H2LbzSACmVk1MpYLE3l+mlnOx7A0EBnMyd2vbemBP4y/fncbaJsloxh1Kke7GZcKCoqe8b2W2ZYfGcyt45hurXzs5eC4qH9+/iFxxniydFM1ckV3OV6q1bYP/Gp1OWFv4z9Rp9qSJlttTEsS+WTmgu4msKayO1jVz5lv8wWmgPGdnuUUMt73X7fiEj53JzZzZUaI3rVbKyOyPBjJRuEElPEA/G704bYilg2vFSuVZiytp+Sscp/romqou+sYDAVpcpJCHet1BQMIWK02AgN1V0MyV2FoolXeyLcv4qlm8PlJ540MB0bA4RhCZsTi54nBAzViShCZ68kfjrrcOPVynoxdMhF4Pp/P50PivLX3rlPhpNOpBZM9lIxT9fN8CCTSB4nQP/88NrOT6XHwfY/ZOqhBnbMzIp4V5JPPbdPcAOp9XxefGvdAUTjvZrAvq1Gq930zU9VEtAwgyTrgVgtlz044hCtz5Qp5rSts0lDnnLN7JuMTmeXgrpyy7G1hC/KsnuiRpRLgru2s1ud51vPoOoiMaB7y2m4gI9fZU82DVNmcO9U+tvep6saEaNYxAbhWBsVLJmEOLnX1PgeYaAQJMduls5vGYCD+ZwfQHe1fDzIOBwZXMd60/iN2tZbab/3A7jav7L4hTqMgwsVhKAQzmAbl/SKoz8WkjAD5UL0QDi/CYE0Tr3pHuqiRDB3P2di3Gmq6RlxThimvmHUR12ijzQBE2akCEpPEoDpquTPocQTxKDKgsX+gkf2RMd1i6yfj5sVq2s9RcM2so9s3j4Sa0F0umxP0KpV1H10RBdn0fjn+hTOd+cj3ydMTg+PTYTy0wT9VF+6t9idZM/hIcnI6rvARcXqDiKT6puJlcE55xeyMcT0PuoNQiIpL8YQJ67VsVnYZ07blZETGOZXLnmgyJe2puuFUM3dMdiFudT+f9Ep0dY+9OmEE1V1JMESdc3I91edZq3usrVHN0SlGRpZ2t50jw6Kkc47ZIHUj0ylRumexM27u8yQyqg4Cz89DYG91FYeeGWnQiqSOU1IYueJD6EU/z3yMqiZjn8p8MuNUPc9nrl2mi6NCa1l4o+rjV5dERJ53g2zrMjNIlh2nZ/9Tx8xhJID393XTZwVLT15tKSKDauWzuspt7ebr+eDbWds66rnGf2rkTz5PbojxwI5pmV4VbhQtFiIh2Q4oI9b6/Pvvf7t7PStjVZ9urScJ7zCXerstmt4k0oEBXerW2cfP6npW790CqsmQXnqFbPg/me38PI8GRCiS51Rkjlt64OyOKA+Fe28TPWYAVsaA2ldaT5tpjya9SMaKy6MO+3JleQC5rncTQ37CL9Nmtxe1VHuTzAiQdXrksBMMN4yUq3Ymu3T2N7xzulG3Mm5dqysy1/OpszVGBtMDXVn5lB4M3T/R3xx84g+K8fzaXZBTnbdAr/4woJImkhsme7/9LL8rJppWSNVDlZqc6i/FqPhiN3ekv4ToFOa2gQR1zwsM8uB1qkmGmR2D25RPK+z3ev7MxdmnSRvKHJMy6gOFkjUINySRAK5GMa7YH0M3z//o+7X9Y9p0Rc8+zejDMJaguGJQ/Q/W5L5UAPlk+mTxb1cJQ63FIEk2g+t7vTymTCStxnQW/nlzMbvaigbTMiTvtlqPCVDYLj1mYORfqgn33qBdFRdJ3tzUNtLRbY9o32xwNt1HSDB5OneZ2o1z8HnW8/k8z3qepe6Vq3YHbMEmHzYkvC9moUsk21EC3RxKRrnS80QyoJt117Am2jISO77ExDVqmJOICJ46PjpafPeuxtkT2yT4bErf+JXLMSZAM2/SC6+oa8hnzG/pq6q1s2i/Ac9DdHT6ka300HKASUi91vT1a+X0E5pzHrNwz28ON8G18nvO16lArFwCfJ4JOKds5rwiu3rv9/39XXMTQeKcTds4M28PwXM2Iyw29yN4zo6kuvoUqFyLYaFRPp9HzTIi8WSsaNn0zbLIIYz22cFo1M/zAzntfUE8+/z77/+r7tFyqFvHh8/Kx4g2QTeZ1kfkShn091KD1eIA1CvXpeWmETaZ5OdQ0s1FCtx9qPuth0xLZy44eCF40dO/II4Ib0FaCR5q1SkMp6OVjpBjlQw4VxcZmbZxtvK7GVzrGUC25Wmvxw5oZvh3v25ZujuflJqYWNcvAOHV4qpDK9mmukmts7ft3NG6wXYDVtsT80qV2NUWcEVk1YlIrlk86HYLtbx8F64Pc4GnHHMSr0Yq+qVBvkgvpBvrV17tHIBuCj+m91cbYHeJvAV5cKZbaKewuaoNgnSPN6kVI/QzhBsMG/fPbzQilxO0hhmUvugiSDDcULNLvat2TQzndfhxm+k/rXkbyevWo3neLto/YTL+zICk018/H//MYFAud/QLu8/pWS2+jMWQGXNF7tbe/IVrDo85L7/TjoOLU9JjnVvPDNGtsbYE7y9BqDqYZMST3yoJkkmHTfXQ/ZMCRiLSVC0igGmSZkHULTtgSUbfJfeRf5lk69L7bi+d+Z+PgP0m1a0VwJ8jY5V/vvkjWrpnI958cp7MLr8fHmK6u07R6rQYUog3bFjjZpaGLDL4eZ7MtFmdXwAypiZOTr3qXDIE7FbmcLA+9pFs+4oO6oq1MtmLA680ZIF/MgJhgxK/PDFCgrEVqm5a0O1epDu84y9Q2O/WNXnMtdT9nn32m+kkqPJWPeEcYGXm8zxAB9GnapfnAEPDEM4+7jrVsz0YgSGZuyWtZ3XrWXnOedYicOpgnElm/8iyDF5PK3P1P5+PMRKfrdWVy5VZDKI6kj0m2AowBHKs5L9da3uvbSaSk5ndlen5E90d5P7dNoK164uu4cw5x/JA10QSAT6fp1te3DNL7ObMV9UQqpuuf57H/FHV6T39uEaPDx3TyKyvMVQMCxWztQszEOccIs45p7fbwEjLTAXg7IP7GkfY7pTJpPMVBgNABp3zfmav8u6yVofbEv89L9bgKiyHLbQRC+7bDMc9wdMqbMg/jN1gVF+o6u8Ms35hkpzdhVp1EtdxcxSXX2iJE2yCeftM3U8hd+nAXG3wf9yVZ1hxQY2xBtKc8fB5pr51GCNk9wpcmSgexpmyR8tdKTBQjoYFl27+3fz9mSC6rgO4K9DgOE4C9jzB/7I+wCgPcQ8BgeqyMZjMT87luCyFdWKN7l6fZV6iewzPQZIpu+W7TFsg1I2gpkrzS6XwDw3BdTBUMIx0f8c7Urd9HTRvzhsrrDHLMtMyZ5hYuYnCgItyHWvRZldlwEcOQPm/B/uYoIDrWSYGegRkrhRUN3PWAngHrrP3TBjP/EAjrRw5UPOagdw757LphARlrrDrAeduDwQG3cNgPJPN7q187nP7jeuYCNDn+QTw8/lcQFNXD1ACTn9FSv086Z7Iqy51jloMroC6AnbP7hz41KQMBKtaZyWyu8siD0u1AFWNhHweL+/1zXzHO3kkuOJRXzWo+vM8DBV6nESnknZkco00b9c+jiwZRbIBFBrIDoAZpiW7muquXrl88EhyiPsyyC76Oak6e79VX6c/56W3Ufuzy4bSP//8E5G5crq8+P+oerctOY4cCdAMgEdRs///qTurysBlHwye1PSZ06NuqUlWZoQ7YFd86TVVwTQatoeUkW8lMFxfZUUEyJqCwc1pbPSJcxG8MfOa1uERoU2VGIxSJPa1Lbset+kWUyXdXlbqtcouOu4BtAiF++Yw8D+z7DKlS83pkWNPG1GZ7gHS3bu6s998xVZX5szf/nRZh6rLFIs7nZXawjFIpdRRCaySAwll2ul1gC5o1LVNzrAp1f65eWAZ6bng1YWtsApX3gm8B+9HWrAdiTAXdbhUw1UjbUAsrkd3IWnw6nBu7un3oM3ubDHnvAe+oKC/EksJJAn5XxTSvogRV5ZiYRc/uhfG/vvFaLihBfge16Ps6fun/t6us0qe/TPYNy4CHoF7q/Ee99ofLwyjW4HfbemKexS2YTqg+35g2jxuuteemB5OFe/8n4N0Liq7u1PXTN10aZK2WLEApwFG4Rm03itl5rY4aO76e7lCkBxMfyXdUr5qYV0BlH5yffp7aFbGcTpATPe+w72JCzsICPjK+poa9KRiZR4aHPiVUtkC+dDtG9Jfkl0bWbdLIv7KD9x9ifuB7+LOcI9z9K1kvWa6b1h1efDuqUYX74/jJwpNw1db1yXRauiHH3lVppUJheV5+ddSiiZR20oni0Bq03CaE77Jt+Na/UvPPYBRNQI4ZlDEuE68fX8G2lF6MfGq7nxLG3/Q3FSusPLs7qlMp2/wvtjIGUk8xcf0tGtWHbj5iSNRSnc5XS9ddvX0m29+3vxUT9PZS0LU5/ez6gSnGd/346EqaT1mCI+zxkANnkYqXgbnHAH07+cFprrW1uh/hz8ZRzCQImUUZ4FxusTKYv6N1llird9M7AyAK6wu3UDdLRH9AHQ1KlfW1gaTDBXyejzPAdhZ+aboeaMt3i0NqFG4gKYEGvO2D5tQP6OHUw2akM4avbRB6xB0PzDTwG5API+eeR2+knXNpoahMndaowmpDgsDf98XQJzQB25KtlistfXizHXbSs0s6gEcoYu4BAeIzDeVQsiVGw3QfZWOc4sB9gYUg67favbPzXsG/z2FNUgVbj7E/YIIoMTAY1YIN1gp0loNvr/ZyltnxqRdqW9Kpv57wTBcIhpbqgRiZMVazfvcfy39oLOR9+rQJ0xcUKVnbm8BRuWDJnG24CjsFCcnWukfExI++7kZMLL46eCeS8Z+5VLSoW7Afq+5TMZdaW9nQaeBsgmo7r/lc0S885q35j7+97bbSc6ufin4iOQA77Qyo2fZtOPrTo8IApyd+M1pYXemIM3DY0dqXirmdkoIWROrOjNSzuj30MN5RXj2/CcUXhR5V2E4sJ4Jpb6AdWN3vg/WAGLjLLy7lDmjn1Uz1KDMOTMRj06TUlTyBoVT58v9RnfYEGJIY5VO8BbWbzTFEpBWr+p42mIbX3UCmm0JBnZ4grBXMxjbkexCqZFVAoz1yk3jREx3WBhs0J/8TSmCuqG47K4p+Wj68lJDoHMjR7Szu6EUqwKOIgfAfJNmxFRmdUsyL1Ske2oaM6qmUfW5EpakNpMhW5unu0LEoNf+OceACAUw0EMwUS+UR8Ps/9zdiak3oX4eSlWCmdlQB7OIs4YmHRD/2b93ROnJ91X2G9boMPm+3f3nzz/clW4nL+HsHg4o7+KvjYnk+3kJ/jw/G9/cIzQF5LCJrZZ7TuhG1yZxVXTq1jD05OdVrs7mCGF1biMbtV2kebeqed9XhRC+SSpSTwjY7YX2Bp19nw3OIDPjnK18rXc7dDiNPqF43c0X0jFKs+lN4b5toReL1wz5N89yM7refAFyTxfUb76/WVcto+O17y+08/cdyPSm7zmtBl4dzsvC6qDcV46DixFpeB+BITQtPlzYG3ebklhjh2iBdDeJVM+G255lGOmylJNzc401qO3GcB+AZYDuD2LzN1ZovtLWvdVmGWbdbfr8etGzWYrLVtPrXAHC/snVZKe0+fvjzD7e/OJb9/fkDpSg0eSDmf38ZnHaPfj+WriXJClNkQtsaBvAHZQJlHhTY6Pf/uiN06HlX4zrbkTjW52F6bFgV21ulLaevZrElSM8dOXXTL5qWheTqeEOm9c/Q9jXJt69qQD6wsKPrRyq6VtHSHOtvWr6nek4bmbnhCDmuaUQiv3RJFKp+MP5fP7VVrtK0CoTF4drFe4y96omzO6gqsFb2k2gSeZbI/P1tPvRdz29H7oeIJGx9wJyDzfoJZT1f4h2G6LEiNk+gqIWMRyFTHxE3PWExRMHMxFGM2XRgMpuNAOPfBJz2avZ5fqTKsi1DUghM1+VY/amC0zvAUQuamzSiyuTWdJ1AkfrT7g6b084yTihxGbdc1XVlZIYboDa5ZdIwuQVwKuLJ6xa0UyjOArfKqVYzk/RXZtajBkMx53QLkztDYoC3Cb1GsV2ZM9kfjQQZuZmSYser6q3ND1ocD3h4V4tarn0QAooXlJK7/kG6MdNkRwSbh7nuMdmSflxMzXR63zUJPj5/FZPVl3cViiWuwfBE1GduHzdYLK2Rk1QS2dTb+cFT+1YT/WMg9JfaKjMSrkBPALXAaP3f2a0UV0oZH8Maa72vlmgzKb6tmW7H4+zOjanhodBq1JGT4t1T3VDSdSXt51vdMH1LekQ6t6jcEwiGX2co6t9lbDbtQdhMrPOD86K8fX5C+PeHb5VahUunk+KD6XbSZHV9+5RY8EtiJ/u3l9BxXbgPYhX0LMrvt1784bKyOSkWWJNTotZTQ++YeAaEDeZE7sjCx4Uw8G1JPH+I+DXHCDMbHi3QKGOgr4vhQmQi9JTISLVbptYs3vqzRG6u5imAA63LaBGV8Ks7rzHeDuwli/q1ZC4mVMzl4D7Xt/KWFapYdM1HVfXAj4g6OZxoqrCw92z3m3/GMy0hQojvXrJLJ3jVVpwvnIdEvAIEW9VSrfvq1haVG4xGVIt9sd/CBgpYs1iU2U0iCzLe+F8UWTf0CvR1Dv9kTnVUyIAB2iFMmren78IoD5BXr3/pYHVIeVqkl0yBsPqyZrK6aaB6jkwEBQj/t119PlnFkZCps2n3Nue88/zo1lthDySETZdAogFKE0Vgff90Owo9q5nut08uCpGN1Zl1avtUgXxWBFHd3dXSyIFjNNMYRcbUWIerjfb3OaW88QJM4QbGtOlIi2hq4WG3UvCiO1S/juSVOXkzMBpi9x9+RvaP8+fmSGNo8yLodk5hwZzU5LH2lxpwrW1zGp/HQWudBnNYEZHUzUKpM3sdSgRwUxveafs5QYashMrvOvM/D5E5znm/pxHUKQuvMx885bKK9nN9PLf2FRzkpnvcmkAGlU1vL1JPdqIVrIyCxUaTJYCZe7I3oxRwNv38Odwei9OrEy8Fy/WK2BmVRrJN1H8qw656wLqzT2OuVP1dhT85Xp3JsalRC5HxS+8fm+iRUJ4Fwnbm+NiGLybp+1up990Noxhx1FdopCht1fRYfskrURPL1FVa8Km4jYgqplY8OnvfiNMG2K8aCPL6sxcLHo2YlEaHkHYdo6H306Hy3woaJK26wuuTr8Xe9nKAaxLeT+tLQGY+ymJ0FYQ2eyPrRPzirVSsKSbk4hbr3Zv5v1BFmIc7XXgRt9JZ0gzU0W7mZvwPRLBUHCfrJi7N236v2/y3JSdn/fzq49Cw5pwCe1LHt5ZI4R/PWyTXd8l+e57bfTGSEREZ2ftQpQ5CqRfHFBY6orvSH2LpAJkhJeZVb7h4WHquTM3s+WKb9N9K3xxT3xzFVvmK9eCm9+AQylnIcUhE6UPLbvWa0IMKbspJITvQtBoSpgxwjhhfL8vVLfDjGyy0WqugKkbp/OtgbUE+4SHlwz5KAxrqgeyff6+HzPTWab5RQ7SnrYxmmSy6BlhOEMTELHFuTN0Q6aeTqddXRCNvm+1s2oiDIQ64wBdfnoNdsy7CG/ptVSuRXV3VpzYlXmp7hoAm6S3/RaDklxh6aI9rDE99eZ5fqS7E3P7+76NVkvX57fJiXN0EH9pIjPmJ3mLwDTp6MeUQ0qUQE3psqged3/zlY29sk54TXMwqbyN7ndjeBdTIqe7soSwRsj5vEYeTbNm7G3rYdeER3WNuhNaJqMjN1XEETpCtEX0p+ttlQppHiSZXeHBjQipRUguaFbZF4qfJTZH/4jcgtcPJYVCbemZCdLUBbPhHNO7929i9r742v6J6ebNTuir6+fNsZk7s+uwWSG/xvvVocx024gJa8PMRTbvIbtEDiFNGjDjfmpqYSVQkz7duvQP3vsAwC0lVAV1XW/BF23puW0EOsfdLhkA0obrhexKo2sBkvn/Xl0Ctw1A71nXvMW8/Ir6zLsuJePW0zcHgkt271m1Ik59X8BKjabniRjoF8fO6bYXz4haM1te+u7lV4qqv2xCKuT9VM3MPaanJ/16dCTiWvB0qjNzdxxyCKUYqSVGEgX3dXUQA/W8u0eECrZmtpzaLkFhEbjHyiwOyEypu0tUqEfQ4EpF795dzoQRe5zQ1W20rFSM1zp1d+1dk9oA7u5GCd7X9eGCjAswemxhUdZKBxZsW0+gmSlN3ozDuXpwzOCcM8DUCFcAJsK1R2sWMmw8vZu16lUx2U2Os0ehzX2hUfO+RT/h3B3FpCYMmh0/Mzh+ZlUDbXJIAMA4qZpqOdeURWoLDm5Ycb1vV+X2zS/RUlmi3LQo55tzI9FFV2a+XS1ZZ77voD2u4ERsqE6YfTJyiZ8BQEmDhFTX+6nKVraBTY/CfFrJGdxGsyERyjBooLqyagrocDPQ3M/zYP5mjRHsKfRKWTziRAir7r5z5mC63U1eB3EmA6LnfT+f/GjHN2NmASOLCU0MAEhERE9rXjGa/NgnHsxij+6uZAcaaa4ASDkFB7NYmNHdzgmh/uG2kAL4fmoXJnRv5P3fGbirzM1tk14oVftqjvclreV4N/BFH462ap3pmncHa57o7srUE79OBe4kKK3k/rF70z6nplcdhnVEAHRTlviCdWr+ELiI3ch1+nxhB+46u7sylaW/WM5eBlgfgFwOO/5rAdEvOYq0m/0N9OtpaF+QY+sh9b9Z/UPXf1oPpQ9e7kEe70vazToPtJLC2OqJwmpP9fwPZlMCt8RwvYr/WUmWubrB+rv+mHDvpR73HuqZzuthwNK990jZjrDqTCkzVyG2DUijAq778+9Lkd+YvP1khRP0FI2VqYunOntKn41EXeEGwvRFttoSYKS9+dE3ICKrMveHIr9uA116EqipJlIvgYgswZFcZMXR1LtS+TmP3CdufmtOSRrihLvjZsMqkNKIZRdAM9tkgvB9dmmELGbmdMLEFql3aq1oA1MUvsKO1FxtX9pfEn73sJlpjjZrqrNJW1AjM83NQm04oM67FnZ3CSVQP80OGQBG4ZdFXY3XC9OTIKbmalZGL6GWAGK6c7aN5MJdo0tCOGzX9PvmzEx1eKg/67YUAbOqm5k5i+xMV7s5waDP1Q3YVth7Ta2Q+ea32KbM78vjxhP2PEdN7k5z4wm3MMGLWucrXw4sPCKczDelMO4doOC0fF+SzzlipAcAr5C8waGchhID6QK8EDDf98V9KfTmqULMPTqr3lfsaFYpcG1G6gP6CfW5h+wSgLv1zOdNM69VqQl0aUAGKFaXh5PMzkUweyrLI66Ofu51j7nBsQRFU/c09NnXVDW63fznOTp8NbTqL9RcLzOnwjyqJZD3ZbO+ttiZ2JDL/TN/ye7pNt9PRgvB5l52icHCXIhmuGnS2JXhK0W9UwqWe12VqjAAjvoWs7q3DmQR7cXNV1kJ/KWLd0r4Aj3dQNMGVwTJ6yr4okkN9PT3ElESBP7+KtrAdP1tdsCCR0vJ2Gzg/OyTbKSxezYO1vbH7v5O6hKDron3q9rEbOCg6/4zE+sudEjWS039uwyRgNUyT9S4oBRW8FLQAn1kvIWYgd2IBXepQ0a/Ba+GG72311cPOTMeTsI99A0aOaX8XVaVAhMXyddYjPWl275aM/hb9bNxVxAZAimotPGtELiygNGr+u1K5D2lZpo76dy2GtsGMj0f1RnnDIbrIF5Fmu7KmSKp2J8Z/PnzD2nvm1UJTk/hzu2N+zKiG4tOatVSVV5300NM2rdIdfT+EIrQAdVaeGjmtHzrC+LN5Tzc3IweIZnC3nZDKRHFUu6Dt4y0yOHDwQzcLYKP0zGGhnB/Ymq4SmT9OWxhThWbGKFUzunjB6QSPTF835Qx+M0X0K6G7HzfT3UOdpn19VjM30dfUrCrHpEWU9Vs4S5jh5kP+okjS2zn5EeVi7W7Kvn+frr68/sZoGtKeUU10yU1+vnz0Ghm7/uRwtjt9Owcb9Rq5eClIjna/0BtqSYMcLLfN/PzVhbNxPt19xPHaG+meSw1d28DkhEx6Oc87hbmn09WN0xdPatnkedOCAhJ3wRQXHsnzAi0oqsiorIzS3VmC+9gVQwS09vq92em15dLuIWU1b+fj75abrZ5mtOd/o2umyF4zrP5vdiNXpNQZr6VI28njYMTp3JxVHcRWHf5kX9FKEKLByJAUdBLggoFFtqFlZxOT1X/PUN3iV+Qf9evvQNsCUZ6VU+NyUz+/b87FO8B3V/QftezuZcT9v/tfKPrv9f+0lSvBYUGgtsEqSth9vAazG2YgcZ+0S13X7iZTvrtt+9Iv/FXH7Yb/9yKx9sR6+vsFWq/V/WorcXs8oirOqXb/vdfBAwgIVkwMNWrcP0iPty1QOP/yvml8yHoEaaeiUvJCMjWlOhSkdXi4zrWprtmZTuEyiOnquhWVVM1PTe5mtx8WZh43c14ULEJxozd6eujmy+jTFJO/eMHykrTVtzraZYNYe7pfy98PUutV/VqgYQBywva3R1xtImb0dzf97cq3cOPmzPCqcITfXBEZd/1Rwtp9cz7po6S2SgWCMyn02hKQ63pWgG+HjPCeNRwjW9vA+xmeU83XdjrbvFS71wbjHySc/+uXhPD8Hg45wkTNsAZB92ctK5r2RjoiMFAGcwDbOTd8M3XyHOOYm3I5WzD/VnFxk6pkoEBinnYquFpsCnb1qVOFrMSy9TdvDxcd2p0lYbjnIjnaDrLfjFtNKX4qaDN3X4/n672cA2TKiI2IPMVf0Tikx8C53m0a9cyXetIJFBdTWlLAAxhC3foTzuU3t/o5uxro5+qlY4PVPZLQu0S3VvF7H7n0i0XI9Sm3XrerGd681Vaq/v7SUXwzYzBzFwTwzlPxAHg4euBmpVXCmG/Pb36+sQuWFb/eR6ukmIw7BnVvtRVEOg4nS7S9DXpROSySipKINDduYaJXX6UfzAjjoS2I8XMQOFkopqxxkwp7I2zYYsXQSawyWgzKonjtQspUo2LRM7gm/zDGwo9V+K438Z1Lelo515cey9w79fvhnDJaOztoddJVNk6ta4WCxS+b8sf7npw75fdIQaQ03MxNNwlA3/RC3HlmEtliSlcydAMCN65VHf6lw9fBuI/ZOyNAe5LvCy4JIXSEjsqCLuHCb7EhfaQ/Yghke8CZKMtcJW+ypghqeATfcvKtdTysTLQDcY3Xrp7kx8jqPY67t9RZIN+HdN7oz+/3QQlM78+OrFL1MmrvTaVV4XxCA8/P2fWlKjT2797KIis1z0gllInqdBq07Re+lPmRmHMiPsyijfTEqr5EbrZlp4a/V7qsNb7KWkSKL+0bLqz7zlJqPcVrgguwtwifHp0ZkmbLEBt72IzGjitgppFmebeixtIciOLu7unKj3oYdlJt+k0ZHCc1A/nxjAz43OOubnZ8xwMIlyAdxx3s2qV25SRm5W0FIsTeOtDJwCTiI2U+EgBFfcLWzhWyUsEqqpuRLa4dWEYADwi7KJDmEI7qQG/srL6fd+F4kAA7/t5zhPuozLCTD+uB2suCCw9pTkHvV0LUFYo36wZlVHw8reiLT6ffM3Wr+FhF+WAa0XQHHXxXDOnsbqNXlnuUbXGjjgR53wh7wUMdPfI6+TbXmVqGcWc5+gVFjVV/UpKAEz/Ha94+TfGt51j2NV6znWIFNrD3+suflcpwNL9I4enNKA9M/P5/eiEWtuNu9odBVWQVimvjI7C2j/Hqla+Y2woXnL7Xrq7yr/KQfBeGuIML3ptrj4i7Hzwbe26eL6iF3T+gW4ueet+S+Z9UXiJcXT694wOlr5dI1/H+/SGZt/bRCCP/hO/ZrDlBS5EIcBtvv9hsEf1rkHfrM37P9gE/12siL+RDGI0BXGLWt9A+wF6dHRKi+u8Ykdxv1XannVl4uZYzDVkiVahYkov0KerhADdIpZS1VKi/yNwVw4t7YJOSIyLp+kiMURdVY74WttP/KtJ7V0BiOFm5S4BMm0CZYwRDo7b5kotkuPu5oxnv851PBm/3AVpC1jLdkTIpgJOypY5kiVsRLN0RlTrRZemeDO5A+fEo69C6NB+DWa890h3n/PcB8stlnDrar+qL2C9cNX5PD8kNZx+f/IrDjPhem421d1lDpldYVoh55yjj006yztStNFI30WQc34OMHJMCDR3DwJhQbOe6q5//79f9Q6Z8XncUJxcX5OkZpgBlPKraUihDgY43SU4IfHX1d3CASrLFGY3M30VolnCJd0sLMxsZkNAFQgza8ZcrljNEhI/gremQ2j7lTkb8Hlfxd8/z6Mbc+TGJMxx4gDT3R6moMuumcbn83IQR44kYdLs6qyqavoucEIG12cHCoujudiCRvNWGugEUT527TY7c6P0uhJS/kw9z0+3wgM4jXrr8/koEF8Ox78NImo2vxBHVdmx/colgc33/SQG5zl+7Lpvhra+jKXHuu6xpeFg7mA7wt/NDZhMRRhJ3qVkZgJjt/qrp5+fo6d5NuZkC0HXhTfbtBHqOjYKwQMQF4unxFczsDtNAuGhdcXuXPhdBGegL3VuMxJ5/+7qE8k7Yt+dYfHxqgXQlaPUuzXhIvVTVVzoQCkLSk7c/CIIwZ2/kU86ocCNOZi/W4dWLVE0XDSmB//p5JlVA1J4kZtJrm2ukmT1wi5SrWWk+1Kw0rvsG6YDyTBoNVhwCyEgmKnb42A77IykaJsv6qAfUKFWIHH1Ud0zoLA0fUg9rQtLm9nOl0ryqd73Y6SU78FY+Ng6mWW++SpehUwD2AiJ3e0bALojnDNhquSiGV3OOwXnGUyKHV2PCg/Qj9G1ZlTu5sPKmtLOYu6he2uGVR0ndBuFCqQUVTiDGffw8OcJM1vrPBHutNLj9ebnKvQFuO8H7rdTvmqo5QNiDlzzbbVq0EdExaDNNzVIiJ6Ga3cnLbPibIaouYN02tTUb2niy3xvBgDdAxunzjezpr+tES6/FQHQGGpeH1p1nZ+fE8f9mMXUOE1C1aDCw0Z/NhCY4fLFw5n61O+/n+q2cDfL9623wg5mRLfqVXeP40dn3/GQyVZP7/3K6s1XMIuWJ5FpxORSTNJQ0D2cFvZNhWNr1u9XM0XN2qnkmNPHIpQ636x3nXdGy9/UE9KdetZxw1bEKs0gLMKcNMnsJFsUVvvz8+z9A5Ay0G9j+W4jgIffgjaJujk9+fnM9HAXJlRvZnPP8/xRTKS7DFw7rmXXVL+VvYGgF5eA1O4Su5aHm3ucE+Gwyax7TqKq6mpnZ1U9qXH2YoZ9lI2hJWuEhAjeuU12F+raG6jKPZzfDBgsit6LYKCl2OEmIsyYmaRZaPRWmy1UtPAR9JuwW7uq7ehNALDdVmvP+mVh//6rqufbxkMORquSzm7oCP3StqD7RsTLZvWXt+9RwJaog/3nxeQK2+k7n941emEn8v4Ugnf08e5igPtL6SK+nOBcSLCn5/oNufm9Oq10AwF2T98vx/ClMWazeK/WELtd7W22L1fqXVgAqnFTTlcdvo1MPTpxtS6JMkTvRmK7jwgO397KPffYUkZpfMXfPzBM8VZmCsa4pMYlt3pxEQAS3VX38zzLjszNt9/4H61ZTnMlSbX5jc2jaLG+2JMUozhHu4MD8iCskzwzod5dfbtucyn1mUZjgDfTzU4cIWfqtsO1erqHOet9Bft1b1MKLqnjZl2Vb46sVX5FvhtR8GD2dNPh/DyPeYzaMNzNfG/gwdSgEO7SBOGu0tUpW+JXEoM1cdhNaTddwiRIz3pXBqQko2liX3IPNw+bccMJhI3rO6zyYdgWtSyQShxpaXuycjARnpUk3y1cWdhSkj659eicad5kUNoCVtOlT+1LvXyXzQVPsXExWSlSP1xgLgB8Pq/yMbX3AujcOCU6LawloAape2JVv1izmHqd1VNa2T1Z+eYnu+4yChqzSvK+z+/vibNM3ihXQ2VMExG4ZhmnjNZjCkcDTjxuoVFUC3u+JfAv8yUZJzDIzyv/AX1Pk/Aw3Ub6mb9wg0YQd71mHKIxNc8JSp93IRFultT0jIWJ2RIz5KE6dSWbYAZZBe3YXN5II57CfQfbJF717i9CyL6k2/SLV9R7x+xeQYQGP5qVIvd0IputFAQApqqgqqD6SnR2YMJNvNkhd6/ENQpRZ/QM7tB9u0uutGClbjezgcuffzEy3hVgSfI9zu9fLkTMu15o5t3HA2s9+15Ly0Vjk472F7rIxJdMXWfy6OcZ4Kty7O8eo0tZAPgF0HeKuvDdXP3UrEXue4/NHu66gmQYajpXKn03Rdx/zYq17o+MvYvk/hQsp/N2c0UWs5KFYlf27zJ2IrhBs6M7HeiuUhQ+CAsbNHFj2bhn/VxRgGYEzGxVQ49Vzl9Z66r1h6NadqInIlZ5qU9P2ib0OUeIqkGXiO6N8TClK2Pa3SMcxPSccADVuTchV8Cm1JrZSDzEMWLCDWYzZc7zaMmFmTqqUJ8yU16ucb2X/X0fzCxOKEJdVMBSZ9WSD8ghXK2ZUYZhuruFG83Nv7FPm8tfA7Cqw3z2VN0oCKdsn73huhgPi+OL1XOe48cZaJEtwADd+amqfF+A7+eVBqOWwNxZaTC5GkcL8+l58/PWh6SfoFvnVPcnM9xVywyaVm0Q4UHQwal2Wu+8uCCvzApGm8qRtNHc6XPTIWXo5zeqWuBEdS1RhHor33zfdxSUPYjNLhb7vlld1/S4044kuOR27db7zkx+kmTcMkMJKL8DYFVl97/v71vvALXCJG5Ggnn+pmpeTDGxV96zcyjlM6/3Tco7YgaOsmPPc66efYaU2ufzvpktgy5q3rfk4tkUmNXa9qiDkNRDo2W0qkh78xXz2t2C19W02l0QAwzk+87C6gQ2qFfOapNGSPLEBsjqjrDKNzOxQjVeoAZfqXtfPGZwt8Y4IofhwqzHbrOTxv7uqbe6u97bUaHMMm7Sl4jj7u7uzOxpdT9oaJUAW3+evt4xXL/S8g7KAJ5bHaOk3u9sf0eH7+4oZhsX0b97gf5qT0ndxYvn1GDNa3tJQPDjAtEXvgKg62sfSaymaelRfWK6Tmb/tHow5Pa/z9KistiGxOkJPzQalrpaXSkwy3Tu8qAXPGt3zZk9efZpJwjIxoTe4w4SaahE3cx0JIpdCzMy3InRIakZXfc8ruXN7/y3O5gEo2bhFkFsZRuIO4lQJelLiyHOWewyNjA/wge9+RnKWTRs15K+3tnWOs0GNdMlH8omjeqF3+key66w2dnCTPXvJ8KuglWRs+5iX+38PG6IcCyqaIPRV9uvnJ9/A0qlQby0gYVOf80a4xLo77I6gFxFxpJVbejm5wlthXfr9FURGPty0bN6vlMpPz0qM84x4kdWiGmjjYLnSADhPlUchIewZSPrreMHgJvR5njw+wiS4UfrvNHiiIC92uTY5nE3iU4bwBCjhsiL4H/HjN6yFaGv060yYeMy8/si5JtZRRN6b3vzUTVJo2AymeCMPjVTGK6Nk24Qq2l0C64HbSQQomKH3+rd9zLfF31NbYqdwVrb9tHq+nkeNQh93qw39YhKoVszrRwJbIBE7z5hM/Pn+dEIXyl7o8DxrZQwGGbOiQh3txmFaixh2F2fz0fV01KUyhsrrGzJBFw0YBVERnIw6prh0OP4WlJwPzTjTFVVpdS0yygCVd1Seuhuwg7Hs4ZcIc87IPtCSzRT/sfND9cJXzWNDbveg2DsxsovAkRGuA5xUsP9igt7WlJgzJhzbiKqxIjyedDUPXm7wRes4NzSJI3HQpcu+P7VLy5m9Rf/kq5ov0YuVwsu5bsw0heBGTPDsi9cSEdfk3aCvrDYF+H6impW+9Q3K0KVeePmCrbB3G5n7vx+zgWHB9f8ZdWpc0FCec2CAD2WQNXcLaJlFXu2mO3fH9927ANH7U0DuNuOxUHZx8LiCim3qsxNuWdtinbYMTpouDPVePiwI2IhJjSJ7DFD0I1bL5CrUOJcXpvYghGV5XXXdo8JIfkmNkuDbzf+YoTOU874b/rSXw1iV11QTZ0eTkNVm35ypWf2ax53m1MkCz0aI0sowr2mPXzQALTNm3GEIffeq2Zmsymsoi+q0kLD5j5GOwNAXXccHdYLxbberff34yF4J+TamFmXvTbH1lvDb8y6iNXBEDUz71isLN/GuKY5mhVlaFhDaVUCsggDblmvDuaaGU5+FJaXwj1mJtx7ZC8EMNVldMM2Y2h8cJkizI3M7l7Cqo/H6lDudqLmYRqu+hlmpmjinRlngYM4HuZvFbubCDuiHrpVwISuzCxTCoWPu6sXRWOm/rFZHOYvOoSBQvhbvKB63ygFl1em1L2yBYCoSo/g6lCNxnyLrPM8qGqW3aVKf8vDDT4blD0zXdmwGcwxrxSWA7pN1/spcxNLhB53q04xkFoL8pOKQBINYMaL4CkWDEtWdxmRmS2/dG8clntwanhvq+5zorv8uKJAZ8bPs3/1ltNA1XRKXKjEpBbyI/mrXrTOpHE48Rw0Gs2eHqS8UdRg6Pp1vui40tlaB2O2rWnS99tZ7nDbzBczF5aLlWfRbMR+axWd5fSMxu6d6EnM/1kXduHbXwHYPJwlRCCk+54Fd9D/jvbyekvqsM2yoJQa6/MXvvQXZZov3a1Bp+4dSxCN1k1JDVBdNKOTo9jdxYWB7Z6TGmUwEQFFGYxpi9Cfcc9J2xulNyN5GMaNIaCRw17OfNrN99JHC7ONJ6a7O+UtU9ouxnQH1FYoVI+SlET87F0aTod3i5rtc1zJVu5u02Phukj9hE7zyiT4xLOshiHcoYJZzgkXCcRl/5RDu4yOgRGhPJ+ZkisXQ9OhtouYHixG2DkHtlbkyXbK8Tumpj3zyrIwoOVrUMwOh9UyxKJWlCnQkdOjPJ6RAWfD9Ev4vyT7Xbl77gyNHkaDEKKIEDKnRXOU7jZ9nhPuBOVfXW/RQN+f/jVye6G0S3a1Fif3kT/fnKsf5xSU7zb11ohyx32XDIPmFoHUtHDkqc7n54RbeISF5qMBF6O/Agkza0zNRgCqGr72DKmZ8rCdZGVHuAQjrkC/a7rLzM05KBn5xRzqFtAfWwC32rlmFDHDnQS6OZAfT4yc6C/t5sJ5ujrM9YEvN5V9/czTPR6u3sOWsWqme9ig8URI/7fNZQ13P0+Q42Ee8fn8ztVDqBhnDaKt73krVcyVggl0X4h2dlmpjmMRLo4RQFXvQGpXa0Ho9aFsjMOV8WBGJUgjTaq/+emVVGyq4D5z3F9doVKCs9C4cZLoVq1yadCTON22EqMx0G6qOWlPnJV+EYNWsIecrXe4VpDOJWAhabw2ZvTmee5xWRLG7Rl6R6Kd2XdXkH50wyK/QM8lE2RRvDL5ryJo0ZoLempMvlvNpaT2ktC39/fg5xWqXFJmaZZV45jdHwE9hZVEEVgl2NwANAkqu5o7qS96jhUp7a6g01xbiRmVfaDxXy8dt19hLre8lZm8hzswU0qUEtdkWiaIm9a1AB0WkdJpINXpXsPCkZK6/7evGH4POgAR9hzHzDosumm4gtTSKU0qelLJoQh+yxacqEHDzCJOTWd9divr3tAYDBoqkzZwjvdaYMQtXTLZHJiuch2m1f48mjVqyzHK3CPUfFTHn/f9hDvNe4aw5zyAZaWtiALTQvwHwBiAJv2JaHXR997eomFr2o0NA5CVum56igP98APPrBUUDN3PTDcbQ8k8YNBHsXTK98fX021Wb0UcqMBkMxPGjF09jdv5MeCcsCfghd/q7j7P+U29eNMzXWMWQszWCdOjbgfNvGbewG2671TnpWpjzUFWvgNEnJVFAe/7jmo9zVVRuUdMd4QbXXlSpE1Po30D4GCAGg31RlWl0QaWmecYDCjQrTFGnw2wmJJLg7KuLI4PWldKZNIsDhcYHKC3RGGJkypzy0r5n/N9Ne7RrKfVZmv0zPX3V885nlma404cI6U0LOUj1XyPkogDNGGt3C6sXYqkZvnM1jh2IrpSxSgUiHxdQhrEnHZBM4q0XAEkEeGAXeMVfn8/+scqXwKkV40sPLbs4Qp6APSUgPyI0M7piOZklkBhfI3EXdj0Op0O0735E19AT1QcDWFWmZhLRuF6ff+yrTp/sFrGuXKYLR7fi7C62A0u0IqLpLjHdN6DXqd9L0h1UbtWPqty1gSv9/er2XV5kbMZ28f/zvZCuUTqLHd914QlOHDfzt22qfNdeNS9haSjMV40PbRk6CNY3F9x8EJUpCXRUGdmpQegp6fNrGsQAOHuyokyVzfqzvhfJhnLRf81lu6QGCsYrc5Qi2SXb6sKemovFUVJGvX0miuKzcxw3Op6rJW/OQA5z8+jKF93zywPOyf0RphxUkS37psRN0VjYD8C8fo2Sgsw83XrV8TP+/nlRoXzmygrnMQ8sqprNnyOjKP0HlwK24S6rErXNyLD/bzvR9BbY3xVgm0WmWmEHWPh6u3QtuiewHcuHHHDZod02434Nk3uKDGtjDbzWPfGzvaAjQ1JyzfNDSO8eLlEk3S/ykxpd6OfXbSJMiCFA/RSQ0U6UXTrbu13ytZ+jts7NrTwui9laydeUF7Mz5YQCCU4z0Na9TutyCOuqE8sZE92KoNBsPi+KUu5LvlkxoGSR7VXboYdBln58zw9ssvPDAprMzEOdpWGDfwcrfoR0V3H/dX5i+4eDnoFj5xpuhH+Zg6ErHAwlaUQxK+2rrv9BGamFdm9OZe6S7rGCXdzi9/3Mz3P8UUL0ZhxI8Pz8yqv22itzpax2ADHifD3LawwZCxIgG7EDiK8riUjtR65hbl3JoEhPKxSIZEc+fJ8ddemtWhLNKDLZrp1oG4t5Y3pdHPNbiByG3rXbtU9f+IpJhq4iSw1Nz1mVqmks3EAKWJnOvF9wPV2WmYqFtfAgWhYzDS/Pm1BDcpt1QEPGewF4l/UbwacOOf9fMR2ukVmTq9/80YF3X+N+FwBbV8G5v9K/neov8fjzlLcI//7n+3mz2mwX9/hZTC1DFz4B1+g14Y301S7xopq/wP7LMBumu3GwytTf8IFvntvgu+P1nXzEK8fWLEaKqa+RAghoaa2wNZgvqE9oGFG8JGmOwnK+vbgamqcK7ExyACbq1bYkaIBjg0w7vG+KQUKpk11fBSzla7bxS1ginWI49AtJbRc0R2cfDuOcxDa6UQW9zRokABHHgpaTdGJ23Ha3/nH5s2Ma/Yx4pOtaGzJj+BKZZJHcVkeGh2s6p4Mnbb1nh///L68JIG7TRf159YjedNru8eMVaWBdydzqNRsVDSqOcJMHTWle9mMqxIwOD3fPOd053BminvrIjN1XLiqVrX+qm5i8PVMTLdbuLl8/7qWxOLQbQaSS/pxg8NIvKaNPdOeh0Qbpts93vfV1UYYZwzU05dVTtkbRxqr9/NGnBm8lec85NRe6b7aCcBcOlaJ3Uad9WM7qM4MarYv90qqvsCBm6EVh+dwQl82xj10m/IORD2wCPRkZnissmITvIUSFAZGn6m3Po3xcM5W+gymlCbWQ6Nkrco2coOpGkkKVCWqur+/r8czV7Q500M+5u01NW+lqMyZPhZV2QCh8A+4hxw1M9DLoBPGPQQHmTtbb5dPz2aYre4gik2zrhFuvP20s1m9O2HO4lcD9JRfKR0A3ddV5RE9qPeN8K6OMH1aHGS9ADqLthmJBFUOXJmDceN/j70spTvQg0EThN5VpCmtwQnSPExSIg0He97bYhQCTHvb4no2jQPdHeFbdnjFRrqYGTHKXJYuhbv4f1/ulpHN+J36F7jXZ4XZpeMe/boM5j8Xie6P/bf/7Ae7ft8NhvvRQqe51A66m42gup6wb6aOP5orbmtmO7rXnjCyyKGznXa3gvnLbNPpTZoqJv0EMJkV4d2gr5NglgjRq+SulN+NfuZMu8JmZterhZgwpJAZcIxG9WWK2Bd1JFjbDFMwqiTdFNq4lyJVqaEPUNsSbNbW2RTiDRhcNoweCfoF1FORh4IItKbVta7J56Xv1S16YEYJ5mbmROycwnkz4zhnAwUBTjVg2qg0ewAz1SkoX8gaBmC+b2wS8x0OlG1Se+IPlNy7D+g5B1do7B7L5+zcO9JClfIy94mbHZ/3pNQpNjPoksBZBPE+VbaqnpEhc+7z6r4mw+558y00DI2qLtxpDtuOMioj0eD48/gTYOd5orPQPTnT7dg+XKG84Aps9njR/jjCD2+aRbdo1RbJBtMXZLu9w9xgvDirDYRN75C/j9ze2ezV5QwAXitN9+Sb8m93y7OzM1R35Vv5yamqSj05uh1VmkjzN9eqXP16BLjitiGHUy2n7mAm33e6jVTCaF/xnDDgoKPnVfv8o/u4S7tC75/TaI1x+tK8tPd99c1dtYLNIi2b/ZivlE2pT+j9fQWnVnVVXt03BPlm16x9ajjcRGtwll5YXF5C2J7OrOnunHq7soVijVKKsoVZa1DNlVFCAkQ1CJkxpbBsSU3aTJf6zA18xvpVnavco256PeQCo3v4vq+qCpW+R5C7++vI2C6zm1KvTg8jTXIgFxh7Wa6qmkHme6m/FQ1qgbX14N2tcxYx3uUAWhg2/nPlm0vYCVTaC0QIki35KjBm1ysu+829AhZWh0qjNSzgij6n27CKp++nN6u2FMqvAXdM9TYAZp7niIjS9iy+xHY5mZmWIZFXgyUShmt0aNyH1ygGWH7d5RE1pa39k4xwDwXBODEKatxXqVTSANN5fcHtnnp+foTvz6iVT0IkuG2zOmaMDInwUdNlhEi4UCPemy2fkOgNw2rddI9FxPReKRL20Kipzcwza3Sgj+osUDmzus+RFL1nNtEToDGkrYfpgNC0qb7yektf1e6JAyWq7yMDdHXEIWcUVtU93flRG2BlV99YjOt5K1WVyb6kmATpzSOe1lZ4c/u0yF4CQ+72QjdJoWuV3RgLbwqmdx27BCKOmTtdD4ZvRZKLrJy5LMisMQQ9bhOTzoEye2lOGPzNV6Ctkq6B+eQLjruFh5w7dg8dwmqLyVH16m1333bp6qp8Kys/We8OvFX1ytiVHRamxHP7Rj+saFx3khZ1SRV/zo9TkZubW7lSUQ/eskZ3O+4zJTpDz4yTx89w3DYxCUTWqxsvf/W9le7c5+eHtIjYm5vEWkk3ndTMbMhBVXeVKbtUKM6MiuPd/fk5fuw8R6dMpYqD8L4vaRJTXrpxNYUnQnbNf/7nz77Jcg9kxZVGz7LoA26ayPQ3wncPiumqKlth24pIRgHoe1bpazq4J4ssLC2T/+pEANg1zb00c+34kASrN9sB2niltsCm0FW+9X47ovUgafAWHdobhI6Lmsw0bhy0ZHuL59SmlbEUCdAN20CKiKBbnHN/hIXebe9CVU2M/itsHqJeXLtHKnRpfyU9ku7M9xq4QXM72czcs89AW1fBvVV2ifVdB3Tf70dt5i5e/CqQNn1TAyZxG/fMZL1Hd0tXtuTqUH/UCJewVxve1L1wBuEOg/IQKIkEmnJ06mxJxZeNyOZpyXChloqrxO3dy76ApBJ0IMSsRS8ZR6H3w5blShLjm80nImdPg/04p88JC4Oiz5xV9f7+erir6NF4TpBQRsqYm53dm74SLkFa+uz0vGx08Sp81xQ3UI3GkLKMc2aLQxOD/Kg/mySGHsbaD2uRcAX2ZiVL5Kq+Iw/vSb1GmRnP0R9P35bTpH8QtSEQWRcfCFOz2DqVzSDt7rc9Q78DPHyGo6YzYLpOhMBT8+gpFSRZeL4q9doHnrRWuNKu1pwufw5K3hkOEXRtaBH+x+Lnf8ve6szxANiDbniI/LUL+6SERtJamwCTmxcEbGGFGdyeGcbxN3NmFPpq3Fc63FosIa4Ww5j9AmPmMzUKZXOv6uz8OT9V7z0vRMm++hkJZraRYWFOdYqeE7+fD4FCAniFDrtHODmtcM1VE+XVm3tVmYfCNAHwWHWhp7Iy8/ycGyEBqJVEkhuYmXUqR4jPz89mFxLuVnd9UU4DzdT367RC/fz8YOZ9q2dIutn7SlMLgpL0CGpQ4vQA5zlZqfHWnqcq9XVIuvo9lEa5VWZT/Ryrbk2Lzblhm1+BXKiEYzalUsaov6aTOPH5fZW21IOHBjI/L8jjkZWrNkdVJt0rU4+w/NJK3TGystw9JRibOR5YIf70zNRwdyF8yQ9tfDNDQp0E0w2jIA4ut6aNZ8fz3SO5jQUpUcqgxMFiOJTASDpOLLv8nzig/j6VC4ZoH51vGhNvoZNRCb767khcjhVTTedFaQW8SGHZs5KuluZlrolhVJ4aWxy9MtMps22zrTfFr/SM0dfLqFS0rSHSdCwdLSrT4gZfAm5KF0/zQNd5oqEHbJcPQmmvDD9Vr3qd9LPrR9hVtdu0EM8VSlx0qaeMgNPIxpbbmEHVFKPn4Oqqq5NgYzyOgvlFTa+0bKD4gDBnFTvLwy/JoiT0GvRteyeMx6K6ekaQKUEBljsL3KDRiMh3fLUH/FSeC+dpxufuaXoEUV2kyTI63fvHWDMC6Vb5nvNI/rZmJDqJ4Zjo1tHcWoBNj7tljofkEwN019jmaNcApsu1N/jN3OpNmShW2Ofe1YTRGRbdXWiSlRXP6Z5B7ZhIZpVFqE4WxthfRNS7uCHjIBw+JZHdABZn3uyWHIizhj2t1aYcse6aS09pCAW2FRZjPdUynGaZuUvkNQNOZjJcx42afAFFSDIz3Q02Bs9KWczez0c/THhkvbpmVJ3RWDJqFCdnNo3P52OgMvTNvWZkOMlPQm4ys6oU59xZP+eHjZrfrPfEARDm+fnQ2amn3PPzanol2zyMqLHMdpevit/NgGB1ymrgVyNgZCksy3jcd+LXY0egQBOjYIOhNGzwzJJ0WrhiaBPAaNUbzvL5AEr6agq1n2kM38/r4bJVd7X4Wym4K6uzdTH3THUqdFOHKU3vDtCTypQ3y0ya0/sbyqLz+xvCrIuOxqmd+KZGIgvsTzcmaz2RlS45tSwUUtQIqp8vIijpXv8FnYHZfMMSZWtm9dZ0Y22YUtpIYACzwHThxuz1gkI6+qUC0siOpXrngj1YTYcG/n1RG1ikZ1qKRJ3/+4vcv4BqQYnbsqLfVtIAUbpKn9WA1k3FWd1dRHXcd5tYiMkXR5xb7gbpXOot6Rv9hMKswq1UOTkA2yPyTUCyUY01xQ39Wiqis+2YGdEDlMd6m6ol8jb0WAjPcK4eaT3qNNgAhu6xCAmsn+dU5TkhslcTLbfgfAhYOHrNmAOBmuzKiADVODLoUjm40uxWbUbaymBog1Zs0F3whINjMPRFonvXuK+N0Pw4aVcaETRGeBxXOooEOXdKgiuvacFNQFDmljLKQ3Hrd03nBVt/KlDo7WBmA5lWkKrvtVeKgGtwbZoNGhx92bs9an/c85yqK8DNLq9OCedHgabb9Kv74mSNedCgsnvSSuiRBiWQN5v0z1kagDNQWDlmeoyu3VMUgnAEYTXAVqnQCYPyiLTpaqTsaifDIsyccpAtvDv112kzGPQcP6YKw5VDboShkUeQJC3zvWq9zfPq2r2+OpWQoh/+875SbfWUApSy0o/TTBHEtF0fMejuT32+KQqo3muxmpfRcrfu7GpUP/6ozvKcINgFM2P4gG9Wy4fY7J7qyZ7suofMUKkVejXVEvFJSSX0qGigqYVgmqSTz3kEiL2pa7nDDoqds6Ak+Xk/egWgLjzQiK7+fD66meSe0Qm0vPc+BlDByIWvp8QjTYGDaYPJvEhM51TKo1ACwc11memo2tpIxXSPCqS6Fru4WRdy3qjZcUZo55WlLHjewoI0a0tddt+Y0S+lf/V3cJ6d1jXOb2suvijLjurz5W+1iJfaCkiVulAL+fea+B7qGyOop7dvYhpuevxcx2UvxzhzrSF7uxivtJ/aNiRqwP6iahRfOAiruKc7ybGbZHOds4MeMwqlj9thaMQ65AHDCELRGaIXaLqVouDhZvzz/MhbHhHniWU49cnsq1C2CTfmzjD6LRdyU26K9q4lUsSCGYecrI9cDrNXxbUg6AefMQw57HYMusxpNs85WN8SjIgw9adLsjl0uNl0igHlcXbrZM69BkdJ67SvocPIgUociVH4c/Xz47pZwXH3WzZ9HROQrXyM26Vu2vgEmiikVIoFGqBglAERT7i7IuxN/oArM7iM770ByOnxc6TnnyoqZsvsRLyV/lfV2hZ/DR0DhR3VALy6Ezd2LVmiEKf382LRcJNwk8cr2+hmzU3yEt6njPfilPnJ7uH0pB6D1u1rJO2tNovKl35MP4vIIFDYafeYq2VQFg/nltYJMFlruIaO8HjfV9p/Gmq+7YA95HSfeDp6ZnIBZu1c+PHn3/oY+c6EQj5E7IEcTr8q4pybwqb/JUetGpSs9m7/ddVsDA8dVbJuAXLfc2sjFczgcETV5zxPVUkGvoeu2hIaWkj+/Dz//vv75W82oDA7wgn7fD6ZaR6YeZ5npo3M6pGMEqyuKvXLU5fpCb9ORuuqT/07ZK8ODNPznGcwYd5VM7K/4ffzCYuqynkt3KBUVPPdFfh2njgeDkzVlhRuOMI15bowRskvjWjRSqurvmKWmQad+n6N1IetOcOcMyvi2sY6DZ7Vvc3ys6eVgJp7dpLcHAXdUFJoGWl6KzVUDRewXzSPdnNWWv3JqQ1D0/tNRlvsUa/ozNA42UoygdzLu1IQdjNTcd3g/hXhSOgD9ggG1BW2ChbswMur3pmZzdxednjvoaVjZuiqrtqNZKsDjfOpiw5jMUz9D23/sUFjEEEjUyOUJDBYRW+cqGo3ij0ppO6Hr/IKJFfN1fu739oiOnraaRYKLd4JGaCHE121yYCC6H1VSt010tYrlE2fqnpTwu3Lq0dY1/4FDG72yTxmNjXTU1cet91JGDoVVaHBJY7X4uyx5Ph+5JDWQvGpJ9zdzvPoOPagmVfWNUwRgNozjFIf6bnbtDIApRiUwTqY7t9QOKubDW8JA1c1/BWGGY33OgFAt57aCBF98jpns3hnDWzy/q6lUh1cQUW7Owfy+MRzNCu1BLYRM3DqRw+xWX58APMNxyYpUMLcnhPnWDhlBhYA6AwQEiSo3DkrBYXWW5J27NZMGei6qivbFCqyoKgArf2osloWxbfemjL/vkt34SUayK7q1Exa6ypkmBPzVrp6IPTIEwDCj5kBol6EwjnBfFOByXp+uq/8o0clLa2Zna59RsxNzfyd9RrSbbo5lOMMU9j1KG/kvpboOS6SbN73Ix/CzznP8xPuKrMEkJkEfn4eA44yf1qK2D0ClDm6soUUK456q9WcbBTniVEexU6RMoVkqdRr9FW4W3U5GXEMlFjL6bh+GjGuXYL75AugG6cmdFIL6tGI3/1mCkHpmexSOqw4CWUyGn0n4usipDTKWhcHq2d3G0xNb1HBDbehhM2mievrdV3RCwgae1CZVAKE/ijb3b3NCtgNAr35d5ubL6Py1YDohddE2+hG9662d4zfBUVWu1kX7ta03eV1Me0rp8FXRmRr/zRpKnqkiXfnl0WlvFT7xIJGd2Ev9GOgzt8lQix0MitNc02ioWxIAjNhipGcrlIqgIYvM0J17cRznFSSJgQ7qyzSV2koaXvvraBcPAPYyyKEK85TN4TF+lB7OsIiNIv0QBhG3593dwgAG5kj49j1D4dzqsNJm0YR3Z0epE3gYmzSbqnkSL/HrDRCXoE6ghTNs7K7zvlj8royuFiV9ZRZgKw3TVKzG0f3ZQvoRiijVYOnKADVKzfjIbW0LXaBPezYqryhfkZMD5wmt0EcKH+j+m//y2g00J2jHXOp3Ommuw97cjUJWPe5YF/dahEn8epBl5uMtu5yaUPWFk1T5Jl+eW0O0r0Qqyuk9c9j/E3yuBHOaYqdc7MeTjdiNI0ovn9kL1F7ptnFQkdDVhXi8Z6RhdvpwPZ1aGYMM/4nozEz3Ra6qDcN3jUNKQ1CzFvPYIAAh0PIWIdtBocoKgD5fp5HZCywRgT9ruyubkS43tkxFU4AmKxWJEN362fSOAb9Rj3t+ImTnd2tH909VOKmWdLCdToIk5U+/M3XzFfd5HSzN/fTo22+yuzqMDengYuqkDQ1yys5wWdawsF6i1fjP/fJldeB1EFvRmu0hQcsO2dgQGc33xkNGYpFAcH3TQ+vasaAUD33EW07a3jUH+/E2cbUTMV+SYUp/5VQzJ61dE5v6Capq8ic1q/aIIZgWNS81cX1UQ5v7oH4duE2AuskcDKztRya9+KZy0yABqgJhrJfSe9oK3ciwJ5aARgAM1Rh9WBbVS36VQfMcBWTAyFL1CU9szik/rN4Yv8i/rrMHYvcAuY+XeY21R62AOO1o4JXmKSbo9rofszNxigd165DYNDEffJqnYiWjhx6PJRWJGG/iYmlQkZo/kRovyRNsZOj4AO3mTpuL3r3HHOaZlPjgk7s0TlD/k1JGpKCmOgmZXx32lI46CnH9uUNW66G6TaPmT7HFBl5YyGgW9bpmAnCLEwQjbZ5oKW3M4uZ7hY/fEXyW69s7v6+L+kEesYjpkUfIsyWZpHBKW5W9C38kTuUuA/jjQxED8XCo3SPqxtvoG8RSxGvdKrBafUZMWpeGMPWFjBLh9uMykzKzLRW64G4SgMdbWKQJJ4Zt+tCGFWBj010d5wgkNUcXUsm1RrYhAHfFgQtd7HbNqbfj9txlmPQzdhyTQ9vOhn/1uvu2eMWb27SSHc3NyAqaO7xeV/RZLpp3jc1nEjiNs3K8hNh1hIBkXJvadiZmffd1wPD8MhJDLpLeIyiUmqtQDd8iZiZFN2il3ZGxfTCQ3nHvP3gnatOUZrTlmGB2JAGWtOQWQ6HnLMYDmj89/3lHv2OQX5eU0WMUPIW8FvdY0emKRDIz2vh22zeTcf77xtxpGvS8CHcxcw6kx7k0Kho/cX5MIoz0oIga8WgNX3PQuaqWpucDOl2cgwsmwGqhpzweN+PmY1dpz5H2KPQiYXylfYOsetTlTqWw09tj1uHatdqoXLdEHNHazRkEdVInrflZiD8RXCraSafnvDQdyRgXdPxDv4XY8d9vHQhieLTOWhbgSC9qaa7+QrzbroqerZCCuRkrs7beMtIsHfzUhFC9wYcfdF7DwGtTORNLQS+V5T+ohohufxIZHm3AkL95C1j/Gg2BxBxul9dFyMzv9vMVL1uW5BFJcwPgHbHPSXg8cUYHBwI9XWbGhCdFUcxcIPplh7H6WbVGeEzkioMpF9Y4AjdCuVvGs2drW8mbYEN/SJRlYDNtbYoOjhMKcxWXTGumblnbDgYC4+QJNJEJkvv5m5Vxd7nMNxWNew6r1sr/PycGFOrDqBmKH0CjWzlUCLzDY+RufFEVxJq4ZivKnkadwhfDsfuLC/x1I5+Lf0ohlDmc29/7Pf9hJ78uWeQAj5mE7VQeGdaXcCj4PvFU5eI8ONdrZXedgPWft/d7XFkX6ArvwndqrFd6V7VPohU61lVxNMz4CCHtumVrdSFERTrQgAokhP889hP4DeniOqZNq167Vs9L0eJ4mJwtei8yEVlkjCP7molhAhowvSMG19Br1kvKm+kvhTRivaQI7S6wz0rw6yqtaQqLm8Jia731YkgEao2We8eC+tqznR1eMCXV5WylBSZv9lRzc5qD8fgrfpWREy3RVjtJILZiJx5y9w6/zZUW5iBA3szzUwmmh5t6Px934ioHlNfGPhmfTrN/ef5AZH1opFvAhwRm+BXgCDWVQI8qTvcAoaqNOx+TiDXUThmypFB1zznUdCC2eamSl+XmUFfdcfM8/Mn39dIP2Hg+76yXfUehTbo6pzZ7saunqkZ5ZFSGn2p0RS6IUS9q4wOgpPLjDbcNL5zRqp10U92OTJWl8CfzlenvDatnbcJCbiN+w3uQuCYOwZ19tVZzK1KbrNQuKus47ZRZambZJ9OrRG9OR+3xkWTzEK6U4Wh5LE7MSzWM3ZBHRo5xe0dIlTLitt6RFLpT8TqR213BQCY9BV5tUtASScRf36mqy9uTsDjliIAdde/c07mGyu0HemEznMydcT3Rg6LITc+EetRdkD+KTPBy35zERbCMiMREZ1FIuh7uQ7OcexQLRhwNNxTzTRh6gyXXgrYYDjfkNH2OFVloo5Fn6BPrKgv3Kc7iKnNfbVBmfuMEsdAY+dOR+6mh5GSJmil6p6Bh09/ubIibEM5dFpVyVBYteZ50iiXnQLKMZMtn6O+q1xUcZPGwDXGtYSHUFijgtKiOt1OzoezpI8Yv+GcOPm+3TVjQueJDeu/xkLUwOiygNDX27UDq5SOO5vkYN+lzNdPZGfEoyziXrfoRISwjfCoHsO4RQMe1mMncLz4DgpxnnmVYt3KHH4r6dFV6lya3c3Z0w6l5g3Ns96rc3OT/N8MmJquy6G978dIecaFzbyfpK0YwwZV5bTP5+1uO4r+gDrlzZgJ2khnJ8s756ZLtGQ2vUndtSq+wRw/0phmZcQxt8o0moHnRPVMV8vQB/T7urlW3at2xRPPlYV8Izj0QrXR2NILzInYNhh3Mz9hVYKjZjDhcU687wszD5Mfx4xjvonXW6moBXUigv8JRKuUAbYjYqbrbYstPFS2ays7XezljOaJqqZCZnJRQJFb+b66d2+UIUuzT5Uqa4B5/KlNtBnYrWarjicIdI66ZXbn1QJAmjG/nZEgt7N0dnnYkRm8pdkkp2oUMryj9GKKy4dxFRX779cJND0S3HdPnDM3P1QbBgeKKeWdZNX3SXNIPrtKoblcqIR5Yv7W0ib1h5KutaMIqcdFC/Symm1egvKBLvjTWuEFqS2L121baCI5n6L0Ur9gHO8uJ3tSZmA3hd0MfHGw2Yjy2t/LY6YEzfutSRfd6AQCC8YIysN0lhulyyA63AfsartRDALZ1A2wt5xGfoPM7ZeYUaO13VWGXQMnMU88w6aN3SqbPQdyporiQzDhUMaHkFfeBEappcAJCxt1tPa97YYUAaZYD6DB7pFcBxqq9brpqdKEPhNhGPYVolV1T6/eScSL/vusuY+b2HZG0LkVSFTnzBoiesskGkTYoSHffPtj7qMoArAmCZMBZ52fXWbRVXFOvp/pUV9NEz/Pk/n2lMo6W2+kKEhpY7Ug7345AOQSwDWnnOfRTX79QQw7wGRmI2fgFkJaL4wDmvfnRYPzqntE4FJ2zdDD5nspuktsVLOdZbQgTXLb+vzGCT8HPdMt5fsnX3N769WPPzMnngvAGoA3c49UZTwKTwPdo/ud7LcaGLcdPc5zBAoBcHOTdl4D4qBn4hzRyqm1Kejm2XmeAGFtVRURRukZLN+08CHCvaskKzL3Rk+2R5gCopVCaNaN8Mh8s8pP6JSjIfM956en3ixzCztdXVPacPXw2gb/mZI9JYsipeKoetcBRPn3gMoUf2nhVSkD4+zoiufnp/KlkhuGWbneQ6V+6KA0o1M3kFvoWyAL+6poyBNybYW+zup213Y/JDNrquneNVNj9C7N+s0hC1ftCHTr8nBadsFastOqFHu4Xn2TF0hlfyZEWFeIqN3WObaNVTMDbrwKgDWyNsRUlzoSOktH6p6AOrGEQ+m/33Hxag262IoGwMV/ZgDFxO5H4+t/bYx9OTS9kraswmRZuCZrM7rrjwpU2bfqkC2UGQaVhlOphpukVOHfyKZyqf4wJ4509DtNqxQLNGe9qSFgGddBy6ZqdLfuVbipUE69u1VFDDjnCZG6m1XUTbfn52Amc0VZ7uzqMHO3HihGcuf3Xp7RNke9XUo+2yQJwLN+47i7NeqcU1m06RpBTz0dZsPud+Nn5LfvKg9z4/uWUD0TrNGtSPrNUZhrfbSdu4mhFlP9Szvd9/Ed4Dkx3Zr9ZdYNWYtnqnJFqpB9H3LT6Z8cLLbl57hHaXnGeERXe5z13+2lUnIVuJl01tqAV8U5U12KudDeOtPmHk+Q8PDw77QO0koqfunYuylBBcdCtsyR7Me2wxIrHALthFnovBZccxsxGB5KyBFdMUDEox/HnMeJfjHZ+5loV5svfiUFtBQjUgFK3PNWA3ieh2C9L9VOJcqVkNPnxAV5ueOwmgzcfHO5Z+/dVVb3XIJXf2vU0rl9qNgKSb39v+9v9bRsMreXdanvOzdkVnfHc6DOEIkcZoZ433cGWSU5CUH1xGlwm1lhEmGa9/VUSM83GDNobqgqscthbsHqzE9+Pm8LyiONpn4IOfIioqe7UDUzc57wGzkueL2n1Sugbgnp1rUDRnjXa/dCkq6wsgggZ60FCsuauecoZgStcGaEr4gezezMBEfyhVT0EKjx32izqRtwtwjr1XRyVy/Jo3agxr0vJQdY/013dzZaEAeM6j3u3AlgmQy7SVwKBNEjJEsWQFVEmAlMhnL0Vqv2vSvufoBZcB7LS+xBr8VNQ8OKoQHp8lt5/asu6zuBzdK7Iru/P+cAo6icOSGe8xvXowhIaPCPDdUxA04IsydnCy6EqpvTws4TEX5V+fcLW9Qa1LLCOce6Uyr0qexJcp6fc8KEapub/lRGztTCLK5NG4OSxvHEGmGJBjrcwmmGE3bOtrYpJ3ImyemqcD7nCPmN2yOm7cnc3KnCL2B6xJXOOTHd52xt4RaeG/7882OG5+coFiKOembGj0lEFJ0K+6aHV7e+XuzUUO/7aosMP5VplxIM30lHE0Hn7Kyx8XKcHjvu1uvH821nXl+uS1y8dseprX+Rs5+4jvmLPokph1iegf7hnuWujdxTicDVvaxoZqBTxsfW0GyKE26SYV6jrAtU22zjxD7GGNBFe4+Hq+WwW+bslRZI1NjfCjCgun394Svmzfzc+4vHLQzsNA/p4Zyh+8MGMJ8REjtvquwM7l7AMYNCpmZmy5DH4WB3d4TPTFZJfjqYStk1JJNPSSDsGiD15/MwMGgM2puJnvrI/TvAGo6GbW4185wfoUmfz3sitlJYx65tiFNXT1M5TRfAxfQoBEKtvOE+lyTEoLrihNOmplp0NLLr54kZeNi//34UrmnhnHnfl2VmTrPf3183V3CMBDtdlfVm1riZxyy5DJJVQ7ru7K7uKQXnkXg/iRHnwg1J08KFMbPP5wNSdIREDRjE43WLX/SIZJbvEtm3e2CIcWHBHDonW8DoMXvfzwy6K6vd1+lNXaY6lLWTkRGsLAdr2swFCZnZ+4q9jBEJIcBZ1uivSnIATHgsKdq8UrhZZ6xt5CKAq3dgT3t4p0S/ujV0x0/XhohxZaNi83q5NgDffpLvVSGFT/WO6DplNyxa7qC9M0jCoCYysC9AgrkNJ9oF140zrZD4OI5p5Xh2lTnvHQOs/nKAloXLDfR9+Kcg0njgu4+MNGYiHBDmM3N+DjCjqruH+YpdxwmvqenxY5gJRZmROUXqcYGsgs+JwRgpx6JHlOI5XToX09UexzHIt8atKt1NwEMcJ4bOaUL9jtPneNeYsbLssGpr3xGLnm6KkTUbkkthrDI3jgGYaosIP+tg1HMj4Z2b5/u5i54B3Z0gukr4jHQDRzknw/NEd2HkZtYJSg0XRtNePN2jb5ztFpVv2wgKqCz0vJ/3cl/jEflK3jcQpF65Qnda9RgB6qglyKw2C/3BRBaJ75IUpqv9hO/cNDMDixkJLFTpOhY+s/vyiZOZrfwpiT00p4jIN+UM2/tmV8c54hXWQGNW1dXlVw6ENTCnuT8nwgvTxvm8Lz3ejUL24XQ2/v7INsT7fsyjAFdpTE11nzjdNQ0PST1Z3TcZZUXuSlww0CzkVjDaW6+kH+6cMUVBAK6VvKaImdzYnEYbKTWUuzuF702YmfHzeV2xW1IGd0E5DWaDDlLmiXOi3opzIG+5r6Sje8gSClTZOUXjiZhe48H7eTFbUzE9fuJ9kwBFC8/kR30aBIe0qhoxkMDPzw/Qn/clcbPogb357PP5WLhijtCoaWWmC5GnRHhDU427uN2e6eRIsNDuVqXlyRkt8fsJF+5hZGZ/i0qMXjWqhteRV93hjHMqUxeLmP/q6VZcCjWMS/xQXZL2nycwSExX4Ya57lSO0V9LttslaVZrnDISMgCOALG7LsisL2gmVX0jrSi7mm6qpTSXSHumx0MFoDqXv07jvzk2+2MLxSlhy73DkMgJ7d+61Lk6LiFuPYNuC2l4Fk3SXebunBJLSOlBt/wSgBbl0tugG5Qjdyfdfbo8NhZ/FMS0GcaszAhXKo6GJA9mFmfiiYuWzVTHOY4xx0OvaaOrghthV+CuZqEK5XyMKoDaOeRXTMGr2+Cg3FymTModR9DsT3h2PU/MzPFoS12W03VOVLevUaMteCK8fVbiCGwOWButu3Yt0OHcwOynKv/p83Ns9UlGpQ9qpwWh/pbjzwJpZpKOeOzxNN3T88lPj3QCJUWmXCELoGPjHKpaTbCZjYGr/4bUmt+K3iYMfM5RUGJ3u8UJR2HbTCWK8qOzTCuCtpYrhaYZjDBHhAFQRAvUU9bTU2YXYZztjO4e0mYg5wEAV1UvdSXs+0Ns5oxrJ++muZmbhaTi9pXkSNkizEFU7dDdzRBOD8bDE8pis4HCOijRdYS0nLM9AC1MJpQ8qg1J7E1ETG0PcFW7xLIzMy05Y1YuPHYTuLTM5eeVqVidEiv9n5HT7ec5NDvuJF1wmd6Ars/7eTvHxsNmcL4xWIsLDkWlrWV3IwRUk12Vw9YzdkFbzCh5ceQhmqrMFwAMP/6DboJvlkxLrQR84PN5Z0cE38HVqFzcrpLEoqWC7DXEqW5MOFN1hTtmKhvb7ALVuru7y+Wp/tcuKC1ZUGxNWCz/VePmA2a9czU2Wm7Etbi7nlICXe/Mosx2tdHdrewgqeTC47/iy5t9gqocKnTEv5jdbHhDm9k5j5mi4Olxu8t39RL2hq7MemVJERkgp66AG+EIwPhxGm5+ru/gjdX/3LN+7q/ZN7lBSptdE/Z/tI4wzEacDLDbv3JcdSHzBv3PQsoqwhSVqiwrkNulTowss7xZ0cRmpZEDjmE9TMctjkfYcTtBoiNopjCokoXI3TDVVef4Of7zuLu7mx/fU9Ix6FXO2CgF6gqgxgzu1H1z3Nzm+rB0tRvRRvSUqYFL2M7NO52b2kSICSiiPUR7DAMnzIx/nmemXEp1zqWgMSt8V+vk6LtyR7i5MmGc5ITbdJLI9yMojwb5+PRS6H0MLfyZZTK2VUd4Cg+FPNrjZvH8DKT96jg/me93wSetoXI42pqYAIgzuILhnvd9zTwzTzxvSxrERVq2z2AyXwnYjbBgyz4+eN/Xzf1xQaHdY7Ts6hmlLQLorA2gG2l77Dt0jQAj94XIARm7M1+NtmGx5NiqAaZuWXZ3VdMjuCg5MkvtJT8/j/IRsCENe6TSND+meQwRHvnmhRgm2Iam/FxAZUFcNDnYHNaqlPJpegqp5aayjNKkisO1RrNpu6CMNhWNPYqHIW2m882NqGvECf3zv/mBpAjT6j/orM/KvUaQwhDVHQolnXWaGvmfQjcbszdVYMRSHYduWOEtZt0pPFJDQ4o9BcxiMOjNsXIL1VB0V/qrICSBex81LNIG8/McYiO3TEKa5qeSxnOOuw2aQL7pvl1pbyVJSRBA6Nb0cPXpSFSKRqnyRdwuma8sP3qB6a4sjQbN3WuBNZLMTgKds9XH0+72vvqyRk2/WL8LRqHrM9WpdYHAm6kTWy0rKm0YwhmjJDjb47plZVA1W1ZNzWo5Bbrw9pNsu7IIPKNVFaC6wRugMg2iL7HXN5h9YdOlnK8Yb74e75Umd5dgA831lxvAl7EfKacVGAPBRMP7u3wvH9JmBaNsyFO2mnNRqMSYwZYinr+k75IQTVBpww0xvb2ioA3lFklHe3zD6aTDcaiSFrsQ6Eemn1WpYAY953hl2vGZiZAGbIRBVWnhGyy+zQjvLqAjbMvMDSrj1T1r8haSQr5XjP8cFb+Aej71IUx3ygX08zxV2f79kmGcc0yHnrsrIbKr1YvFm/QhxaVHdI9Ed1oLusuUcUulgapCAygWV50mV4qm4d6jfwcLAxgRmSrR3iaQmT4nNHkN5jwnPxuXmm9qXzZbObmWz5/nUVKFJK6d/VbKMhcnNJm8v//SnKCZvZ+X5plJw3OeZ/yLiZs7h5J/2OayFhswz0w1AUgxyRUMWFefOO725utxZhq2Kpruijhq36XZhqts+XgPKRN/vmnuNBgsq9H6IR1AY47/uEd2ZxXDSO/m8/DnmP82MZimuZK0BQWmoimNqht71SLEjddexRitqghWlYcLsDQPVSToB5w7SwKTlc/zdKrZZu4VZcg0kieg4H6zyooIcelZfSKqXtCyEuQ0Yqd+5JtzwD1fMGub0i++3m/eTdEonLAHG2Mg/OAu6zNAhCJSOd1ukcqooJOTb2o7rJ5zjmZ5NfNMln4TrYZdpUT70twATiUtlh7onlmiUyNrxHkr7ymGntroRHVtugGTn4yI3/fj5uBG1TYGmBOnu1uCztJHN0ZmJggpj4B5fn7e98XM5/dVv7z2KvFlmFXi08iRTXMFMKqvgsFpWaVMym134cieKcpkdLzqZNeJBqxYW1DL98C/15CorO4F6DGQkJZLBxfNNKf7VRKPYnmm568xSEwOe2cy6fj3atEucE9yHV122YKRR5DLHukLKa0FNPheCSDgrlYBoitccPQ42ZhzPISn658wc0V1YdwpjqdFgK3vkqHE5j1KN03WTV/CxhWOX7WLpHxAuFdXbT/dVq3JWdxTEdHar4B6J8LVST4ap6oUAfLnz5O1oWeobbeR59TW/GhDuAO9qv8RJ6t6r+vFiycqc91CEkRs46MuxM28sy1fFQFeuMRbVV0Ms81NSgmbakybRffehxhOtYUN1jNuvHrzKnI7AHToyAIiMX8DXaUIxs6+0rD1OwgnjeNV2INvebP7iqp0bIbmnekRamzL96XRiIjIrK42P12ln1GsFlUEgfWzd6fKcDAjxWf3OK2AE+fNT8nZaH7HH10BJmVrxKl3HW11k+8G44xWBLGvZHDtB4Aczz3Dofsz08LNobbumTD+uPkUFVnhXrWJIJLm0LTcTlWW5PY9I/N6OAYSnIgyGugb76YEPVR08MqWZiWalXUiXi1tYjF7FJQGqTIANNx89pbno+gnWPf8OT/ZyTC520CERfW2JQOtBQ7UQUIQx/23EkD+vs9zBFKrej6rzCzc+6IZFLohAcwJxaIBOGLEHSzFmvtQBm9U1fXjQwYxbCmOjKkjewFxr6hZAFopdeY+Pe+8UtcgOyIC/vkkDcLTMMjp4wLftBJhOF0T4Vq2lH7hJuXIVKnde9nH2U7HMpDuyyXgYuFEV5LsKd3KNzt+8AULBOMbCEYIucqZIS0i0I3RZc/McRO/2mitstcjQqkGgq5XrLvkpJE0lkKujY41BjdVW4YNU5v1yVEG8i6pwwWE8ov764fSJP8fiGm3Ci0By+L69gbyJtjPbIUAZ1zn8IwvLt80B8aCgxaC52vZnJmOkIZq3IEpLkRcRk7XwkfqHAJEt4RbVm3U/noA9fajqs4T9x617j7HwnxlpqbDcIT6jgpTq4zSudQ5MZJgdj0/pyt/fs5Fz5pkhAa4pakV/9kGV0nAxgdLXLOTHKFY8iJhR2/cXTcJ/eFXf0vA0JXuKvkFMEoX00cIQPFIdAUodZCmUlzd2EajY257Q76pGOfKBq363Q2MUIwM7U6Ct1lUFADtP8GwPX5Ui9GCR6WtXuKdY6SijJfVlMC0Scj42tiOJDocGBVbtdJ0gfMcgBpgdyetEnUJkrWttmuDMhvIwDQkNE3YNjyURj+a6bEw4bYDc+vsmnbzLGV/mxnjCcn1YHjikfYxTnTWQGHIN7MFIt1o7J/gv92fns4aqIaBA9WrsnqUYWcX6Khqg1Bpb2lEuYIQ6ReqUlyOuUFokCGzAi4icQGu64ih6gFMOhDjdFdpWFuzqU13G5yTWQlOdfkmPNtqAI0n/PP2zLiZZDB0kJNT5pzqn59npjC7er1ZMjCLE9oZ8wS6Q+ruKZ3n01MjhkD507o8UtNxvml7paLZqKnWYL2ay37LTzx2Pv1Wp/vpYe8FJh2PozviefPVGKUER4EtGwE38HN+P79GZbFZvk1l/csciyaWFBV9XddbW1XiA/VaCFHBF2S5SH24p7p81ZRW5fadPwmMgZjt2t03T4ZSRdP1lpMoyrRl6IHwkAEts6gu+4gF9Hu2d1ixHArTJjZWekFT0eljNF3SexH0BmjjpmMpO6RvZM1iLMoYar2Ljaz1KH8pAnzBDO0zO0U5x41Hy5aZdNHm3PCuG69GjO2MP+4bJeK4lt91hAp8VqyLNuNBbRmWBPhyEXZlL2qE785izq7xoLXWlNL1Oph4AkCjFJdpQi9XPKrODHaXezih/fOrabSFeRZ90qUUYQp+kgUIU3EeXcBu+z+nUckCEog/z1FWQrhXvQoGrpTRkScCnBPnfd8wbxszqykxo52jc9vCVLi16eTKxoYYnlq1jJmOj+WdZpTAo1d06RwlVY1K5Fc4vM7JlX0JMRzFYjq3+USO4Mp83/fTYmlVnFpiFKaq3vcFUZkjdN8gn4DewFpBOs3M3OX92UVXcutdP7aroLshTuI2DPPaEWRUJrWyjRpWtnx7MNXmpnQKFz0tE4Lsaz1dk11jsxi96hjf7JmuxOhHGDO44Qlz9nHDyCvI/FT3X7O00TJL56MQDMqkxpW3ig584mijJKkccg6vunsEZshpWZNySWR+Bl1TWN9lT6npTudnad9f/xFBszdTsQfbEby07UjQIjxEyjNB3voEUiHSAOERIWqBOg+rvyizhAM9nZ2qG5up8HCzcHc/BGis9+1qo/+chyp0JKdH9ltXaTvw84TOznMeDt766I9c9YJQ0G53Z1Z+cgZ68GwthMuWy6rgYUbLSlGvqpPTn3lGc3S2rFuYBbwW7OgTR8/NlRG5zj39u8JcBZCs9D5zJUq8YWcmUTI9YhaXlzbM0YoF1lFIdBupI4xcoFUjvIaeq8WYrsn31YAE8mvgGIGe7ztrld5xBGvmuNDvjeyX3mzPCrBu+8XNMBZNoD9wE/ozbIM5N7loYZ9dhmYt2pTnH21SZMpL1W1TJ0w853SZjaJ0JMiYqhB1TUhohi4jnsfDDcommjHy+ROkRMmMoKKAJBz453/+6NpzM9uZQwfJhDs32HgMM13dGWHnhN9OYRI/59H8LgXAOQa2fFtCjcw4XZUfHcQhBQnRXe7cdsl9StrDFHGxxrf9fMYMTg4qQiFVqVTo6aYhguGktoqpOOvjNBMnPF+F6HR3FWxCLpKeZs1I1d5DcBPiyPfvggNyCyPnm041swAooR6JBfu6zLdbNd/k8ES0Qq+MOtPmO2dF4K1mu4cRKTxqpdyyB6tce9xnM78wg3meoyMoM4lNBpUebsBwJ3VJrLfAzDF7EwjNqCyV9lS1y3shi3v1mJ3zCGYF5RRG1zBEnSno1Wraw/WmVHV4yIlWS+awhvlJf46H4e1//uf5eet88L+flzwyZ3p4ky4VMNkGD2/0FUzxfd84p3vCvbixmm++NJlUYefQbAbhzLxhruD75s/zyL4kgNndppDdI90q2HV9/9NvpoeHn1f5dJ+CxMGzWTrfidJWaAhgfj+/4bE6WcyCaVgVkGIduxpUtYjEBdCABgy49uiBcezz+X3O0XjrEZg5zyNtibxUI8aA4+5vvtsQqzQL4ZkorCFWJVs7xAgC881tt2nMZH7y8tKyQbJE/9xJ3ByVRTcY3KMr3Z58P3FiujI/Js+LdI1qrjdyXHRCl+IL9lx0qtG9pnUX0lwhwKZaxMqeamhyrI1Z9TjdvTuQzkya+9SblUVjrYoBvBohuTLNWG/N/emlBdBnGHFKYdvcQZw7oY5zB46d/BVPu5qekb6pa+N2F/j5NljNTPVM2XIuy0GoD+p+qsAaB+RY63ATWE+jCS7UHeBmBve9idzptpYJ3RkEiQm3xqyt+ISuIQ/OWGiJN17UZK9PPf9xIuvN9yMiQWzC/lKbulzmSn5uXRpQJ5oyiAh5jJslU2ecoz+ek8XeQVg6FOUSklQ4N0FSvKnr6uBdvGbtnPrp+NeiO1I0XcCl9OOAEx5aXrdDyCBn3LyYhdfARlyaEDZOi4jIKpsht+6qqtU2ebl7QZ91zj9vzfu+5/kDdmW30hN3CWgJIncGHxhgbu9n44T06ECn0ucjKN+uccmP25jR9PIMRLX7EN11zvl8fnUaSvtvdJn4AGa+MPaWLSrWEqR1dzgttkJAp0TL7mCmdmqPIzxEDD4NfkLIOLjMmBmN0T3m5jGjqDDa+6ZPa6URkusWbqHrda3gUvP8RKMryywIhKJtULSjAS374mdnCbzFbNzzrTFTkhphWblJNd3aPNxclK96PSvTzBoIOgwntqtAkiqt2o0ivrVEwn0ULaAf1qXdznrNrWqqyzyqSr8ayOm28M4SKampZ2boJhVCV0rEujDFnmKuTUMynvOc9/OSrKyf+FP4zK1wIux9P24WJ1oMkKbZGgvPeoUUZSW3sQSDQZGxofy6rCTq16xpJhuU7NbzZu6CqAVLpv5hlzJWJdDqu0mOFt4vuU2YTkCnh7n+gRkh5iXK1Nzf95UQe1rNSN0zU3CPt1+BdZe5waw3npfBATCKqlM3Z3Vn5fPzdFac05lxzlzHF8367eWKNa81FFg9VVIQAToaxNyWUi07E5ThWcAJNG+Z2YJCtvI2QjsUYYrn7BX/ayQjN1GGs2aC/fjajFLi6+6aVriU5HlaaWiyfxNuytZkHCEgi+wZcdyB5nQc3xuOe38dD4l3pcDVgnsiPPTlDt3Y7cEBwq0w0wXOOaGLTCnINBKyDTaBz/v5ef5UVZjJ08OzS5ES/Fv0+rQT8eenscKnrDwn8q04TmNlYldLLLg9az2RC0FGVNkGIhyhFs/dC6SAcHNcrdpxay4wJUER5XuYNouZAlj1apDRurivPKaBcHa1fX4LGPcQF/l+Ms4R22O+1Tpm3oOahDqwBlKVaF5whWGBcY4fFbeo4a8rS7s8IUyDVcq48ColIjWwUjnb6NoxFbA5a3qdb+jn58fPUSI5ae5xl25OQ9nf+I+dTZfAu60yqhSynnk1lOriBWDIzp72sHPOJvBjZubzScDOORuCJNPtYBRt0v1EAHAPkG5mMl9z1XInTjeyGsbGUApko5mH8WBCKuMdvqnB8FgovbU33Xc5ZuNinMdDRNwn35X4qTIb9sW4hDyotkmQfa4fghi8+ebmP9sFYXIGbxfY53Fy6DCsSsRVIizH78KA0npXDUAKJhFaYj7kPuvbLKjLqbq68/NWbgev62Xbn356Uk5mnTuZ7xPHLRQ+GrFpwwbLN885dybVuKQpqo1Adch20V1vatntboNNXRidynGCh8nNpoaiDXXYsBelEOJEmOZiZfeNSsF6Rl1RGxuH1koIGqUWlRUyIqDnNbynS9oebgmPQDOFXvRFYFbnCGHv1GdodsVgtOnCTL0bGsq1HIJKjRh8R3XN4p0pXAaKhyJWOY2ZlrsloICwabWQXmnQdNb9pZpkVYMGA/G3Y55LH+36gPmLWU31+jeXERAn3+CYjdmYw7nK93ALTjgN44Y4JluPO5WgcI6bddzg5Qh3Qxx35wnjpDmeZytcznF3g+wmaC0HCxYBXYkuJfuawYINQTG7HywiT/x5ngg+x2QzEi7kzqPabbTJBNAtVjls1WTPE+7255/jDqLj8XD7nz9/4uwKiO0Um8VKDOEMhxaduG0w2Jbc1n9jRIQdmaUDg/7+Y3HcfFRBE27KN3XjdGImws8xcGhDNjhGhIdVZ3GkDqNbVZpThLjr8SXhLpZNqgKJ51xBOmgfxeb09htU6Sk285mbMSc7n0G6TGloXZ+ohB8ENo6KM5NVETHA+/4+zx9RTxLUrzbxes8sDAOL6CrNEPsEY5ahUkb2oBea4BWHzPf96lnhfGa7u8dFy6p3/xpUzVJSMxQSKp7WvK9+qUcgMnpaZQm6k6qT8MzXeI7jObDPS/6M0dwx9r6LntNY1QYQlgt4cWoEN2a3bLpanQU0v6UYV6la9i2sUuN5nfNomMzME96rG7whkQ0zOv2drGxMuccr4k6ppINUkCEAs+5d+1rqdSMNb5aHYebzmzRTaa2f6C43n3tnnvPnNz9huzWCnAaQgOWU/oTPc97MeKKkM6YPpt6U0AfuJDPT3Lrawyr3SRD43dqeAFzTWfdkVjhAfj4feaojvLNpo43zeeJ9X1GdHlFVuMVzImbM9uzLqrCYzd0s99DvNzMcdYbA9QGZiw+DnHoDXVck863nOZBudKj7vksvyHQLPUdV6iCd3tV9HVU0GqpS9PKF9THyYRClmrMBjPOKM7Cq1OEuAEZU31FHzWB/XiXfKNz0avakH7ctgFRxzaYc4xLa2A/hokC70Y6YvysE4k6v3XRgZpNrNpMKMsdAJUsS+85IFyLSR/OuXNbnuJkBDmxsnBmec8DWLD+dES7hpijzc46A4l5GHV2lHLeZ+efPk5nXqdYzfddxLtUAAH8kSURBVPRpSqfLO4WZ4TIg5/HujnPez+/z49NDN0ydsNV7dGm/3kvaOKwtxQQ0tVWXcZZ6ME5lWJCLCwFj7lK4ma9NwZ0XcjAOIpxk9XSlJCc9SWNXnuPA/Pw82lFm4G6VH6z7FbfXWEpdUHpwjDy6cmBOVoq/LTRm/AQ2ygzVJVnV9n4AM5L6cAbVdRdDeWVZNREhEYi7v2+aM86RdEGV53oaK0suFaN39tjU9GThuuCqW0slQYavFWvXI4gJCfPMnBk/6r9MUZcnoqj+GdBX9kGl1MIkarYrxnJz7cTu3tMc+YEx4HRKYGo3+q1nZAR3erFQ09UMl5v353ned5zzE/w5/JRQYCvZKDkMUySxOLULAgJE1Us6uK24Pz9/3szwEIDeIzYJChurnT0dUxilv5q7IoFp5lrFqksau+q869S8/WK/i+XqpKqmA9Ogdb5mHvHIXK0m3gVkjdMYwIONJvB+Xg+HwRE9Bcxv5p+fn+4iUNXnRHeLxRni388vILuWSkEIKk6EjA16Imi0QippMiv/Of9P9Ufjvn5Zd/dV5Zut4K+e8xDMLsVcdxaG03h/XwUw2MJNlCpIN/qbeVvoUDXNIqjAtenRAwaaTd15mKQYchjwzaMxt3rT4xzifV8z6yyFZxjNzKYVR9o0U6T+IjYkZmrGDEQYOTbToBtULGy25EHvzrBinJnQy9gjUex1COk2Vx28XuVNFe5p+qIfOhtG3yydm3ci3Y0i/sFV8cwS3S0PShNqi2i1tYhrXveAHPsh/axgfbgh3AxN4DlhaHduewQ1PYSM3GpX9FhwvKtOHKCn2LMx4+7Y22WEYMGPvKHTmOfZvmVeoki1Bz9P9IqJ5dEdI3+Ov1kzo5QhiDbI5Kij2IByF6fVHuhCyC9ShTD0eJjeZO1h8jaXiLquCLpTLiLFyRBltKFSIswIKCZ62s3iiarXFPxk4xYLrGPgUK8Mh+bU7T4zEVGdUgA+zx/j0VOCntgCQTDCs5SC4uSkLIgzo76UURFTc/B5XxXoaC5Qxi/kFbzREXcl1yS0E2uVvKszZM30mwophSptq4ZwmhhgbGjREnfTtUcMzcCbSrQXx/Q4laXervxkOWU047tlvs/zbD+KKLCu7iEs3GQTqyqLWG+OGYZvvuFnRk0XRjefUEtUvu9lhjkNBTMJ/RKtNiafoxAFH+UeYpz28xN/Mv/8zv/7ecMjCUkJe9i5aV0bsjjgjZsnzbTcdIcfUWS6m/Vre4RhanD8yc58X3PvmuJr5tPNIR1G60yxdlSbkolA85LGFENaZyqefJNn3DAUF2rmxyPzNeP7lrvKRqtnhCNXNuE16WYW6uz0UWMRCEWJVdFD7akzEx4W/vl8qDJLd9GMVQ2gWopANdu5fZOXxOfTu9/Vr1LA12DEeCvttbsR2HQXcaRSWXd10AtKGxnAKiueMyjRY5nqkJDYYisf1Hbg3IxrKa8w0hFuidDILELrqU/18VD9mZGvVFyY3avGKsto7s9bHzPrLjd7M8OVV0y5N90jO7O3rDuOi2SfDVmlchj3AgPE1nRdv4r7dGtDEk+2kqZqd02RC+iQGzBUC0wRSxbrUqLEgPsm6Z9ZYYjiVMd4245HPgNRUKPhkrMVwWYwKkRzjGNkuJmNDYww9IbCOszga/QaqSNFmj4/Z8WpsmZNnaN6qXl+Hl1BX8BK4t2ZUnCAifIymxbr3hHeS/KOKlNaaZ3rM5UqNylTXKeK/3y9y6EDfaa4bRYdJ6h8vEUFQNk+DKQCuCBQVTuG9NPd6dufPONyWZbZ7oqUf0K/qE6KbrviqwjDoDHhB3ipu0T7GchNGA3h2QGOwQzIEjoJo2e+HmZkvUUL7mbwcY99dqD2V8XtFojt/hoJYN44p1crykVRruTsixzqbpvBvlyE5IsDVL+CmMytqmEkzAZ0zCDf9LOCP5Hml7Cdc87otwNFM4DAIOL0NDhZFS6sX155gJZvgXQ/NChJkFr2/UinNcRUkeMenZOowa5xmfuRKCwZpBvf3LSUHqWZgk7rSSgpc87BsQnbzscBqtY2MTOLYs0M2WgzTdD2vmlmVdP1gbZZvTuGynKjZKw1v5JcYhAREg6qeYqjtMLV3YyEoFtuLgOb7cVrtNUdg1Di9EQ8kH6L7c4ZukdWmcPc0CUGxI21wgZqyq6+MyM41TzGFvKO7rKwz+f9UYvFYMbYyM7LgQlAVlgrul+LI7RbAghietKGCocQbNa6NIAZcY/Qivbme/ypzN9OEm6h3nqPyE+6I9wr84ltggT2K+rpxojrAUDlZ1xFo5lPdXZZM5XHCWwyec76fyXZrJEJS6r/sPjND2A93e/vzBQmIqoqQuFK81YGzMxKqdYQ2sPOmdvjp4+Oo8sSq3yfgVuqOrTHw2vGQh9jU/HjWCtP56y2pqRvsdlMdDG/em/tiznNKr6umIfYW2f2uOEaBmY/iuFwnJCK37breyJM3HOE6uvGDU5udpbS3Nww45vu2SYdyBIn5UYLmzefPz+Z73OiR3zDuCt4YSLUQsPKcceJowfDjJ0lXc1znvd9zfbhJ29aGljTsa4M0DilejY6SaO55yflP7+effOw9/ejb9DDQEzOeqQ2DHXOCUFmuLXfKpORg+82mU+4i4bpVORc//wcgW/Vc70dLe/bmGljdmK2Ncz2OTVUZjhnqjFAh5nnqyyIFVoNy8OFYrcs5tWwpnuzjQG0SDw9e+DX34Eb49Hv553pOM++dpsfMtIoRTz5vooo6Rk/sdzc3Z/l03N3XUuZJUOHIhyEHdcCBNbds41IG8Jet89ECkMz5W4TwHN+9AtC5mFoqpWs5VSmNGZb+CeQiqCzic7uqelGLyy7m6++uVq9I3HU/KHdpbvdTJnshhCBdmL+/CB+y9E5E/ZUv28XmrjsaEkHdEJYv+rQSJ7jM6Mitqz6dNN43CvL3QkrxTDRwq0GZkpDu8jYlgfhRGSVIAqCPSXDJjY00XZ/dBfcoKV1Ddw1HiG5LGnTo785gOFu7sapnoYRlWlO2eiclvlWZ3i870tzN5wT+xGZg8jKGc7UstmAjkgaO3tcjKctCWT+1sdgAYSdmg8A+i43i8JhMwGDm0nnG6dlnzc5qKzzHAHcuqUz31ZYjRB2rHKApDDlFQtVagkQPNLd0kS97zvVHWqe41DwDIzcLnB3PU9QMmB7T0nd3TLKEx62tww5pNwHy0R+PbQKcq4OP3o+Sd6DG3V5V99svqUV9nq79+uO/eJyDVobsJPmov3kjXqe+S/6Lz0wb4WAkKDpmaodg7t9RU33TtVJA7hp9kcAzlFOYjjdGA4J5RkUvq/MnIij4lyy/WjRnuMY2Ez++XO4gyLd2VnhvBO3fkqzze1ZMowkpt1kqho37w2/a9K609yPHDYEweNPWQ5m3JaK6/ZQ7JXV+06E0Lw1QOoAxKg0RdODhmDlWIAWz5FALI4bqWr7qcGWX2KmnxOZNdNuri86zvE7zNFl7dmlqqtevtycqDnnUKDeDJVvW+XG6BlFxXqEujrrfePn0Uavb0pqtOr68+cnc8tdNRQNuwvnHNv457UKxXkIfQv6N4jwDUPWK2hjFfhcw4EgZ6nN/Haw6NFTjlcPpW7ZG5i0kOcWbi6l+QxMQczENBVtP2DXTKd5fD7aY5Reaf22VNhxTmZKWWjuHiHIS+OeUUVenJ6I09VtmyJpJtUyyXb3zIJ3d7sddXxoyu6eMB+zmTrP807+eP/x/t/3RUUiE02AYarBGsCzm9SuOoMmQsEoCnsYzebMbi0IGKaqm8YgdJoq8g1T1Dut0dbWPfS5xfbS8w0EmGA2ulHGO/29HjoiYjoz2xe4KHd38H1f94Ab3ow4o82FC+CJFQBp/B7xMZDRTNlQGoorznk/rx2oFV1Yw8/zz1u/PbVfAOyJH5Gf3a0ozPBQo1HPvJZmNuC3L0WrlLu5B6AI1a6uE6enqrJ7XN9pprJMTgTBYUvqllP5gStyDDepQc/rzPEzwLSIEErsp49uzN/PG0+4+qW1TXabNGwzZquO1uBEGLCdejPVVTAfzuMhDiPFRuzZLqRl+4c9jnI+BYsuALt2UyldXSqmndN14XNjHSRDFP8hmEeTO43obRdQ9sNqbqHwvCFMna9dKyWZLQ7bCGgQW/Y6rdI5czOndbkxgg48x23aCaLd+Rx3J9HhnO7YkMuRDVgWsDhiTfSQDac9HN2cMh17ghgcZnT35zzZ9X4+UsiQfQ6nbUMp4DMdJyQYPXCoiqMHsJCsZ3lSdH92Q8Kg4c5UmbDZDPznISkP9vl5CHZDF2RXyq5//KyOw7y790oja9pdcYc+aD9+11+En1EunoVswFgahBd+pwLSJcKs7SZxbrOT/JIK/W4jTrhL3r7wGWUjRRxLVZHiwgxAdz8nsDjspm9iAxjOdIGUTcmdlYkeuuIBVnf6ft6IQ46NaSrp6an+8/M/g856e/D8eRRa4BYSFhirgAUQrjCJpIcM8e1uoAnmjPDq7mxIYKrQ4JSiGX0Tt0GIHmBrwoLRFXLgTu0ct2sXsjb1DBRZSvU4yt9IjFIOZF/w1bwbWAj1xjWrXrNzTmQOR8x+P+H//P9M/duyZdlxJYiN4e5z7XMiIiOvQAIku7v6QaYXfZLMWt+jh9Y3lOmbZG1Wqi4VL0UCBIg7MuKs6Rc9jLmDzIc0GDMiGGfvteZ0H9fHvH/tP+/beVWOX57gTOvGFhXuNLOQFghkY7rbzae2REoRbm00O7JCsqtWhHxD8uYcyk92DYxEttU6ZRWSOhgYVNdufZTamBk7CYhOsnJLda05P3fW6TKI7g63dmZvs6guM9t51L2xvPebTKy6nJxGp5jYmha7GDPm3LmV9ipTzt6fa5rgWmpBmEY1p7NdOlfYGb392JAw1mhf0dVkK8PueGUNMPV9m/BJHqIX3b2uS46Wni9vbNM45LrOQyv4G0LTzsQMg4EmLPTMMZUgq3Ndl97P9Vi5sxt0zrQSqDQtHxLLfaa7i2RlSfgvh7zRe6r65IRDWS06cQiOQCqhw1RT3gz7ULPnKj0YL044J4yzy+KM5uKWTHyesHqlcJypceYA6M+N/vwizaZPBnieCbwCic6WIOB3zsivcZtOoxEOhMEwa7kRTvgy+X4BOsYfamBCOEdhYZwVIfcLgvP0uxnpisPExGNJyiWZlghwM1zL3H2Gue8iw2OFi9SBHh3DzDwdoGMchnPgK2S40xIsTshCmqhy4rDKy7sxVS8vawbHudmq5GT76q5wxSyChDnop/+ud4V+VvWXAdPlEdM4dQ5PByBacjjIb0utILrwZgDqYHTFBxzAzTuLJ3JMnpIiECS1uQDj7qc9TmAWoZxsPrshe+Dup9K029fqUUNp6cEoMf6SQDydR+7MXQPZdjQUWqGc1obu0pxJcOeJ07r3m7sPYMvnuYq4O7nOtCqHn/szD45DZJ+JXyV0Zo4+XkEM4nFxhrB73+6uhGSHjQmgtMokVJehZ7ZBmHveW0Hp1e1Bd++BtARVSvs8Sp3BqLHhuh4CT7rH4M+HXfyC8pOxLrwuvkZ//vx2+cvb+XKAxhm3Zu7ZEZfbgnqdgL1T3Zw1J4FSiER4vN1v5u6uW3B7BMeFwiilkWREaEkLXwPszEGX2igwbLHQ5/WN555RVZetOWUmJijAYENlkdoA3Ten3WKotmsPV9JfVaXCLcy89m6brqbT3Wp6XdGVJ8cJiLVOxOZuj4Vppzr7SlD729vb9bgiAsDOfcWjUD0w4+XXW25pTyq7FZc2JY3N589va11CO55RWfgPPnNU1hh2PiMbQY8AjqzLxk7P5cyB0Xqqq1LBLxIKlzyaip9CD9fRQc6T44EyUEc1ZPaMaWJlPcWYTu+5k77AExVJENNrrc4t4Ib/3j0pXOfIgmdGwSEjfeRwpg7ozDPBSs3yVM0JvFVvV5Hmpi5l+xIVKmSMRyIpQmCm+osFj8rge/5nnN74gzK5E13uFgaVN5jBzZfzMqmAZi0jZoW5Ifw4flcYpp8Uq+KO+bTLdmety+1yyJ9EGlH7JknUtRRsjBlVidjeO641XYRcAj4zbtAGSY1uQBfBfkaDPJnzmetasnEwlEPFIwfAmSRgEKshW4qZ95QRw/HLpeHpGtqYzP9nXRuAauuVKIBUNWlLAqtDUuuUhUv36RE7N9Au22+4wJYTweCKcNfN3NZ+9vrDh1ljzOFmYW4taYbZfrvNfYbTuK6HHrt5NjUCcNjZvbvMjsylukX4CCd1t8wCKvzqGTeXJFG677MUknTb95vZ6sFJJSfCXMWT+vGkyKSZu91ve6An6ciCVCZz3gD5DKUlnplpuXtGFfOD6aksKRNkP1aGiWZq7U3yuwkJqUw7EQLK6ppiu6/z9epl6wMOHZW39jLNZA2QqcZK955BNU7uUEV4wMH+6tV+esvPO/d0NcY9q3bLFr+AiePHLjdv5ZRLU+t0IU14NgTEqDh7xTVSGVbrUlyxBF0b+YVCzEyt6DSzxpfkgO7maeus5ukD0emm82B6lN9nziGre2obZgV+9u1Xl68//vmnP3/Kqpav20zyO+sp+eA1ii5zDZWTo0euKFDYM7PuHRpHnnr0aanZSn2ufXAlq064fjbdZyNNZj+nATej2753XEveVA+735TEyRnEsqyBQxeGkx6x7zSTmXZavd60UKSaOP8sl/dvKe1rnnq1qWwGAUSsyio9JQRqSNORTmJqDiBP5bQYZqY51O+wweRdpoNbU0730OQnOIlbfeLzKtukXoOeFHT3KYCyGNUTtozTlAlohlWtd0qLiGQlPd2AwvrmTLvPshep+41H5AtgYJqvyWlS4aY1urVJoAsugHeeYm+o0iTc3Dqc59An19L/tusKdEZQYC0IYMKBgaq+aGMw9xPztZawFFeUencRvZblTgEpnA73wQnsBKKzXl5e7vsmdWuXjXkYv0AlSm9GH7qYYFjeb8JtQLsu74GUATPAv7e1mNGBeff6snMLkdeJr0Qcu5YE9OYKB+0z43ettQ7TRlQm3H0FdBxVu9F1XX1J9RSH5w6wqtfjRfipkGvdld1jTnFbs8vc3LwdBKK6zG2qd92mtWL6BF+ANfk8NxpAVrFONF3bVNV1PaoSpw6FPVM57p5n9Ma9t9mB+/vJM0vN4r4UjaARxEnI3llD54pV1ccaStl0nw+cE2NPTYVU9qaiBqkPeuZc4GAJ+lHN0xz5gdTW3W0R0koIC9U0VpV62Yz6iDUCaczBzNBEURinte7orqVZeGiUF/bnK4CcanrU8yJ1Z1cv98v76xf/9Nb3nbtXst2cC0MYnJjguiv18qsqRwQOEd0Sm2DmWdvUmEFWmbvTM6XxnpkRdtyoljd4Rmkf4uq0Qgoj8vCj4SG7BnMy8k4BS5XCwHmaIvpocNhfv3v5v/yvP37zeP8vv/7df/mHf/7z58neGFVkYzCZdSLMZhRx2l10SXdOCqXTS1nf5qpYkKJCP514I4PducOjso7cVr4n3f052h0xR+INKMM57jtfHo/77S0naVapbxlVp/oZasbOAcgj828JvM0DwGRJ/HL2RBJot5V7n9J2UH/IF5eAODEYsgon2s86080L4uo5/DKeH3jHHG4uqRHw7J2e2ZmSp5zh7qj1j25roDniBFf6s2W4u4coyXQVlnB2hjmyq+MdI0mnKbh3TlorxehCYM4RivSR/dSQ1r0xk1kSdwrpG5UvnkQzhDmn3S2MbnOFG3sFHxFkXcKjDcvmesRg3EHZApxrBR2dKavyWjZoJ908c8tFDPRaPlPX8jkOeQNwPVy2RNEZGp4qm+T1EkCpK9ZoRWk0R64mYBrYO1csmXU1UT+ui4d+50zzOXirsPOoiUc0fZv1CpqF+NErQthy7zyUAPHk5w7hKuUClZZmRqB2untVX9eqTrezABJj4Vc8unOIqVyPte/7kNgKoQFn8FgLNl2KW08DM1PTTBCo3Md+9dxPdSxXdWVauB5OJblrIaI5UCe6h6Q7KDnN6a2JCPCYWcwMtMzk4OiLukcXMpk7XVSkhw5vhlNP5FORnFlCI7XttkK+FKVrJ2KKp6d6ZhAetKlsPaPj1JSkn7C7Z9LVjua+c6/1qGozx6RO9srqg6+10WKtfafGfMK+YLgRkXvrNRabpPNxJOqC5a4DrHe6R2YRGuHXzLx7fWTeH1/tD58+Odbi49ZP2dhHyaP1UBeHaUSTK9XNqJhyW1rJSU5BiM3hFdErrupWUBTpPFjuubRATo07pL71UJqKm+q9ZGEkAexM64GbAifPIjHjBAwOvr6urz98/Ltvf/Yx1m9/+9u//vST2wXzt9o2xMzlV09r2ew5i92xIwhGMANt+sR6T53SEoymA973fap+jJUZHm/39oiIpTbwvYvPki8TuXmw9YGdIVE2pbUeXSUM5NCwJHiiBqtb2T+S8Fr3vfcK1+PUOG6DPtq2FscpseCBamfM7K6tCTx3Su9AN8mH5IUUU2UmCdmM+teeuL1mfFNuxEFUIKdObfU5g1Iukye9QYvCiDPjHMOYmno0E57EAvG8nf8uGx3ZyM2gZi7zqi2EoqtkuejT+iaE5/iEtSnw6EE1vhY47AaO0IMYN6rIJ0jvWpctYxhixeNaYePsCJITK9zGAI9wO9PaWoaR4zIfL9d0G9odbqJ24HYSWyXiNBWmkxz5OjmQUqhCPn+RFGrLHSW1HbsjnXUnaWv5QFfhxPLMxKhHoRXyFuEVVm8pdkT9OePnEuhJQGeCCap1UQ4BTNNdslIalLUF4JCRestwUCZg4roGcq/2Wqu7uYS6FE06+au7ris+v91OJ9s8BJZp1FMkUTizMtxieeXEoKa7OAa73+64lqq9QWDKzeYs2xOxJCbLJwx0KK9+CpM9aCqkJsmhLIMma652ItoZXgQEzsgmNzCvLIY3itMc9vGqUMzEIVqJiDiqNKkU/OhbqlMafhBylYVC0mtEh67r0aeizyjBrllVnsRdUnMxanTQAiQCXcWe+6Z7VYUHycyBTpy8JTfCIGKZMXc/hznV2lBRet09X9y2g6nya6Hx7tU+bPv6Zd5++umecFuMuDP1oXcN6LTzDEnERsMc14CC5HQ/0I1xRe4tAjYiYC4bj/rHv3RNnBFpTNCvQql0f0nB190414yIjVGOf52kVRnGOusNswz8vO+fPl1//NNfX/16+/SX6zIZRcYNNR5LUZ7i1SPi7b6l01X73uPxcr/dmNl5kzZo8ziV9k6IlSXWWjXd04uR0w3QMF13JWjoiYh97+nGOICqNrlpADXEWbCznaxKyUKEwuowHNKCaEyzbdh9MCj6CRqDjeI8zRSZc0pJlJGAU2kidHRndkMwqR3hFN0sD7+DKp3+asUSkI6aRk/4tTtnRjlp0w1a5vYIDKq6qp9Q+BHYexj6SLh6TzfcoWRAkqBVJU7JX3VJ3orBycU8j9ZgRl2Pok9EBrXZKag6u8N54gZzFJ+nE0e/QRgsBwYRCQ7YE9hx9OVYjmvZY7nbXBcfF40TZirXpcGNEc4ezcGxYjqdjhmPBZSIusfjkshkXZFVTqgWsbq7W3pWN661cCBwdoNuucvd8237l0wuGpTjYudzgz7OmbhO7M+RTE0BkAXFDBzzK557VwM4RoRpM+47dU7u7GtFVgrAkYsU6IevXXumzaVGgZtV5b13XOHLnlq6wRdJTtVMi6DCqWDhDCX7uNYFgM8AuMqRFwLn0p5YzjGgpxmnMVLH3xmbcBieQ+If/lCC2dRg7qfy9ymeCe2unQ3zLikL63DSkNKGUgoBgAoqh0QrLX0Os9uQahDDAY/LzhMFwzTC11GknW4sPvXdmnmN/64ltZFHzETWK9SFU4d1qay1lnIXYh1uQTdBZlZ3uMeKqqms5iiUWwlrKqLrwVS7q2TDqVOGFEjalT102DTMESsqy+VPd+8hujh2rfj4gX99u//06c+f8xosGAy2p3WFimOuaefJpNXPbU4Mus8EdyDaGRh7Zq2oGie6SoeFbjuUNoPzsuti3nlHLEzpD6gud9+Z1/XQNWv48hhY1l4rMJjmWu/QTVgs/Omvn/6P//J//tt33/z43cdsutm4ZU+EZ20N9TInz94RATPloHXP589v5mqhclGM3anJ/dQdHwcDAHiYhnRD+ZHEUfBf7m1OJVHtKjox2JUKOQ+JJZ4FXvd+g569msMZ0Br9VMYrrlKC14GQFZsQ1PmMvARPmdpJvlRThsSRk+42JU2kDtPWosPDPh1BpfKV9b0+xVnPXcddcrsTfjfKQ6VwfCiLk9T0Tbeu1mzAJ/l5qOkzhn/RZ55EL3uiuxLwtPoRwZnJfR93gEKN+hzxojqOVtSOIPzQFKoLnML0oPTEmiGMy805Yb2MyxCGK+wKhs3lDPYzy34el8tasRwWDkx3reUtt/90OGlOmkJ+jOwRFnewXyPdV03pLCPGTINg2Ymon+tygPH66K7Huu7cZyrChHOetSXTRzAjntmOn4bHIEm4uyzx1dNqExsM6rpWFab7Wq5ogXBvdJjxQQwl2DNjYlN4ji9lq7izh5fCJRsHsiPCfWfpY7cBUGiOnDcWO9/8qV5Q5x3JqrrWUlwmRL+H7fuO5Vm5lkXdLUsRelasKcjqKVWodNadqV+jZ1dYtkcMIKulMoK0uBpJx94nOnRKScstl9GKKytxbOI1U0utk8LHW44h94gwi/DMrClzm5nrWjigbZlZNyQN1C1gYblzdMBxnM/UQw7NpycsPr+9RYTAX0l09CArHHGGXe0RM1iunImx8IvWx8Qv8ufMcgQAH9n5RJcZCeauyZMNJxhaPKQdro92ipl8Gmgsn6/fxfcf1qc//XnweDOnhcqI9IQR5Km1gHoZs2sZCe5KhQCIs1NLuHuQiDiaEHMDWJkHFRlERE2dAb3TYF0ntzVPPdCER1fSTg+EsDcZ8bYJ2ZMYhtVd1ZX1+c9//Omnt1/9+g93ta/XP//pUzxesuVgEzMEDrOGNhgFnmg+2AbfXeSzUe4ovqmLoaaZg8AMugbWYe7uesn3zrX8NKHRZ24iJFZsYhQ+ai6hHsgrVnaV3FLGcBdKdojTsNylLTbiVB6ODfuQGUbmKcJU+6GmVCslNFCUypmfzMPA1IBC5ZjBTHK7o5vvbBobvSJKQ900x2Ry7EOACI9idXHgbp3lFjtvA1s/2hTIVuhpOLpAq6qpVgAUZ1KyJTNgpGmJFbkTZ60G6aCyHcuVSnboEaXE9syAagI/o9iRElWdXXiabOVWmkq+Ll8Op859PoIr7OUlrkAYVzB8Xl+v7vTwWBblU9lJhq3LjeHhEnp1zbW8OpVxoX4uFE6BKxyYzIwVES4XG5+XniO6SyUiHt5yKRqz96mRkPSYxhD7gQbkgBET0iei7nxKM1O5BU4vN9CPgYvoSZg4ErjOBU5vhT4JHerjXp3iid8fAxpNWCjICDq1z3ghyeUoVYazLLJTb2bmnuZMX4+oqqmCQUkw+o01FYzniIvqlIIrlHAdEbCZLphCgCf39tBKSJJ753VJGytZi1KdCaK60RWxaheVvD+j4DEjuWKmPWznTdjOXWeAVwT2SQaOtSpTIKKAAlue2QC1rElvbWQjzc/LrNjhmZaoXNf19LjbtZbUlo3xwZdQJLkAZX/rL3g9zYyZRTs5zObuFlkFzor1+d5S/4T5gVwU9DPonjoVk8PhWpeFdbY9LeHdPQMeYqgk71FD1kz7tS5e7wvfvq8/fvr0+ac/2Mt3wPlBED5ZB6AsKCJKa2bV9GzyoFlTMLOpcl8i8OUGakVWKMTmYMujlEoRUBJRDKAjTGbUxhm9tTpipHQacq7ryszwlb0NGLOaBiyLNvPHv+yfOPFyvb19MrNBd+U634XqzEypU+He+tmGbtbTj8eV1QR2p89Ut1T6Akd8rfvtzZwe0ZUG1wFEcF2BVo6K79wGczqYbqavStxmVudpqdsSSnwJhsLz/DPz+05tdRr5NetN9fJFw9t9r7DMCkNxNAjOYVxEmeJZeUYz4XdP5elzj+nSkzZPVZJ31TNRygZAHyGyxh33GE6rS8SOG8YMZ/sBNKF7yBhBgmaumZQ88jAcV9jI9y6lvwLp7FmHYAY14j2NeEfVCTwp6iM7GKVIASMrUndPF0Y4Z6HrSMDN3KCI/2V8fawrcAUfl6/FRyCcj2XuE6vNLNbqTjqdl9QN14lXHg5oXNcD3cudgFto3/LLdQgscV2POKCabspW3aksOPo/nlBfD6tWPRTnYAbxiHd3fwKJqpfXR3fvowoZ6Sa7Gw5oIety9+4Oj+pCWB1/CZ1MG72hAEiu8KyanumNMKFPvdPcuxBhRs8qo5/wMYqu0/QFDzdaNsLpIXXMFwXBGABY+NFbdveCHWP5jMGuuKpTAXOdY8vcXI5Hee51FouswFrLjZlK0wbNq1Krn0yePY0iMRpRxYAbMLSZNmLcB+0qEMe4rx6F6I5OWPS4+3WtrsrcesTiWuccLwCj60QBMj1dX94Zj723uxk9BDFXCYkykuS9t1BvFGSN1isua489lXAk5efWPp6lSYoezNykV+Nzvc30tO+6Z4Vb1PQUTDc7ZAmimWm8knvOKSnurGuJkbMZC7XNlLr/JNqgx/Xw9+/WDx/rr/enP9dfwr4qizG+7bSI3omarlHaNQhzVOa1HllVVf6IsFMAO2iRtFXVgLK55GceTncHjSCbsHFzDve+41pTXerUVrnEDI0SjgCWmWu5DDX687sn1gX0MV6GVQFr3Zn3pwQNxsk28723CfRr8DJfzvburJqnbapq11xLT5p4MxFcTY91kH3ZGgnrwbVi37sx0qZChOjpP5i7buXFzgm7b8k3R2nkkjbSKsWwobufPUjCbWYa9DnYhmSXXdoeRmVzYFWFL8VssAVnlZlrrNclsGvrVhiMTgrwtHcpt0M6HffVnTOH+ZUqWn3zxpWZJD2iUxmjUPKeEQzvTPPQckEcMWIfQ48T6qKyypaeU3l8Ux2xjkZIauoqldG4WeGYMeeJGglg5FlaMJKHA6dcVYfUNCbNxpxuXGFXcIU9wq5la+H1sseyYF8Xgv36Em5YQWLcQJaChI22LjMBkplx4qw7lkmNzgNZFXlIc5MDjqDSJ7unx9eSQwoUd4PpAzxqfce03MWqaRTX0na/rOt+u23FTJE8eMAQPT0t9+jBCXWp9Ix1eNxvbxEu94bCRSQSCZc13RwGjF8BgMbcst31CofBjCraMZ6NRHoOARXHinRKdQYchZd0tx57o9ekh2fuWC51MqY9DDX19Bi68mTdZhAjihYnW04KJzcqwGuoMUnzCEhqZsOXXxlBUPnJEX7vnNykXEvsysz28JnJSgzDAxytYLpy6uQKzsysuPbOp2gT7itzf9G0DtF5dnOMnPegce8tcZk2U/cgDZJmspWmjRPnYLVT+BW6D3UBAqzapVEo2+I0wFQnTNEaPoQ1umc6XRUVdBqsMFNmjqdL/roeOzfIyYq4ZloRWmYGyGLjALLLjWOwGSM+vFvV/Xnfb7/7wzCKNrS11ufP0tGq0h3EGF096/WM6Krq5kmVQVKey+6jCdEHXKhUdYG4LUm5U6l2xACO3rCezIbB3eoci3II++Dke0uF5s+obULmuJmuRmTtyQLKLp9CXBdGqmVrym+lmDzKzKUQVrYTNFrmFkP2FCFy39kDd91gI43TzvSI2jfQEZ5Zclqc19h9qs0j9wYPpAJn71qx1E+ma8mOzxzakE4CuB2xbJ64QxUxMrM8rp7iQAWQWafOlyY3XLd6ryAdsMxTM7qJZ2bQpShWKI0capnnCd2DQUDBQRqfEntzr72fusw5zzOmO+W5XCueQQ+tirqz2QyOw/DoOdvN53CCh+TNrIgl8Fck1pGxnpVRBOrTCDLHCqsMKCj3eLprE6UQUQ9zpxERFtaP4GPhuvhy/r0eD/fZry/a7Wety2xotpbLcoEZEsZ5fHiZ6ZleqnTB4SDWy6rdIG1FVmHmWqv6pKMSsJAGhVUHZiCBMDpqV0TE5RzHjD2uma5KuKB/q6q4BLwYKePegCdjTwuT4sJI0gD3vO/1eKxrKRQks67H1V0rQgSdUNlMSQn01ROcWHEQohk5h7razCzUygkeFRxosFHjEDOTxpm0g45gqv1xVaW7jzvJ7tRX5VK1EHoPI2Lv0kQU5qgayvwDmxlfMd1dWdXmsHDh3iSrR1PbTJu50QfoSlmlMkslxF21J01p46pPEZGto1to5lCZiGamHwZkz7ifAYpKfzNwfA6lbhYBNunzLPU9ieQSNRpjrQifmqbwhz4X8jFvl/BNkXokTnTRkzeGVEan9gQYTBXN6D61r8d6qi8gN1nmdvPqMTsVeeQXrSE8Agb2lwQWO3IDonqMMbLONrAYy7v7h/vx6e3Tr/7wb/uDk++6U31Cogt7OmXgtihpORQWfkj2IS2z7bSl6YzQKGk9Kobue6fcMmaaSvzx+tJZeR5zdJWNJdWXzeFAJYLd1X1GzYGg80oJJ2cUXXB/NmPZKWLNTptZ12Om0CZBD8msjBWHfaGKdGRSE+QkES0r65STYHJ3Tz+uS1osd9/7NtpgMluNKffe7k7l0VMhRfIEAcN8Juw3UKcEcWqGxoirevc0Zqom3DQbTunLA565CD2dmRiYuzOqurpjxVGFKsxHxLuryBMgshQAUKdejQamXGwcfukUE+qjvIfpWSvu+xaz3FUwm6wV69636Zmf6QbpGk01zGU9dWjP0fiJ30zP4BSnSpX3VJKetlwdJPL02sHOFUoJYKSAHV1IlaKObp5G9yYaU2CHcS13x+tlj+DD+e4lLp/Xd/FyeTivxbBebm59hZm7OyCdmdPXqkwjY7ks3bFCvu6QGWgUCjBxSaPJmZMIGcGBKVJD1KOZybW5Ys1pNuV6WXOuEtCgAk2HHy3AWepNImCoD6P0J/T51LtFEQMwiF5pAGawCHlOtRkrsi1zSwtDc0C8Pc24VlSVeUhoWwPKG6V/dvM0DejnHgaMzJSsQ+Hwo29tXYs2DjPNlTMrLuvtsXInJc1H0aIyw90i3n56CzMD2Z26kKYlqEP3rPA+VQSsTOniMaxu+fXNx8AeIzii0WZEsQpVl1TvWU6t/GF0T4RMi+RA+WVTh7qk2VT3gDXNnh5BbXaaBqgwUIBVGesaTqMVdUsawa6zCksxSdJgtkK+fdC7pI0zRUccEb2+k3PVYhoW6t+ZM1SCwWjeVeWxnlVdAOHaRaSXGNg8tdiq9DM7Brfs8WMXkBh/JnTRyJzy+np9P8i799tP//yX3/DxY1yvRdSMkwjrPT300z7mJLSQifKmQvAMJW/zE3kwY1bxSeddKzTLa5Gb3Hrxu1qtMfIcgegqRgjxP9J1nD/KzbP7qDZ7YOxdUpbO7nhc6CMR1jf1ZUw2I0gUuif77pp1xf32ZjIHPEWZmW0n2pV5l18GIib2fWvb7fwPB9bJ7Tq4tXSfsuZqnKQdzb6GO/3ocrcKnyk9EhiQwfZn8DKBbKXLSe45ShGvbgI7a6blQqAqsgwG7pKtlzOzPPYuNx9pM5RI3CrSSTODAwUZsDSUeVjtE0g1g3niy7m301QE1sqH5RednjqfoV1cH3Z1kzJJP6NN0BImPEU81In25SWFJLdSCOI/qFNNV9Pg+cI8yV5278nNzpnkpAXCzAyX4+Wyh8+7l3i9fNm8f7FrmTtfHuHMx2WYfqyw4N73uo5bR4EQ+tuFmeqAlWCFmVh+iEOzFaH7WLkRg9PNSwnpzUGm4h+yjczTY2jn2sDhRGekH0C43d3Pm+5MujJhmLmobnAUH2vPYEYTTysoR9/XCrkE5CStSl3CkpZWqhPGZd30UIPhSBO8RPVXc2CPJc2euuFmprtgKpzpOYUTVD3yCJMM/yJ2OuELPCXxXTVTythHT+WOE6M4Z7PysJbbSb7faQ73NHmin7qH7lKS90zdN42ExXqkpuDq801hpk0GLT3KihWEAdCJpoV0Klu4dqOniKfiEG4GSyQwyrqho7LWugYCWM9YJIG8WNPRGiEKRpIN8y4hnSc50LGU/uYR++0+3yrmZT3EFXfXSXcAnroxm+G9b3Oz5cCsiHoatHSxmcHDfdjVcqmPJI1VA6p9Qnhe18HRMKQbyZqCtcMel3/39budvHn/5i+/K/vZcMEjS5Ir0ULMKrV96ZLrUmA6WuFufSIYOaiBNANucqA4yd4ZK/a+Dc7nxKpfIzA9fPV0RDTGpqe6upzu0nY8acHHeiVPEGeEy0zRduQ3JA/rTnb2TJk73XLnuq5932Yei8ePDViPYPR9p+J3dKXJ+q9Li8PBWcwr1cyTsdbZMUGpJ0c3H0MEXM+MqvWySBN2TLbalFqdU9CuVh4xM+uKyqouvVemsDEgwrtyesxFyHtXxVq7Utxv+FUYG2jlfHIBo+tQA+QXQLX7+ArVooqjYgDdp0sgwwBZGzPLQ49auGWlOqTsuEN0iBeApxDIaD45EobR7NgddEc+Q3IkLa9dfspPnvHBKW34cdvoidLxRtihBnomE9PoPShDxqUgB7572OV4Db4+7P1rPAKvj3i5sFZ7OLkfV7iJWUkUX18fY7OWTw/kXZsxow6NwYQ7qA5nhSYBYOau52lLKeuMbgH9bDMEhMkokMbdyJNlOTwiPQyOk7/L6FACY5ws9J4BTPIZnQm1e0XMYLJ9LQ+ryZo0M7PoTD84ki7ZszvPSL4Mp3FpsZgvPa895+8/I60TY3nuDNeQV2bmtKw6NnHtJSo+l1+BUPNayrr71P4KjQQnltc+et/u8rVqp5sHaVVbeqn7TveY6cxttNqpuDFNk9Wt0xAEDajR3Qdw7zepUZ6J2cScom3A5RTH8zJ4Jp6fIoFY+ryiTnFRG2XEb6PLcePudHaWLHBKMNAfaCeFdWbkAywLPwmXgJFZfULaWv7GASBzWN8S8560iewkaO4cYXamVj3VjOgy0N/Z3DPry+OpQay6DjZqxIxKO8/QcMzPY/rIVKtt+q9djR5MpsW6rvXVx0WLtr90/fTrP/0L3v1CcgEZB1uIdo/FEAc9O+fgFYowrC43byFpgPIYT+diNd1OoZUHB25e0x4H0Ee3mi81W2YWaDUVdHPLnerg7Kkr1on8I0iseByZQHd3V40vGH166hQkcVSX7o4ZqEwBmB51apsCB0sv2NkI8yCtOP8Cu9pmsitczIpX1RTdgFbV4sk31f+DrHysx1EZY6DQRCB8Ze3RJ5DFE0Aa+21b2Jwvlk5DgLB/b80E1gpgZBrQ+suhFGK7NgHZ6Izcc0Qcmbn8MqJGZpFTFCxZJ2hd46TwMYJoqH3sjLR8ljQIdx8q+Up0btW5w54cgLoeqQ/lSTWcrKfBf/hnZMpmqS4DxwEGO7eE9ng+7zAFEXKmqlHVneya2UQBpa7yx7KH4fXCx/fx+sBXX70s1mPx5UX3e61HXA83Sp0lHGZ0PcXlXQUpud0x40tJ2talVNT0sFiuhOB1rdz58vJSuRHH4TLEVI8NAQ7HTaKmWNE9doh3aaxZWaAxUM1BrwhdeCtsgEzg2eeohwRUijq5fLpgY2M9LRcgYtEs9yZhh1ECQRGS4Xo01Mcrhp6ny7qbR65dCAn2fdCVQ8U7VwHj4bG8qib7LBi6RCEnM6u6pzwM6BVLq/BUgzTH3u3mykGROjwOy1dp9EETo5sHsDked1JPs7tgnOfCaGZcce3cAw4UPi6ec+rJDUrtIMVoZpdKATGTY0H3mB7x42ZeU35QAqtM7a9OI72eYTXmdgY4kmo6pAheBSWwe5oVHvfeJB+Px947dxG08NCEqL6RLg9FGbvoFhxJiHYOduOgo8ZJHbviMc9durOvkLK3ZA8xm2EbXZNdT5uJKZHnlNJ1mwfJwsm9mZ5YS7fkMvvwYf3Yr12990+//f0/97c/TrxDtZxxCpQZVu05LQ4zNO9WgYrALNCs97ajAxN9BAoOAmfgyoSY0lrE1tY8agE0O7WaM7DBeUxdVgYYuTPpDPd975kppPmJ2FOEhltIsaopkqC7DFzqzBK9pkeu4tmuY7Q+/4y58YlfzLN+VoHptsJsFBFa1S+vr+Kihk1w51bk03SzkarAxEzNUN7Do7/cdx5/FpglG44NUJVuJqlxrICNgXVPWQqRJ3l+OEhTNFSqrZI7DU69jUqLVhivivM4z+TOPpEeVpWurptuqHD4iLVtZjBsniVW9JI0kTgSDqX5P+uAxbeBPSVwn1DimB4L3QZH3CmLL5piAoFDUknnjeNdm9JTi+Y8w0RrphtdUzfRbrnCr8teHvbu8vcPe7fw/prXV3+sfrn8cdGsg7CHlsKMx0KN1BzujqN2oa+l9V4fskSGPX2FmoqDZGOM5b562iOqk25dQzcaa6fOisf1oJJRQsgCIfdAPItCs2nmNp19rVUzauIUjGNkkPfbrVpKCasGA2taTJd8eBEueUNPKyPkuUsBxhDiRNZB10biLpH/dK9M11g6wrFJEj2j0MQ5Cq611r1vN3fnwIxB1+CrqFf48t6zTB2iiFhVinJomll4V6tIcfnKyrXW/bbD9bG6H4nFc0CYLguvKlEhel5dxcTKueRRZRz6i6iWAbW65/zGQU2zAIciivy0DIqbHRIF7NSri6dfykgyYkY+qQVSbOFRNZmZ+87UWljVpHWWh3fXNEVtuXt45K7K0b1CyZiVzhGBYU/R/13uViI0RLjI9CHDnkqztLJ4PFcQLA83r06M5EIYjCnzfKpLbgDxUCdKUzOBya7GcbdphDfQAg0QxOf+9pt3dK+cvP/8r7//H/36Y7z7GshwH9qEVcJCWn7PLjNixuRDPrm8fe5ScqCo9xzYNFzwRXWfzoZ4q7zWqirSpfgULGnPtEUz26k0bxMkYhFdScBjde2Z2ZWxVkHdoi7cPLvdDFDYsoGUWSozx2TsDBtklhlz5+N6aWyjVXfvUvGDhLYn46aTZrPvda1YUdU2vffW5PelAUbmvSFiraqqmawUMthn6EBleThb9ejKmzqqdpH8rbegu+6Gg26k7f15RQiIC/e9k4Ifj/FuzMzhau0wJ/S8GQ9mKJEf5olxHWMHKcGd0hbm7Hk+vnx69tsdHlmpt11HmcbJmmRTAa6a44UK9kDGOnBmmvB5dgEoG1SOtfPo8zQmztkCjvJH4i6xBUagp7LQPbU53XkTG/3mbmvZy2UfXtfrwsd317ur3z347jVeXvxaDBsLU1/u6XfExGN1lZvPjK+oTClXBwiLCW1yM09cYbo1OV2+7v1W3b4cwy715Ez3iF/hybAyRRUoSaGruxJgbkV+HbMMTdatDlMSg8P8CRrPuhYNe6eH358+v7y+TMOVBYBDnsnpe9/3y7VU8tUj6cTszCexNZnp4bUzQhjheWdrbzv4HgYdcSki1AS2zFC2eQUKNY6DVVvMFy6sYU6FbgJ0o7wyUh6rVDHi/FwkappkVDYd2kEi1oFZKn1F3vuMX6UdBLvS3WAMHmlQTckdN1RHEKS7mEHu7KK70+B0QJU0AyKz/PKeyVEmDLPKzaBr/AyeIzezprnB5BZp5ujpSQyyUknsXTWDfe+D8TXQTVcc8Qk0Ul2M5jWLkPRCjmLlSoJc7t3TaPmEkx1+SpqEMhEYlo5vcQ/CrMbYaIWXUSoj8FqPOhmiLYZQWkMcO8Io2TFxSDUPVRPw5fXx9na/e42//cVXNPLXf/n1H39VBK/3/rpKLi1al/q9igC6FdgpNgKjHUtNFAo7OiHJbiThFmNtNL/87b4jIjOn4ejQ3hbeNVNNsLvuOkZwEU3OyLyr5hM/e6xgHFfREZXbyWTi00WZsptNmA9m71zXAiTFEo4GKWdqikTWuIU5SAylzGE1VqzceYbu4dvbrW16sjx87zQ1hRNO7qezT89S+AIORqWR2d0r8+V6fN5tZvver+9f885YVup/ISvbPSyYuZ2RrIgAOEC4lxqB3IDs0/k4nTnKOpFHx0gz1ECs40gah8oycwCdZcr16NZp7eGgAOEzUK61pvtalwKcjwDosLUEMd1ObyqN/Ak8jnJ7YHTpnQFOyxyiD/6ZPzFfSN8jV9UlpWXxqUPhCOeUWipvzO2ecfnrY71/tQ/v1vuHf3ixj+/jJebdq708uILr0iI6OPECBCyWG1FZDVK8RTxzbw50enTMHqGLysxTSipht5qhMK7OH8L4xZKnvugOt5QKdjCAxzKzvJNOfKkAgw27q6s6PDwEXzvsWegnfLLm8fIwN3CUwn0xugecyoRzhYcr2BxuttaV+zaY0GCRwHo1952aMnV2r7UkA+1Kki11tZnKB2vyca2deUgb/LvEawB3t5MgglKwoK60aQhOdD71RQQgdL1r5uTSkwJbjmXuWENN1Fm4ZXZzspQaPaUV5nnGyffkESWUYbDv/cxtofxiftSi3V3reuw75TWVbH+tAAyndurIkNFt7jXZGBf6SV4RsiWriBzota7c5UvlATQPnOQDKxSe5YUkOHh72+tambnWqqeZRSOGsn8jIit5zkq6SowVnKCaJLCn2DZoC6Xmom2+JKePZKzHUGYz254p0AfFdkO1ljLJVFLVetq1u4wBm6l6XKvv/Pbrd9flthDxp3/8zX+vx489P9rrtcJS1k16sTSNGqx7PA5k8oTJ/N5bb/fMgMgZawYHgzq5+tY9ugwGs++koXNO+c+zY1YPr3Y4d/UxVI91N+xojqQ+j7AngUna4OAj1tNVpVg3e/6NBC5o/N25FdfjYU6r0xrf67rm1FDPsTz6SUmKMICNkm5yjqW5IbDfAA7K5llvKyvNzOy3WkFzv/c2WmcGLTMHzbHa5QoaXiZy7nAtuYGjMmuJZGRINoX2CCEHGu7RPZyRbMePs0EQ2zTaXNlEToMZqyZiEdAYO93mnru0EuMoZNXkrvVgzmBu+redMYNRLB5TQhk5fbxC0pWfcumn4Odp7MAXtbeALUnVR+6AUdKx0p6rOyffiCIzfN69xofXeP/gt1/F+4e9e7GPHzyYLw8+Ho6uFSaJxJzXE9pdSD5eHkZCEfx2DDxfWn/PPaAlkniGtD6vvSMsJxX5R9CsstbygXUWiA6Ee+6inQS5rvLl5pRBuLuBVlSX/npdNU9RL6kmuBHBoxil02zh4a6YMgJ2Pa777RZU3ujwwIyfud4k8V2xdt44yg+6r6qe1uGsY3o8FpRiRNKgTLaUl3b5NHbup6Snzb3u2z0OQSaKyy13Rjwkn7E5eYK501TnBAixyL3j8FonRgbkAWGtsVbMzLXWvQvTc+69mByT0Sq12CafgW61+9SfuoG8rlO0q8g6gGhcjyv3xnDQ61prrX1vX5eCaARcCB6V8rdTsLIRQmKPJzsVWu7WNbJWgQQtgjTeb4I2Tfkhwg70HY+qyoYnfclNL9POxBHPHaexNFWSTJjZDKfPAJN7m4e2fu2bNf2kH2fFJT6TUCkjusctzsRLwa+qrNEX7cU2Bs0JFprG18dVNV+9v/7TLz4um8Cf/+nXv/qUd+SPeH3n7l+8rFzGMyyCRoNl54ipw1hE5dGLvsbrp/0ZNvf9pndHv0F/i65u4qw3g8wCUN0RC2icgJ5hz64dsVSC1L1zJ82UQxfPl1lKOJwmZ7r75MjSOdO7h9MRK6t65mU9MrfqMqo6wnsqwvOZnA7w89vt4Sp6a6iOikNpwLUrHJlinQsNgFVXlYramdVu3t1VqXtdSicNwgz55GNXekgCQBhH8GMNA08A96CJBJ9UxxeNzcRacw75Zw9YHyOFLhKpJTILo4YsqfqOQkhmEdWaelh1s9ldVWWkm2NMI8U8NWggB6aiwdyfLWyOXErCWurM0QbyVOceGPwofM6/pHVtPPcL/YWnmsJ7q6f31D24bT4/Lvv48fWr99eHV/vmXXx44Vfv4vXi48Wu9ViXFOk0s8p7JigUYg7Zc0SK4e6WO3sE1apkWNo6bTKHSwVmXY/cm67QpzYPZXNiZBu3WEHjHOP/VHafBMXzZ60rxGoQUzVynp/RTY/rYGQxr6G1uEwaAhLIHKkVntc/jsT++QgMFGZ275sDhhuOnWg4dJP/W1qQwVSPYotOsIcgslYIRJnREa72jpqsei5kUgAZCDe7326SsZY00C8vr40JVwjm7F3mBrPeCXN1WHmsBoOKv51Bt9nCSFEJM2XiTyONpAcxtuyoMtzETcOsp/feOuZMAb7E5/y8Yj1DLCzzVqFu9zFSHULKTIuwUf3LonYVcQ4NdGdfPh2/ipOGxwlJFtNNmqkCTKhcTogvFZksrbrIN/MvvfbTEE2tMQowPYLC18yYXWj0JI459LBVlb3W1d3iA0ibOn2h06PLHGMHzXQ/PKiGFrTHo7ue5gOaMXtU3yFZiB5WIyz8zvrq48v/vOxxrQ/Xn/+Pf/jXn377yT7+TXz4uK4ws0RPlvgRDKfZPTCbL4dSlsHk8Xjr2zRxYZSkLSe2hz1PUZCWJTnETI+H9ZSkMg3gOIfPXCklRvUYWNOVxcshgR2ZmR6mV6L/fZOgkntnmDt7QLPMlJZfe7FSHauH7l0VHl39cr3WFAc8PUpfDLPTJX03j/0Q1KcrOHuAUv07hl6ubi+NBQMFYt97X+tyt+w63nrBItWtfE0eihvGL60fZpb3ZrjMNSjpr/pslkSYfUEJ3GJkhukxd/OanNw3hW6bsrpUZglz0zv4ZGKeCTYnJ31OnuhRBx15ScvIM3Tal1RQ3ZR9DsVz2OunPjJ/EqhDhJ3cMa2LoOwgxwL/xuran4ib8+n1YV9/8/L1h/Xdx5cPr/j6fbxbfP9+XRfd53osMwKNrtr7enkoZqZSchJ0jwx0hi/B1C6y7b63tGFOh9GcmcXCSJS1FtCDsbGnTpNGt8swqgj27u3hBzfmCSPQaJt3mUhgazTOxtzyDNeKkCxIgr3qdgPICNuT0CM8HX4JGzJanVrcRmM4YlWrc7oiLsKGHeFqZp9qE1NIVnZWrRXk05bQTXczHxznCt3QeJrThibfmVUBY+ta+77nXKJuRjR8LTFhIrgFtKgfZkgPB6Z6KhNCh0cpXRYwTFECOylh6F57C/aqPnPcObR6uKxPZgsGusfkwpvH4wEoi0qGF++pK0LT/ZcL09wq28Q0YIanWQymS4Ldk9VulrvceCng5TDkA1DpPeqCsJPSDik0RoC1O0gJG80szO773qIxTVpheERXeUTfW3KCrcaYrpxy2pN3g2xcJ8oNVNROpl6ecYaatk58Pw6ELeHHdDMcJ1V3KhM4aULK4HWze29jUFgHutEMM+Ldy/W3P37zcl3rZf2Xv//9b//tv+P+Bb7+GR4GNxg9lqSuwJe6qPM5inqagR7o6fGgxTNaPXMGucucynGCvkpDyefQPDp9zlEQYcwogZPOEafpbIwI0TkSLyumSbtOY8zozmnryWWrpmqazyR5xYbKaAoeCyEw9uxfk5VXyVZKRtejpf+/eoLUvABg0GHXALs3MGYx04YvCAQLHaryHZJ2LRrY1Tv3sasZn+0Rslt47q1SZRF8UpSZaGr1/ND45G8dAFknap9m6pBpDNyjOgEbYdhaK41zfHP9/DdoZDMzAZuemexqQrk3plQ7ulEaaJJ0XeqNPsL9kfnrqDUOqSgtQj/NwTIOzskCEmOkUwdQJWxPVe+36c35/Fj9+nj5+qvH99+9fPPV9c379dULvv76sXzW4zxHsnUObHqueNHbR9CuqKnKvh4XCPfVWe6xlvK4iB6lVjFcMN6hRk5kU9WosNd1tdszbO1MlGZZKSTWCH9xQGorN/e630YqrswZWCwNDeasOqHWz6/7pH3b05kFEmiS4cuMA9t3CnowskpNaM1Wmo6tWOAX7GxOxNZRfOlw4MvjIXl9Vyo45LEurbBKeLU5j6wO6ogFylk5sZSbHaOuXJO2xE86PTvzBEV4WHjs+4axOX4m1CE6ZhCxujYH3eOxuntnTauoqGItKe262uA0DlQWyK5Wb9fj5eXz589mrrVOvJWHtjOGo2fMlz7bmmGNqcUxh2DVCGh29+7zTWPGQk2/jhHcZ/fesa6emhrZrDn6b+iGOYfoKgkrpaai2wiLhhmiSgJi5eNUq3NYVs9DHOEIRTLdT8DUF2yldgEzTSWSui+5qKCQjWt11X3n43qQTJ1fStLHCdQ8lHLrUWjUwEBSRi03N/Mx7PutqzwuUJnI8/JYP37/1fsPL9989fL/+S+/+odf/bfPnz9f3/5svXs/Nlw2GI49EbMvGnPy2Zfty/OtQHYOrXHQZKVPWU1NlYdX9XGqAAqt9EFjVPOr57IBG+WzT+codeosHA0YXVHmKIk38IwO1gAYtMbsTKMdJ+Sc4GXZdC3MXR1OijXvrtaKwCOij8pUZh0PCQM+iy+EyFfXUz9kUv64OxqNzurw6KrOquprLbkDuhG0iOveW+eJ5hsORD6dw6iHZvttfyFpw1elIk4FvLT7uZOIw4ZUJVSTpa0RDYyvUFxv71rLc0pqWm1Uy7xUP5e51pV7TC1SGs9hg8Mbn09vTjGGUuWOsOe40U4yAc9COjpUocVRpkSMkkWleeudMzPVk2+c5mwi3ev9u/XdNy/fff3ys29fvvnKP77GyzWvL+aBWN55j/xic7JCNXzoA5nqWAEfM3QNfCys1B6MZxavkZCQes66onHGY++9ltNchDIhfY5qw4aQV0nZu09jwwzwlFiR1PhwLZ6FWzGsYliRua+18lm41JxztphBJTtQirDNTLjREONa7cOt1fAzZc4+7wLcXdFkRg5MzVG6xavKjD2KXbHuJ08zJ6h45tTUqTiyn6FGfswIXlUELI4YUqmXehjkhwZYXZg2t8yamjZ4uIo/A43daQCCJrFxlbspQ2SmaTjthFLQA0b60jVeHlGZ971PZyYOoAqyM0l2VWaBEww8fZAAOxlrptn9/C2YZru5gMie7lujrekR8Iiq6mp9pUdP6T7dld3V+x4Ln+wJRe+OWQzazNugvSwzZ5pmaijravaMDQY1Geva+9YIKRmZBpIS1VY4SSA1sYSEHFGnP9VEg9Em28+siOfKb/pDzL129ZSe8zCHsSvBkHSh0bUHw/Al4s54CjDM+JXPf/rbj+9WfPXy2//6L//653/+E77/hT3e8fUdV4hbGEXEGAGpxwjYALnbzqhOj3Xc5MrmP9VRx2TfKrwz66N1P9kjJKYmfIF4e7vTlW+uaATXK7Su1Z3Sw2T1WkuKeT+IZM+0VrIVi4J7jcLNFeYj+qeE/k9b94qgBtoGT1T6ESr2NOvMrYeC42F9ki1lLs166hk2jmnN6T3k9FxriVLtGTqfwbdPEFm3spzJI/mWxNrwcJAzZcoWVt+Zx+dbotitOVSDv8OlFDiW4ifE3SWn/ZDQjUi0FGYzVEKTGA2hajr6ZesjT3QHhagOwmP0DsLmBHWqYQlP+eBBkCUEET0ozlc8mYTF032a/ro42f15ZqPfLs9vv3n54bv3P//u8d3Hxzcf4+sP18uDmO32TJy2QTWnpVslCGdno9sM8QhxNqVMxuM143RT3QxZ5j7WVelS0QLmll0G9QObgpB0dMS6wHFxS/0Fu5qao2ezJxKisVLze02eUpzplmkpBCiRRh+DcdQOop1dKSARuTcPO33onCu8hKOEsSQWZ7jdUokUFEg6PbzcOdPHTYdRPisI0B0z11Iwz4SbZA6dPSZSwfctGyaU+62jiM+qLiNlchZ4tfdbxNJEa4yaAmY9Vt7ZRO+cHpdtWoIhN++Z6pZywg666p0T4bW3WZxCK0Pu1NlRlXGtI31TEwBOudhR31TFivDj4N339uUK181b9KmASwy4s7JLaTNmBmA9rqxUUpA268bunFhhZt1j5vsUHAtZaZ5gJnnLkFmuBoZsc3cjbJ0YtaEyL/Xws49jkIdi0Qc6uXNdj6pSrJivxQUQggtWXHiu1GY+XfTQUSjH6WFHT5i75b7V3m6uzbyMATJRmW/mD/eQ1La7u9Jj1Z3HrmGM8Ffib//mm9fX6/3H3/+Xv//dr3713/jy3fXNL+3d6/WINvaMP9YYUKBDPLk+8xHzAOzcYT6AYpRO+d5MVzNO8/P0RPgRpJtr/zX37ARPpW/3VKZuab1IlSkv1Z7tptJdQWeHNNOarYTsomoRvU8qzcDQSp0xRlipmAvUMu6GnRmuVG29rzimENLsBMwZKRfL2aNN2kd++VkkLqLZdT2me631+X7rGStzt7YxKDeC+jZ0kFWWeXSXnkaxD4I09y43655dZa7lz9njHj2NnKm2IVS02+pdhq2oqtNVhzkAhJnVmPud6g5E0MS4TBfNFbjSPbXbHBhWKfVM6iOv46TD8aQPj+SQrf18no2ihwTWNYzTHlJVxEwlGnV/4uyqvwTu11f+8M37n//s/Q/fPH727etX7+P9g+/euVnjlG/aTI3S4Kertr5og/g8EuMMWGVuO7XeGzqFuztzruVundpLPa7o6spU7MGIQZI+VAKq44eg2AwNBSqSkjbW3DDIVNb3xFqULkDTmNliPHXemGnSthIQMKe82knOftuDw5J7hNMr91kEu4XJzIyH6xL69NNnc58jsVG2JGtnhLfZyW7RicF2+lFCN7qral+xWrxgtEDdnUfWac+6YBKocWNDMfysLAv5ihFxPa1/qK5KdeGNh2sbVppyfFmQ1YMRsfa9jehqc69qEm93mQd42vJk5DE1kTDcBHRa3rmupZlCy9bem0bB4jPdUxGhC6q7O9sc/lgoVNda6+3zW+WccA/5IfG0PlJfD6vrWXwBB/fbm0eQXMsHQTN37ju1Tq64lCXKoXwAVT2Vj+vhFpm7q3yt0HI0rMppxIp5Zp+8fX67Ho/zkt+zVige7+2+tYvpYnOaLa9OxZLst7cVIax8ZtyJse7uO0dCEw+ecPXSMlO13SLESfE0GxM+03Et6E3ttnDAiPnu28f/7eVnX391/Z9//8d/+NUf3v6Q8/bd56++YSx7ueqtJzjEWZaRUmjK7iMC8+3ea0Vj/LDlFcsPiPEExJ6qynrWG3H59Xl/PksJ6KQvO7ri7hVLuWGizCnfkFH9111TXXE9ek51wVqiLoSTnm5FZQAQzBoMYq0R2OhDc3NTrCbNHMwsmZA5qE6S5tZZigNTq2OlUg1senamW8sborUQ4C2tGog5UXrV+QWVgmNGBcp26lkGowZKMxre3m7SGmVm1ZM1stRM8d5vZ2wPWvjeZfRho6Vps+xd3WFhhv2WzaZxZqpAwkRFpPIwrJtOeIQsL7TzbPOEgxJHS3Qk/dJGQly4ihOrjTxUBcp4hOUGa4UhKF0uq/ZGJ/qnqU+P2N9+vX749v3Pvnn58Yd3H9/F1x+vx0W3MvZ06T1QyhgAundP9XB6vVzHmVxDUAWKK5RImOipGXeLcK09ZpKNjdydc7DTBuEe05W7xRBQmgqZvBogFRfawp+7q5LTHq4a0eO9EgLbh4xTpSHNnDZKOyehOo1puk+NCABBMX7yl0aFdN1DHwDuka2O0rsEOZ1M5pmBBCLngRxx8uNhnYUBAgbuZ6McgJojMAFgzoEQBdFJQnior1LRICZmzknoG7TM7R5CNw40ZGZGveadkPYs+DQWkNYlofEzkJrWpqxR4YbYJdX/aVTRMd0zU01aLJNEsgawI78jZuBnD6akaZ5dwMS1ZmbvPTND7Nzmji53X7Eo/bIUMTzN8jAs9cO14ESFXuHs6apFTDFppegPBdu6x32/YbDWGiAsMgvQE8O3twSG7oNnT9kcO2WslVlKMlWsqwwKRo+4Mm/MKcnKLEHGlRkRukAqRTObTDfdBeOgY61pal04YU9nVZdtb2s7lopvTqt8E0A1LTorwr/56C+P73/4+t3/9POf/r//9Id//PXf35//jJdv/cPX61o+rnZDe8RQeuQkzMIrkw2a1XQIXalxOxl+13XtTCVHdHV4ELa34qLpdlyXGO7KFaueikFAQtmWfEVLIE3V8wdk8IjMVMeT+9KjLCiWPAo0OCSQUITyfd9majwSRNkGhRRBGkPgbCEHcyb45M1kIRw90vri3FXn4ieNAv3EShQCGraqnvMgaO6Zec4UlOSEHla1r+s6Ikz1hfSZQFhbsDVdKCLOdKZeRYxM71VIS3OnBiZYxNLpM0R1meCEOm+fnP0tPe8IslJsw9FYHyxFA/hpijaCp+dd0CxsZig8eqg2Sgx6pH3CzMz9hqmpT8Rnw0/v3vH7797/7PvXX/zw7oevX755v96/2Pv3l5ob3DlHlhq6aK5ryTOkEFGpMORU0Kpc3ebsgbkZTmxXd8XiU5Q0J7Nk6OFODjzrdEbV8b33wDIL5jOIoLZ8XyH3T9sEQqnDZqge7BPEYTSVT4t/wqntOxQgnn3jGPkrRfy63Fx0KpDGwGdkPGKtmUJOVVIxpVL4V3mcoIGpZwgd5fa2Q+xWJ+hm7pF9G9gSFITtnaptaRVynDOtpieWGW2aBCKE2baSqGdmhZmFiJ2dW1dR7eLl13VtCc8wNAtzZ5cEBNXNQp98c0hfX5kDSAgov0blk7zCgWLn2Ycngo6qE0mVpdV9pwdXXEY0mqSDiS8BKiqkZZaE80eNJnM2aaRlJmldaW7wo5YY5a/RJKEaHCZyACgqb9B14u26U1oUgQm7C5xw18Bu1jtzuUtMMj0roir1I7lRDt7uk02vUIm9b8yYR1b1oKaQKc+zaA4pU41WWdNlEpgS5oEBHdWYSqNVjjtoqNxSm+ok6+mIC6Pzoi0chMMnTuX9S/Bvfv7V1x/e/fDt++8//va//dPvfvOr3/THH/v163j//np98RW4G0a6hS/61IzEVwfJHhCWtTGIFYTveyu/VC323b1WZA0mMapPGQunu4tGrMLJ6qLaZcUlSXex9w7tEFJPHVNlDWantHoFTGf58pMZNEfhLJjVaCoFOw74p97fjHOkLeNadLp3puBLwiRg7VHZkdE4RVNbnCjCtinlJ4iDpKZ7KehQCmDZBB7XI2vz6KHU3Xi029PTdXISu7rmBL9ostKmL52x2OOqlt1B78zeScqqU3zmD484ZIWruldm97HrP8uFok/SNcWmHpuWoqVErBweswVdkKP0dXM5p6Rt1ZRaugM7c6rq/uvMT6i/Pq7+7tvrFz989fMf3n//7cv337x8eIl3L+bTZh3OHkQ4ESrJEZPk4W5WSF3t0h3NgVu1Ox2GavmCS6lpAx7c6qgwHVmNmd0Kw+QZI2C0a13d2Q2LkI+hp5x2sAHj3qkOOBuvrKwcUAy/JFIRjpb7tZ2eU8cIgXEPCcCM4QK2aGD7isNrso3s6jAHMSrwJCMcPHHJZuQYHKSFklb1n8jcOaCbV2X3GF1p3p3liiM1s5MfbsIV5+hWpETwodwlkkExq8GhGYfucfwFM7vT4OGhXFqLQE+xvhS8T1ZklnnAbJ49LQCNpzhtuk+sKAzGE6FOiPAM99w5UNLpyXvRkNt7i8sFJEJx6flq13BiBetgW0qrEazki8qxyS4UBwq6l+9/1LAxXXf1irgixF/knbFcwV5VEr+avLxC9+77jhWSiQ2bkE91cjADJxo0CzOv6unxFZn7yxRgGkEa88yoaMzla9euanoL4bDWanJeYDQjXLmyfVQYmjkNA3E7kidptNfvirWOxoMMM8nBu9qGK071fNuQbExNmTNifRPr8Xp9+BDff3P91//z1//tX/7xr7/7l8e3v6wPH9f7r2wtf1mGaAFJqq5Bg2rOUeusoXnv/bguwtB5cn+NKmVTMcWgp0BnVvEYN8bM9i5V66lmMrM8rPKUTJycUaJ30hwgzaFerZ19RA7MnQLAFa3znCgUzdbnuAc4VjvXdUlpk7LY9BEXxLoEEU/P9LhLesBYsXMrZ1ROH3KUrT+DAQUKXeu669bxNM/ClJnJkoyblW1nkp+9E4TT+4gwT20ABwZb61G9t9j1wVP3lW4uMf6KVV1OA5B3io7qLHNns58nkvxiBwE/4iwAYzwBBiRgkjEoYlZVCU8U8/zAmqpO1noq7VUGAg5man/mdL59It46/7T49vHb9eP3H//2x48/fv/uu4+PDx/Wh5cgxikxWJmNW0irRaGGcvfCpZWSxO4IK1keDvUNS5/gVqfTHBZmClt0qWcJzvV46GvdlQq0AGutBZbUKMB0z4rYWV013XTzwzy3kT0Iw9tWNjnWFUrMjrMg6ujErnSz0QIRFmGiIU+ljOiagfB3KU/uXUIv9WArqczXmhp9DLsPtOs0bV5uMTPKBRmVoAyWxwxiRXVZmxk9fN97cBQBgM0kgNH8p59Zx/8zOV9/B7XqCF0fKfs9Ipa0jorgnTmZ/GZe3UbEjOLdD49pDhvqknRYzSm3msb96R51HBtl0O3qQ+4N1vJWMVx3WHRPZl4rAM6YArne7vuZRiSVPGBPGuok9LqUghEmgf9MT+Jaj8psFIAeXOsadWMdTQO7R7XjZ+M7bWqcnilc10OSC3PmLnOY21RnlZvv3DQ+Xh4cVHVP358/q/Dvcb2MpBAaW8yeQnjcuXvOmnK/5bpWRHzhK2mO7rf9pjkx9PcBaB4Sg9IwGR57b705HEZcpM10ZZfVtS6H5X3z3AjS8LG7YD6VNIUSoTrD59tvX19ef/zZNx9+9vf/9l/+67/+25/+/v7T63zzo72+X++/scfya/lj7UrGaQSrUUnWpPR5bjtT/Ji779qdKR8TekufcCywMxLs09zNOsYwChQCbabQIrhoQ/hRzmVtN8iXq3wFMzMPnqUPmiHAHsLNDl40FKfX02utUcXxMS82zXbly3pUJ2m+bLoap7NgRvRib/UBKGp4CGL5e/TnQpvwzyw7UAAOnnICQxROQDQJQ2Cqa8v3R/bs2YK5snJqaMc3UJ0tg0JJ5/aU1/cR4J4j3TjVpgCfJt3FhneWO09N2Dnuhz0zFGnMcyEQ8xzzR6JQJbodOEdKF1nzpkG3/yAdsenOtzdUTf7U/Rn3XyM+f/M+vv/249/84uMvfv7hh29ev/3q8fLCcHhgsqtyZixcy/QpuCZiLW/vKnNWDmnrYfP8KfHk/umOE9AGGyegfdFOz8jYaX5mVVl47dRGLBTuP5h8G4RHCE/be4C2wfQmTpEtunbeR/ckGGPZdJu5zsHqxJzY5AEY+I928WdXyuBENSgMGlMwOTwU6TFqF/CuRCNHIcoG4jI/miQpEQ7UORHR0ycDdWRRotxAVU039kCQrPwT8yyKMGjO0JNtJ97j5Kro+j3frFSs0tr2WHhXuVNGGQ92tpnFYPLe1xV9cgEFzUDFGmIeRHHolji2GBx54FqnE1ihQMKUu3vFiojuqqwIyeyGRkk4FKNYU1MK0DdJ6ORj0rWx4kRAn+3VdUpNmI0sK1MrlhbpyjTAw01SjaYMqJogtUUMQOK6gqZQIyiHVphqV8+gOoEBZ9khqVR/Ud3h3jOY0+PYJ7naBxMvL1U1LX85T5aCxVQ+CTdM94rHgbdAnFmszFjT7kerp09JO/2dt8nB1ojwFKHCVmCnCP7qqbm7CmzC37+P1+ubj1+/+/HnX/33f/y3f/qX3//rH/7+0x+u6+tf8vrw+PiNv2yuBT1kQZWugQPQI7IbPRYKux83z91mjpPFjCuiBxbMXWExUn+tQ9gQfFyPu7ZcE0cDKzemG9BifXSiidgAEBHicpr6la6OZbGsStXXUaWsXd2XWSVZancbTv0FZnLndDeo9DRJ2jUKpOIfNFqjP+dfZwbDiKjqIfoo5nUNNGl4xtDm217XqieMO8CUUk6HDXdPZX+Ggagqtnz8djZ3sKoks9GRItmiZiFx8tVN0YPdxrDQ+jmZZeZT40GYBiNzmUt4YGSM1IRWU5IVYnR5qGhJKnk5Ayiqr6psurP6/szZvf9o+Pzyrn745vWXv/j4Nz98/PFn77//5vXdw14eVm9vwDijZd9RULwNaF17xo4BCILWRpUSPeoy62O9noPgY070pjIBZpToTiiBwChdgKJERNsOYOaXr+xtdAvx5A2c4OtwB5i1gy4FYGbq4ALRWXOakXDOmbHK8hVTeWwDAwqidMX+oGeoYL5+Ji8N0BTXKOnNEb7S6K6fSwClJMceB0uYGXe3M5eQZnOs75WZV5x7oruhtYkwX9MlQtiMir05Ul0z7RmD0WxNUFTotZQXPQ7buzpP2Dt4ShUFMWEmwgwMgrHihEqcFj0oWlnMwLpW7nreDTya9znToomoFsbocBAzbs6nG8M9FNTT0xFx3zkzk9iVei2N7R6D6R5ZparSY51JDHDznbLJmbmhISNyNaoHaDP3JRPy0Gh+utpKhKH7yT+c8StE1tV5ggGMnlGRAeHRlaN7ljYYGtBtSqvWb5iQcs7CuvK0CJkbvAX9MADQ4DCpJ2Vu7Dkz5ukFhe28I9aJvNQVWMMwmk01x1pGWQPNw+Z+ezMPX8suHB1kKg18Mrc7HOGXfbteXt/9/Bc//+ZffvXH//4/fvff//E3f/zTf3+bl/r8g7/7Ot6/v67F6xUwG9GE52ktZMsO5moX6AOVD1csI/e+47nnNmYgmePk3m7+9vbmHk2sxyP3dnd1pl9XKEdBvJqIUNJWRHft3PL6DSauIEn4TNFcvuU2hMHcjdrNh2Cf1vsxs5aPCRwbo7fRCpUnpRl83l/6MDVkiQFgmDErLdzKOtOMauNSJGQBEnKQrGw4wyLzTUv4MZ2RVQoPPSE2lMi0jrQSQFVaiPeOt7e3ntZXLpUnzDLL6LARa9Cn1VIGHKtqDjvnaN9ncsZhoOybKvIQqyC30cw885EANAdjsGKPQvwnUbXffuLsfPujz/2y8ofvX3/x48e//fGrX/7w1ddfPb768HgsAOk+szjdWz0n1Kl65OpAYCb3VsWTOqrRo625NChKPDjIrhXR3WhWNm0wMENXVzc05w+nZ++M8KeDYZSn8vn+PCOc66xZkqJgNBfLKCvZTIPjNECIg6Z459CCMqO4m+a+qab58WQANYLm5QGk4OK1rvNdpDxrpx19CYjGjLK+B7FWV5r6cauPy9Wg0ATlJJqZonMxs+LIqbtrZmT9QQ+9s+qMi0fKjJ7mgICJupXXT0sk7fTGkDr3tWD21FoBoilxpipbytw7S/EjrOoV3uj77Y71RMrC78+3VdFwMknIK5bo0DxuxgNLnjcQU9Ue1jP60d3iJLMQ1b3WKlVTRgyw9yZtci/3595KzXfzHIcBNHBSX072yYFcl623/abJo1XToQkbkdWccfenvgPAmMqPUqzOdI5fcXZz0p7d2hIV8NmCMHM6hgAlaWCUdjc0i7OLk9mprcsN4bZToeoy1rCnqtpXzDOZs6fdglrfngY62YPduCLsFNieilrSPFZXUwnEvD7nX4UWELiuB/1UwdHw+uKPl3fv31/f//yrv/vbb/7+H3779//0m9//+R9/+uOv/MMPP13vX7/6Ll4f63HNY/V0XFeeqGy1IisKf1WnjZl0/VlkZLaFWXjuiog7M9zoWkool2repVNPJl2jY3p5vL3dNI7ywNU7RsBOeuDUnGADoBrVt1nIvlCAgc0CbOZI3ZUg311DZteyEP/Z2UabwX3vMKfbInYlYFnV024wj7e322wsfIhpqMeChMhYheeZpq0ujQ0sZt+ZmkGkIKA9s9rHsD+/gW5u7DFwdl0rGi3PYFZpylE+h6xdh8s98+Sx8kpa/WzeFGDTpHTl47QSuF81mE6db6IfqD7MrjEZp49JeGZqume/obr2X2buvP/E+vR41Pffvv/bX/7s73757S9/+PDt19fH12sFnS14KrNaYL0p992O9q9bBeApw4TKqrt7t5nJC9k1VOnFcLqWRz/zMqenswdtbgrNt6G0fIV5XKu6V3hNg33OfVU8Dnr3KGUWDSLWKWucrsfLi0Sw5KkRwBkLrY/BdGQ07sZIj1RNKMd3pGyUFsvMzE1Bs9oIjfBYd23oUxgc7QyeIJeBc5ySR/WQHDMdWUI5q2amRL6YzEo44HF36/0rtF6Kfe9Y3gVzunvvGvTeZW0g61lGrQLkATrT45lobVadHl75rIydjlhVaUoc4oSiFg2smi6JAknaWpfCqee0QMOIqnm7t7lJ5C3JUXddj4e8V+5BliCqdVKUSUxV7yzZKWHm4deKt7dTMq6HlzDzANDd2eXhRnoEZtiofdq7jNyVWoA/78/mIVNDTSueAYAGUtg5ueb5jy4YxSRIKHfoCJVj7xYwx6ctjmTeae6Ve11rGn26wKYypdaRTBvdoMnNYGTXrBVHUY6pFGjLqnHnE/Sf+VIVUkmziIUnHWMqCfoiBScy08xirc5yxFt9mmmVmnlcMvPQ7PSlTLqvdeGH71+/+eb173789j/9zz/7p1/9/h//+Xe//sP/+PSnNX/9k7//+vr4rT2WvbybKq6gS7OHnqKxMXE98k7jKHtanjs1/sAsKzVcGGfg6gtSLptmugg2pneasfL2WCCmyy1mlKKDyg1y7jePdSIe5UJvAOMR1pSKt2dUEi7jbimQxF3aZ3PL3NVKsZ4ZdXkOso9/0dDTZoGDqq2ZKQWHoEAqwlar4d7bzMS16soFkTvDLeJx77euXGtN131vLfj73td66O8mrwYNrXTbA9Ar04bdZeZU4pBkSOdHnqw2QkF1EAV9MkftqKLJ7OIJaW4S5mjdN9N0shRdEOiZ7JO0VFn7jZN9/6Xrc7/92fnpqwe++/nL3/7y27/58Ztf/vybbz++vn+vwq6efANk1lNDXmflYWXJtZYNc3pGKqahy9BeKo/aKhwFtcdU9hwK8oQoGKHI4+taO9s4NdMJWwYooBMgCu3L++61Lq28mDl+42k7MYtaI8wIhCpAMHPwZGka+dTL6++flSuunpwZgrGWkak6mrAV14zM9Fzun3e12oQs+mlZUVsn+MzdOicYjNY9w4przQHo2sJplve+riVBvmJ/RHvJAlmVhLkvHk+vBGUId8LoMGN1QnoJtKJ6lodcBRqb8OwfdPMsdUxK4uH6fd2mTqGe9jCSMYO1Lv0d/PSJ83o8tH1YeGY9HmEW975lBZueE4EU3pUr1nC6hH7MMZfTOiVinW6FsHuE6gfKzDOnZ1YsB3ZXZl2iKZaz6aTT/JmKNz3rWvrs69y0MukoU5Jv9/6iAFMx7/AEStsTXyN40FsNRKeBQEq+lgoQTzDhVhb0aLfoWBdJyld8ipEV1phSsmv3VFJsDabbGwpgqCxZgocYzt47IrKLBsFWRz1hur0OYUWiazoHZu5X9jY3JfNAcTZoAidcdkD33sdGZMbcVXMbbbm/LPtwxddfv/zd33zzn/7Tz/7+f/z2n/7HH/7197//6+9+f3/6w/jL9fFHX4/H6zuGxWP5w6YmInbt7sIg90TMsQhRLVQDkM6sFlvjHtr55SwaI0oftiJTZLqqk2w17Wvdb1thWFWtLNUTZpdH5yAASme9DM0mTVGARFUZWFVrXU5kNWgRZkRNy5TeM0pcwekrYlevWHfeEpsZ1QmlvCmFjJ6YcRCKORLNmNkk70yj0mRlSpgTnCCz66CmQZuucO/uvetaF2lbQUAAOIlBjx25izTP3nrjJJnns+LXbKrc1u6tX167zf00m48SlmZgmU1FK5oJVBmNuNWz76473/5MfO77j4b79aW+//blb375zd/94ptf/Oyr775599W76wqzaVoTk2iCDve4phJmF0OZW3NYFZjFzm1x+OXqzE50x7XsmdQ2XdK5ASOupU6Jk8Bcq+w5Q/fQTCkDFhK3Cn5hxOlxk77W3cwpuk0mbQXSxGPte7tZiYWiMDdFnEBBAHBm7rXCDKih4XG9Zt59nmfn0VZCNNZd6hdb6hM7o8Zw39tXYEZesBmucH0sknt1F2FVKS04ietaws1w8PqWXCBru7uUL2LHCvDw7JqeJsMMHG3YyvI0XzTuvUfc+FEGm+IfMVPTK6IkUKYDJ9cBxHSHe3YJeeB//n/+3xlPzdgMGt211uppLQ5iluskAeNLtDrA6YHDnFOo6syyYETolwHtZjP08KmycAJZQ54ofN1bNnjLJIWmHSpjDpvdfl16xFdcWw1lQCxVP+K6rl13Z5ubBGRu3l3mwbNqjYRbMDwZChCgcxpZ2z20hXWmyHRpGLT9VXeXyF5Tslt4CH0iWH36wgRVpYiTSuJLVIgL8STh9PveopF0uGDGfVXtp/QNrvqdHnMLiy0477j8j3SxS23p5ha7syqFXw3h5rRQiGGlmAyEhxy8ilC6q9/e6t9+/+lf/vXP/79/+u2vf/PnP/4pN9/j+urlw3f28hov79bL4vLrWtkFHyrQRg/QM+paJQmm62AwMyfwqsrMxnjabs/Lr0Hp6EGkK4Mj6C3zZY1YcbVvOjmmDYBaNEVUuNlUglbVaPiSV0MeSO8elkqRRoFIIwJZKgJMbc1EIxORGPtlq3rPyaFrc3vwepv9zLxj96g7ibTc2+nqTarMFZGp9FECuOLadQvKM40OClaqnsHuIhChhoxxuvRJONEpx92gQBuCBlZXKUEEnOna4+5K1egs+gnOhPiJRvXMNBqVb+ipvHu/de96+4z5afIvV+yv3vFn37//xS8+/s0vPv74w8dvP75798LHMhrQU1sWtuouYSCB2PVJChGakXOtpT2pW6PbxuC6rpna9/nxBTQrxV4At75VKrMBx+/KZwH34xF7pz6xcIHGOKqUaTO6RaPePr1FOP3Epg6gqBLpoHQN2Km0goJmzc7f4UiovqDKVUqCigjZ4Mxt+sQTdTX9mDAlY9IYSuekvrI6sV2ZPKifgw30SDRMhIeS+sL1FxOWIS0zq7P73AGSzPVU0FvxU+E4rhodk+d8JphVj2tlJkyJG5hpt2fkJX2UAaJ++fNNtUWgUpXUEo4pTz/8+M0OnbTWmi3dKhV5Pz2qcXNy6/300+5b05xxeKIJvLy+TPeyK3vTrTvlZxNQ2N3T8HCjVefRJYPaDuUc9nV1l5ZuwlfECRw15QXpL39i4G3ZDA3OsMEsW9m592EUQRh5bi2Ajcvf3fMJMzB2Tiw3e5B0M1llzmCj81SIivp15OuTekyTKQiZA7vXiu7O7Kc12nAywY+oYLpVYiUUUnni0qJV34LXYi1FQhpODG2mro1VXpkZywwBUo7Hmcn79gha9NTp9p5T9Ktx4AvykMfgbuZ8IV8f18cP148/fvW//i/f/frf/vwv//z7f/jnP//rb3716de/uWs9vvl5XK/++tXjsa6Xiy9rrONaVWURSqfYlSQiVlZ27fAl47yZjRDAwXVdZ2U8/hojkL1Dt50RYANVJ4fmqOyqMZMKfCa7uxPkv2fd6MDU8lFVNOlreXxMHP2aylKmBsxyenLMYc5D64oQNK+qXTeUgESAqMHnfqO7sON5wruVTahp76nYIcV0QpYfzH1vpThoTAExh3BW1wcAdpaZxD5STkCPU09x+JRHDWZS4kKp6cVja2yECk9bech1C2yxnprKyWIn7rf77c/TO++/kp9Rnx7XfPOLl1/+/Gf/89989+MPH77/5vWrD9e7x3Jr6wQxWRJiOQ3wltq4O3FLyjEt2bSdYGQjGmyGLXOrTGDc4jSkzJN1t4NnOq26hj0QQsXzi0g6713hfrrB55S0CBeSxiarqnKt6zxMptqWyV3kEQgIYc+9hRwK7AXg9FZU30z1KZp/U6v58ZbhICfuYbEzpSLVWYxuuuYwd7MJArP3GBEWmDHa1JhJZIxRvnJ31yiC4nCWY6SWs6xuIY66b2pKhaAbs9al2BwzlQGMIndFa3XPdV1v9+1uJhGNWfVoh6bAAUooLH6i4DD37uxKSpxDaVKaJm8CIe0tpEDvmSyaLafBNLHKerF3rXWpME2La9V0p4eTnjtBZn0m5npcvU1JnADFxas2YVe6u3SpdY5e/flDDzmAhNE3mbLyAgBerpezuVcbTQ5JXdI981abgEeEr8ocQkZqeUAAfu6/tvwRJ4P9aR8lRJrXUYlNRLj06wKRn1JmDKvR01dcfUL/+XYrEMaq0kTnPYsoq/Qbh7RDAxgHWGvlvWHIXaQfD8sJBwvtYSJaGtVVscJXIOuYCWTZkJT+iMs5tMx7SWwzGD91ek/hKYgm4yTacF4fXN+/fvvd6//0d9/+X/+w/+VXf/r1r//0z7/5w+/+8I+f/sS2D2+v717efbTrspfH4/XFrkdn23J2LY+aBuAWQuqpoR6A+kkanaD5fe/1vI81+Ywi0ivdo6Ykma3qdXmj4rEqd/iqqmfgD6fH3KXdwrBnqvK6Lpp4PGUwKJdZJyM0ULtHVbZMssWhYJVuEDU0PQnVBZIWhsG9d2JMOQrhmhYdLr7BB33kHoAxu1ZcP33+dIXdd0YsSS11ZlWWfm49gjzosCKVLEcCEaCLoCifrtPGKihBqoov6TFm1jNTOhouLX9mlm+f687KrPuTfLzE3fXJ8fb+wtffXt9/87Nf/vzrH3/+8Wffffj265fXyx4X3QbE5OxKG5vq6TpMRA/DcOdhM3yJ6nP3GWRlD5Yj4oW9cVT8PtO105wnHWSmThwFe+oWjw7QxC/WACuu7OxsgPeWrYcMF2qa0nFBosSwNlA3B89bKcE7Bop7i6hnMXVVq58OgMLdbrWSHFOLoEgbltj463qpzMLk/RYR0yOspmdmyMGz1PgoUzzi3FWwqu6asX2M7m7Tc8oBVdpayrU9Gp56Jqe7hZYSUeLT+uzfCNWvPYsIpUqeJiG+cMWaQ/lrxERPP/wlK7V2P7HwAbzrVFnGulTIeuICa2wm6B76SUgY5mSaF1ra/ZYuRaCz8ivDI3PMqUJDJchL/7vWyszKeoLszL1pfj2WKt1n0KWs8nSaxObX9fjLp7826+3tzd2BdgsQZkBq2jAMsvegY4VUfa2RxA/+paeke4o1RnTHCqP3qRKLrC0Ac61rJqXcp3vnzJcTqieuILB3Wji6Ivzt861JheG193U9BAJIn8Gj2SJBRVrNYOc2Y3jknupxg4UrnSrW6moYa+/H9VAWJIjuMkd1RcTeGz2Px7s7345VanxQ+qkNfmJhDiF0yHM9Fo6ThHN00BG6v+20LZap/a5rmV+Gl3h899Xjb3788NOnn//2d59+/Zs//vq3f/yHf/rd7//8h7/+22/4ePXXb376Pde7bz3WenmFtT8ednndJb1/s0FSMYTSow+zS/JkJWSZ8USVa9jnAPC1Jos0hMmWMjx91nZqKiW1qK5yi9xbNA3NO2ddCzaYgU9n6xtR4yAaI+YpYiojVu281hEvCsZUXUalTF7dux/xWj51WozKgBNhL69r9klQkCR68JbFqbVWlxJ64e5jmJ6j4FH4oJsgEYaNNDoiS3pUl02q+oZKcxM2orhHKZpELdauzuO8lOAts3rf0133p+mu/SfO5vx0Rb77Gt9///6Xv/jm5999+Pn3H3/4+t27h797WZgyI3v7kLSNXm4DwK0rM9OMgM9WsLmOEmt2RFxrZVZNduUQu96AOeEWy6cQseSmPBIa8wgz88+fPsVyjL4tVGd3kdx1uxn0fNqzSmTUG5omNitPmotU4+osqmpzk6uDMoqHUX/hHunrbexa8Xbf1wrYyWcSoC0/Y03N4LpWdvK4RFNvIk4ZETkt7YlHSMlq7k+7He+9DeNrfdGarLWytvTHqh18BqnNmKGKLoSSPcwqj2OqMLdhZw2hhKKaGTM3s71T4AFApTPM4bOghMfulvFQE4/TSKuqnnJflR3he+8Ri2gHVR4nqvif//f/jYq9F/8mbwrm8XjJytqb7uYx0/vzm8eqSbcVYTTQ2DtrgJ7qWo/LzUfWG03xOKL+zPs5IZKw7jLj+9cPqkg2sNG5S/VJ4XatJW7w8Nf0x/W49xvJCJ/hYNz97dNnyJ1H76r1WASlObjWtbe2Y5Cgsxu9U4LpcK+uFUGzzMbJYNBnTJGHmbWuZeSW1m3viEdEGNknAfiumu5+eTwayH3TniRDE8RaS5c31HUj/auv53TDId1saiSljxUKvZF2FqTGmZkBbd93LHdf1UOxEc+y4hq4W2fWTGUN+Xh5UdWJLe/sXbf5cnM3bgUTOR+P1+7q6giXtriGe/ef//r2b7/7/C//+vt//Jff/uOvfv/nn+rtM229H399vL6P62Ev72yFrYevYAQIf0SqqiLsGTYbkhv3KXNnd/mKEZqMQ7p11xyLCu3pErIvUVnPRHug1ZqjdIbOgR1K6xgmZyJWtjID+ginesL97SR9ghwCqmM5nATaaNQuOJKIjGgqkA4+NQinzJIYjE0XnsYngfDdkxI0ty4w8BglVINlkHI6UwUyWsNmpmuoIvLWVk2elOkzGaK1Q05nTWMqK3dXZ969756p/Rn9eeqTTT4iP3zwb797+fkPH3782Ycfvnv/w7cfvnr/eHmsy9CVTlZnRPR9T2UBdITH/faWe9eU8ordw0WidBnN/RKMgVaUqeW+T7vIs+vbjDQL984CTWNZT/cUZZEFVF1ytE/EjAwx4356Pk5eC04hmhhXymHeve/bzBRthkKsUD6aZLhxRe7sxkgJ03U9Xqu2rEUkOUdvc0CFKgonPfKZoVsfVateK2kQvmS3ny8R5zjH+UcRIPou0TilFMYxUkV1bWaZNaAbuqdUhZ35jPhFrAh5j0eeBhyFsUeXzEMlSHlaOmavbHc3N03Y932bGY1O85OR0ztbycpnmsBkZqwl4fjMEOD/+//1vylFSsmXHr7fNonn/xgppn2FzdiKv/7lLx+++kpSPx22ikaQTm5GKCgws6719vkNaF9X7g10Zbty/CvNg8O1QskB+94entlDPB4ved/gRKzqJmxdSwZ03R8ei5jKFIzlHtWlbwhG1dS4L2HFEgNJuK1udHLWtWRT0B84g3WFKBeLc0hJCADtQJk0W9dLfzFxVFcXZfw/XwMws3eaIeJSRQxmaBPrkZWYMz2eNAVoqYidOdOqu6E4oZkGw/0l3n26/6KBizSZyNYKk3xCUISZOnKnW4lM5wQ1m2GLCueYhS/PnXXfFg43gRWHKNb/1thitvd8vvtPf71/+7uf/uFf//Drf/m3X//bX/76U2ZZtdnjFfFijw/r8ZXFw66gmb8s6lVcMQI7SZosJwZpN4GT1aGOJ1CKNAtTOAcGVRXm5t7TylXN6um2sOMHdp+p+227S0lHuT1UO6bLtRMeUhugoLcGIPJ+Mw/zExO97x0rjMxbSGOTdpqNaeZWuY0mVzyGJeknOj/vda3M9vCc7H1m9t7bdJsq+WfnDN1InviXrh4gwjILMxqtcieHIhh618mYywGnszuTw8rdu3p/mr73/Zfpu/PzzI3+vNb+8FV8++37X/7s4w/fvv/xhw9ff3j95qt3L4/r5bGMMBgOXSlzhlwzXZ0gaN61q1JEA6qV1gAq1/aKE3s+7ug8HkBpdWbGzRqT917XdWiNU9OU54ik9oTpU23kqVj8rqrS79JKKMxEeKy5y7OpKpWZmSlJ0s1ONZtGO7mv1QWq1/Hfg6QMsmeu8MzsKkViUCV7pdYQTgM8SYX66nVnWLCyptti6ax/fn3hRsAyd1ebB4mdb5VbqP0MPNzV/tI4DyqUNj80g/lU0RSkYefx6FGWUdcGtXFKMssvnVlHZKH6NuNUV5W5DynVvqn5UCyjxwnpMk5V1nZfBi8kwc6O5fzP//v/Y3qj2+PSt07gvMYzUzljYwrPyWoBO3FdV1edmhu5Wykk98KcAAlzn5nuPOrPrS5mJQh1Vz8eL2ZmQInIJwDTUS5BCOSSmAEMMmPlNKgLE9PqcZ6huUo+IGxRvr9Yi42snClfFw+UP/j/F3WGO3IcNxCuItmze+eTEjuSESNO8v6PJkiWIOnku51usvyDvfATLLDo5rDJr6o6I23ruXbyXO8qcy0Pp1vHva5zxWWYcVtWeCj7kLVT9NEHotd2EYaq81wduRAeNMuZJO4VKpsKdfcYYeCWphKVi26kK5d5LK0d7d2ic5SUdM9ZBcZwaNtv9U7Z9yO61XCieRdfbCnjNLcU2+G66iyhCpfrNWs56bbZXFSNy5FLnWUxpZV8XXj+fv7x9eXDx2+fPnz9/PX5y7fn8zUTAzxsPMrNLk92PSwe7TjMR5nMxw6GNJXgbrTIWmip2X7VtKpeQjU5YmilN7Zohg23NfXf+8Xm2uHu9/ZzC/Wrw9NXWr8R/w660hbln7OZkL0pBmCcc12vD7lOVot2he0M0BN5bKMqCQVQlbJhkGqWGbV/3XZZ6dxH88pkH7VC4z37jPVOBm0HxHtta8hLKigTDYHPVFbNVeeNyjxfVVXrB/RCvbqvwXp84r9+jne/Pvz2/u3P/3zz/u2bp6eHyyWiWQsPqAib583NMheFFBrOztsNyv4WqNI2W2BV8oiuJk0VGak9JHGw973WjHg3z/dxV3l4rhLgOynoHMcBMMLWnC0m6ua/IT+AIpyWK7vpbJCsj25H38BQVR04mGtPOfq/DG97NQOllbjb9Pf3XlBXHpVorMzK5ePozreQWs1fRPOqbaxROT1G00o05FrowVN0BOlunI/r0WmGtRZbjlCpDllzb+xtxKhd8dlc4srsbX63F92DStpRo9yO3/tG19bCdN3nDslAr4jR3qJVd7PVVpt7o/xVUmbbcAKkqVZVpcdQopTh25o7aE/mz5UzUW04MM/pcaxKQoLF4edcZk5zW6doFkcCNvx8fT2uD21hOI4BZaqs/SFtJyAbo+WeDCuKpUzZGLl+3Fa6aQxfudB58aU4DkqgzAMFhucq88g1x3Cw2eCapU6Fbb9+AVVd+FrhvUkMC3djrqptNN5jtPaoYqetElYpoujmjJ4CpTIFiyi0zq6zh53uqTSitGY56eaueQNZkDn9iAjvBSAIhgEoM/TJIJ0C0ZnLNqz7HXiAOxEqsWdxsCP3I6YoGgIhh8SgaeUZI1rUXgWSq4pjSNkXpw2tZFKxJEZAKiTMKAxg1WolWkgrdRmwsIUzIa2itbYufgp/+3h59+7N///3n9sPfP8xn59fPn/58vHj508f/vj2/P3ldt6+fUwS8SAfHFfGwXGhR1wGyIKN69FaRpLd7PdyApggKxXDkJBVrZ1VtAe1VTCuKiPbIr/Pv6iaZaCMldqygDKAlZlLW17ZAzmloE5GXdmpOD3wAYSXP/9sfoJE6xUk0V330mbq13+rYFCz87QwV3brCdGHZ3O7YJezTQqtnSF8f4x2W7XaIQpSrTVvU01ypGotVConUqyFnIa0OqH5cHW78HK5/Pr7v6/O3//7/vGCX345/vGTXw9eD7+GRVg4W3vJsHmbNhBBd6P3mAt9WTCYU+2Ml7KZ5dSc57g80Ay1zW2kiRa+wGXNuIWq6KO/vjDzsHVOt6CFj1prrd7Ou69O/ik1E4q/B/3F3SFNhblbzrkTT+4PjtrWEsTOKgNpjNBa3oYC1uHkMPNOLpFoMBte52ocGUAaay03wry474VzpJdhi6TZEbYRsCh1J73xYtqoXiSaPGLdbvRxW9kY5H6FEDY8F11lEXMt81g9oR1RmSvL3UWajR4Ui6S7Ej4CqDbscwu2EMD6u7uJqt4HbMwkXLO21XHjZh6Egkxt8xBBiINs6sn6fYmcRcRh22cbVjb+AlUgQaVi7k/UAAAAAElFTkSuQmCC", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Run for generating images.\n", + "\n", + "prompt = \"photo of zwx dog in a bucket\" #@param {type:\"string\"}\n", + "negative_prompt = \"\" #@param {type:\"string\"}\n", + "num_samples = 4 #@param {type:\"number\"}\n", + "guidance_scale = 7.5 #@param {type:\"number\"}\n", + "num_inference_steps = 24 #@param {type:\"number\"}\n", + "height = 512 #@param {type:\"number\"}\n", + "width = 512 #@param {type:\"number\"}\n", + "\n", + "with autocast(\"cuda\"), torch.inference_mode():\n", + " images = pipe(\n", + " prompt,\n", + " height=height,\n", + " width=width,\n", + " negative_prompt=negative_prompt,\n", + " num_images_per_prompt=num_samples,\n", + " num_inference_steps=num_inference_steps,\n", + " guidance_scale=guidance_scale,\n", + " generator=g_cuda\n", + " ).images\n", + "\n", + "for img in images:\n", + " display(img)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "WMCqQ5Tcdsm2" + }, + "outputs": [], + "source": [ + "#@markdown Run Gradio UI for generating images.\n", + "import gradio as gr\n", + "\n", + "def inference(prompt, negative_prompt, num_samples, height=512, width=512, num_inference_steps=50, guidance_scale=7.5):\n", + " with torch.autocast(\"cuda\"), torch.inference_mode():\n", + " return pipe(\n", + " prompt, height=int(height), width=int(width),\n", + " negative_prompt=negative_prompt,\n", + " num_images_per_prompt=int(num_samples),\n", + " num_inference_steps=int(num_inference_steps), guidance_scale=guidance_scale,\n", + " generator=g_cuda\n", + " ).images\n", + "\n", + "with gr.Blocks() as demo:\n", + " with gr.Row():\n", + " with gr.Column():\n", + " prompt = gr.Textbox(label=\"Prompt\", value=\"photo of zwx dog in a bucket\")\n", + " negative_prompt = gr.Textbox(label=\"Negative Prompt\", value=\"\")\n", + " run = gr.Button(value=\"Generate\")\n", + " with gr.Row():\n", + " num_samples = gr.Number(label=\"Number of Samples\", value=4)\n", + " guidance_scale = gr.Number(label=\"Guidance Scale\", value=7.5)\n", + " with gr.Row():\n", + " height = gr.Number(label=\"Height\", value=512)\n", + " width = gr.Number(label=\"Width\", value=512)\n", + " num_inference_steps = gr.Slider(label=\"Steps\", value=24)\n", + " with gr.Column():\n", + " gallery = gr.Gallery()\n", + "\n", + " run.click(inference, inputs=[prompt, negative_prompt, num_samples, height, width, num_inference_steps, guidance_scale], outputs=gallery)\n", + "\n", + "demo.launch(debug=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "lJoOgLQHnC8L" + }, + "outputs": [], + "source": [ + "#@title (Optional) Delete diffuser and old weights and only keep the ckpt to free up drive space.\n", + "\n", + "#@markdown [ ! ] Caution, Only execute if you are sure u want to delete the diffuser format weights and only use the ckpt.\n", + "import shutil\n", + "from glob import glob\n", + "import os\n", + "for f in glob(OUTPUT_DIR+os.sep+\"*\"):\n", + " if f != WEIGHTS_DIR:\n", + " shutil.rmtree(f)\n", + " print(\"Deleted\", f)\n", + "for f in glob(WEIGHTS_DIR+\"/*\"):\n", + " if not f.endswith(\".ckpt\") or not f.endswith(\".json\"):\n", + " try:\n", + " shutil.rmtree(f)\n", + " except NotADirectoryError:\n", + " continue\n", + " print(\"Deleted\", f)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jXgi8HM4c-DA" + }, + "outputs": [], + "source": [ + "#@title Free runtime memory\n", + "exit()" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "2.7.16 (default, Oct 10 2019, 22:02:15) \n[GCC 8.3.0]" + }, + "vscode": { + "interpreter": { + "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" + } + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": {} + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/diffusers/examples/dreambooth/README.md b/diffusers/examples/dreambooth/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ff1349943eb01ab2a15ce1c85f8f68bb2971f6ce --- /dev/null +++ b/diffusers/examples/dreambooth/README.md @@ -0,0 +1,506 @@ +To reduce VRAM usage to 9.92 GB, pass `--gradient_checkpointing` and `--use_8bit_adam` flag to use 8 bit adam optimizer from [bitsandbytes](https://github.com/TimDettmers/bitsandbytes). + +Model with just [xformers](https://github.com/facebookresearch/xformers) memory efficient flash attention uses 15.79 GB VRAM with `--gradient_checkpointing` else 17.7 GB. Both have no loss in precision at all. gradient_checkpointing recalculates intermediate activations to save memory at cost of some speed. + +Caching the outputs of VAE and Text Encoder and freeing them also helped in reducing memory. + +You can now convert to ckpt format using this script to use in UIs like AUTOMATIC1111. https://github.com/ShivamShrirao/diffusers/raw/main/scripts/convert_diffusers_to_original_stable_diffusion.py Check colab notebook for example usage. + +[![DreamBooth Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ShivamShrirao/diffusers/blob/main/examples/dreambooth/DreamBooth_Stable_Diffusion.ipynb) + +Use the table below to choose the best flags based on your memory and speed requirements. Tested on Tesla T4 GPU. + +| `fp16` | `train_batch_size` | `gradient_accumulation_steps` | `gradient_checkpointing` | `use_8bit_adam` | GB VRAM usage | Speed (it/s) | +| ---- | ------------------ | ----------------------------- | ----------------------- | --------------- | ---------- | ------------ | +| fp16 | 1 | 1 | TRUE | TRUE | 9.92 | 0.93 | +| no | 1 | 1 | TRUE | TRUE | 10.08 | 0.42 | +| fp16 | 2 | 1 | TRUE | TRUE | 10.4 | 0.66 | +| fp16 | 1 | 1 | FALSE | TRUE | 11.17 | 1.14 | +| no | 1 | 1 | FALSE | TRUE | 11.17 | 0.49 | +| fp16 | 1 | 2 | TRUE | TRUE | 11.56 | 1 | +| fp16 | 2 | 1 | FALSE | TRUE | 13.67 | 0.82 | +| fp16 | 1 | 2 | FALSE | TRUE | 13.7 | 0.83 | +| fp16 | 1 | 1 | TRUE | FALSE | 15.79 | 0.77 | + +# DreamBooth training example + +[DreamBooth](https://arxiv.org/abs/2208.12242) is a method to personalize text2image models like stable diffusion given just a few(3~5) images of a subject. +The `train_dreambooth.py` script shows how to implement the training procedure and adapt it for stable diffusion. + + +## Running locally with PyTorch + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +pip install git+https://github.com/ShivamShrirao/diffusers.git +pip install -U -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +Or for a default accelerate configuration without answering questions about your environment + +```bash +accelerate config default +``` + +Or if your environment doesn't support an interactive shell e.g. a notebook + +```python +from accelerate.utils import write_basic_config +write_basic_config() +``` + +### Dog toy example + +Now let's get our dataset. Download images from [here](https://drive.google.com/drive/folders/1BO_dyz-p65qhBRRMRA4TbZ8qW4rB99JZ) and save them in a directory. This will be our training data. + +And launch the training using + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=400 +``` + +### Training with prior-preservation loss + +Prior-preservation is used to avoid overfitting and language-drift. Refer to the paper to learn more about it. For prior-preservation we first generate images using the model with a class prompt and then use those during training along with our data. +According to the paper, it's recommended to generate `num_epochs * num_samples` images for prior-preservation. 200-300 works well for most cases. The `num_class_images` flag sets the number of images to generate with the class prompt. You can place existing images in `class_data_dir`, and the training script will generate any additional images so that `num_class_images` are present in `class_data_dir` during training time. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Using prompt per image + +You can use `--read_prompts_from_txts` to make the script use a separate prompt for each image. This makes the training act like regular fine-tuning and not like Dreambooth. For each image, you need to create a txt file with the prompt. For example, if image is named `dog.png` create `dog.png.txt`. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --resolution=512 \ + --train_batch_size=1 \ + --train_text_encoder \ + --mixed_precision="fp16" \ + --use_8bit_adam \ + --gradient_accumulation_steps=1 \ + --gradient_checkpointing \ + --learning_rate=1e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=800 \ + --read_prompts_from_txts +``` + +### Training on a 16GB GPU: + +With the help of gradient checkpointing and the 8-bit optimizer from bitsandbytes it's possible to run train dreambooth on a 16GB GPU. + +To install `bitandbytes` please refer to this [readme](https://github.com/TimDettmers/bitsandbytes#requirements--installation). + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=2 --gradient_checkpointing \ + --use_8bit_adam \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + + +### Training on a 12GB GPU: + +It is possible to run dreambooth on a 12GB GPU by using the following optimizations: +- [gradient checkpointing and the 8-bit optimizer](#training-on-a-16gb-gpu) +- [xformers](#training-with-xformers) +- [setting grads to none](#set-grads-to-none) + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 --gradient_checkpointing \ + --use_8bit_adam \ + --enable_xformers_memory_efficient_attention \ + --set_grads_to_none \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + + +### Training on a 8 GB GPU: + +By using [DeepSpeed](https://www.deepspeed.ai/) it's possible to offload some +tensors from VRAM to either CPU or NVME allowing to train with less VRAM. + +DeepSpeed needs to be enabled with `accelerate config`. During configuration +answer yes to "Do you want to use DeepSpeed?". With DeepSpeed stage 2, fp16 +mixed precision and offloading both parameters and optimizer state to cpu it's +possible to train on under 8 GB VRAM with a drawback of requiring significantly +more RAM (about 25 GB). See [documentation](https://huggingface.co/docs/accelerate/usage_guides/deepspeed) for more DeepSpeed configuration options. + +Changing the default Adam optimizer to DeepSpeed's special version of Adam +`deepspeed.ops.adam.DeepSpeedCPUAdam` gives a substantial speedup but enabling +it requires CUDA toolchain with the same version as pytorch. 8-bit optimizer +does not seem to be compatible with DeepSpeed at the moment. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch --mixed_precision="fp16" train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --sample_batch_size=1 \ + --gradient_accumulation_steps=1 --gradient_checkpointing \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Fine-tune text encoder with the UNet. + +The script also allows to fine-tune the `text_encoder` along with the `unet`. It's been observed experimentally that fine-tuning `text_encoder` gives much better results especially on faces. +Pass the `--train_text_encoder` argument to the script to enable training `text_encoder`. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_text_encoder \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --use_8bit_adam \ + --gradient_checkpointing \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Using DreamBooth for pipelines other than Stable Diffusion + +The [AltDiffusion pipeline](https://huggingface.co/docs/diffusers/api/pipelines/alt_diffusion) also supports dreambooth fine-tuning. The process is the same as above, all you need to do is replace the `MODEL_NAME` like this: + +``` +export MODEL_NAME="CompVis/stable-diffusion-v1-4" --> export MODEL_NAME="BAAI/AltDiffusion-m9" +or +export MODEL_NAME="CompVis/stable-diffusion-v1-4" --> export MODEL_NAME="BAAI/AltDiffusion" +``` + +### Inference + +Once you have trained a model using the above command, you can run inference simply using the `StableDiffusionPipeline`. Make sure to include the `identifier` (e.g. sks in above example) in your prompt. + +```python +from diffusers import StableDiffusionPipeline +import torch + +model_id = "path-to-your-trained-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A photo of sks dog in a bucket" +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("dog-bucket.png") +``` + +### Inference from a training checkpoint + +You can also perform inference from one of the checkpoints saved during the training process, if you used the `--checkpointing_steps` argument. Please, refer to [the documentation](https://huggingface.co/docs/diffusers/main/en/training/dreambooth#performing-inference-using-a-saved-checkpoint) to see how to do it. + +## Training with Low-Rank Adaptation of Large Language Models (LoRA) + +Low-Rank Adaption of Large Language Models was first introduced by Microsoft in [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685) by *Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu, Yuanzhi Li, Shean Wang, Lu Wang, Weizhu Chen* + +In a nutshell, LoRA allows to adapt pretrained models by adding pairs of rank-decomposition matrices to existing weights and **only** training those newly added weights. This has a couple of advantages: +- Previous pretrained weights are kept frozen so that the model is not prone to [catastrophic forgetting](https://www.pnas.org/doi/10.1073/pnas.1611835114) +- Rank-decomposition matrices have significantly fewer parameters than the original model, which means that trained LoRA weights are easily portable. +- LoRA attention layers allow to control to which extent the model is adapted towards new training images via a `scale` parameter. + +[cloneofsimo](https://github.com/cloneofsimo) was the first to try out LoRA training for Stable Diffusion in +the popular [lora](https://github.com/cloneofsimo/lora) GitHub repository. + +### Training + +Let's get started with a simple example. We will re-use the dog example of the [previous section](#dog-toy-example). + +First, you need to set-up your dreambooth training example as is explained in the [installation section](#Installing-the-dependencies). +Next, let's download the dog dataset. Download images from [here](https://drive.google.com/drive/folders/1BO_dyz-p65qhBRRMRA4TbZ8qW4rB99JZ) and save them in a directory. Make sure to set `INSTANCE_DIR` to the name of your directory further below. This will be our training data. + +Now, you can launch the training. Here we will use [Stable Diffusion 1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5). + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +**___Note: It is quite useful to monitor the training progress by regularly generating sample images during training. [wandb](https://docs.wandb.ai/quickstart) is a nice solution to easily see generating images during training. All you need to do is to run `pip install wandb` before training and pass `--report_to="wandb"` to automatically log images.___** + + +```bash +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" +``` + +For this example we want to directly store the trained LoRA embeddings on the Hub, so +we need to be logged in and add the `--push_to_hub` flag. + +```bash +huggingface-cli login +``` + +Now we can start training! + +```bash +accelerate launch train_dreambooth_lora.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --checkpointing_steps=100 \ + --learning_rate=1e-4 \ + --report_to="wandb" \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=500 \ + --validation_prompt="A photo of sks dog in a bucket" \ + --validation_epochs=50 \ + --seed="0" \ + --push_to_hub +``` + +**___Note: When using LoRA we can use a much higher learning rate compared to vanilla dreambooth. Here we +use *1e-4* instead of the usual *2e-6*.___** + +The final LoRA embedding weights have been uploaded to [patrickvonplaten/lora_dreambooth_dog_example](https://huggingface.co/patrickvonplaten/lora_dreambooth_dog_example). **___Note: [The final weights](https://huggingface.co/patrickvonplaten/lora/blob/main/pytorch_attn_procs.bin) are only 3 MB in size which is orders of magnitudes smaller than the original model.** + +The training results are summarized [here](https://api.wandb.ai/report/patrickvonplaten/xm6cd5q5). +You can use the `Step` slider to see how the model learned the features of our subject while the model trained. + +### Inference + +After training, LoRA weights can be loaded very easily into the original pipeline. First, you need to +load the original pipeline: + +```python +from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler +import torch + +pipe = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) +pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) +pipe.to("cuda") +``` + +Next, we can load the adapter layers into the UNet with the [`load_attn_procs` function](https://huggingface.co/docs/diffusers/api/loaders#diffusers.loaders.UNet2DConditionLoadersMixin.load_attn_procs). + +```python +pipe.unet.load_attn_procs("patrickvonplaten/lora_dreambooth_dog_example") +``` + +Finally, we can run the model in inference. + +```python +image = pipe("A picture of a sks dog in a bucket", num_inference_steps=25).images[0] +``` + +## Training with Flax/JAX + +For faster training on TPUs and GPUs you can leverage the flax training example. Follow the instructions above to get the model and dataset before running the script. + +____Note: The flax example don't yet support features like gradient checkpoint, gradient accumulation etc, so to use flax for faster training we will need >30GB cards.___ + + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install -U -r requirements_flax.txt +``` + + +### Training without prior preservation loss + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +python train_dreambooth_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --learning_rate=5e-6 \ + --max_train_steps=400 +``` + + +### Training with prior preservation loss + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +python train_dreambooth_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --learning_rate=5e-6 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + + +### Fine-tune text encoder with the UNet. + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +python train_dreambooth_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_text_encoder \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --learning_rate=2e-6 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Training with xformers: +You can enable memory efficient attention by [installing xFormers](https://github.com/facebookresearch/xformers#installing-xformers) and padding the `--enable_xformers_memory_efficient_attention` argument to the script. This is not available with the Flax/JAX implementation. + +You can also use Dreambooth to train the specialized in-painting model. See [the script in the research folder for details](https://github.com/huggingface/diffusers/tree/main/examples/research_projects/dreambooth_inpaint). + +### Set grads to none + +To save even more memory, pass the `--set_grads_to_none` argument to the script. This will set grads to None instead of zero. However, be aware that it changes certain behaviors, so if you start experiencing any problems, remove this argument. + +More info: https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html + +### Experimental results +You can refer to [this blog post](https://huggingface.co/blog/dreambooth) that discusses some of DreamBooth experiments in detail. Specifically, it recommends a set of DreamBooth-specific tips and tricks that we have found to work well for a variety of subjects. diff --git a/diffusers/examples/dreambooth/concepts_list.json b/diffusers/examples/dreambooth/concepts_list.json new file mode 100644 index 0000000000000000000000000000000000000000..b7924ee6bf879ac78225ffac9304a9e025fe6f88 --- /dev/null +++ b/diffusers/examples/dreambooth/concepts_list.json @@ -0,0 +1,8 @@ +[ + { + "instance_prompt": "photo of zwx dog", + "class_prompt": "photo of a dog", + "instance_data_dir": "../../../data/alvan", + "class_data_dir": "../../../data/dog" + } +] \ No newline at end of file diff --git a/diffusers/examples/dreambooth/launch.sh b/diffusers/examples/dreambooth/launch.sh new file mode 100644 index 0000000000000000000000000000000000000000..e7d3c9caceeb37565231a03554175678ca360da3 --- /dev/null +++ b/diffusers/examples/dreambooth/launch.sh @@ -0,0 +1,25 @@ +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export OUTPUT_DIR="../../../models/alvan_shivam" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --pretrained_vae_name_or_path="stabilityai/sd-vae-ft-mse" \ + --output_dir=$OUTPUT_DIR \ + --revision="fp16" \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --seed=3434554 \ + --resolution=512 \ + --train_batch_size=1 \ + --train_text_encoder \ + --mixed_precision="fp16" \ + --use_8bit_adam \ + --gradient_accumulation_steps=1 \ + --learning_rate=1e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=50 \ + --sample_batch_size=4 \ + --max_train_steps=800 \ + --save_interval=400 \ + --save_sample_prompt="photo of zwx dog" \ + --concepts_list="concepts_list.json" diff --git a/diffusers/examples/dreambooth/launch_inpaint.sh b/diffusers/examples/dreambooth/launch_inpaint.sh new file mode 100644 index 0000000000000000000000000000000000000000..bddf034d6f71992e7e721f4aa7047d1891fa933a --- /dev/null +++ b/diffusers/examples/dreambooth/launch_inpaint.sh @@ -0,0 +1,26 @@ +export MODEL_NAME="runwayml/stable-diffusion-inpainting" +export OUTPUT_DIR="../../../models/dress_inpainting" + +accelerate launch train_inpainting_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --pretrained_vae_name_or_path="stabilityai/sd-vae-ft-mse" \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --seed=3434554 \ + --resolution=512 \ + --train_batch_size=2 \ + --train_text_encoder \ + --mixed_precision="fp16" \ + --gradient_accumulation_steps=1 \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=300 \ + --sample_batch_size=4 \ + --max_train_steps=15000 \ + --save_interval=1000 \ + --save_min_steps=6000 \ + --save_infer_steps=35 \ + --concepts_list="concepts_list.json" \ + --not_cache_latents \ + --hflip diff --git a/diffusers/examples/dreambooth/requirements.txt b/diffusers/examples/dreambooth/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..370578985672e10b73f6788a6be3060dcb456088 --- /dev/null +++ b/diffusers/examples/dreambooth/requirements.txt @@ -0,0 +1,8 @@ +accelerate +torchvision +transformers>=4.25.1 +ftfy +tensorboard +Jinja2 +safetensors +xformers \ No newline at end of file diff --git a/diffusers/examples/dreambooth/requirements_flax.txt b/diffusers/examples/dreambooth/requirements_flax.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f85ad523a3b46b65abf0138c05ecdd656e6845c --- /dev/null +++ b/diffusers/examples/dreambooth/requirements_flax.txt @@ -0,0 +1,8 @@ +transformers>=4.25.1 +flax +optax +torch +torchvision +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/dreambooth/train_dreambooth.py b/diffusers/examples/dreambooth/train_dreambooth.py new file mode 100644 index 0000000000000000000000000000000000000000..684d9f57ab5675f4550888853bfc9a727f09f5b1 --- /dev/null +++ b/diffusers/examples/dreambooth/train_dreambooth.py @@ -0,0 +1,869 @@ +import argparse +import hashlib +import itertools +import random +import json +import logging +import math +import os +from contextlib import nullcontext +from pathlib import Path +from typing import Optional + +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from torch.utils.data import Dataset + +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from diffusers import AutoencoderKL, DDIMScheduler, DDPMScheduler, StableDiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler +from diffusers.utils.import_utils import is_xformers_available +from huggingface_hub import HfFolder, Repository, whoami +from PIL import Image +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + + +torch.backends.cudnn.benchmark = True + + +logger = get_logger(__name__) + + +def parse_args(input_args=None): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--pretrained_vae_name_or_path", + type=str, + default=None, + help="Path to pretrained vae or vae identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--save_sample_prompt", + type=str, + default=None, + help="The prompt used to generate sample outputs to save.", + ) + parser.add_argument( + "--save_sample_negative_prompt", + type=str, + default=None, + help="The negative prompt used to generate sample outputs to save.", + ) + parser.add_argument( + "--n_save_sample", + type=int, + default=4, + help="The number of samples to save.", + ) + parser.add_argument( + "--save_guidance_scale", + type=float, + default=7.5, + help="CFG for save sample.", + ) + parser.add_argument( + "--save_infer_steps", + type=int, + default=20, + help="The number of inference steps for save sample.", + ) + parser.add_argument( + "--pad_tokens", + default=False, + action="store_true", + help="Flag to pad tokens to length 77.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If not have enough images, additional images will be" + " sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution" + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--log_interval", type=int, default=10, help="Log every N steps.") + parser.add_argument("--save_interval", type=int, default=10_000, help="Save weights every N steps.") + parser.add_argument("--save_min_steps", type=int, default=0, help="Start saving weights after N steps.") + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument("--not_cache_latents", action="store_true", help="Do not precompute and cache latents from VAE.") + parser.add_argument("--hflip", action="store_true", help="Apply horizontal flip data augmentation.") + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--concepts_list", + type=str, + default=None, + help="Path to json containing multiple concepts, will overwrite parameters like instance_prompt, class_prompt, etc.", + ) + parser.add_argument( + "--read_prompts_from_txts", + action="store_true", + help="Use prompt per image. Put prompts in the same directory as images, e.g. for image.png create image.png.txt.", + ) + + if input_args is not None: + args = parser.parse_args(input_args) + else: + args = parser.parse_args() + + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + concepts_list, + tokenizer, + with_prior_preservation=True, + size=512, + center_crop=False, + num_class_images=None, + pad_tokens=False, + hflip=False, + read_prompts_from_txts=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + self.with_prior_preservation = with_prior_preservation + self.pad_tokens = pad_tokens + self.read_prompts_from_txts = read_prompts_from_txts + + self.instance_images_path = [] + self.class_images_path = [] + + for concept in concepts_list: + inst_img_path = [ + (x, concept["instance_prompt"]) + for x in Path(concept["instance_data_dir"]).iterdir() + if x.is_file() and not str(x).endswith(".txt") + ] + self.instance_images_path.extend(inst_img_path) + + if with_prior_preservation: + class_img_path = [(x, concept["class_prompt"]) for x in Path(concept["class_data_dir"]).iterdir() if x.is_file()] + self.class_images_path.extend(class_img_path[:num_class_images]) + + random.shuffle(self.instance_images_path) + self.num_instance_images = len(self.instance_images_path) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + + self.image_transforms = transforms.Compose( + [ + transforms.RandomHorizontalFlip(0.5 * hflip), + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_path, instance_prompt = self.instance_images_path[index % self.num_instance_images] + + if self.read_prompts_from_txts: + with open(str(instance_path) + ".txt") as f: + instance_prompt = f.read().strip() + + instance_image = Image.open(instance_path) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + + example["instance_images"] = self.image_transforms(instance_image) + example["instance_prompt_ids"] = self.tokenizer( + instance_prompt, + padding="max_length" if self.pad_tokens else "do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.with_prior_preservation: + class_path, class_prompt = self.class_images_path[index % self.num_class_images] + class_image = Image.open(class_path) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example["class_images"] = self.image_transforms(class_image) + example["class_prompt_ids"] = self.tokenizer( + class_prompt, + padding="max_length" if self.pad_tokens else "do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +class LatentsDataset(Dataset): + def __init__(self, latents_cache, text_encoder_cache): + self.latents_cache = latents_cache + self.text_encoder_cache = text_encoder_cache + + def __len__(self): + return len(self.latents_cache) + + def __getitem__(self, index): + return self.latents_cache[index], self.text_encoder_cache[index] + + +class AverageMeter: + def __init__(self, name=None): + self.name = name + self.reset() + + def reset(self): + self.sum = self.count = self.avg = 0 + + def update(self, val, n=1): + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = Path(args.output_dir, "0", args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + if args.train_text_encoder and args.gradient_accumulation_steps > 1 and accelerator.num_processes > 1: + raise ValueError( + "Gradient accumulation is not supported when training the text encoder in distributed training. " + "Please set gradient_accumulation_steps to 1. This feature will be supported in the future." + ) + + if args.seed is not None: + set_seed(args.seed) + + if args.concepts_list is None: + args.concepts_list = [ + { + "instance_prompt": args.instance_prompt, + "class_prompt": args.class_prompt, + "instance_data_dir": args.instance_data_dir, + "class_data_dir": args.class_data_dir + } + ] + else: + with open(args.concepts_list, "r") as f: + args.concepts_list = json.load(f) + + if args.with_prior_preservation: + pipeline = None + for concept in args.concepts_list: + class_images_dir = Path(concept["class_data_dir"]) + class_images_dir.mkdir(parents=True, exist_ok=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + if pipeline is None: + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + vae=AutoencoderKL.from_pretrained( + args.pretrained_vae_name_or_path or args.pretrained_model_name_or_path, + subfolder=None if args.pretrained_vae_name_or_path else "vae", + revision=None if args.pretrained_vae_name_or_path else args.revision, + torch_dtype=torch_dtype + ), + torch_dtype=torch_dtype, + safety_checker=None, + revision=args.revision + ) + pipeline.scheduler = DDIMScheduler.from_config(pipeline.scheduler.config) + if is_xformers_available(): + pipeline.enable_xformers_memory_efficient_attention() + pipeline.set_progress_bar_config(disable=True) + pipeline.to(accelerator.device) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(concept["class_prompt"], num_new_images) + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size) + + sample_dataloader = accelerator.prepare(sample_dataloader) + + with torch.autocast("cuda"), torch.inference_mode(): + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + images = pipeline( + example["prompt"], + num_inference_steps=args.save_infer_steps + ).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained( + args.tokenizer_name, + revision=args.revision, + ) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="tokenizer", + revision=args.revision, + ) + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="text_encoder", + revision=args.revision, + ) + vae = AutoencoderKL.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="vae", + revision=args.revision, + ) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="unet", + revision=args.revision, + torch_dtype=torch.float32 + ) + + vae.requires_grad_(False) + if not args.train_text_encoder: + text_encoder.requires_grad_(False) + + if is_xformers_available(): + vae.enable_xformers_memory_efficient_attention() + unet.enable_xformers_memory_efficient_attention() + else: + logger.warning("xformers is not available. Make sure it is installed correctly") + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + if args.train_text_encoder: + text_encoder.gradient_checkpointing_enable() + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + params_to_optimize = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) if args.train_text_encoder else unet.parameters() + ) + optimizer = optimizer_class( + params_to_optimize, + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + noise_scheduler = DDPMScheduler.from_config(args.pretrained_model_name_or_path, subfolder="scheduler") + + train_dataset = DreamBoothDataset( + concepts_list=args.concepts_list, + tokenizer=tokenizer, + with_prior_preservation=args.with_prior_preservation, + size=args.resolution, + center_crop=args.center_crop, + num_class_images=args.num_class_images, + pad_tokens=args.pad_tokens, + hflip=args.hflip, + read_prompts_from_txts=args.read_prompts_from_txts, + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad( + {"input_ids": input_ids}, + padding=True, + return_tensors="pt", + ).input_ids + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + } + return batch + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn, pin_memory=True + ) + + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + vae.to(accelerator.device, dtype=weight_dtype) + if not args.train_text_encoder: + text_encoder.to(accelerator.device, dtype=weight_dtype) + + if not args.not_cache_latents: + latents_cache = [] + text_encoder_cache = [] + for batch in tqdm(train_dataloader, desc="Caching latents"): + with torch.no_grad(): + batch["pixel_values"] = batch["pixel_values"].to(accelerator.device, non_blocking=True, dtype=weight_dtype) + batch["input_ids"] = batch["input_ids"].to(accelerator.device, non_blocking=True) + latents_cache.append(vae.encode(batch["pixel_values"]).latent_dist) + if args.train_text_encoder: + text_encoder_cache.append(batch["input_ids"]) + else: + text_encoder_cache.append(text_encoder(batch["input_ids"])[0]) + train_dataset = LatentsDataset(latents_cache, text_encoder_cache) + train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=1, collate_fn=lambda x: x, shuffle=True) + + del vae + if not args.train_text_encoder: + del text_encoder + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + if args.train_text_encoder: + unet, text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, text_encoder, optimizer, train_dataloader, lr_scheduler + ) + else: + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth") + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + + def save_weights(step): + # Create the pipeline using using the trained modules and save it. + if accelerator.is_main_process: + if args.train_text_encoder: + text_enc_model = accelerator.unwrap_model(text_encoder, keep_fp32_wrapper=True) + else: + text_enc_model = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision) + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet, keep_fp32_wrapper=True), + text_encoder=text_enc_model, + vae=AutoencoderKL.from_pretrained( + args.pretrained_vae_name_or_path or args.pretrained_model_name_or_path, + subfolder=None if args.pretrained_vae_name_or_path else "vae", + revision=None if args.pretrained_vae_name_or_path else args.revision, + ), + safety_checker=None, + torch_dtype=torch.float16, + revision=args.revision, + ) + pipeline.scheduler = DDIMScheduler.from_config(pipeline.scheduler.config) + if is_xformers_available(): + pipeline.enable_xformers_memory_efficient_attention() + save_dir = os.path.join(args.output_dir, f"{step}") + pipeline.save_pretrained(save_dir) + with open(os.path.join(save_dir, "args.json"), "w") as f: + json.dump(args.__dict__, f, indent=2) + + if args.save_sample_prompt is not None: + pipeline = pipeline.to(accelerator.device) + g_cuda = torch.Generator(device=accelerator.device).manual_seed(args.seed) + pipeline.set_progress_bar_config(disable=True) + sample_dir = os.path.join(save_dir, "samples") + os.makedirs(sample_dir, exist_ok=True) + with torch.autocast("cuda"), torch.inference_mode(): + for i in tqdm(range(args.n_save_sample), desc="Generating samples"): + images = pipeline( + args.save_sample_prompt, + negative_prompt=args.save_sample_negative_prompt, + guidance_scale=args.save_guidance_scale, + num_inference_steps=args.save_infer_steps, + generator=g_cuda + ).images + images[0].save(os.path.join(sample_dir, f"{i}.png")) + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + print(f"[*] Weights saved at {save_dir}") + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + global_step = 0 + loss_avg = AverageMeter() + text_enc_context = nullcontext() if args.train_text_encoder else torch.no_grad() + for epoch in range(args.num_train_epochs): + unet.train() + if args.train_text_encoder: + text_encoder.train() + for step, batch in enumerate(train_dataloader): + with accelerator.accumulate(unet): + # Convert images to latent space + with torch.no_grad(): + if not args.not_cache_latents: + latent_dist = batch[0][0] + else: + latent_dist = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist + latents = latent_dist.sample() * 0.18215 + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + with text_enc_context: + if not args.not_cache_latents: + if args.train_text_encoder: + encoder_hidden_states = text_encoder(batch[0][1])[0] + else: + encoder_hidden_states = batch[0][1] + else: + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and model_pred into two parts and compute the loss on each part separately. + model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Compute prior loss + prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + # if accelerator.sync_gradients: + # params_to_clip = ( + # itertools.chain(unet.parameters(), text_encoder.parameters()) + # if args.train_text_encoder + # else unet.parameters() + # ) + # accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad(set_to_none=True) + loss_avg.update(loss.detach_(), bsz) + + if not global_step % args.log_interval: + logs = {"loss": loss_avg.avg.item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step > 0 and not global_step % args.save_interval and global_step >= args.save_min_steps: + save_weights(global_step) + + progress_bar.update(1) + global_step += 1 + + if global_step >= args.max_train_steps: + break + + accelerator.wait_for_everyone() + + save_weights(global_step) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) \ No newline at end of file diff --git a/diffusers/examples/dreambooth/train_dreambooth_flax.py b/diffusers/examples/dreambooth/train_dreambooth_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..d20a0d25c19e0dbdac4a9022c4056495bd9f53ef --- /dev/null +++ b/diffusers/examples/dreambooth/train_dreambooth_flax.py @@ -0,0 +1,704 @@ +import argparse +import hashlib +import logging +import math +import os +from pathlib import Path +from typing import Optional + +import jax +import jax.numpy as jnp +import numpy as np +import optax +import torch +import torch.utils.checkpoint +import transformers +from flax import jax_utils +from flax.training import train_state +from flax.training.common_utils import shard +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from jax.experimental.compilation_cache import compilation_cache as cc +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel, set_seed + +from diffusers import ( + FlaxAutoencoderKL, + FlaxDDPMScheduler, + FlaxPNDMScheduler, + FlaxStableDiffusionPipeline, + FlaxUNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion import FlaxStableDiffusionSafetyChecker +from diffusers.utils import check_min_version + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +# Cache compiled models across invocations of this script. +cc.initialize_cache(os.path.expanduser("~/.cache/jax/compilation_cache")) + +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--pretrained_vae_name_or_path", + type=str, + default=None, + help="Path to pretrained vae or vae identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If there are not enough images already present in" + " class_data_dir, additional images will be sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--save_steps", type=int, default=None, help="Save a checkpoint every X steps.") + parser.add_argument("--seed", type=int, default=0, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.instance_data_dir is None: + raise ValueError("You must specify a train data directory.") + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = Path(instance_data_root) + if not self.instance_data_root.exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path = list(Path(instance_data_root).iterdir()) + self.num_instance_images = len(self.instance_images_path) + self.instance_prompt = instance_prompt + self._length = self.num_instance_images + + if class_data_root is not None: + self.class_data_root = Path(class_data_root) + self.class_data_root.mkdir(parents=True, exist_ok=True) + self.class_images_path = list(self.class_data_root.iterdir()) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + self.class_prompt = class_prompt + else: + self.class_data_root = None + + self.image_transforms = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_image = Image.open(self.instance_images_path[index % self.num_instance_images]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + example["instance_images"] = self.image_transforms(instance_image) + example["instance_prompt_ids"] = self.tokenizer( + self.instance_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.class_data_root: + class_image = Image.open(self.class_images_path[index % self.num_class_images]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example["class_images"] = self.image_transforms(class_image) + example["class_prompt_ids"] = self.tokenizer( + self.class_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def get_params_to_save(params): + return jax.device_get(jax.tree_util.tree_map(lambda x: x[0], params)) + + +def main(): + args = parse_args() + + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + # Setup logging, we only want one process per machine to log things on the screen. + logger.setLevel(logging.INFO if jax.process_index() == 0 else logging.ERROR) + if jax.process_index() == 0: + transformers.utils.logging.set_verbosity_info() + else: + transformers.utils.logging.set_verbosity_error() + + if args.seed is not None: + set_seed(args.seed) + + rng = jax.random.PRNGKey(args.seed) + + if args.with_prior_preservation: + class_images_dir = Path(args.class_data_dir) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, safety_checker=None, revision=args.revision + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(args.class_prompt, num_new_images) + total_sample_batch_size = args.sample_batch_size * jax.local_device_count() + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=total_sample_batch_size) + + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not jax.process_index() == 0 + ): + prompt_ids = pipeline.prepare_inputs(example["prompt"]) + prompt_ids = shard(prompt_ids) + p_params = jax_utils.replicate(params) + rng = jax.random.split(rng)[0] + sample_rng = jax.random.split(rng, jax.device_count()) + images = pipeline(prompt_ids, p_params, sample_rng, jit=True).images + images = images.reshape((images.shape[0] * images.shape[1],) + images.shape[-3:]) + images = pipeline.numpy_to_pil(np.array(images)) + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + + # Handle the repository creation + if jax.process_index() == 0: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer and add the placeholder token as a additional special token + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision + ) + else: + raise NotImplementedError("No tokenizer specified!") + + train_dataset = DreamBoothDataset( + instance_data_root=args.instance_data_dir, + instance_prompt=args.instance_prompt, + class_data_root=args.class_data_dir if args.with_prior_preservation else None, + class_prompt=args.class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad( + {"input_ids": input_ids}, padding="max_length", max_length=tokenizer.model_max_length, return_tensors="pt" + ).input_ids + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + } + batch = {k: v.numpy() for k, v in batch.items()} + return batch + + total_train_batch_size = args.train_batch_size * jax.local_device_count() + if len(train_dataset) < total_train_batch_size: + raise ValueError( + f"Training batch size is {total_train_batch_size}, but your dataset only contains" + f" {len(train_dataset)} images. Please, use a larger dataset or reduce the effective batch size. Note that" + f" there are {jax.local_device_count()} parallel devices, so your batch size can't be smaller than that." + ) + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=total_train_batch_size, shuffle=True, collate_fn=collate_fn, drop_last=True + ) + + weight_dtype = jnp.float32 + if args.mixed_precision == "fp16": + weight_dtype = jnp.float16 + elif args.mixed_precision == "bf16": + weight_dtype = jnp.bfloat16 + + if args.pretrained_vae_name_or_path: + # TODO(patil-suraj): Upload flax weights for the VAE + vae_arg, vae_kwargs = (args.pretrained_vae_name_or_path, {"from_pt": True}) + else: + vae_arg, vae_kwargs = (args.pretrained_model_name_or_path, {"subfolder": "vae", "revision": args.revision}) + + # Load models and create wrapper for stable diffusion + text_encoder = FlaxCLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", dtype=weight_dtype, revision=args.revision + ) + vae, vae_params = FlaxAutoencoderKL.from_pretrained( + vae_arg, + dtype=weight_dtype, + **vae_kwargs, + ) + unet, unet_params = FlaxUNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", dtype=weight_dtype, revision=args.revision + ) + + # Optimization + if args.scale_lr: + args.learning_rate = args.learning_rate * total_train_batch_size + + constant_scheduler = optax.constant_schedule(args.learning_rate) + + adamw = optax.adamw( + learning_rate=constant_scheduler, + b1=args.adam_beta1, + b2=args.adam_beta2, + eps=args.adam_epsilon, + weight_decay=args.adam_weight_decay, + ) + + optimizer = optax.chain( + optax.clip_by_global_norm(args.max_grad_norm), + adamw, + ) + + unet_state = train_state.TrainState.create(apply_fn=unet.__call__, params=unet_params, tx=optimizer) + text_encoder_state = train_state.TrainState.create( + apply_fn=text_encoder.__call__, params=text_encoder.params, tx=optimizer + ) + + noise_scheduler = FlaxDDPMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000 + ) + noise_scheduler_state = noise_scheduler.create_state() + + # Initialize our training + train_rngs = jax.random.split(rng, jax.local_device_count()) + + def train_step(unet_state, text_encoder_state, vae_params, batch, train_rng): + dropout_rng, sample_rng, new_train_rng = jax.random.split(train_rng, 3) + + if args.train_text_encoder: + params = {"text_encoder": text_encoder_state.params, "unet": unet_state.params} + else: + params = {"unet": unet_state.params} + + def compute_loss(params): + # Convert images to latent space + vae_outputs = vae.apply( + {"params": vae_params}, batch["pixel_values"], deterministic=True, method=vae.encode + ) + latents = vae_outputs.latent_dist.sample(sample_rng) + # (NHWC) -> (NCHW) + latents = jnp.transpose(latents, (0, 3, 1, 2)) + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise_rng, timestep_rng = jax.random.split(sample_rng) + noise = jax.random.normal(noise_rng, latents.shape) + # Sample a random timestep for each image + bsz = latents.shape[0] + timesteps = jax.random.randint( + timestep_rng, + (bsz,), + 0, + noise_scheduler.config.num_train_timesteps, + ) + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(noise_scheduler_state, latents, noise, timesteps) + + # Get the text embedding for conditioning + if args.train_text_encoder: + encoder_hidden_states = text_encoder_state.apply_fn( + batch["input_ids"], params=params["text_encoder"], dropout_rng=dropout_rng, train=True + )[0] + else: + encoder_hidden_states = text_encoder( + batch["input_ids"], params=text_encoder_state.params, train=False + )[0] + + # Predict the noise residual + model_pred = unet.apply( + {"params": params["unet"]}, noisy_latents, timesteps, encoder_hidden_states, train=True + ).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(noise_scheduler_state, latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and noise_pred into two parts and compute the loss on each part separately. + model_pred, model_pred_prior = jnp.split(model_pred, 2, axis=0) + target, target_prior = jnp.split(target, 2, axis=0) + + # Compute instance loss + loss = (target - model_pred) ** 2 + loss = loss.mean() + + # Compute prior loss + prior_loss = (target_prior - model_pred_prior) ** 2 + prior_loss = prior_loss.mean() + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = (target - model_pred) ** 2 + loss = loss.mean() + + return loss + + grad_fn = jax.value_and_grad(compute_loss) + loss, grad = grad_fn(params) + grad = jax.lax.pmean(grad, "batch") + + new_unet_state = unet_state.apply_gradients(grads=grad["unet"]) + if args.train_text_encoder: + new_text_encoder_state = text_encoder_state.apply_gradients(grads=grad["text_encoder"]) + else: + new_text_encoder_state = text_encoder_state + + metrics = {"loss": loss} + metrics = jax.lax.pmean(metrics, axis_name="batch") + + return new_unet_state, new_text_encoder_state, metrics, new_train_rng + + # Create parallel version of the train step + p_train_step = jax.pmap(train_step, "batch", donate_argnums=(0, 1)) + + # Replicate the train state on each device + unet_state = jax_utils.replicate(unet_state) + text_encoder_state = jax_utils.replicate(text_encoder_state) + vae_params = jax_utils.replicate(vae_params) + + # Train! + num_update_steps_per_epoch = math.ceil(len(train_dataloader)) + + # Scheduler and math around the number of training steps. + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel & distributed) = {total_train_batch_size}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + + def checkpoint(step=None): + # Create the pipeline using the trained modules and save it. + scheduler, _ = FlaxPNDMScheduler.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="scheduler") + safety_checker = FlaxStableDiffusionSafetyChecker.from_pretrained( + "CompVis/stable-diffusion-safety-checker", from_pt=True + ) + pipeline = FlaxStableDiffusionPipeline( + text_encoder=text_encoder, + vae=vae, + unet=unet, + tokenizer=tokenizer, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"), + ) + + outdir = os.path.join(args.output_dir, str(step)) if step else args.output_dir + pipeline.save_pretrained( + outdir, + params={ + "text_encoder": get_params_to_save(text_encoder_state.params), + "vae": get_params_to_save(vae_params), + "unet": get_params_to_save(unet_state.params), + "safety_checker": safety_checker.params, + }, + ) + + if args.push_to_hub: + message = f"checkpoint-{step}" if step is not None else "End of training" + repo.push_to_hub(commit_message=message, blocking=False, auto_lfs_prune=True) + + global_step = 0 + + epochs = tqdm(range(args.num_train_epochs), desc="Epoch ... ", position=0) + for epoch in epochs: + # ======================== Training ================================ + + train_metrics = [] + + steps_per_epoch = len(train_dataset) // total_train_batch_size + train_step_progress_bar = tqdm(total=steps_per_epoch, desc="Training...", position=1, leave=False) + # train + for batch in train_dataloader: + batch = shard(batch) + unet_state, text_encoder_state, train_metric, train_rngs = p_train_step( + unet_state, text_encoder_state, vae_params, batch, train_rngs + ) + train_metrics.append(train_metric) + + train_step_progress_bar.update(jax.local_device_count()) + + global_step += 1 + if jax.process_index() == 0 and args.save_steps and global_step % args.save_steps == 0: + checkpoint(global_step) + if global_step >= args.max_train_steps: + break + + train_metric = jax_utils.unreplicate(train_metric) + + train_step_progress_bar.close() + epochs.write(f"Epoch... ({epoch + 1}/{args.num_train_epochs} | Loss: {train_metric['loss']})") + + if jax.process_index() == 0: + checkpoint() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/dreambooth/train_dreambooth_lora.py b/diffusers/examples/dreambooth/train_dreambooth_lora.py new file mode 100644 index 0000000000000000000000000000000000000000..923b6506028177382006eef2a64edc42647cbd4a --- /dev/null +++ b/diffusers/examples/dreambooth/train_dreambooth_lora.py @@ -0,0 +1,1019 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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 + +import argparse +import hashlib +import logging +import math +import os +import warnings +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import AutoTokenizer, PretrainedConfig + +import diffusers +from diffusers import ( + AutoencoderKL, + DDPMScheduler, + DiffusionPipeline, + DPMSolverMultistepScheduler, + UNet2DConditionModel, +) +from diffusers.loaders import AttnProcsLayers +from diffusers.models.cross_attention import LoRACrossAttnProcessor +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version, is_wandb_available +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def save_model_card(repo_name, images=None, base_model=str, prompt=str, repo_folder=None): + img_str = "" + for i, image in enumerate(images): + image.save(os.path.join(repo_folder, f"image_{i}.png")) + img_str += f"![img_{i}](./image_{i}.png)\n" + + yaml = f""" +--- +license: creativeml-openrail-m +base_model: {base_model} +instance_prompt: {prompt} +tags: +- stable-diffusion +- stable-diffusion-diffusers +- text-to-image +- diffusers +- lora +inference: true +--- + """ + model_card = f""" +# LoRA DreamBooth - {repo_name} + +These are LoRA adaption weights for {base_model}. The weights were trained on {prompt} using [DreamBooth](https://dreambooth.github.io/). You can find some example images in the following. \n +{img_str} +""" + with open(os.path.join(repo_folder, "README.md"), "w") as f: + f.write(yaml + model_card) + + +def import_model_class_from_model_name_or_path(pretrained_model_name_or_path: str, revision: str): + text_encoder_config = PretrainedConfig.from_pretrained( + pretrained_model_name_or_path, + subfolder="text_encoder", + revision=revision, + ) + model_class = text_encoder_config.architectures[0] + + if model_class == "CLIPTextModel": + from transformers import CLIPTextModel + + return CLIPTextModel + elif model_class == "RobertaSeriesModelWithTransformation": + from diffusers.pipelines.alt_diffusion.modeling_roberta_series import RobertaSeriesModelWithTransformation + + return RobertaSeriesModelWithTransformation + else: + raise ValueError(f"{model_class} is not supported.") + + +def parse_args(input_args=None): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + required=True, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--validation_prompt", + type=str, + default=None, + help="A prompt that is used during validation to verify that the model is learning.", + ) + parser.add_argument( + "--num_validation_images", + type=int, + default=4, + help="Number of images that should be generated during validation with `validation_prompt`.", + ) + parser.add_argument( + "--validation_epochs", + type=int, + default=50, + help=( + "Run dreambooth validation every X epochs. Dreambooth validation consists of running the prompt" + " `args.validation_prompt` multiple times: `args.num_validation_images`." + ), + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If there are not enough images already present in" + " class_data_dir, additional images will be sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="lora-dreambooth-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints can be used both as final" + " checkpoints in case they are better than the last checkpoint, and are also suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--lr_num_cycles", + type=int, + default=1, + help="Number of hard resets of the lr in cosine_with_restarts scheduler.", + ) + parser.add_argument("--lr_power", type=float, default=1.0, help="Power factor of the polynomial scheduler.") + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument( + "--prior_generation_precision", + type=str, + default=None, + choices=["no", "fp32", "fp16", "bf16"], + help=( + "Choose prior generation precision between fp32, fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to fp16 if a GPU is available else fp32." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + if input_args is not None: + args = parser.parse_args(input_args) + else: + args = parser.parse_args() + + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + else: + # logger is not available yet + if args.class_data_dir is not None: + warnings.warn("You need not use --class_data_dir without --with_prior_preservation.") + if args.class_prompt is not None: + warnings.warn("You need not use --class_prompt without --with_prior_preservation.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = Path(instance_data_root) + if not self.instance_data_root.exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path = list(Path(instance_data_root).iterdir()) + self.num_instance_images = len(self.instance_images_path) + self.instance_prompt = instance_prompt + self._length = self.num_instance_images + + if class_data_root is not None: + self.class_data_root = Path(class_data_root) + self.class_data_root.mkdir(parents=True, exist_ok=True) + self.class_images_path = list(self.class_data_root.iterdir()) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + self.class_prompt = class_prompt + else: + self.class_data_root = None + + self.image_transforms = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_image = Image.open(self.instance_images_path[index % self.num_instance_images]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + example["instance_images"] = self.image_transforms(instance_image) + example["instance_prompt_ids"] = self.tokenizer( + self.instance_prompt, + truncation=True, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + if self.class_data_root: + class_image = Image.open(self.class_images_path[index % self.num_class_images]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example["class_images"] = self.image_transforms(class_image) + example["class_prompt_ids"] = self.tokenizer( + self.class_prompt, + truncation=True, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + return example + + +def collate_fn(examples, with_prior_preservation=False): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = torch.cat(input_ids, dim=0) + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + } + return batch + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = Path(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + if args.report_to == "wandb": + if not is_wandb_available(): + raise ImportError("Make sure to install wandb if you want to use it for logging during training.") + import wandb + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Generate class images if prior preservation is enabled. + if args.with_prior_preservation: + class_images_dir = Path(args.class_data_dir) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + if args.prior_generation_precision == "fp32": + torch_dtype = torch.float32 + elif args.prior_generation_precision == "fp16": + torch_dtype = torch.float16 + elif args.prior_generation_precision == "bf16": + torch_dtype = torch.bfloat16 + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + torch_dtype=torch_dtype, + safety_checker=None, + revision=args.revision, + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(args.class_prompt, num_new_images) + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size) + + sample_dataloader = accelerator.prepare(sample_dataloader) + pipeline.to(accelerator.device) + + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + images = pipeline(example["prompt"]).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_name, revision=args.revision, use_fast=False) + elif args.pretrained_model_name_or_path: + tokenizer = AutoTokenizer.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="tokenizer", + revision=args.revision, + use_fast=False, + ) + + # import correct text encoder class + text_encoder_cls = import_model_class_from_model_name_or_path(args.pretrained_model_name_or_path, args.revision) + + # Load scheduler and models + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + text_encoder = text_encoder_cls.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + + # We only train the additional adapter LoRA layers + vae.requires_grad_(False) + text_encoder.requires_grad_(False) + unet.requires_grad_(False) + + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move unet, vae and text_encoder to device and cast to weight_dtype + unet.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + text_encoder.to(accelerator.device, dtype=weight_dtype) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # now we will add new LoRA weights to the attention layers + # It's important to realize here how many attention weights will be added and of which sizes + # The sizes of the attention layers consist only of two different variables: + # 1) - the "hidden_size", which is increased according to `unet.config.block_out_channels`. + # 2) - the "cross attention size", which is set to `unet.config.cross_attention_dim`. + + # Let's first see how many attention processors we will have to set. + # For Stable Diffusion, it should be equal to: + # - down blocks (2x attention layers) * (2x transformer layers) * (3x down blocks) = 12 + # - mid blocks (2x attention layers) * (1x transformer layers) * (1x mid blocks) = 2 + # - up blocks (2x attention layers) * (3x transformer layers) * (3x down blocks) = 18 + # => 32 layers + + # Set correct lora layers + lora_attn_procs = {} + for name in unet.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = unet.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(unet.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = unet.config.block_out_channels[block_id] + + lora_attn_procs[name] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim + ) + + unet.set_attn_processor(lora_attn_procs) + lora_layers = AttnProcsLayers(unet.attn_processors) + + accelerator.register_for_checkpointing(lora_layers) + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + # Optimizer creation + optimizer = optimizer_class( + lora_layers.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Dataset and DataLoaders creation: + train_dataset = DreamBoothDataset( + instance_data_root=args.instance_data_dir, + instance_prompt=args.instance_prompt, + class_data_root=args.class_data_dir if args.with_prior_preservation else None, + class_prompt=args.class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, + batch_size=args.train_batch_size, + shuffle=True, + collate_fn=lambda examples: collate_fn(examples, args.with_prior_preservation), + num_workers=args.dataloader_num_workers, + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + num_cycles=args.lr_num_cycles, + power=args.lr_power, + ) + + # Prepare everything with our `accelerator`. + lora_layers, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + lora_layers, optimizer, train_dataloader, lr_scheduler + ) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth-lora", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the mos recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and model_pred into two parts and compute the loss on each part separately. + model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Compute prior loss + prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + if accelerator.sync_gradients: + params_to_clip = lora_layers.parameters() + accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + if accelerator.is_main_process: + if args.validation_prompt is not None and epoch % args.validation_epochs == 0: + logger.info( + f"Running validation... \n Generating {args.num_validation_images} images with prompt:" + f" {args.validation_prompt}." + ) + # create pipeline + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet), + text_encoder=accelerator.unwrap_model(text_encoder), + revision=args.revision, + torch_dtype=weight_dtype, + ) + pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) + pipeline = pipeline.to(accelerator.device) + pipeline.set_progress_bar_config(disable=True) + + # run inference + generator = torch.Generator(device=accelerator.device).manual_seed(args.seed) + images = [ + pipeline(args.validation_prompt, num_inference_steps=25, generator=generator).images[0] + for _ in range(args.num_validation_images) + ] + + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("validation", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "validation": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + del pipeline + torch.cuda.empty_cache() + + # Save the lora layers + accelerator.wait_for_everyone() + if accelerator.is_main_process: + unet = unet.to(torch.float32) + unet.save_attn_procs(args.output_dir) + + # Final inference + # Load previous pipeline + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, revision=args.revision, torch_dtype=weight_dtype + ) + pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) + pipeline = pipeline.to(accelerator.device) + + # load attention processors + pipeline.unet.load_attn_procs(args.output_dir) + + # run inference + if args.validation_prompt and args.num_validation_images > 0: + generator = torch.Generator(device=accelerator.device).manual_seed(args.seed) if args.seed else None + images = [ + pipeline(args.validation_prompt, num_inference_steps=25, generator=generator).images[0] + for _ in range(args.num_validation_images) + ] + + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("test", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "test": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + if args.push_to_hub: + save_model_card( + repo_name, + images=images, + base_model=args.pretrained_model_name_or_path, + prompt=args.instance_prompt, + repo_folder=args.output_dir, + ) + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) \ No newline at end of file diff --git a/diffusers/examples/dreambooth/train_inpainting_dreambooth.py b/diffusers/examples/dreambooth/train_inpainting_dreambooth.py new file mode 100644 index 0000000000000000000000000000000000000000..8f962967c14c8a9db53203598bcb22476a4c4ca7 --- /dev/null +++ b/diffusers/examples/dreambooth/train_inpainting_dreambooth.py @@ -0,0 +1,876 @@ +import argparse +import hashlib +import itertools +import json +import math +import os +import random +import shutil +from contextlib import nullcontext +from pathlib import Path +from typing import Optional + +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, whoami +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers import (AutoencoderKL, DDIMScheduler, DDPMScheduler, + StableDiffusionInpaintPipeline, UNet2DConditionModel) +from diffusers.optimization import get_scheduler + +torch.backends.cudnn.benchmark = True + + +logger = get_logger(__name__) + + +def parse_args(input_args=None): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--pretrained_vae_name_or_path", + type=str, + default=None, + help="Path to pretrained vae or vae identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default="fp16", + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + # parser.add_argument( + # "--save_sample_prompt", + # type=str, + # default=None, + # help="The prompt used to generate sample outputs to save.", + # ) + parser.add_argument( + "--save_sample_negative_prompt", + type=str, + default=None, + help="The negative prompt used to generate sample outputs to save.", + ) + parser.add_argument( + "--n_save_sample", + type=int, + default=4, + help="The number of samples to save.", + ) + parser.add_argument( + "--save_guidance_scale", + type=float, + default=7.5, + help="CFG for save sample.", + ) + parser.add_argument( + "--save_infer_steps", + type=int, + default=50, + help="The number of inference steps for save sample.", + ) + parser.add_argument( + "--pad_tokens", + default=False, + action="store_true", + help="Flag to pad tokens to length 77.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If not have enough images, additional images will be" + " sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution" + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--log_interval", type=int, default=10, help="Log every N steps.") + parser.add_argument("--save_interval", type=int, default=10_000, help="Save weights every N steps.") + parser.add_argument("--save_min_steps", type=int, default=0, help="Start saving weights after N steps.") + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--not_cache_latents", action="store_true", help="Do not precompute and cache latents from VAE.") + parser.add_argument("--hflip", action="store_true", help="Apply horizontal flip data augmentation.") + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--concepts_list", + type=str, + default=None, + help="Path to json containing multiple concepts, will overwrite parameters like instance_prompt, class_prompt, etc.", + ) + + if input_args is not None: + args = parser.parse_args(input_args) + else: + args = parser.parse_args() + + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + return args + + +def get_cutout_holes(height, width, min_holes=8, max_holes=32, min_height=16, max_height=128, min_width=16, max_width=128): + holes = [] + for _n in range(random.randint(min_holes, max_holes)): + hole_height = random.randint(min_height, max_height) + hole_width = random.randint(min_width, max_width) + y1 = random.randint(0, height - hole_height) + x1 = random.randint(0, width - hole_width) + y2 = y1 + hole_height + x2 = x1 + hole_width + holes.append((x1, y1, x2, y2)) + return holes + + +def generate_random_mask(image): + mask = torch.zeros_like(image[:1]) + holes = get_cutout_holes(mask.shape[1], mask.shape[2]) + for (x1, y1, x2, y2) in holes: + mask[:, y1:y2, x1:x2] = 1. + if random.uniform(0, 1) < 0.25: + mask.fill_(1.) + masked_image = image * (mask < 0.5) + return mask, masked_image + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + concepts_list, + tokenizer, + with_prior_preservation=True, + size=512, + center_crop=False, + num_class_images=None, + pad_tokens=False, + hflip=False + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + self.with_prior_preservation = with_prior_preservation + self.pad_tokens = pad_tokens + + self.instance_images_path = [] + self.class_images_path = [] + + for concept in concepts_list: + inst_img_path = [(x, concept["instance_prompt"]) for x in Path(concept["instance_data_dir"]).iterdir() if x.is_file()] + self.instance_images_path.extend(inst_img_path) + + if with_prior_preservation: + class_img_path = [(x, concept["class_prompt"]) for x in Path(concept["class_data_dir"]).iterdir() if x.is_file()] + self.class_images_path.extend(class_img_path[:num_class_images]) + + random.shuffle(self.instance_images_path) + self.num_instance_images = len(self.instance_images_path) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + + self.image_transforms = transforms.Compose( + [ + transforms.RandomHorizontalFlip(0.5 * hflip), + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_path, instance_prompt = self.instance_images_path[index % self.num_instance_images] + instance_image = Image.open(instance_path) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + example["instance_images"] = self.image_transforms(instance_image) + example["instance_masks"], example["instance_masked_images"] = generate_random_mask(example["instance_images"]) + example["instance_prompt_ids"] = self.tokenizer( + instance_prompt, + padding="max_length" if self.pad_tokens else "do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.with_prior_preservation: + class_path, class_prompt = self.class_images_path[index % self.num_class_images] + class_image = Image.open(class_path) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example["class_images"] = self.image_transforms(class_image) + example["class_masks"], example["class_masked_images"] = generate_random_mask(example["class_images"]) + example["class_prompt_ids"] = self.tokenizer( + class_prompt, + padding="max_length" if self.pad_tokens else "do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +class LatentsDataset(Dataset): + def __init__(self, latents_cache, text_encoder_cache): + self.latents_cache = latents_cache + self.text_encoder_cache = text_encoder_cache + + def __len__(self): + return len(self.latents_cache) + + def __getitem__(self, index): + return self.latents_cache[index], self.text_encoder_cache[index] + + +class AverageMeter: + def __init__(self, name=None): + self.name = name + self.reset() + + def reset(self): + self.sum = self.count = self.avg = 0 + + def update(self, val, n=1): + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = Path(args.output_dir, "0", args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + if args.train_text_encoder and args.gradient_accumulation_steps > 1 and accelerator.num_processes > 1: + raise ValueError( + "Gradient accumulation is not supported when training the text encoder in distributed training. " + "Please set gradient_accumulation_steps to 1. This feature will be supported in the future." + ) + + if args.seed is not None: + set_seed(args.seed) + + if args.concepts_list is None: + args.concepts_list = [ + { + "instance_prompt": args.instance_prompt, + "class_prompt": args.class_prompt, + "instance_data_dir": args.instance_data_dir, + "class_data_dir": args.class_data_dir + } + ] + else: + with open(args.concepts_list, "r") as f: + args.concepts_list = json.load(f) + + if args.with_prior_preservation: + pipeline = None + for concept in args.concepts_list: + class_images_dir = Path(concept["class_data_dir"]) + class_images_dir.mkdir(parents=True, exist_ok=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + if pipeline is None: + pipeline = StableDiffusionInpaintPipeline.from_pretrained( + args.pretrained_model_name_or_path, + vae=AutoencoderKL.from_pretrained( + args.pretrained_vae_name_or_path or args.pretrained_model_name_or_path, + revision=None if args.pretrained_vae_name_or_path else args.revision, + torch_dtype=torch_dtype + ), + torch_dtype=torch_dtype, + safety_checker=None, + revision=args.revision + ) + pipeline.set_progress_bar_config(disable=True) + pipeline.to(accelerator.device) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(concept["class_prompt"], num_new_images) + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size) + + sample_dataloader = accelerator.prepare(sample_dataloader) + + inp_img = Image.new("RGB", (512, 512), color=(0, 0, 0)) + inp_mask = Image.new("L", (512, 512), color=255) + + with torch.autocast("cuda"),torch.inference_mode(): + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + images = pipeline( + prompt=example["prompt"], + image=inp_img, + mask_image=inp_mask, + num_inference_steps=args.save_infer_steps + ).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained( + args.tokenizer_name, + revision=args.revision, + ) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="tokenizer", + revision=args.revision, + ) + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="text_encoder", + revision=args.revision, + ) + vae = AutoencoderKL.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="vae", + revision=args.revision, + ) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="unet", + revision=args.revision, + torch_dtype=torch.float32 + ) + + vae.requires_grad_(False) + if not args.train_text_encoder: + text_encoder.requires_grad_(False) + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + if args.train_text_encoder: + text_encoder.gradient_checkpointing_enable() + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + params_to_optimize = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) if args.train_text_encoder else unet.parameters() + ) + optimizer = optimizer_class( + params_to_optimize, + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + noise_scheduler = DDPMScheduler.from_config(args.pretrained_model_name_or_path, subfolder="scheduler") + + train_dataset = DreamBoothDataset( + concepts_list=args.concepts_list, + tokenizer=tokenizer, + with_prior_preservation=args.with_prior_preservation, + size=args.resolution, + center_crop=args.center_crop, + num_class_images=args.num_class_images, + pad_tokens=args.pad_tokens, + hflip=args.hflip + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + mask_values = [example["instance_masks"] for example in examples] + masked_image_values = [example["instance_masked_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + mask_values += [example["class_masks"] for example in examples] + masked_image_values += [example["class_masked_images"] for example in examples] + + pixel_values = torch.stack(pixel_values).to(memory_format=torch.contiguous_format).float() + mask_values = torch.stack(mask_values).to(memory_format=torch.contiguous_format).float() + masked_image_values = torch.stack(masked_image_values).to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad( + {"input_ids": input_ids}, + padding=True, + return_tensors="pt", + ).input_ids + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + "mask_values": mask_values, + "masked_image_values": masked_image_values + } + return batch + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn, pin_memory=True, num_workers=8 + ) + + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + vae.to(accelerator.device, dtype=weight_dtype) + if not args.train_text_encoder: + text_encoder.to(accelerator.device, dtype=weight_dtype) + + if not args.not_cache_latents: + latents_cache = [] + text_encoder_cache = [] + for batch in tqdm(train_dataloader, desc="Caching latents"): + with torch.no_grad(): + batch["pixel_values"] = batch["pixel_values"].to(accelerator.device, non_blocking=True, dtype=weight_dtype) + batch["input_ids"] = batch["input_ids"].to(accelerator.device, non_blocking=True) + latents_cache.append(vae.encode(batch["pixel_values"]).latent_dist) + if args.train_text_encoder: + text_encoder_cache.append(batch["input_ids"]) + else: + text_encoder_cache.append(text_encoder(batch["input_ids"])[0]) + train_dataset = LatentsDataset(latents_cache, text_encoder_cache) + train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=1, collate_fn=lambda x: x, shuffle=True) + + del vae + if not args.train_text_encoder: + del text_encoder + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + if args.train_text_encoder: + unet, text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, text_encoder, optimizer, train_dataloader, lr_scheduler + ) + else: + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth") + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + + def save_weights(step): + # Create the pipeline using using the trained modules and save it. + if accelerator.is_main_process: + if args.train_text_encoder: + text_enc_model = accelerator.unwrap_model(text_encoder, keep_fp32_wrapper=True) + else: + text_enc_model = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision) + scheduler = DDIMScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", clip_sample=False, set_alpha_to_one=False) + pipeline = StableDiffusionInpaintPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet, keep_fp32_wrapper=True).to(torch.float16), + text_encoder=text_enc_model.to(torch.float16), + vae=AutoencoderKL.from_pretrained( + args.pretrained_vae_name_or_path or args.pretrained_model_name_or_path, + subfolder=None if args.pretrained_vae_name_or_path else "vae", + revision=None if args.pretrained_vae_name_or_path else args.revision, + ), + safety_checker=None, + scheduler=scheduler, + torch_dtype=torch.float16, + revision=args.revision, + ) + save_dir = os.path.join(args.output_dir, f"{step}") + pipeline.save_pretrained(save_dir) + with open(os.path.join(save_dir, "args.json"), "w") as f: + json.dump(args.__dict__, f, indent=2) + + shutil.copy("train_inpainting_dreambooth.py", save_dir) + + pipeline = pipeline.to(accelerator.device) + pipeline.set_progress_bar_config(disable=True) + for idx, concept in enumerate(args.concepts_list): + g_cuda = torch.Generator(device=accelerator.device).manual_seed(args.seed) + sample_dir = os.path.join(save_dir, "samples", str(idx)) + os.makedirs(sample_dir, exist_ok=True) + inp_img = Image.new("RGB", (512, 512), color=(0, 0, 0)) + inp_mask = Image.new("L", (512, 512), color=255) + with torch.inference_mode(): + for i in tqdm(range(args.n_save_sample), desc="Generating samples"): + images = pipeline( + prompt=concept["instance_prompt"], + image=inp_img, + mask_image=inp_mask, + negative_prompt=args.save_sample_negative_prompt, + guidance_scale=args.save_guidance_scale, + num_inference_steps=args.save_infer_steps, + generator=g_cuda + ).images + images[0].save(os.path.join(sample_dir, f"{i}.png")) + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + print(f"[*] Weights saved at {save_dir}") + unet.to(torch.float32) + text_enc_model.to(torch.float32) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + global_step = 0 + loss_avg = AverageMeter() + text_enc_context = nullcontext() if args.train_text_encoder else torch.no_grad() + for epoch in range(args.num_train_epochs): + unet.train() + if args.train_text_encoder: + text_encoder.train() + random.shuffle(train_dataset.class_images_path) + for step, batch in enumerate(train_dataloader): + with accelerator.accumulate(unet): + # Convert images to latent space + with torch.no_grad(): + if not args.not_cache_latents: + latent_dist = batch[0][0] + else: + latent_dist = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist + masked_latent_dist = vae.encode(batch["masked_image_values"].to(dtype=weight_dtype)).latent_dist + latents = latent_dist.sample() * 0.18215 + masked_image_latents = masked_latent_dist.sample() * 0.18215 + mask = F.interpolate(batch["mask_values"], scale_factor=1 / 8) + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + with text_enc_context: + if not args.not_cache_latents: + if args.train_text_encoder: + encoder_hidden_states = text_encoder(batch[0][1])[0] + else: + encoder_hidden_states = batch[0][1] + else: + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + latent_model_input = torch.cat([noisy_latents, mask, masked_image_latents], dim=1) + # Predict the noise residual + noise_pred = unet(latent_model_input, timesteps, encoder_hidden_states).sample + + if args.with_prior_preservation: + # Chunk the noise and noise_pred into two parts and compute the loss on each part separately. + noise_pred, noise_pred_prior = torch.chunk(noise_pred, 2, dim=0) + noise, noise_prior = torch.chunk(noise, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(noise_pred.float(), noise.float(), reduction="none").mean([1, 2, 3]).mean() + + # Compute prior loss + prior_loss = F.mse_loss(noise_pred_prior.float(), noise_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(noise_pred.float(), noise.float(), reduction="mean") + + accelerator.backward(loss) + # if accelerator.sync_gradients: + # params_to_clip = ( + # itertools.chain(unet.parameters(), text_encoder.parameters()) + # if args.train_text_encoder + # else unet.parameters() + # ) + # accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad(set_to_none=True) + loss_avg.update(loss.detach_(), bsz) + + if not global_step % args.log_interval: + logs = {"loss": loss_avg.avg.item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step > 0 and not global_step % args.save_interval and global_step >= args.save_min_steps: + save_weights(global_step) + + progress_bar.update(1) + global_step += 1 + + if global_step >= args.max_train_steps: + break + + accelerator.wait_for_everyone() + + save_weights(global_step) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/diffusers/examples/imagic/Imagic_Stable_Diffusion.ipynb b/diffusers/examples/imagic/Imagic_Stable_Diffusion.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..8fe60fcb785214e537b184e59cadaf6807fa4132 --- /dev/null +++ b/diffusers/examples/imagic/Imagic_Stable_Diffusion.ipynb @@ -0,0 +1,951 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XU7NuMAA2drw", + "outputId": "7eb9b063-664f-4a42-e960-728ec9608c42" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tesla T4, 15109 MiB, 15109 MiB\n" + ] + } + ], + "source": [ + "#@markdown Check type of GPU and VRAM available.\n", + "!nvidia-smi --query-gpu=name,memory.total,memory.free --format=csv,noheader" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BzM7j0ZSc_9c" + }, + "source": [ + "https://github.com/ShivamShrirao/diffusers/tree/main/examples/imagic" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wnTMyW41cC1E" + }, + "source": [ + "## Install Requirements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aLWXPZqjsZVV" + }, + "outputs": [], + "source": [ + "!wget -q https://github.com/ShivamShrirao/diffusers/raw/main/examples/imagic/train_imagic.py\n", + "%pip install -qq git+https://github.com/ShivamShrirao/diffusers\n", + "%pip install -q -U --pre triton\n", + "%pip install -q accelerate==0.12.0 transformers ftfy bitsandbytes gradio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 271, + "referenced_widgets": [ + "85ae06d199054a0f9dba8d4f8ddcd292", + "860d219443fb4d9d855b04c9b45abfab", + "d791ae6e4eed4c5392b9824c48d915fa", + "2d9b7caa504246278947ddc969f7ab1c", + "a985ff5ec73f49ebb935adcf61ebd2ba", + "f11ffe66ef704b9ab0ceb3c1d37f762c", + "cef2596e604549ffa90da5409c4cc04c", + "22c1dc0720b7480690e7e7c18ca8f117", + "d266296b6d4748068f4e1edb5b337651", + "2cc8d1453cb645eb8161fdc4c2de239a", + "82b8f6b91af54469837e86db4963f774", + "cf5dbd481b784518b144473ba4886337", + "005547c148784e24ae52225f823597a9", + "be2d27f9856046a29f654281729caf3c" + ] + }, + "id": "y4lqqWT_uxD2", + "outputId": "c00dc515-3be6-4ec7-8bdb-fb3e3def0912" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Login successful\n", + "Your token has been saved to /root/.huggingface/token\n" + ] + } + ], + "source": [ + "#@title Login to HuggingFace 🤗\n", + "\n", + "#@markdown You need to accept the model license before downloading or using the Stable Diffusion weights. Please, visit the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4), read the license and tick the checkbox if you agree. You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work.\n", + "from huggingface_hub import notebook_login\n", + "!git config --global credential.helper store\n", + "notebook_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XfTlc8Mqb8iH" + }, + "source": [ + "### Install xformers from precompiled wheel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n6dcjPnnaiCn" + }, + "outputs": [], + "source": [ + "%pip install -q https://github.com/metrolobo/xformers_wheels/releases/download/1d31a3ac_various_6/xformers-0.0.14.dev0-cp37-cp37m-linux_x86_64.whl\n", + "# These were compiled on Tesla T4, should also work on P100, thanks to https://github.com/metrolobo\n", + "\n", + "# If precompiled wheels don't work, install it with the following command. It will take around 40 minutes to compile.\n", + "# %pip install git+https://github.com/facebookresearch/xformers@1d31a3a#egg=xformers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G0NV324ZcL9L" + }, + "source": [ + "## Settings and run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "Rxg0y5MBudmd" + }, + "outputs": [], + "source": [ + "#@markdown Name/Path of the initial model.\n", + "MODEL_NAME = \"CompVis/stable-diffusion-v1-4\" #@param {type:\"string\"}\n", + "\n", + "#@markdown Target Text.\n", + "TARGET_TEXT = \"A bird spreading wings.\" #@param {type:\"string\"}\n", + "\n", + "#@markdown If model weights should be saved directly in google drive (takes around 4-5 GB).\n", + "save_to_gdrive = True #@param {type:\"boolean\"}\n", + "if save_to_gdrive:\n", + " from google.colab import drive\n", + " drive.mount('/content/drive')\n", + "\n", + "#@markdown Enter the directory name to save model at.\n", + "OUTPUT_DIR = \"stable_diffusion_weights/imagic\" #@param {type:\"string\"}\n", + "if save_to_gdrive:\n", + " OUTPUT_DIR = \"/content/drive/MyDrive/\" + OUTPUT_DIR\n", + "else:\n", + " OUTPUT_DIR = \"/content/\" + OUTPUT_DIR\n", + "\n", + "print(f\"[*] Weights will be saved at {OUTPUT_DIR}\")\n", + "!mkdir -p $OUTPUT_DIR" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "fe-GgtnUVO_e" + }, + "outputs": [], + "source": [ + "#@markdown Upload your 1 image by running this cell.\n", + "\n", + "import os\n", + "from google.colab import files\n", + "import shutil\n", + "\n", + "uploaded = files.upload()\n", + "for filename in uploaded.keys():\n", + " INPUT_IMAGE = os.path.join(OUTPUT_DIR, filename)\n", + " shutil.move(filename, INPUT_IMAGE)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qn5ILIyDJIcX" + }, + "source": [ + "# Start Training" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-ioxxvHoicPs" + }, + "source": [ + "Add `--gradient_checkpointing` flag to reduce VRAM usage.\n", + "\n", + "remove `--use_8bit_adam` flag for full precision, uses more VRAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jjcSXTp-u-Eg" + }, + "outputs": [], + "source": [ + "!accelerate launch train_imagic.py \\\n", + " --pretrained_model_name_or_path=$MODEL_NAME \\\n", + " --output_dir=$OUTPUT_DIR \\\n", + " --input_image=$INPUT_IMAGE \\\n", + " --target_text=\"{TARGET_TEXT}\" \\\n", + " --seed=3434554 \\\n", + " --resolution=512 \\\n", + " --mixed_precision=\"fp16\" \\\n", + " --use_8bit_adam \\\n", + " --gradient_accumulation_steps=1 \\\n", + " --emb_learning_rate=1e-3 \\\n", + " --learning_rate=1e-6 \\\n", + " --emb_train_steps=500 \\\n", + " --max_train_steps=1000\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5V8wgU0HN-Kq" + }, + "source": [ + "## Convert weights to ckpt to use in web UIs like AUTOMATIC1111." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "baL22PHzOLeP" + }, + "outputs": [], + "source": [ + "#@markdown Download script\n", + "!wget -q https://github.com/ShivamShrirao/diffusers/raw/main/scripts/convert_diffusers_to_original_stable_diffusion.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "89Az5NUxOWdy" + }, + "outputs": [], + "source": [ + "#@markdown Run conversion.\n", + "ckpt_path = OUTPUT_DIR + \"/model.ckpt\"\n", + "\n", + "half_arg = \"\"\n", + "#@markdown Whether to convert to fp16, takes half the space (2GB), might loose some quality.\n", + "fp16 = False #@param {type: \"boolean\"}\n", + "if fp16:\n", + " half_arg = \"--half\"\n", + "!python convert_diffusers_to_original_stable_diffusion.py --model_path $OUTPUT_DIR --checkpoint_path $ckpt_path $half_arg\n", + "print(f\"[*] Converted ckpt saved at {ckpt_path}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ToNG4fd_dTbF" + }, + "source": [ + "## Inference" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "gW15FjffdTID" + }, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "from torch import autocast\n", + "from diffusers import StableDiffusionPipeline, DDIMScheduler\n", + "from IPython.display import display\n", + "\n", + "model_path = OUTPUT_DIR # If you want to use previously trained model saved in gdrive, replace this with the full path of model in gdrive\n", + "\n", + "scheduler = DDIMScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule=\"scaled_linear\", clip_sample=False, set_alpha_to_one=False)\n", + "pipe = StableDiffusionPipeline.from_pretrained(model_path, scheduler=scheduler, torch_dtype=torch.float16).to(\"cuda\")\n", + "target_embeddings = torch.load(os.path.join(model_path, \"target_embeddings.pt\")).to(\"cuda\")\n", + "optimized_embeddings = torch.load(os.path.join(model_path, \"optimized_embeddings.pt\")).to(\"cuda\")\n", + "g_cuda = None" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "cellView": "form", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oIzkltjpVO_f", + "outputId": "91cfc509-9e61-4031-c885-2587eacacb3d" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#@markdown Can set random seed here for reproducibility.\n", + "g_cuda = torch.Generator(device='cuda')\n", + "seed = 4324 #@param {type:\"number\"}\n", + "g_cuda.manual_seed(seed)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "cellView": "form", + "id": "K6xoHWSsbcS3", + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/50 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#@title Run for generating images.\n", + "\n", + "alpha = 0.9 #@param {type:\"number\"}\n", + "num_samples = 4 #@param {type:\"number\"}\n", + "guidance_scale = 3 #@param {type:\"number\"}\n", + "num_inference_steps = 50 #@param {type:\"number\"}\n", + "height = 512 #@param {type:\"number\"}\n", + "width = 512 #@param {type:\"number\"}\n", + "\n", + "edit_embeddings = alpha*target_embeddings + (1-alpha)*optimized_embeddings\n", + "\n", + "with autocast(\"cuda\"), torch.inference_mode():\n", + " images = pipe(\n", + " text_embeddings=edit_embeddings,\n", + " height=height,\n", + " width=width,\n", + " num_images_per_prompt=num_samples,\n", + " num_inference_steps=num_inference_steps,\n", + " guidance_scale=guidance_scale,\n", + " generator=g_cuda\n", + " ).images\n", + "\n", + "for img in images:\n", + " display(img)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "WMCqQ5Tcdsm2" + }, + "outputs": [], + "source": [ + "#@markdown Run Gradio UI for generating images.\n", + "import gradio as gr\n", + "\n", + "def inference(alpha, num_samples, height=512, width=512, num_inference_steps=50, guidance_scale=7.5):\n", + " with torch.autocast(\"cuda\"), torch.inference_mode():\n", + " edit_embeddings = alpha*target_embeddings + (1-alpha)*optimized_embeddings\n", + " return pipe(\n", + " text_embeddings=edit_embeddings, height=int(height), width=int(width),\n", + " num_images_per_prompt=int(num_samples),\n", + " num_inference_steps=int(num_inference_steps), guidance_scale=guidance_scale,\n", + " generator=g_cuda\n", + " ).images\n", + "\n", + "with gr.Blocks() as demo:\n", + " with gr.Row():\n", + " with gr.Column():\n", + " alpha = gr.Number(label=\"Prompt\", value=0.9)\n", + " run = gr.Button(value=\"Generate\")\n", + " with gr.Row():\n", + " num_samples = gr.Number(label=\"Number of Samples\", value=4)\n", + " guidance_scale = gr.Number(label=\"Guidance Scale\", value=3)\n", + " with gr.Row():\n", + " height = gr.Number(label=\"Height\", value=512)\n", + " width = gr.Number(label=\"Width\", value=512)\n", + " num_inference_steps = gr.Slider(label=\"Steps\", value=50)\n", + " with gr.Column():\n", + " gallery = gr.Gallery()\n", + "\n", + " run.click(inference, inputs=[alpha, num_samples, height, width, num_inference_steps, guidance_scale], outputs=gallery)\n", + "\n", + "demo.launch(debug=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jXgi8HM4c-DA" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + }, + "vscode": { + "interpreter": { + "hash": "e7370f93d1d0cde622a1f8e1c04877d8463912d04d973331ad4851f04de6915a" + } + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "005547c148784e24ae52225f823597a9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "22c1dc0720b7480690e7e7c18ca8f117": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2cc8d1453cb645eb8161fdc4c2de239a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "2d9b7caa504246278947ddc969f7ab1c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "ButtonView", + "button_style": "", + "description": "Login", + "disabled": false, + "icon": "", + "layout": "IPY_MODEL_82b8f6b91af54469837e86db4963f774", + "style": "IPY_MODEL_cf5dbd481b784518b144473ba4886337", + "tooltip": "" + } + }, + "82b8f6b91af54469837e86db4963f774": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "85ae06d199054a0f9dba8d4f8ddcd292": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_860d219443fb4d9d855b04c9b45abfab", + "IPY_MODEL_d791ae6e4eed4c5392b9824c48d915fa", + "IPY_MODEL_2d9b7caa504246278947ddc969f7ab1c", + "IPY_MODEL_a985ff5ec73f49ebb935adcf61ebd2ba" + ], + "layout": "IPY_MODEL_f11ffe66ef704b9ab0ceb3c1d37f762c" + } + }, + "860d219443fb4d9d855b04c9b45abfab": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_cef2596e604549ffa90da5409c4cc04c", + "placeholder": "​", + "style": "IPY_MODEL_22c1dc0720b7480690e7e7c18ca8f117", + "value": "

Copy a token from your Hugging Face\ntokens page and paste it below.
Immediately click login after copying\nyour token or it might be stored in plain text in this notebook file.
" + } + }, + "a985ff5ec73f49ebb935adcf61ebd2ba": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "HTMLView", + "description": "", + "description_tooltip": null, + "layout": "IPY_MODEL_005547c148784e24ae52225f823597a9", + "placeholder": "​", + "style": "IPY_MODEL_be2d27f9856046a29f654281729caf3c", + "value": "\nPro Tip: If you don't already have one, you can create a dedicated\n'notebooks' token with 'write' access, that you can then easily reuse for all\nnotebooks. " + } + }, + "be2d27f9856046a29f654281729caf3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "DescriptionStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "DescriptionStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "" + } + }, + "cef2596e604549ffa90da5409c4cc04c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cf5dbd481b784518b144473ba4886337": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "ButtonStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "ButtonStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "button_color": null, + "font_weight": "" + } + }, + "d266296b6d4748068f4e1edb5b337651": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d791ae6e4eed4c5392b9824c48d915fa": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "PasswordModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "PasswordModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "PasswordView", + "continuous_update": true, + "description": "Token:", + "description_tooltip": null, + "disabled": false, + "layout": "IPY_MODEL_d266296b6d4748068f4e1edb5b337651", + "placeholder": "​", + "style": "IPY_MODEL_2cc8d1453cb645eb8161fdc4c2de239a", + "value": "" + } + }, + "f11ffe66ef704b9ab0ceb3c1d37f762c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": "center", + "align_self": null, + "border": null, + "bottom": null, + "display": "flex", + "flex": null, + "flex_flow": "column", + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": "50%" + } + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/diffusers/examples/imagic/README.md b/diffusers/examples/imagic/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f8e140672b4585a47e4e0803c95ff4bf51b3c0f5 --- /dev/null +++ b/diffusers/examples/imagic/README.md @@ -0,0 +1,22 @@ +To further reduce VRAM usage, pass `--gradient_checkpointing` and `--use_8bit_adam` flag to use 8 bit adam optimizer from [bitsandbytes](https://github.com/TimDettmers/bitsandbytes). + +Training takes around 11GB VRAM and 18-20 minutes on Tesla T4 in colab free tier. + +[![Imagic Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ShivamShrirao/diffusers/blob/main/examples/imagic/Imagic_Stable_Diffusion.ipynb) + +# Imagic training example + +[Imagic](https://arxiv.org/abs/2210.09276) is a method for Text-Based Real Image editing with models like stable diffusion with just one image of a subject. +The `train_imagic.py` script shows how to implement the training procedure and adapt it for stable diffusion. + +Below are examples produced using the colab notebook. + +| Target Text | Input Image | Edited Image | +|-------------|-------------|--------------| +|A photo of Barack Obama smiling with a big grin.|![Obama](imgs/obama.jpg)|![Obama](imgs/obama_edited.png)| +|A bird spreading wings|![Bird](imgs/bird.jpg)|![Bird](imgs/bird_edited.png)| + + +TODO: Update README, Please refer to the colab notebook for example usage until then. + +![Imagic](imgs/imagic_paper.jpeg) \ No newline at end of file diff --git a/diffusers/examples/imagic/imgs/Official_portrait_of_Barack_Obama.jpg b/diffusers/examples/imagic/imgs/Official_portrait_of_Barack_Obama.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24a015a5f3f1a33c1d2c82d85a5b9ab49acb7e01 Binary files /dev/null and b/diffusers/examples/imagic/imgs/Official_portrait_of_Barack_Obama.jpg differ diff --git a/diffusers/examples/imagic/imgs/bird.jpg b/diffusers/examples/imagic/imgs/bird.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2eeed77c0dd912d17b51d988635ec657d49e5889 Binary files /dev/null and b/diffusers/examples/imagic/imgs/bird.jpg differ diff --git a/diffusers/examples/imagic/imgs/bird_edited.png b/diffusers/examples/imagic/imgs/bird_edited.png new file mode 100644 index 0000000000000000000000000000000000000000..837964b57afa25c2ed0467af0d41a735c152e9fa Binary files /dev/null and b/diffusers/examples/imagic/imgs/bird_edited.png differ diff --git a/diffusers/examples/imagic/imgs/imagic_paper.jpeg b/diffusers/examples/imagic/imgs/imagic_paper.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..6d05a2232a72c23f742b44c411fd68a47d7dc394 Binary files /dev/null and b/diffusers/examples/imagic/imgs/imagic_paper.jpeg differ diff --git a/diffusers/examples/imagic/imgs/obama.jpg b/diffusers/examples/imagic/imgs/obama.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5505e916aa1f1f9ebcf9f707f4fb69e0ca72fde2 Binary files /dev/null and b/diffusers/examples/imagic/imgs/obama.jpg differ diff --git a/diffusers/examples/imagic/imgs/obama_edited.png b/diffusers/examples/imagic/imgs/obama_edited.png new file mode 100644 index 0000000000000000000000000000000000000000..6fdce3bdaca864a12eb9915c3efc6a80d78f8fb6 Binary files /dev/null and b/diffusers/examples/imagic/imgs/obama_edited.png differ diff --git a/diffusers/examples/imagic/launch.sh b/diffusers/examples/imagic/launch.sh new file mode 100644 index 0000000000000000000000000000000000000000..2116372f9360c1cc6981b882d1f23b12a3e98506 --- /dev/null +++ b/diffusers/examples/imagic/launch.sh @@ -0,0 +1,18 @@ +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export OUTPUT_DIR="../../../models/imagic" +export INPUT_IMAGE="imgs/Official_portrait_of_Barack_Obama.jpg" + +accelerate launch train_imagic.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --output_dir=$OUTPUT_DIR \ + --input_image=$INPUT_IMAGE \ + --target_text="A photo of Barack Obama smiling with a big grin." \ + --seed=3434554 \ + --resolution=512 \ + --mixed_precision="fp16" \ + --use_8bit_adam \ + --gradient_accumulation_steps=1 \ + --emb_learning_rate=1e-3 \ + --learning_rate=1e-6 \ + --emb_train_steps=500 \ + --max_train_steps=1000 diff --git a/diffusers/examples/imagic/train_imagic.py b/diffusers/examples/imagic/train_imagic.py new file mode 100644 index 0000000000000000000000000000000000000000..1d646f4f6a59a00e6ac5910d4bca487009ea25b8 --- /dev/null +++ b/diffusers/examples/imagic/train_imagic.py @@ -0,0 +1,398 @@ +import argparse +import math +import os +from pathlib import Path +from typing import Optional + +import torch +import torch.nn.functional as F +import torch.utils.checkpoint + +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from diffusers import AutoencoderKL, DDPMScheduler, StableDiffusionPipeline, UNet2DConditionModel +from huggingface_hub import HfFolder, Repository, whoami +from PIL import Image +import numpy as np +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + + +logger = get_logger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--input_image", + type=str, + default=None, + required=True, + help="Path to input image to edit.", + ) + parser.add_argument( + "--target_text", + type=str, + default=None, + help="The target text describing the output image.", + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution" + ) + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--emb_train_steps", + type=int, + default=500, + help="Total number of training steps to perform.", + ) + parser.add_argument( + "--max_train_steps", + type=int, + default=1000, + help="Total number of training steps to perform.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--emb_learning_rate", + type=float, + default=1e-3, + help="Learning rate for optimizing the embeddings.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-6, + help="Learning rate for fine tuning the model.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--log_interval", type=int, default=10, help="Log every N steps.") + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + return args + + +class AverageMeter: + def __init__(self, name=None): + self.name = name + self.reset() + + def reset(self): + self.sum = self.count = self.avg = 0 + + def update(self, val, n=1): + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(): + args = parse_args() + logging_dir = Path(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + repo = Repository(args.output_dir, clone_from=repo_name) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer", use_auth_token=True) + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder", use_auth_token=True) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", use_auth_token=True) + unet = UNet2DConditionModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="unet", use_auth_token=True) + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.Adam8bit + else: + optimizer_class = torch.optim.Adam + + noise_scheduler = DDPMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000 + ) + + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + text_encoder.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + + # Encode the input image. + input_image = Image.open(args.input_image).convert("RGB") + + image_transforms = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + init_image = image_transforms(input_image) + init_image = init_image[None].to(device=accelerator.device, dtype=weight_dtype) + with torch.inference_mode(): + init_latents = vae.encode(init_image).latent_dist.sample() + init_latents = 0.18215 * init_latents + + # Encode the target text. + text_ids = tokenizer( + args.target_text, + padding="max_length", + truncation=True, + max_length=tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + text_ids = text_ids.to(device=accelerator.device) + with torch.inference_mode(): + target_embeddings = text_encoder(text_ids)[0] + + del vae, text_encoder + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + target_embeddings = target_embeddings.float() + optimized_embeddings = target_embeddings.clone() + + # Optimize the text embeddings first. + optimized_embeddings.requires_grad_(True) + optimizer = optimizer_class( + [optimized_embeddings], # only optimize embeddings + lr=args.emb_learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + # weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + unet, optimizer = accelerator.prepare(unet, optimizer) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("imagic", config=vars(args)) + + def train_loop(pbar, optimizer, params): + loss_avg = AverageMeter() + for step in pbar: + with accelerator.accumulate(unet): + noise = torch.randn_like(init_latents) + bsz = init_latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=init_latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(init_latents, noise, timesteps) + + noise_pred = unet(noisy_latents, timesteps, optimized_embeddings).sample + + loss = F.mse_loss(noise_pred.float(), noise.float(), reduction="mean") + + accelerator.backward(loss) + # if accelerator.sync_gradients: # results aren't good with it, may be will need more training with it. + # accelerator.clip_grad_norm_(params, args.max_grad_norm) + optimizer.step() + optimizer.zero_grad(set_to_none=True) + loss_avg.update(loss.detach_(), bsz) + + if not step % args.log_interval: + logs = {"loss": loss_avg.avg.item()} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=step) + + accelerator.wait_for_everyone() + + progress_bar = tqdm(range(args.emb_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Optimizing embedding") + + train_loop(progress_bar, optimizer, optimized_embeddings) + + optimized_embeddings.requires_grad_(False) + if accelerator.is_main_process: + torch.save(target_embeddings.cpu(), os.path.join(args.output_dir, "target_embeddings.pt")) + torch.save(optimized_embeddings.cpu(), os.path.join(args.output_dir, "optimized_embeddings.pt")) + with open(os.path.join(args.output_dir, "target_text.txt"), "w") as f: + f.write(args.target_text) + + # Fine tune the diffusion model. + optimizer = optimizer_class( + accelerator.unwrap_model(unet).parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + # weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + optimizer = accelerator.prepare(optimizer) + + progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Fine Tuning") + unet.train() + + train_loop(progress_bar, optimizer, unet.parameters()) + + # Create the pipeline using using the trained modules and save it. + if accelerator.is_main_process: + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet), + use_auth_token=True + ) + pipeline.save_pretrained(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/inference/README.md b/diffusers/examples/inference/README.md new file mode 100644 index 0000000000000000000000000000000000000000..52d66be8e228d312f1d079e6c8123448b6fa86fd --- /dev/null +++ b/diffusers/examples/inference/README.md @@ -0,0 +1,8 @@ +# Inference Examples + +**The inference examples folder is deprecated and will be removed in a future version**. +**Officially supported inference examples can be found in the [Pipelines folder](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines)**. + +- For `Image-to-Image text-guided generation with Stable Diffusion`, please have a look at the official [Pipeline examples](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines#examples) +- For `In-painting using Stable Diffusion`, please have a look at the official [Pipeline examples](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines#examples) +- For `Tweak prompts reusing seeds and latents`, please have a look at the official [Pipeline examples](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines#examples) diff --git a/diffusers/examples/inference/image_to_image.py b/diffusers/examples/inference/image_to_image.py new file mode 100644 index 0000000000000000000000000000000000000000..86b46c4e606e039cb2ad80b341b2685694f883b4 --- /dev/null +++ b/diffusers/examples/inference/image_to_image.py @@ -0,0 +1,9 @@ +import warnings + +from diffusers import StableDiffusionImg2ImgPipeline # noqa F401 + + +warnings.warn( + "The `image_to_image.py` script is outdated. Please use directly `from diffusers import" + " StableDiffusionImg2ImgPipeline` instead." +) diff --git a/diffusers/examples/inference/inpainting.py b/diffusers/examples/inference/inpainting.py new file mode 100644 index 0000000000000000000000000000000000000000..8aad208ff34eb4d4ba1c6acfdfe0f97ac9afc4bc --- /dev/null +++ b/diffusers/examples/inference/inpainting.py @@ -0,0 +1,9 @@ +import warnings + +from diffusers import StableDiffusionInpaintPipeline as StableDiffusionInpaintPipeline # noqa F401 + + +warnings.warn( + "The `inpainting.py` script is outdated. Please use directly `from diffusers import" + " StableDiffusionInpaintPipeline` instead." +) diff --git a/diffusers/examples/research_projects/README.md b/diffusers/examples/research_projects/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ef50d423e68ff5c641e4419bd30f84787aebf839 --- /dev/null +++ b/diffusers/examples/research_projects/README.md @@ -0,0 +1,14 @@ +# Research projects + +This folder contains various research projects using 🧨 Diffusers. +They are not really maintained by the core maintainers of this library and often require a specific version of Diffusers that is indicated in the requirements file of each folder. +Updating them to the most recent version of the library will require some work. + +To use any of them, just run the command + +``` +pip install -r requirements.txt +``` +inside the folder of your choice. + +If you need help with any of those, please open an issue where you directly ping the author(s), as indicated at the top of the README of each folder. diff --git a/diffusers/examples/research_projects/colossalai/README.md b/diffusers/examples/research_projects/colossalai/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7c428bbce736de2ba25f189ff19d4c8216c53fc5 --- /dev/null +++ b/diffusers/examples/research_projects/colossalai/README.md @@ -0,0 +1,111 @@ +# [DreamBooth](https://github.com/huggingface/diffusers/tree/main/examples/dreambooth) by [colossalai](https://github.com/hpcaitech/ColossalAI.git) + +[DreamBooth](https://arxiv.org/abs/2208.12242) is a method to personalize text2image models like stable diffusion given just a few(3~5) images of a subject. +The `train_dreambooth_colossalai.py` script shows how to implement the training procedure and adapt it for stable diffusion. + +By accommodating model data in CPU and GPU and moving the data to the computing device when necessary, [Gemini](https://www.colossalai.org/docs/advanced_tutorials/meet_gemini), the Heterogeneous Memory Manager of [Colossal-AI](https://github.com/hpcaitech/ColossalAI) can breakthrough the GPU memory wall by using GPU and CPU memory (composed of CPU DRAM or nvme SSD memory) together at the same time. Moreover, the model scale can be further improved by combining heterogeneous training with the other parallel approaches, such as data parallel, tensor parallel and pipeline parallel. + +## Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install -r requirements.txt +``` + +## Install [ColossalAI](https://github.com/hpcaitech/ColossalAI.git) + +**From PyPI** +```bash +pip install colossalai +``` + +**From source** + +```bash +git clone https://github.com/hpcaitech/ColossalAI.git +cd ColossalAI + +# install colossalai +pip install . +``` + +## Dataset for Teyvat BLIP captions +Dataset used to train [Teyvat characters text to image model](https://github.com/hpcaitech/ColossalAI/tree/main/examples/images/diffusion). + +BLIP generated captions for characters images from [genshin-impact fandom wiki](https://genshin-impact.fandom.com/wiki/Character#Playable_Characters)and [biligame wiki for genshin impact](https://wiki.biligame.com/ys/%E8%A7%92%E8%89%B2). + +For each row the dataset contains `image` and `text` keys. `image` is a varying size PIL png, and `text` is the accompanying text caption. Only a train split is provided. + +The `text` include the tag `Teyvat`, `Name`,`Element`, `Weapon`, `Region`, `Model type`, and `Description`, the `Description` is captioned with the [pre-trained BLIP model](https://github.com/salesforce/BLIP). + +## Training + +The arguement `placement` can be `cpu`, `auto`, `cuda`, with `cpu` the GPU RAM required can be minimized to 4GB but will deceleration, with `cuda` you can also reduce GPU memory by half but accelerated training, with `auto` a more balanced solution for speed and memory can be obtained。 + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +torchrun --nproc_per_node 2 train_dreambooth_colossalai.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=400 \ + --placement="cuda" +``` + + +### Training with prior-preservation loss + +Prior-preservation is used to avoid overfitting and language-drift. Refer to the paper to learn more about it. For prior-preservation we first generate images using the model with a class prompt and then use those during training along with our data. +According to the paper, it's recommended to generate `num_epochs * num_samples` images for prior-preservation. 200-300 works well for most cases. The `num_class_images` flag sets the number of images to generate with the class prompt. You can place existing images in `class_data_dir`, and the training script will generate any additional images so that `num_class_images` are present in `class_data_dir` during training time. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +torchrun --nproc_per_node 2 train_dreambooth_colossalai.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=800 \ + --placement="cuda" +``` + +## Inference + +Once you have trained a model using above command, the inference can be done simply using the `StableDiffusionPipeline`. Make sure to include the `identifier`(e.g. sks in above example) in your prompt. + +```python +from diffusers import StableDiffusionPipeline +import torch + +model_id = "path-to-save-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A photo of sks dog in a bucket" +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("dog-bucket.png") +``` diff --git a/diffusers/examples/research_projects/colossalai/inference.py b/diffusers/examples/research_projects/colossalai/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..3b115c2d2b8f5bcdb3a0c053a6c71b91a965c573 --- /dev/null +++ b/diffusers/examples/research_projects/colossalai/inference.py @@ -0,0 +1,12 @@ +import torch + +from diffusers import StableDiffusionPipeline + + +model_id = "path-to-your-trained-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A photo of sks dog in a bucket" +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("dog-bucket.png") diff --git a/diffusers/examples/research_projects/colossalai/requirement.txt b/diffusers/examples/research_projects/colossalai/requirement.txt new file mode 100644 index 0000000000000000000000000000000000000000..f80467dcff521bfed1fa72109e1e01e92ab05646 --- /dev/null +++ b/diffusers/examples/research_projects/colossalai/requirement.txt @@ -0,0 +1,7 @@ +diffusers +torch +torchvision +ftfy +tensorboard +Jinja2 +transformers \ No newline at end of file diff --git a/diffusers/examples/research_projects/colossalai/train_dreambooth_colossalai.py b/diffusers/examples/research_projects/colossalai/train_dreambooth_colossalai.py new file mode 100644 index 0000000000000000000000000000000000000000..6136f7233900447cbd411409bc7c92d675c99d37 --- /dev/null +++ b/diffusers/examples/research_projects/colossalai/train_dreambooth_colossalai.py @@ -0,0 +1,687 @@ +import argparse +import hashlib +import math +import os +from pathlib import Path +from typing import Optional + +import colossalai +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from colossalai.context.parallel_mode import ParallelMode +from colossalai.core import global_context as gpc +from colossalai.logging import disable_existing_loggers, get_dist_logger +from colossalai.nn.optimizer.gemini_optimizer import GeminiAdamOptimizer +from colossalai.nn.parallel.utils import get_static_torch_model +from colossalai.utils import get_current_device +from colossalai.utils.model.colo_init_context import ColoInitContext +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import AutoTokenizer, PretrainedConfig + +from diffusers import AutoencoderKL, DDPMScheduler, DiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler + + +disable_existing_loggers() +logger = get_dist_logger() + + +def import_model_class_from_model_name_or_path(pretrained_model_name_or_path: str): + text_encoder_config = PretrainedConfig.from_pretrained( + pretrained_model_name_or_path, + subfolder="text_encoder", + revision=args.revision, + ) + model_class = text_encoder_config.architectures[0] + + if model_class == "CLIPTextModel": + from transformers import CLIPTextModel + + return CLIPTextModel + elif model_class == "RobertaSeriesModelWithTransformation": + from diffusers.pipelines.alt_diffusion.modeling_roberta_series import RobertaSeriesModelWithTransformation + + return RobertaSeriesModelWithTransformation + else: + raise ValueError(f"{model_class} is not supported.") + + +def parse_args(input_args=None): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default="a photo of sks dog", + required=False, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If there are not enough images already present in" + " class_data_dir, additional images will be sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--placement", + type=str, + default="cpu", + help="Placement Policy for Gemini. Valid when using colossalai as dist plan.", + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument("--save_steps", type=int, default=500, help="Save checkpoint every X updates steps.") + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + if input_args is not None: + args = parser.parse_args(input_args) + else: + args = parser.parse_args() + + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + else: + if args.class_data_dir is not None: + logger.warning("You need not use --class_data_dir without --with_prior_preservation.") + if args.class_prompt is not None: + logger.warning("You need not use --class_prompt without --with_prior_preservation.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = Path(instance_data_root) + if not self.instance_data_root.exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path = list(Path(instance_data_root).iterdir()) + self.num_instance_images = len(self.instance_images_path) + self.instance_prompt = instance_prompt + self._length = self.num_instance_images + + if class_data_root is not None: + self.class_data_root = Path(class_data_root) + self.class_data_root.mkdir(parents=True, exist_ok=True) + self.class_images_path = list(self.class_data_root.iterdir()) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + self.class_prompt = class_prompt + else: + self.class_data_root = None + + self.image_transforms = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_image = Image.open(self.instance_images_path[index % self.num_instance_images]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + example["instance_images"] = self.image_transforms(instance_image) + example["instance_prompt_ids"] = self.tokenizer( + self.instance_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.class_data_root: + class_image = Image.open(self.class_images_path[index % self.num_class_images]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example["class_images"] = self.image_transforms(class_image) + example["class_prompt_ids"] = self.tokenizer( + self.class_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +# Gemini + ZeRO DDP +def gemini_zero_dpp(model: torch.nn.Module, placememt_policy: str = "auto"): + from colossalai.nn.parallel import GeminiDDP + + model = GeminiDDP( + model, device=get_current_device(), placement_policy=placememt_policy, pin_memory=True, search_range_mb=64 + ) + return model + + +def main(args): + if args.seed is None: + colossalai.launch_from_torch(config={}) + else: + colossalai.launch_from_torch(config={}, seed=args.seed) + + local_rank = gpc.get_local_rank(ParallelMode.DATA) + world_size = gpc.get_world_size(ParallelMode.DATA) + + if args.with_prior_preservation: + class_images_dir = Path(args.class_data_dir) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if get_current_device() == "cuda" else torch.float32 + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + torch_dtype=torch_dtype, + safety_checker=None, + revision=args.revision, + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(args.class_prompt, num_new_images) + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size) + + pipeline.to(get_current_device()) + + for example in tqdm( + sample_dataloader, + desc="Generating class images", + disable=not local_rank == 0, + ): + images = pipeline(example["prompt"]).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + + # Handle the repository creation + if local_rank == 0: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + logger.info(f"Loading tokenizer from {args.tokenizer_name}", ranks=[0]) + tokenizer = AutoTokenizer.from_pretrained( + args.tokenizer_name, + revision=args.revision, + use_fast=False, + ) + elif args.pretrained_model_name_or_path: + logger.info("Loading tokenizer from pretrained model", ranks=[0]) + tokenizer = AutoTokenizer.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="tokenizer", + revision=args.revision, + use_fast=False, + ) + # import correct text encoder class + text_encoder_cls = import_model_class_from_model_name_or_path(args.pretrained_model_name_or_path) + + # Load models and create wrapper for stable diffusion + + logger.info(f"Loading text_encoder from {args.pretrained_model_name_or_path}", ranks=[0]) + + text_encoder = text_encoder_cls.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="text_encoder", + revision=args.revision, + ) + + logger.info(f"Loading AutoencoderKL from {args.pretrained_model_name_or_path}", ranks=[0]) + vae = AutoencoderKL.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="vae", + revision=args.revision, + ) + + logger.info(f"Loading UNet2DConditionModel from {args.pretrained_model_name_or_path}", ranks=[0]) + with ColoInitContext(device=get_current_device()): + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision, low_cpu_mem_usage=False + ) + + vae.requires_grad_(False) + text_encoder.requires_grad_(False) + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + + if args.scale_lr: + args.learning_rate = args.learning_rate * args.train_batch_size * world_size + + unet = gemini_zero_dpp(unet, args.placement) + + # config optimizer for colossalai zero + optimizer = GeminiAdamOptimizer( + unet, lr=args.learning_rate, initial_scale=2**5, clipping_norm=args.max_grad_norm + ) + + # load noise_scheduler + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + + # prepare dataset + logger.info(f"Prepare dataset from {args.instance_data_dir}", ranks=[0]) + train_dataset = DreamBoothDataset( + instance_data_root=args.instance_data_dir, + instance_prompt=args.instance_prompt, + class_data_root=args.class_data_dir if args.with_prior_preservation else None, + class_prompt=args.class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad( + {"input_ids": input_ids}, + padding="max_length", + max_length=tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + } + return batch + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn, num_workers=1 + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader)) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps, + num_training_steps=args.max_train_steps, + ) + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + vae.to(get_current_device(), dtype=weight_dtype) + text_encoder.to(get_current_device(), dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader)) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # Train! + total_batch_size = args.train_batch_size * world_size + + logger.info("***** Running training *****", ranks=[0]) + logger.info(f" Num examples = {len(train_dataset)}", ranks=[0]) + logger.info(f" Num batches each epoch = {len(train_dataloader)}", ranks=[0]) + logger.info(f" Num Epochs = {args.num_train_epochs}", ranks=[0]) + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}", ranks=[0]) + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}", ranks=[0]) + logger.info(f" Total optimization steps = {args.max_train_steps}", ranks=[0]) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(args.max_train_steps), disable=not local_rank == 0) + progress_bar.set_description("Steps") + global_step = 0 + + torch.cuda.synchronize() + for epoch in range(args.num_train_epochs): + unet.train() + for step, batch in enumerate(train_dataloader): + torch.cuda.reset_peak_memory_stats() + # Move batch to gpu + for key, value in batch.items(): + batch[key] = value.to(get_current_device(), non_blocking=True) + + # Convert images to latent space + optimizer.zero_grad() + + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * 0.18215 + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and model_pred into two parts and compute the loss on each part separately. + model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(model_pred.float(), target.float(), reduction="none").mean([1, 2, 3]).mean() + + # Compute prior loss + prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + optimizer.backward(loss) + + optimizer.step() + lr_scheduler.step() + logger.info(f"max GPU_mem cost is {torch.cuda.max_memory_allocated()/2**20} MB", ranks=[0]) + # Checks if the accelerator has performed an optimization step behind the scenes + progress_bar.update(1) + global_step += 1 + logs = { + "loss": loss.detach().item(), + "lr": optimizer.param_groups[0]["lr"], + } # lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + + if global_step % args.save_steps == 0: + torch.cuda.synchronize() + torch_unet = get_static_torch_model(unet) + if local_rank == 0: + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=torch_unet, + revision=args.revision, + ) + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + pipeline.save_pretrained(save_path) + logger.info(f"Saving model checkpoint to {save_path}", ranks=[0]) + if global_step >= args.max_train_steps: + break + + torch.cuda.synchronize() + unet = get_static_torch_model(unet) + + if local_rank == 0: + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=unet, + revision=args.revision, + ) + + pipeline.save_pretrained(args.output_dir) + logger.info(f"Saving model checkpoint to {args.output_dir}", ranks=[0]) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/diffusers/examples/research_projects/dreambooth_inpaint/README.md b/diffusers/examples/research_projects/dreambooth_inpaint/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dec919587935ec6e08a08e9299d62b0edc17449c --- /dev/null +++ b/diffusers/examples/research_projects/dreambooth_inpaint/README.md @@ -0,0 +1,118 @@ +# Dreambooth for the inpainting model + +This script was added by @thedarkzeno . + +Please note that this script is not actively maintained, you can open an issue and tag @thedarkzeno or @patil-suraj though. + +```bash +export MODEL_NAME="runwayml/stable-diffusion-inpainting" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth_inpaint.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=400 +``` + +### Training with prior-preservation loss + +Prior-preservation is used to avoid overfitting and language-drift. Refer to the paper to learn more about it. For prior-preservation we first generate images using the model with a class prompt and then use those during training along with our data. +According to the paper, it's recommended to generate `num_epochs * num_samples` images for prior-preservation. 200-300 works well for most cases. + +```bash +export MODEL_NAME="runwayml/stable-diffusion-inpainting" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth_inpaint.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + + +### Training with gradient checkpointing and 8-bit optimizer: + +With the help of gradient checkpointing and the 8-bit optimizer from bitsandbytes it's possible to run train dreambooth on a 16GB GPU. + +To install `bitandbytes` please refer to this [readme](https://github.com/TimDettmers/bitsandbytes#requirements--installation). + +```bash +export MODEL_NAME="runwayml/stable-diffusion-inpainting" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth_inpaint.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=2 --gradient_checkpointing \ + --use_8bit_adam \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Fine-tune text encoder with the UNet. + +The script also allows to fine-tune the `text_encoder` along with the `unet`. It's been observed experimentally that fine-tuning `text_encoder` gives much better results especially on faces. +Pass the `--train_text_encoder` argument to the script to enable training `text_encoder`. + +___Note: Training text encoder requires more memory, with this option the training won't fit on 16GB GPU. It needs at least 24GB VRAM.___ + +```bash +export MODEL_NAME="runwayml/stable-diffusion-inpainting" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth_inpaint.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_text_encoder \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --use_8bit_adam \ + --gradient_checkpointing \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` diff --git a/diffusers/examples/research_projects/dreambooth_inpaint/requirements.txt b/diffusers/examples/research_projects/dreambooth_inpaint/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f17dfab9653b70b379d36dae1103eb0f4728806e --- /dev/null +++ b/diffusers/examples/research_projects/dreambooth_inpaint/requirements.txt @@ -0,0 +1,7 @@ +diffusers==0.9.0 +accelerate +torchvision +transformers>=4.21.0 +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint.py b/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..789440e750f1a1050079eccb47f0768a14dd3be1 --- /dev/null +++ b/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint.py @@ -0,0 +1,812 @@ +import argparse +import hashlib +import itertools +import math +import os +import random +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from PIL import Image, ImageDraw +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDPMScheduler, + StableDiffusionInpaintPipeline, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def prepare_mask_and_masked_image(image, mask): + image = np.array(image.convert("RGB")) + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + + mask = np.array(mask.convert("L")) + mask = mask.astype(np.float32) / 255.0 + mask = mask[None, None] + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + + masked_image = image * (mask < 0.5) + + return mask, masked_image + + +# generate random masks +def random_mask(im_shape, ratio=1, mask_full_image=False): + mask = Image.new("L", im_shape, 0) + draw = ImageDraw.Draw(mask) + size = (random.randint(0, int(im_shape[0] * ratio)), random.randint(0, int(im_shape[1] * ratio))) + # use this to always mask the whole image + if mask_full_image: + size = (int(im_shape[0] * ratio), int(im_shape[1] * ratio)) + limits = (im_shape[0] - size[0] // 2, im_shape[1] - size[1] // 2) + center = (random.randint(size[0] // 2, limits[0]), random.randint(size[1] // 2, limits[1])) + draw_type = random.randint(0, 1) + if draw_type == 0 or mask_full_image: + draw.rectangle( + (center[0] - size[0] // 2, center[1] - size[1] // 2, center[0] + size[0] // 2, center[1] + size[1] // 2), + fill=255, + ) + else: + draw.ellipse( + (center[0] - size[0] // 2, center[1] - size[1] // 2, center[0] + size[0] // 2, center[1] + size[1] // 2), + fill=255, + ) + + return mask + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If not have enough images, additional images will be" + " sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints can be used both as final" + " checkpoints in case they are better than the last checkpoint and are suitable for resuming training" + " using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.instance_data_dir is None: + raise ValueError("You must specify a train data directory.") + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = Path(instance_data_root) + if not self.instance_data_root.exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path = list(Path(instance_data_root).iterdir()) + self.num_instance_images = len(self.instance_images_path) + self.instance_prompt = instance_prompt + self._length = self.num_instance_images + + if class_data_root is not None: + self.class_data_root = Path(class_data_root) + self.class_data_root.mkdir(parents=True, exist_ok=True) + self.class_images_path = list(self.class_data_root.iterdir()) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + self.class_prompt = class_prompt + else: + self.class_data_root = None + + self.image_transforms_resize_and_crop = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + ] + ) + + self.image_transforms = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_image = Image.open(self.instance_images_path[index % self.num_instance_images]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + instance_image = self.image_transforms_resize_and_crop(instance_image) + + example["PIL_images"] = instance_image + example["instance_images"] = self.image_transforms(instance_image) + + example["instance_prompt_ids"] = self.tokenizer( + self.instance_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.class_data_root: + class_image = Image.open(self.class_images_path[index % self.num_class_images]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + class_image = self.image_transforms_resize_and_crop(class_image) + example["class_images"] = self.image_transforms(class_image) + example["class_PIL_images"] = class_image + example["class_prompt_ids"] = self.tokenizer( + self.class_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(): + args = parse_args() + logging_dir = Path(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + if args.train_text_encoder and args.gradient_accumulation_steps > 1 and accelerator.num_processes > 1: + raise ValueError( + "Gradient accumulation is not supported when training the text encoder in distributed training. " + "Please set gradient_accumulation_steps to 1. This feature will be supported in the future." + ) + + if args.seed is not None: + set_seed(args.seed) + + if args.with_prior_preservation: + class_images_dir = Path(args.class_data_dir) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + pipeline = StableDiffusionInpaintPipeline.from_pretrained( + args.pretrained_model_name_or_path, torch_dtype=torch_dtype, safety_checker=None + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(args.class_prompt, num_new_images) + sample_dataloader = torch.utils.data.DataLoader( + sample_dataset, batch_size=args.sample_batch_size, num_workers=1 + ) + + sample_dataloader = accelerator.prepare(sample_dataloader) + pipeline.to(accelerator.device) + transform_to_pil = transforms.ToPILImage() + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + bsz = len(example["prompt"]) + fake_images = torch.rand((3, args.resolution, args.resolution)) + transform_to_pil = transforms.ToPILImage() + fake_pil_images = transform_to_pil(fake_images) + + fake_mask = random_mask((args.resolution, args.resolution), ratio=1, mask_full_image=True) + + images = pipeline(prompt=example["prompt"], mask_image=fake_mask, image=fake_pil_images).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder") + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae") + unet = UNet2DConditionModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="unet") + + vae.requires_grad_(False) + if not args.train_text_encoder: + text_encoder.requires_grad_(False) + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + if args.train_text_encoder: + text_encoder.gradient_checkpointing_enable() + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + params_to_optimize = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) if args.train_text_encoder else unet.parameters() + ) + optimizer = optimizer_class( + params_to_optimize, + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + + train_dataset = DreamBoothDataset( + instance_data_root=args.instance_data_dir, + instance_prompt=args.instance_prompt, + class_data_root=args.class_data_dir if args.with_prior_preservation else None, + class_prompt=args.class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + pior_pil = [example["class_PIL_images"] for example in examples] + + masks = [] + masked_images = [] + for example in examples: + pil_image = example["PIL_images"] + # generate a random mask + mask = random_mask(pil_image.size, 1, False) + # prepare mask and masked image + mask, masked_image = prepare_mask_and_masked_image(pil_image, mask) + + masks.append(mask) + masked_images.append(masked_image) + + if args.with_prior_preservation: + for pil_image in pior_pil: + # generate a random mask + mask = random_mask(pil_image.size, 1, False) + # prepare mask and masked image + mask, masked_image = prepare_mask_and_masked_image(pil_image, mask) + + masks.append(mask) + masked_images.append(masked_image) + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad({"input_ids": input_ids}, padding=True, return_tensors="pt").input_ids + masks = torch.stack(masks) + masked_images = torch.stack(masked_images) + batch = {"input_ids": input_ids, "pixel_values": pixel_values, "masks": masks, "masked_images": masked_images} + return batch + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + if args.train_text_encoder: + unet, text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, text_encoder, optimizer, train_dataloader, lr_scheduler + ) + else: + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + accelerator.register_for_checkpointing(lr_scheduler) + + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + vae.to(accelerator.device, dtype=weight_dtype) + if not args.train_text_encoder: + text_encoder.to(accelerator.device, dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Convert masked images to latent space + masked_latents = vae.encode( + batch["masked_images"].reshape(batch["pixel_values"].shape).to(dtype=weight_dtype) + ).latent_dist.sample() + masked_latents = masked_latents * vae.config.scaling_factor + + masks = batch["masks"] + # resize the mask to latents shape as we concatenate the mask to the latents + mask = torch.stack( + [ + torch.nn.functional.interpolate(mask, size=(args.resolution // 8, args.resolution // 8)) + for mask in masks + ] + ) + mask = mask.reshape(-1, 1, args.resolution // 8, args.resolution // 8) + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # concatenate the noised latents with the mask and the masked latents + latent_model_input = torch.cat([noisy_latents, mask, masked_latents], dim=1) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + noise_pred = unet(latent_model_input, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and noise_pred into two parts and compute the loss on each part separately. + noise_pred, noise_pred_prior = torch.chunk(noise_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(noise_pred.float(), target.float(), reduction="none").mean([1, 2, 3]).mean() + + # Compute prior loss + prior_loss = F.mse_loss(noise_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(noise_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + if accelerator.sync_gradients: + params_to_clip = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) + if args.train_text_encoder + else unet.parameters() + ) + accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + accelerator.wait_for_everyone() + + # Create the pipeline using using the trained modules and save it. + if accelerator.is_main_process: + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet), + text_encoder=accelerator.unwrap_model(text_encoder), + ) + pipeline.save_pretrained(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint_lora.py b/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint_lora.py new file mode 100644 index 0000000000000000000000000000000000000000..5d6f249d84697cb70e2ae15bfa751f831781933a --- /dev/null +++ b/diffusers/examples/research_projects/dreambooth_inpaint/train_dreambooth_inpaint_lora.py @@ -0,0 +1,833 @@ +import argparse +import hashlib +import math +import os +import random +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from PIL import Image, ImageDraw +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, DDPMScheduler, StableDiffusionInpaintPipeline, UNet2DConditionModel +from diffusers.loaders import AttnProcsLayers +from diffusers.models.cross_attention import LoRACrossAttnProcessor +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def prepare_mask_and_masked_image(image, mask): + image = np.array(image.convert("RGB")) + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + + mask = np.array(mask.convert("L")) + mask = mask.astype(np.float32) / 255.0 + mask = mask[None, None] + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + + masked_image = image * (mask < 0.5) + + return mask, masked_image + + +# generate random masks +def random_mask(im_shape, ratio=1, mask_full_image=False): + mask = Image.new("L", im_shape, 0) + draw = ImageDraw.Draw(mask) + size = (random.randint(0, int(im_shape[0] * ratio)), random.randint(0, int(im_shape[1] * ratio))) + # use this to always mask the whole image + if mask_full_image: + size = (int(im_shape[0] * ratio), int(im_shape[1] * ratio)) + limits = (im_shape[0] - size[0] // 2, im_shape[1] - size[1] // 2) + center = (random.randint(size[0] // 2, limits[0]), random.randint(size[1] // 2, limits[1])) + draw_type = random.randint(0, 1) + if draw_type == 0 or mask_full_image: + draw.rectangle( + (center[0] - size[0] // 2, center[1] - size[1] // 2, center[0] + size[0] // 2, center[1] + size[1] // 2), + fill=255, + ) + else: + draw.ellipse( + (center[0] - size[0] // 2, center[1] - size[1] // 2, center[0] + size[0] // 2, center[1] + size[1] // 2), + fill=255, + ) + + return mask + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If not have enough images, additional images will be" + " sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="dreambooth-inpaint-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints can be used both as final" + " checkpoints in case they are better than the last checkpoint and are suitable for resuming training" + " using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.instance_data_dir is None: + raise ValueError("You must specify a train data directory.") + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = Path(instance_data_root) + if not self.instance_data_root.exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path = list(Path(instance_data_root).iterdir()) + self.num_instance_images = len(self.instance_images_path) + self.instance_prompt = instance_prompt + self._length = self.num_instance_images + + if class_data_root is not None: + self.class_data_root = Path(class_data_root) + self.class_data_root.mkdir(parents=True, exist_ok=True) + self.class_images_path = list(self.class_data_root.iterdir()) + self.num_class_images = len(self.class_images_path) + self._length = max(self.num_class_images, self.num_instance_images) + self.class_prompt = class_prompt + else: + self.class_data_root = None + + self.image_transforms_resize_and_crop = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + ] + ) + + self.image_transforms = transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + instance_image = Image.open(self.instance_images_path[index % self.num_instance_images]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + instance_image = self.image_transforms_resize_and_crop(instance_image) + + example["PIL_images"] = instance_image + example["instance_images"] = self.image_transforms(instance_image) + + example["instance_prompt_ids"] = self.tokenizer( + self.instance_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + if self.class_data_root: + class_image = Image.open(self.class_images_path[index % self.num_class_images]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + class_image = self.image_transforms_resize_and_crop(class_image) + example["class_images"] = self.image_transforms(class_image) + example["class_PIL_images"] = class_image + example["class_prompt_ids"] = self.tokenizer( + self.class_prompt, + padding="do_not_pad", + truncation=True, + max_length=self.tokenizer.model_max_length, + ).input_ids + + return example + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(): + args = parse_args() + logging_dir = Path(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + if args.train_text_encoder and args.gradient_accumulation_steps > 1 and accelerator.num_processes > 1: + raise ValueError( + "Gradient accumulation is not supported when training the text encoder in distributed training. " + "Please set gradient_accumulation_steps to 1. This feature will be supported in the future." + ) + + if args.seed is not None: + set_seed(args.seed) + + if args.with_prior_preservation: + class_images_dir = Path(args.class_data_dir) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + pipeline = StableDiffusionInpaintPipeline.from_pretrained( + args.pretrained_model_name_or_path, torch_dtype=torch_dtype, safety_checker=None + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(args.class_prompt, num_new_images) + sample_dataloader = torch.utils.data.DataLoader( + sample_dataset, batch_size=args.sample_batch_size, num_workers=1 + ) + + sample_dataloader = accelerator.prepare(sample_dataloader) + pipeline.to(accelerator.device) + transform_to_pil = transforms.ToPILImage() + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + bsz = len(example["prompt"]) + fake_images = torch.rand((3, args.resolution, args.resolution)) + transform_to_pil = transforms.ToPILImage() + fake_pil_images = transform_to_pil(fake_images) + + fake_mask = random_mask((args.resolution, args.resolution), ratio=1, mask_full_image=True) + + images = pipeline(prompt=example["prompt"], mask_image=fake_mask, image=fake_pil_images).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder") + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae") + unet = UNet2DConditionModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="unet") + + # We only train the additional adapter LoRA layers + vae.requires_grad_(False) + text_encoder.requires_grad_(False) + unet.requires_grad_(False) + + weight_dtype = torch.float32 + if args.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif args.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu. + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + unet.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + text_encoder.to(accelerator.device, dtype=weight_dtype) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # now we will add new LoRA weights to the attention layers + # It's important to realize here how many attention weights will be added and of which sizes + # The sizes of the attention layers consist only of two different variables: + # 1) - the "hidden_size", which is increased according to `unet.config.block_out_channels`. + # 2) - the "cross attention size", which is set to `unet.config.cross_attention_dim`. + + # Let's first see how many attention processors we will have to set. + # For Stable Diffusion, it should be equal to: + # - down blocks (2x attention layers) * (2x transformer layers) * (3x down blocks) = 12 + # - mid blocks (2x attention layers) * (1x transformer layers) * (1x mid blocks) = 2 + # - up blocks (2x attention layers) * (3x transformer layers) * (3x down blocks) = 18 + # => 32 layers + + # Set correct lora layers + lora_attn_procs = {} + for name in unet.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = unet.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(unet.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = unet.config.block_out_channels[block_id] + + lora_attn_procs[name] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim + ) + + unet.set_attn_processor(lora_attn_procs) + lora_layers = AttnProcsLayers(unet.attn_processors) + + accelerator.register_for_checkpointing(lora_layers) + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + optimizer = optimizer_class( + lora_layers.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + + train_dataset = DreamBoothDataset( + instance_data_root=args.instance_data_dir, + instance_prompt=args.instance_prompt, + class_data_root=args.class_data_dir if args.with_prior_preservation else None, + class_prompt=args.class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + def collate_fn(examples): + input_ids = [example["instance_prompt_ids"] for example in examples] + pixel_values = [example["instance_images"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if args.with_prior_preservation: + input_ids += [example["class_prompt_ids"] for example in examples] + pixel_values += [example["class_images"] for example in examples] + pior_pil = [example["class_PIL_images"] for example in examples] + + masks = [] + masked_images = [] + for example in examples: + pil_image = example["PIL_images"] + # generate a random mask + mask = random_mask(pil_image.size, 1, False) + # prepare mask and masked image + mask, masked_image = prepare_mask_and_masked_image(pil_image, mask) + + masks.append(mask) + masked_images.append(masked_image) + + if args.with_prior_preservation: + for pil_image in pior_pil: + # generate a random mask + mask = random_mask(pil_image.size, 1, False) + # prepare mask and masked image + mask, masked_image = prepare_mask_and_masked_image(pil_image, mask) + + masks.append(mask) + masked_images.append(masked_image) + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = tokenizer.pad({"input_ids": input_ids}, padding=True, return_tensors="pt").input_ids + masks = torch.stack(masks) + masked_images = torch.stack(masked_images) + batch = {"input_ids": input_ids, "pixel_values": pixel_values, "masks": masks, "masked_images": masked_images} + return batch + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, collate_fn=collate_fn + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + lora_layers, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + lora_layers, optimizer, train_dataloader, lr_scheduler + ) + # accelerator.register_for_checkpointing(lr_scheduler) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth-inpaint-lora", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Convert masked images to latent space + masked_latents = vae.encode( + batch["masked_images"].reshape(batch["pixel_values"].shape).to(dtype=weight_dtype) + ).latent_dist.sample() + masked_latents = masked_latents * vae.config.scaling_factor + + masks = batch["masks"] + # resize the mask to latents shape as we concatenate the mask to the latents + mask = torch.stack( + [ + torch.nn.functional.interpolate(mask, size=(args.resolution // 8, args.resolution // 8)) + for mask in masks + ] + ) + mask = mask.reshape(-1, 1, args.resolution // 8, args.resolution // 8) + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # concatenate the noised latents with the mask and the masked latents + latent_model_input = torch.cat([noisy_latents, mask, masked_latents], dim=1) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + noise_pred = unet(latent_model_input, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and noise_pred into two parts and compute the loss on each part separately. + noise_pred, noise_pred_prior = torch.chunk(noise_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(noise_pred.float(), target.float(), reduction="none").mean([1, 2, 3]).mean() + + # Compute prior loss + prior_loss = F.mse_loss(noise_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(noise_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + if accelerator.sync_gradients: + params_to_clip = lora_layers.parameters() + accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + accelerator.wait_for_everyone() + + # Save the lora layers + if accelerator.is_main_process: + unet = unet.to(torch.float32) + unet.save_attn_procs(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/research_projects/intel_opts/README.md b/diffusers/examples/research_projects/intel_opts/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fc606df7d17084e1fda9063e46f6399ac3e590cc --- /dev/null +++ b/diffusers/examples/research_projects/intel_opts/README.md @@ -0,0 +1,17 @@ +## Diffusers examples with Intel optimizations + +**This research project is not actively maintained by the diffusers team. For any questions or comments, please make sure to tag @hshen14 .** + +This aims to provide diffusers examples with Intel optimizations such as Bfloat16 for training/fine-tuning acceleration and 8-bit integer (INT8) for inference acceleration on Intel platforms. + +## Accelerating the fine-tuning for textual inversion + +We accelereate the fine-tuning for textual inversion with Intel Extension for PyTorch. The [examples](textual_inversion) enable both single node and multi-node distributed training with Bfloat16 support on Intel Xeon Scalable Processor. + +## Accelerating the inference for Stable Diffusion using Bfloat16 + +We start the inference acceleration with Bfloat16 using Intel Extension for PyTorch. The [script](inference_bf16.py) is generally designed to support standard Stable Diffusion models with Bfloat16 support. + +## Accelerating the inference for Stable Diffusion using INT8 + +Coming soon ... diff --git a/diffusers/examples/research_projects/intel_opts/inference_bf16.py b/diffusers/examples/research_projects/intel_opts/inference_bf16.py new file mode 100644 index 0000000000000000000000000000000000000000..8431693a45c8bb13418386dcd02906c420b858d7 --- /dev/null +++ b/diffusers/examples/research_projects/intel_opts/inference_bf16.py @@ -0,0 +1,49 @@ +import intel_extension_for_pytorch as ipex +import torch +from PIL import Image + +from diffusers import StableDiffusionPipeline + + +def image_grid(imgs, rows, cols): + assert len(imgs) == rows * cols + + w, h = imgs[0].size + grid = Image.new("RGB", size=(cols * w, rows * h)) + grid_w, grid_h = grid.size + + for i, img in enumerate(imgs): + grid.paste(img, box=(i % cols * w, i // cols * h)) + return grid + + +prompt = ["a lovely in red dress and hat, in the snowly and brightly night, with many brighly buildings"] +batch_size = 8 +prompt = prompt * batch_size + +device = "cpu" +model_id = "path-to-your-trained-model" +model = StableDiffusionPipeline.from_pretrained(model_id) +model = model.to(device) + +# to channels last +model.unet = model.unet.to(memory_format=torch.channels_last) +model.vae = model.vae.to(memory_format=torch.channels_last) +model.text_encoder = model.text_encoder.to(memory_format=torch.channels_last) +model.safety_checker = model.safety_checker.to(memory_format=torch.channels_last) + +# optimize with ipex +model.unet = ipex.optimize(model.unet.eval(), dtype=torch.bfloat16, inplace=True) +model.vae = ipex.optimize(model.vae.eval(), dtype=torch.bfloat16, inplace=True) +model.text_encoder = ipex.optimize(model.text_encoder.eval(), dtype=torch.bfloat16, inplace=True) +model.safety_checker = ipex.optimize(model.safety_checker.eval(), dtype=torch.bfloat16, inplace=True) + +# compute +seed = 666 +generator = torch.Generator(device).manual_seed(seed) +with torch.cpu.amp.autocast(enabled=True, dtype=torch.bfloat16): + images = model(prompt, guidance_scale=7.5, num_inference_steps=50, generator=generator).images + + # save image + grid = image_grid(images, rows=2, cols=4) + grid.save(model_id + ".png") diff --git a/diffusers/examples/research_projects/intel_opts/textual_inversion/README.md b/diffusers/examples/research_projects/intel_opts/textual_inversion/README.md new file mode 100644 index 0000000000000000000000000000000000000000..14e8b160fb1fb2de72cd37ddb4e4abcab83356fa --- /dev/null +++ b/diffusers/examples/research_projects/intel_opts/textual_inversion/README.md @@ -0,0 +1,68 @@ +## Textual Inversion fine-tuning example + +[Textual inversion](https://arxiv.org/abs/2208.01618) is a method to personalize text2image models like stable diffusion on your own images using just 3-5 examples. +The `textual_inversion.py` script shows how to implement the training procedure and adapt it for stable diffusion. + +## Training with Intel Extension for PyTorch + +Intel Extension for PyTorch provides the optimizations for faster training and inference on CPUs. You can leverage the training example "textual_inversion.py". Follow the [instructions](https://github.com/huggingface/diffusers/tree/main/examples/textual_inversion) to get the model and [dataset](https://huggingface.co/sd-concepts-library/dicoo2) before running the script. + +The example supports both single node and multi-node distributed training: + +### Single node training + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export DATA_DIR="path-to-dir-containing-dicoo-images" + +python textual_inversion.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --seed=7 \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --max_train_steps=3000 \ + --learning_rate=2.5e-03 --scale_lr \ + --output_dir="textual_inversion_dicoo" +``` + +Note: Bfloat16 is available on Intel Xeon Scalable Processors Cooper Lake or Sapphire Rapids. You may not get performance speedup without Bfloat16 support. + +### Multi-node distributed training + +Before running the scripts, make sure to install the library's training dependencies successfully: + +```bash +python -m pip install oneccl_bind_pt==1.13 -f https://developer.intel.com/ipex-whl-stable-cpu +``` + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export DATA_DIR="path-to-dir-containing-dicoo-images" + +oneccl_bindings_for_pytorch_path=$(python -c "from oneccl_bindings_for_pytorch import cwd; print(cwd)") +source $oneccl_bindings_for_pytorch_path/env/setvars.sh + +python -m intel_extension_for_pytorch.cpu.launch --distributed \ + --hostfile hostfile --nnodes 2 --nproc_per_node 2 textual_inversion.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --seed=7 \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --max_train_steps=750 \ + --learning_rate=2.5e-03 --scale_lr \ + --output_dir="textual_inversion_dicoo" +``` +The above is a simple distributed training usage on 2 nodes with 2 processes on each node. Add the right hostname or ip address in the "hostfile" and make sure these 2 nodes are reachable from each other. For more details, please refer to the [user guide](https://github.com/intel/torch-ccl). + + +### Reference + +We publish a [Medium blog](https://medium.com/intel-analytics-software/personalized-stable-diffusion-with-few-shot-fine-tuning-on-a-single-cpu-f01a3316b13) on how to create your own Stable Diffusion model on CPUs using textual inversion. Try it out now, if you have interests. diff --git a/diffusers/examples/research_projects/intel_opts/textual_inversion/requirements.txt b/diffusers/examples/research_projects/intel_opts/textual_inversion/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..17b32ea8a2714f1e1d6cf6442aa0f0b65f8d58c0 --- /dev/null +++ b/diffusers/examples/research_projects/intel_opts/textual_inversion/requirements.txt @@ -0,0 +1,7 @@ +accelerate +torchvision +transformers>=4.21.0 +ftfy +tensorboard +Jinja2 +intel_extension_for_pytorch>=1.13 diff --git a/diffusers/examples/research_projects/intel_opts/textual_inversion/textual_inversion_bf16.py b/diffusers/examples/research_projects/intel_opts/textual_inversion/textual_inversion_bf16.py new file mode 100644 index 0000000000000000000000000000000000000000..f446efc0b0c06f62d861c081e65817880a21cb46 --- /dev/null +++ b/diffusers/examples/research_projects/intel_opts/textual_inversion/textual_inversion_bf16.py @@ -0,0 +1,649 @@ +import argparse +import itertools +import math +import os +import random +from pathlib import Path +from typing import Optional + +import intel_extension_for_pytorch as ipex +import numpy as np +import PIL +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami + +# TODO: remove and import from diffusers.utils when the new version of diffusers is released +from packaging import version +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, DDPMScheduler, PNDMScheduler, StableDiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler +from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker +from diffusers.utils import check_min_version + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + + +logger = get_logger(__name__) + + +def save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path): + logger.info("Saving embeddings") + learned_embeds = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[placeholder_token_id] + learned_embeds_dict = {args.placeholder_token: learned_embeds.detach().cpu()} + torch.save(learned_embeds_dict, save_path) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--save_steps", + type=int, + default=500, + help="Save learned_embeds.bin every X updates steps.", + ) + parser.add_argument( + "--only_save_embeds", + action="store_true", + default=False, + help="Save only the embeddings for the new concept.", + ) + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--train_data_dir", type=str, default=None, required=True, help="A folder containing the training data." + ) + parser.add_argument( + "--placeholder_token", + type=str, + default=None, + required=True, + help="A token to use as a placeholder for the concept.", + ) + parser.add_argument( + "--initializer_token", type=str, default=None, required=True, help="A token to use as initializer word." + ) + parser.add_argument("--learnable_property", type=str, default="object", help="Choose between 'object' and 'style'") + parser.add_argument("--repeats", type=int, default=100, help="How many times to repeat the training data.") + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution." + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=5000, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=True, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.train_data_dir is None: + raise ValueError("You must specify a train data directory.") + + return args + + +imagenet_templates_small = [ + "a photo of a {}", + "a rendering of a {}", + "a cropped photo of the {}", + "the photo of a {}", + "a photo of a clean {}", + "a photo of a dirty {}", + "a dark photo of the {}", + "a photo of my {}", + "a photo of the cool {}", + "a close-up photo of a {}", + "a bright photo of the {}", + "a cropped photo of a {}", + "a photo of the {}", + "a good photo of the {}", + "a photo of one {}", + "a close-up photo of the {}", + "a rendition of the {}", + "a photo of the clean {}", + "a rendition of a {}", + "a photo of a nice {}", + "a good photo of a {}", + "a photo of the nice {}", + "a photo of the small {}", + "a photo of the weird {}", + "a photo of the large {}", + "a photo of a cool {}", + "a photo of a small {}", +] + +imagenet_style_templates_small = [ + "a painting in the style of {}", + "a rendering in the style of {}", + "a cropped painting in the style of {}", + "the painting in the style of {}", + "a clean painting in the style of {}", + "a dirty painting in the style of {}", + "a dark painting in the style of {}", + "a picture in the style of {}", + "a cool painting in the style of {}", + "a close-up painting in the style of {}", + "a bright painting in the style of {}", + "a cropped painting in the style of {}", + "a good painting in the style of {}", + "a close-up painting in the style of {}", + "a rendition in the style of {}", + "a nice painting in the style of {}", + "a small painting in the style of {}", + "a weird painting in the style of {}", + "a large painting in the style of {}", +] + + +class TextualInversionDataset(Dataset): + def __init__( + self, + data_root, + tokenizer, + learnable_property="object", # [object, style] + size=512, + repeats=100, + interpolation="bicubic", + flip_p=0.5, + set="train", + placeholder_token="*", + center_crop=False, + ): + self.data_root = data_root + self.tokenizer = tokenizer + self.learnable_property = learnable_property + self.size = size + self.placeholder_token = placeholder_token + self.center_crop = center_crop + self.flip_p = flip_p + + self.image_paths = [os.path.join(self.data_root, file_path) for file_path in os.listdir(self.data_root)] + + self.num_images = len(self.image_paths) + self._length = self.num_images + + if set == "train": + self._length = self.num_images * repeats + + self.interpolation = { + "linear": PIL_INTERPOLATION["linear"], + "bilinear": PIL_INTERPOLATION["bilinear"], + "bicubic": PIL_INTERPOLATION["bicubic"], + "lanczos": PIL_INTERPOLATION["lanczos"], + }[interpolation] + + self.templates = imagenet_style_templates_small if learnable_property == "style" else imagenet_templates_small + self.flip_transform = transforms.RandomHorizontalFlip(p=self.flip_p) + + def __len__(self): + return self._length + + def __getitem__(self, i): + example = {} + image = Image.open(self.image_paths[i % self.num_images]) + + if not image.mode == "RGB": + image = image.convert("RGB") + + placeholder_string = self.placeholder_token + text = random.choice(self.templates).format(placeholder_string) + + example["input_ids"] = self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids[0] + + # default to score-sde preprocessing + img = np.array(image).astype(np.uint8) + + if self.center_crop: + crop = min(img.shape[0], img.shape[1]) + ( + h, + w, + ) = ( + img.shape[0], + img.shape[1], + ) + img = img[(h - crop) // 2 : (h + crop) // 2, (w - crop) // 2 : (w + crop) // 2] + + image = Image.fromarray(img) + image = image.resize((self.size, self.size), resample=self.interpolation) + + image = self.flip_transform(image) + image = np.array(image).astype(np.uint8) + image = (image / 127.5 - 1.0).astype(np.float32) + + example["pixel_values"] = torch.from_numpy(image).permute(2, 0, 1) + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def freeze_params(params): + for param in params: + param.requires_grad = False + + +def main(): + args = parse_args() + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with="tensorboard", + logging_dir=logging_dir, + ) + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer and add the placeholder token as a additional special token + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Add the placeholder token in tokenizer + num_added_tokens = tokenizer.add_tokens(args.placeholder_token) + if num_added_tokens == 0: + raise ValueError( + f"The tokenizer already contains the token {args.placeholder_token}. Please pass a different" + " `placeholder_token` that is not already in the tokenizer." + ) + + # Convert the initializer_token, placeholder_token to ids + token_ids = tokenizer.encode(args.initializer_token, add_special_tokens=False) + # Check if initializer_token is a single token or a sequence of tokens + if len(token_ids) > 1: + raise ValueError("The initializer token must be a single token.") + + initializer_token_id = token_ids[0] + placeholder_token_id = tokenizer.convert_tokens_to_ids(args.placeholder_token) + + # Load models and create wrapper for stable diffusion + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="text_encoder", + revision=args.revision, + ) + vae = AutoencoderKL.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="vae", + revision=args.revision, + ) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="unet", + revision=args.revision, + ) + + # Resize the token embeddings as we are adding new special tokens to the tokenizer + text_encoder.resize_token_embeddings(len(tokenizer)) + + # Initialise the newly added placeholder token with the embeddings of the initializer token + token_embeds = text_encoder.get_input_embeddings().weight.data + token_embeds[placeholder_token_id] = token_embeds[initializer_token_id] + + # Freeze vae and unet + freeze_params(vae.parameters()) + freeze_params(unet.parameters()) + # Freeze all parameters except for the token embeddings in text encoder + params_to_freeze = itertools.chain( + text_encoder.text_model.encoder.parameters(), + text_encoder.text_model.final_layer_norm.parameters(), + text_encoder.text_model.embeddings.position_embedding.parameters(), + ) + freeze_params(params_to_freeze) + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + optimizer = torch.optim.AdamW( + text_encoder.get_input_embeddings().parameters(), # only optimize the embeddings + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + + train_dataset = TextualInversionDataset( + data_root=args.train_data_dir, + tokenizer=tokenizer, + size=args.resolution, + placeholder_token=args.placeholder_token, + repeats=args.repeats, + learnable_property=args.learnable_property, + center_crop=args.center_crop, + set="train", + ) + train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=args.train_batch_size, shuffle=True) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + text_encoder, optimizer, train_dataloader, lr_scheduler + ) + + # Move vae and unet to device + vae.to(accelerator.device) + unet.to(accelerator.device) + + # Keep vae and unet in eval model as we don't train these + vae.eval() + unet.eval() + + unet = ipex.optimize(unet, dtype=torch.bfloat16, inplace=True) + vae = ipex.optimize(vae, dtype=torch.bfloat16, inplace=True) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("textual_inversion", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + global_step = 0 + + text_encoder.train() + text_encoder, optimizer = ipex.optimize(text_encoder, optimizer=optimizer, dtype=torch.bfloat16) + + for epoch in range(args.num_train_epochs): + for step, batch in enumerate(train_dataloader): + with torch.cpu.amp.autocast(enabled=True, dtype=torch.bfloat16): + with accelerator.accumulate(text_encoder): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"]).latent_dist.sample().detach() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn(latents.shape).to(latents.device) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint( + 0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device + ).long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + loss = F.mse_loss(model_pred, target, reduction="none").mean([1, 2, 3]).mean() + accelerator.backward(loss) + + # Zero out the gradients for all token embeddings except the newly added + # embeddings for the concept, as we only want to optimize the concept embeddings + if accelerator.num_processes > 1: + grads = text_encoder.module.get_input_embeddings().weight.grad + else: + grads = text_encoder.get_input_embeddings().weight.grad + # Get the index for tokens that we want to zero the grads for + index_grads_to_zero = torch.arange(len(tokenizer)) != placeholder_token_id + grads.data[index_grads_to_zero, :] = grads.data[index_grads_to_zero, :].fill_(0) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + if global_step % args.save_steps == 0: + save_path = os.path.join(args.output_dir, f"learned_embeds-steps-{global_step}.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + accelerator.wait_for_everyone() + + # Create the pipeline using using the trained modules and save it. + if accelerator.is_main_process: + if args.push_to_hub and args.only_save_embeds: + logger.warn("Enabling full model saving because --push_to_hub=True was specified.") + save_full_model = True + else: + save_full_model = not args.only_save_embeds + if save_full_model: + pipeline = StableDiffusionPipeline( + text_encoder=accelerator.unwrap_model(text_encoder), + vae=vae, + unet=unet, + tokenizer=tokenizer, + scheduler=PNDMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler"), + safety_checker=StableDiffusionSafetyChecker.from_pretrained("CompVis/stable-diffusion-safety-checker"), + feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"), + ) + pipeline.save_pretrained(args.output_dir) + # Save the newly trained embeddings + save_path = os.path.join(args.output_dir, "learned_embeds.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/research_projects/multi_subject_dreambooth/README.md b/diffusers/examples/research_projects/multi_subject_dreambooth/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cf7dd31d0797ad1e22fb7d5ab192de2dada490df --- /dev/null +++ b/diffusers/examples/research_projects/multi_subject_dreambooth/README.md @@ -0,0 +1,291 @@ +# Multi Subject DreamBooth training + +[DreamBooth](https://arxiv.org/abs/2208.12242) is a method to personalize text2image models like stable diffusion given just a few(3~5) images of a subject. +This `train_multi_subject_dreambooth.py` script shows how to implement the training procedure for one or more subjects and adapt it for stable diffusion. Note that this code is based off of the `examples/dreambooth/train_dreambooth.py` script as of 01/06/2022. + +This script was added by @kopsahlong, and is not actively maintained. However, if you come across anything that could use fixing, feel free to open an issue and tag @kopsahlong. + +## Running locally with PyTorch +### Installing the dependencies + +Before running the script, make sure to install the library's training dependencies: + +To start, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install -e . +``` + +Then cd into the folder `diffusers/examples/research_projects/multi_subject_dreambooth` and run the following: +```bash +pip install -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +Or for a default accelerate configuration without answering questions about your environment + +```bash +accelerate config default +``` + +Or if your environment doesn't support an interactive shell e.g. a notebook + +```python +from accelerate.utils import write_basic_config +write_basic_config() +``` + +### Multi Subject Training Example +In order to have your model learn multiple concepts at once, we simply add in the additional data directories and prompts to our `instance_data_dir` and `instance_prompt` (as well as `class_data_dir` and `class_prompt` if `--with_prior_preservation` is specified) as one comma separated string. + +See an example with 2 subjects below, which learns a model for one dog subject and one human subject: + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export OUTPUT_DIR="path-to-save-model" + +# Subject 1 +export INSTANCE_DIR_1="path-to-instance-images-concept-1" +export INSTANCE_PROMPT_1="a photo of a sks dog" +export CLASS_DIR_1="path-to-class-images-dog" +export CLASS_PROMPT_1="a photo of a dog" + +# Subject 2 +export INSTANCE_DIR_2="path-to-instance-images-concept-2" +export INSTANCE_PROMPT_2="a photo of a t@y person" +export CLASS_DIR_2="path-to-class-images-person" +export CLASS_PROMPT_2="a photo of a person" + +accelerate launch train_multi_subject_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir="$INSTANCE_DIR_1,$INSTANCE_DIR_2" \ + --output_dir=$OUTPUT_DIR \ + --train_text_encoder \ + --instance_prompt="$INSTANCE_PROMPT_1,$INSTANCE_PROMPT_2" \ + --with_prior_preservation \ + --prior_loss_weight=1.0 \ + --class_data_dir="$CLASS_DIR_1,$CLASS_DIR_2" \ + --class_prompt="$CLASS_PROMPT_1,$CLASS_PROMPT_2"\ + --num_class_images=50 \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=1e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=1500 +``` + +This example shows training for 2 subjects, but please note that the model can be trained on any number of new concepts. This can be done by continuing to add in the corresponding directories and prompts to the corresponding comma separated string. + +Note also that in this script, `sks` and `t@y` were used as tokens to learn the new subjects ([this thread](https://github.com/XavierXiao/Dreambooth-Stable-Diffusion/issues/71) inspired the use of `t@y` as our second identifier). However, there may be better rare tokens to experiment with, and results also seemed to be good when more intuitive words are used. + +### Inference + +Once you have trained a model using above command, the inference can be done simply using the `StableDiffusionPipeline`. Make sure to include the `identifier`(e.g. sks in above example) in your prompt. + +```python +from diffusers import StableDiffusionPipeline +import torch + +model_id = "path-to-your-trained-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda") + +prompt = "A photo of a t@y person petting an sks dog" +image = pipe(prompt, num_inference_steps=200, guidance_scale=7.5).images[0] + +image.save("person-petting-dog.png") +``` + +### Inference from a training checkpoint + +You can also perform inference from one of the checkpoints saved during the training process, if you used the `--checkpointing_steps` argument. Please, refer to [the documentation](https://huggingface.co/docs/diffusers/main/en/training/dreambooth#performing-inference-using-a-saved-checkpoint) to see how to do it. + +## Additional Dreambooth documentation +Because the `train_multi_subject_dreambooth.py` script here was forked from an original version of `train_dreambooth.py` in the `examples/dreambooth` folder, I've included the original applicable training documentation for single subject examples below. + +This should explain how to play with training variables such as prior preservation, fine tuning the text encoder, etc. which is still applicable to our multi subject training code. Note also that the examples below, which are single subject examples, also work with `train_multi_subject_dreambooth.py`, as this script supports 1 (or more) subjects. + +### Single subject dog toy example + +Let's get our dataset. Download images from [here](https://drive.google.com/drive/folders/1BO_dyz-p65qhBRRMRA4TbZ8qW4rB99JZ) and save them in a directory. This will be our training data. + +And launch the training using + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --output_dir=$OUTPUT_DIR \ + --instance_prompt="a photo of sks dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --max_train_steps=400 +``` + +### Training with prior-preservation loss + +Prior-preservation is used to avoid overfitting and language-drift. Refer to the paper to learn more about it. For prior-preservation we first generate images using the model with a class prompt and then use those during training along with our data. +According to the paper, it's recommended to generate `num_epochs * num_samples` images for prior-preservation. 200-300 works well for most cases. The `num_class_images` flag sets the number of images to generate with the class prompt. You can place existing images in `class_data_dir`, and the training script will generate any additional images so that `num_class_images` are present in `class_data_dir` during training time. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + + +### Training on a 16GB GPU: + +With the help of gradient checkpointing and the 8-bit optimizer from bitsandbytes it's possible to run train dreambooth on a 16GB GPU. + +To install `bitandbytes` please refer to this [readme](https://github.com/TimDettmers/bitsandbytes#requirements--installation). + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=2 --gradient_checkpointing \ + --use_8bit_adam \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Training on a 8 GB GPU: + +By using [DeepSpeed](https://www.deepspeed.ai/) it's possible to offload some +tensors from VRAM to either CPU or NVME allowing to train with less VRAM. + +DeepSpeed needs to be enabled with `accelerate config`. During configuration +answer yes to "Do you want to use DeepSpeed?". With DeepSpeed stage 2, fp16 +mixed precision and offloading both parameters and optimizer state to cpu it's +possible to train on under 8 GB VRAM with a drawback of requiring significantly +more RAM (about 25 GB). See [documentation](https://huggingface.co/docs/accelerate/usage_guides/deepspeed) for more DeepSpeed configuration options. + +Changing the default Adam optimizer to DeepSpeed's special version of Adam +`deepspeed.ops.adam.DeepSpeedCPUAdam` gives a substantial speedup but enabling +it requires CUDA toolchain with the same version as pytorch. 8-bit optimizer +does not seem to be compatible with DeepSpeed at the moment. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch --mixed_precision="fp16" train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --sample_batch_size=1 \ + --gradient_accumulation_steps=1 --gradient_checkpointing \ + --learning_rate=5e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Fine-tune text encoder with the UNet. + +The script also allows to fine-tune the `text_encoder` along with the `unet`. It's been observed experimentally that fine-tuning `text_encoder` gives much better results especially on faces. +Pass the `--train_text_encoder` argument to the script to enable training `text_encoder`. + +___Note: Training text encoder requires more memory, with this option the training won't fit on 16GB GPU. It needs at least 24GB VRAM.___ + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export INSTANCE_DIR="path-to-instance-images" +export CLASS_DIR="path-to-class-images" +export OUTPUT_DIR="path-to-save-model" + +accelerate launch train_dreambooth.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_text_encoder \ + --instance_data_dir=$INSTANCE_DIR \ + --class_data_dir=$CLASS_DIR \ + --output_dir=$OUTPUT_DIR \ + --with_prior_preservation --prior_loss_weight=1.0 \ + --instance_prompt="a photo of sks dog" \ + --class_prompt="a photo of dog" \ + --resolution=512 \ + --train_batch_size=1 \ + --use_8bit_adam \ + --gradient_checkpointing \ + --learning_rate=2e-6 \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --num_class_images=200 \ + --max_train_steps=800 +``` + +### Using DreamBooth for other pipelines than Stable Diffusion + +Altdiffusion also support dreambooth now, the runing comman is basically the same as abouve, all you need to do is replace the `MODEL_NAME` like this: +One can now simply change the `pretrained_model_name_or_path` to another architecture such as [`AltDiffusion`](https://huggingface.co/docs/diffusers/api/pipelines/alt_diffusion). + +``` +export MODEL_NAME="CompVis/stable-diffusion-v1-4" --> export MODEL_NAME="BAAI/AltDiffusion-m9" +or +export MODEL_NAME="CompVis/stable-diffusion-v1-4" --> export MODEL_NAME="BAAI/AltDiffusion" +``` + +### Training with xformers: +You can enable memory efficient attention by [installing xFormers](https://github.com/facebookresearch/xformers#installing-xformers) and padding the `--enable_xformers_memory_efficient_attention` argument to the script. This is not available with the Flax/JAX implementation. + +You can also use Dreambooth to train the specialized in-painting model. See [the script in the research folder for details](https://github.com/huggingface/diffusers/tree/main/examples/research_projects/dreambooth_inpaint). \ No newline at end of file diff --git a/diffusers/examples/research_projects/multi_subject_dreambooth/requirements.txt b/diffusers/examples/research_projects/multi_subject_dreambooth/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbf6c5bec69c6d473db01ff4f15f38e3d7d7a1b3 --- /dev/null +++ b/diffusers/examples/research_projects/multi_subject_dreambooth/requirements.txt @@ -0,0 +1,6 @@ +accelerate +torchvision +transformers>=4.25.1 +ftfy +tensorboard +Jinja2 \ No newline at end of file diff --git a/diffusers/examples/research_projects/multi_subject_dreambooth/train_multi_subject_dreambooth.py b/diffusers/examples/research_projects/multi_subject_dreambooth/train_multi_subject_dreambooth.py new file mode 100644 index 0000000000000000000000000000000000000000..3865deb2e3a9c3d6d2c258f3cd4bc665e3823541 --- /dev/null +++ b/diffusers/examples/research_projects/multi_subject_dreambooth/train_multi_subject_dreambooth.py @@ -0,0 +1,883 @@ +import argparse +import hashlib +import itertools +import logging +import math +import os +import warnings +from pathlib import Path +from typing import Optional + +import datasets +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import AutoTokenizer, PretrainedConfig + +import diffusers +from diffusers import AutoencoderKL, DDPMScheduler, DiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def import_model_class_from_model_name_or_path(pretrained_model_name_or_path: str, revision: str): + text_encoder_config = PretrainedConfig.from_pretrained( + pretrained_model_name_or_path, + subfolder="text_encoder", + revision=revision, + ) + model_class = text_encoder_config.architectures[0] + + if model_class == "CLIPTextModel": + from transformers import CLIPTextModel + + return CLIPTextModel + elif model_class == "RobertaSeriesModelWithTransformation": + from diffusers.pipelines.alt_diffusion.modeling_roberta_series import RobertaSeriesModelWithTransformation + + return RobertaSeriesModelWithTransformation + else: + raise ValueError(f"{model_class} is not supported.") + + +def parse_args(input_args=None): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--instance_data_dir", + type=str, + default=None, + required=True, + help="A folder containing the training data of instance images.", + ) + parser.add_argument( + "--class_data_dir", + type=str, + default=None, + required=False, + help="A folder containing the training data of class images.", + ) + parser.add_argument( + "--instance_prompt", + type=str, + default=None, + required=True, + help="The prompt with identifier specifying the instance", + ) + parser.add_argument( + "--class_prompt", + type=str, + default=None, + help="The prompt to specify images in the same class as provided instance images.", + ) + parser.add_argument( + "--with_prior_preservation", + default=False, + action="store_true", + help="Flag to add prior preservation loss.", + ) + parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.") + parser.add_argument( + "--num_class_images", + type=int, + default=100, + help=( + "Minimal class images for prior preservation loss. If there are not enough images already present in" + " class_data_dir, additional images will be sampled with class_prompt." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument("--train_text_encoder", action="store_true", help="Whether to train the text encoder") + parser.add_argument( + "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images." + ) + parser.add_argument("--num_train_epochs", type=int, default=1) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints can be used both as final" + " checkpoints in case they are better than the last checkpoint, and are also suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=5e-6, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--lr_num_cycles", + type=int, + default=1, + help="Number of hard resets of the lr in cosine_with_restarts scheduler.", + ) + parser.add_argument("--lr_power", type=float, default=1.0, help="Power factor of the polynomial scheduler.") + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument( + "--prior_generation_precision", + type=str, + default=None, + choices=["no", "fp32", "fp16", "bf16"], + help=( + "Choose prior generation precision between fp32, fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to fp16 if a GPU is available else fp32." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + if input_args is not None: + args = parser.parse_args(input_args) + else: + args = parser.parse_args() + + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.with_prior_preservation: + if args.class_data_dir is None: + raise ValueError("You must specify a data directory for class images.") + if args.class_prompt is None: + raise ValueError("You must specify prompt for class images.") + else: + # logger is not available yet + if args.class_data_dir is not None: + warnings.warn("You need not use --class_data_dir without --with_prior_preservation.") + if args.class_prompt is not None: + warnings.warn("You need not use --class_prompt without --with_prior_preservation.") + + return args + + +class DreamBoothDataset(Dataset): + """ + A dataset to prepare the instance and class images with the prompts for fine-tuning the model. + It pre-processes the images and the tokenizes prompts. + """ + + def __init__( + self, + instance_data_root, + instance_prompt, + tokenizer, + class_data_root=None, + class_prompt=None, + size=512, + center_crop=False, + ): + self.size = size + self.center_crop = center_crop + self.tokenizer = tokenizer + + self.instance_data_root = [] + self.instance_images_path = [] + self.num_instance_images = [] + self.instance_prompt = [] + self.class_data_root = [] + self.class_images_path = [] + self.num_class_images = [] + self.class_prompt = [] + self._length = 0 + + for i in range(len(instance_data_root)): + self.instance_data_root.append(Path(instance_data_root[i])) + if not self.instance_data_root[i].exists(): + raise ValueError("Instance images root doesn't exists.") + + self.instance_images_path.append(list(Path(instance_data_root[i]).iterdir())) + self.num_instance_images.append(len(self.instance_images_path[i])) + self.instance_prompt.append(instance_prompt[i]) + self._length += self.num_instance_images[i] + + if class_data_root is not None: + self.class_data_root.append(Path(class_data_root[i])) + self.class_data_root[i].mkdir(parents=True, exist_ok=True) + self.class_images_path.append(list(self.class_data_root[i].iterdir())) + self.num_class_images.append(len(self.class_images_path)) + if self.num_class_images[i] > self.num_instance_images[i]: + self._length -= self.num_instance_images[i] + self._length += self.num_class_images[i] + self.class_prompt.append(class_prompt[i]) + else: + self.class_data_root = None + + self.image_transforms = transforms.Compose( + [ + transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def __len__(self): + return self._length + + def __getitem__(self, index): + example = {} + for i in range(len(self.instance_images_path)): + instance_image = Image.open(self.instance_images_path[i][index % self.num_instance_images[i]]) + if not instance_image.mode == "RGB": + instance_image = instance_image.convert("RGB") + example[f"instance_images_{i}"] = self.image_transforms(instance_image) + example[f"instance_prompt_ids_{i}"] = self.tokenizer( + self.instance_prompt[i], + truncation=True, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + if self.class_data_root: + for i in range(len(self.class_data_root)): + class_image = Image.open(self.class_images_path[i][index % self.num_class_images[i]]) + if not class_image.mode == "RGB": + class_image = class_image.convert("RGB") + example[f"class_images_{i}"] = self.image_transforms(class_image) + example[f"class_prompt_ids_{i}"] = self.tokenizer( + self.class_prompt[i], + truncation=True, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids + + return example + + +def collate_fn(num_instances, examples, with_prior_preservation=False): + input_ids = [] + pixel_values = [] + + for i in range(num_instances): + input_ids += [example[f"instance_prompt_ids_{i}"] for example in examples] + pixel_values += [example[f"instance_images_{i}"] for example in examples] + + # Concat class and instance examples for prior preservation. + # We do this to avoid doing two forward passes. + if with_prior_preservation: + for i in range(num_instances): + input_ids += [example[f"class_prompt_ids_{i}"] for example in examples] + pixel_values += [example[f"class_images_{i}"] for example in examples] + + pixel_values = torch.stack(pixel_values) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + + input_ids = torch.cat(input_ids, dim=0) + + batch = { + "input_ids": input_ids, + "pixel_values": pixel_values, + } + return batch + + +class PromptDataset(Dataset): + "A simple dataset to prepare the prompts to generate class images on multiple GPUs." + + def __init__(self, prompt, num_samples): + self.prompt = prompt + self.num_samples = num_samples + + def __len__(self): + return self.num_samples + + def __getitem__(self, index): + example = {} + example["prompt"] = self.prompt + example["index"] = index + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = Path(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + # Currently, it's not possible to do gradient accumulation when training two models with accelerate.accumulate + # This will be enabled soon in accelerate. For now, we don't allow gradient accumulation when training two models. + # TODO (patil-suraj): Remove this check when gradient accumulation with two models is enabled in accelerate. + if args.train_text_encoder and args.gradient_accumulation_steps > 1 and accelerator.num_processes > 1: + raise ValueError( + "Gradient accumulation is not supported when training the text encoder in distributed training. " + "Please set gradient_accumulation_steps to 1. This feature will be supported in the future." + ) + + # Parse instance and class inputs, and double check that lengths match + instance_data_dir = args.instance_data_dir.split(",") + instance_prompt = args.instance_prompt.split(",") + assert all( + x == len(instance_data_dir) for x in [len(instance_data_dir), len(instance_prompt)] + ), "Instance data dir and prompt inputs are not of the same length." + + if args.with_prior_preservation: + class_data_dir = args.class_data_dir.split(",") + class_prompt = args.class_prompt.split(",") + assert all( + x == len(instance_data_dir) + for x in [len(instance_data_dir), len(instance_prompt), len(class_data_dir), len(class_prompt)] + ), "Instance & class data dir or prompt inputs are not of the same length." + else: + class_data_dir = args.class_data_dir + class_prompt = args.class_prompt + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Generate class images if prior preservation is enabled. + if args.with_prior_preservation: + for i in range(len(class_data_dir)): + class_images_dir = Path(class_data_dir[i]) + if not class_images_dir.exists(): + class_images_dir.mkdir(parents=True) + cur_class_images = len(list(class_images_dir.iterdir())) + + if cur_class_images < args.num_class_images: + torch_dtype = torch.float16 if accelerator.device.type == "cuda" else torch.float32 + if args.prior_generation_precision == "fp32": + torch_dtype = torch.float32 + elif args.prior_generation_precision == "fp16": + torch_dtype = torch.float16 + elif args.prior_generation_precision == "bf16": + torch_dtype = torch.bfloat16 + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + torch_dtype=torch_dtype, + safety_checker=None, + revision=args.revision, + ) + pipeline.set_progress_bar_config(disable=True) + + num_new_images = args.num_class_images - cur_class_images + logger.info(f"Number of class images to sample: {num_new_images}.") + + sample_dataset = PromptDataset(class_prompt[i], num_new_images) + sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size) + + sample_dataloader = accelerator.prepare(sample_dataloader) + pipeline.to(accelerator.device) + + for example in tqdm( + sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process + ): + images = pipeline(example["prompt"]).images + + for i, image in enumerate(images): + hash_image = hashlib.sha1(image.tobytes()).hexdigest() + image_filename = ( + class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg" + ) + image.save(image_filename) + + del pipeline + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load the tokenizer + if args.tokenizer_name: + tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_name, revision=args.revision, use_fast=False) + elif args.pretrained_model_name_or_path: + tokenizer = AutoTokenizer.from_pretrained( + args.pretrained_model_name_or_path, + subfolder="tokenizer", + revision=args.revision, + use_fast=False, + ) + + # import correct text encoder class + text_encoder_cls = import_model_class_from_model_name_or_path(args.pretrained_model_name_or_path, args.revision) + + # Load scheduler and models + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + text_encoder = text_encoder_cls.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + + vae.requires_grad_(False) + if not args.train_text_encoder: + text_encoder.requires_grad_(False) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + if args.train_text_encoder: + text_encoder.gradient_checkpointing_enable() + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Use 8-bit Adam for lower memory usage or to fine-tune the model in 16GB GPUs + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`." + ) + + optimizer_class = bnb.optim.AdamW8bit + else: + optimizer_class = torch.optim.AdamW + + # Optimizer creation + params_to_optimize = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) if args.train_text_encoder else unet.parameters() + ) + optimizer = optimizer_class( + params_to_optimize, + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Dataset and DataLoaders creation: + train_dataset = DreamBoothDataset( + instance_data_root=instance_data_dir, + instance_prompt=instance_prompt, + class_data_root=class_data_dir if args.with_prior_preservation else None, + class_prompt=class_prompt, + tokenizer=tokenizer, + size=args.resolution, + center_crop=args.center_crop, + ) + + train_dataloader = torch.utils.data.DataLoader( + train_dataset, + batch_size=args.train_batch_size, + shuffle=True, + collate_fn=lambda examples: collate_fn(len(instance_data_dir), examples, args.with_prior_preservation), + num_workers=1, + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + num_cycles=args.lr_num_cycles, + power=args.lr_power, + ) + + # Prepare everything with our `accelerator`. + if args.train_text_encoder: + unet, text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, text_encoder, optimizer, train_dataloader, lr_scheduler + ) + else: + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move vae and text_encoder to device and cast to weight_dtype + vae.to(accelerator.device, dtype=weight_dtype) + if not args.train_text_encoder: + text_encoder.to(accelerator.device, dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("dreambooth", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num batches each epoch = {len(train_dataloader)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the mos recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + if args.train_text_encoder: + text_encoder.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + if args.with_prior_preservation: + # Chunk the noise and model_pred into two parts and compute the loss on each part separately. + model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0) + target, target_prior = torch.chunk(target, 2, dim=0) + + # Compute instance loss + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Compute prior loss + prior_loss = F.mse_loss(model_pred_prior.float(), target_prior.float(), reduction="mean") + + # Add the prior loss to the instance loss. + loss = loss + args.prior_loss_weight * prior_loss + else: + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + if accelerator.sync_gradients: + params_to_clip = ( + itertools.chain(unet.parameters(), text_encoder.parameters()) + if args.train_text_encoder + else unet.parameters() + ) + accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + # Create the pipeline using using the trained modules and save it. + accelerator.wait_for_everyone() + if accelerator.is_main_process: + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet), + text_encoder=accelerator.unwrap_model(text_encoder), + revision=args.revision, + ) + pipeline.save_pretrained(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/diffusers/examples/research_projects/onnxruntime/README.md b/diffusers/examples/research_projects/onnxruntime/README.md new file mode 100644 index 0000000000000000000000000000000000000000..204d9c951c996fedabc169d9a32781be9f4c4cc1 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/README.md @@ -0,0 +1,5 @@ +## Diffusers examples with ONNXRuntime optimizations + +**This research project is not actively maintained by the diffusers team. For any questions or comments, please contact Prathik Rao (prathikr), Sunghoon Choi (hanbitmyths), Ashwini Khade (askhade), or Peng Wang (pengwa) on github with any questions.** + +This aims to provide diffusers examples with ONNXRuntime optimizations for training/fine-tuning unconditional image generation, text to image, and textual inversion. Please see individual directories for more details on how to run each task using ONNXRuntime. \ No newline at end of file diff --git a/diffusers/examples/research_projects/onnxruntime/text_to_image/README.md b/diffusers/examples/research_projects/onnxruntime/text_to_image/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cd9397939ac2399ac161f19623430636a4c3c9ad --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/text_to_image/README.md @@ -0,0 +1,74 @@ +# Stable Diffusion text-to-image fine-tuning + +The `train_text_to_image.py` script shows how to fine-tune stable diffusion model on your own dataset. + +___Note___: + +___This script is experimental. The script fine-tunes the whole model and often times the model overfits and runs into issues like catastrophic forgetting. It's recommended to try different hyperparamters to get the best result on your dataset.___ + + +## Running locally with PyTorch +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +### Pokemon example + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-4`, so you'll need to visit [its card](https://huggingface.co/CompVis/stable-diffusion-v1-4), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. + +
+ +## Use ONNXRuntime to accelerate training +In order to leverage onnxruntime to accelerate training, please use train_text_to_image.py + +The command to train a DDPM UNetCondition model on the Pokemon dataset with onnxruntime: + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export dataset_name="lambdalabs/pokemon-blip-captions" +accelerate launch --mixed_precision="fp16" train_text_to_image.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$dataset_name \ + --use_ema \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --gradient_checkpointing \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --lr_scheduler="constant" --lr_warmup_steps=0 \ + --output_dir="sd-pokemon-model" +``` + +Please contact Prathik Rao (prathikr), Sunghoon Choi (hanbitmyths), Ashwini Khade (askhade), or Peng Wang (pengwa) on github with any questions. \ No newline at end of file diff --git a/diffusers/examples/research_projects/onnxruntime/text_to_image/requirements.txt b/diffusers/examples/research_projects/onnxruntime/text_to_image/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b597d5464f1ebe39f0b1f51a23b2237925263a4a --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/text_to_image/requirements.txt @@ -0,0 +1,7 @@ +accelerate +torchvision +transformers>=4.25.1 +datasets +ftfy +tensorboard +modelcards diff --git a/diffusers/examples/research_projects/onnxruntime/text_to_image/train_text_to_image.py b/diffusers/examples/research_projects/onnxruntime/text_to_image/train_text_to_image.py new file mode 100644 index 0000000000000000000000000000000000000000..4bca25167b0efd9e54e61de91d8289b8d7be8b02 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/text_to_image/train_text_to_image.py @@ -0,0 +1,727 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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 + +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import datasets +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from datasets import load_dataset +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from onnxruntime.training.ortmodule import ORTModule +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import AutoencoderKL, DDPMScheduler, StableDiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler +from diffusers.training_utils import EMAModel +from diffusers.utils import check_min_version +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__, log_level="INFO") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that 🤗 Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--image_column", type=str, default="image", help="The column of the dataset containing an image." + ) + parser.add_argument( + "--caption_column", + type=str, + default="text", + help="The column of the dataset containing a caption or a list of captions.", + ) + parser.add_argument( + "--max_train_samples", + type=int, + default=None, + help=( + "For debugging purposes or quicker training, truncate the number of training examples to this " + "value if set." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="sd-model-finetuned", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA model.") + parser.add_argument( + "--non_ema_revision", + type=str, + default=None, + required=False, + help=( + "Revision of pretrained non-ema model identifier. Must be a branch, tag or git identifier of the local or" + " remote repository specified with --pretrained_model_name_or_path." + ), + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + # Sanity checks + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("Need either a dataset name or a training folder.") + + # default to using the same revision for the non-ema model if not specified + if args.non_ema_revision is None: + args.non_ema_revision = args.revision + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +dataset_name_mapping = { + "lambdalabs/pokemon-blip-captions": ("image", "text"), +} + + +def main(): + args = parse_args() + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load scheduler, tokenizer and models. + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision + ) + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.non_ema_revision + ) + + # Freeze vae and text_encoder + vae.requires_grad_(False) + text_encoder.requires_grad_(False) + + # Create EMA for the unet. + if args.use_ema: + ema_unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + ema_unet = EMAModel(ema_unet.parameters()) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "Please install bitsandbytes to use 8-bit Adam. You can do so by running `pip install bitsandbytes`" + ) + + optimizer_cls = bnb.optim.AdamW8bit + else: + optimizer_cls = torch.optim.AdamW + + optimizer = optimizer_cls( + unet.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + # Downloading and loading a dataset from the hub. + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + ) + else: + data_files = {} + if args.train_data_dir is not None: + data_files["train"] = os.path.join(args.train_data_dir, "**") + dataset = load_dataset( + "imagefolder", + data_files=data_files, + cache_dir=args.cache_dir, + ) + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets. + # We need to tokenize inputs and targets. + column_names = dataset["train"].column_names + + # 6. Get the column names for input/target. + dataset_columns = dataset_name_mapping.get(args.dataset_name, None) + if args.image_column is None: + image_column = dataset_columns[0] if dataset_columns is not None else column_names[0] + else: + image_column = args.image_column + if image_column not in column_names: + raise ValueError( + f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}" + ) + if args.caption_column is None: + caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1] + else: + caption_column = args.caption_column + if caption_column not in column_names: + raise ValueError( + f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}" + ) + + # Preprocessing the datasets. + # We need to tokenize input captions and transform the images. + def tokenize_captions(examples, is_train=True): + captions = [] + for caption in examples[caption_column]: + if isinstance(caption, str): + captions.append(caption) + elif isinstance(caption, (list, np.ndarray)): + # take a random caption if there are multiple + captions.append(random.choice(caption) if is_train else caption[0]) + else: + raise ValueError( + f"Caption column `{caption_column}` should contain either strings or lists of strings." + ) + inputs = tokenizer( + captions, max_length=tokenizer.model_max_length, padding="max_length", truncation=True, return_tensors="pt" + ) + return inputs.input_ids + + # Preprocessing the datasets. + train_transforms = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def preprocess_train(examples): + images = [image.convert("RGB") for image in examples[image_column]] + examples["pixel_values"] = [train_transforms(image) for image in images] + examples["input_ids"] = tokenize_captions(examples) + return examples + + with accelerator.main_process_first(): + if args.max_train_samples is not None: + dataset["train"] = dataset["train"].shuffle(seed=args.seed).select(range(args.max_train_samples)) + # Set the training transforms + train_dataset = dataset["train"].with_transform(preprocess_train) + + def collate_fn(examples): + pixel_values = torch.stack([example["pixel_values"] for example in examples]) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + input_ids = torch.stack([example["input_ids"] for example in examples]) + return {"pixel_values": pixel_values, "input_ids": input_ids} + + # DataLoaders creation: + train_dataloader = torch.utils.data.DataLoader( + train_dataset, + shuffle=True, + collate_fn=collate_fn, + batch_size=args.train_batch_size, + num_workers=args.dataloader_num_workers, + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + + unet = ORTModule(unet) + + if args.use_ema: + accelerator.register_for_checkpointing(ema_unet) + + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu and cast to weight_dtype + text_encoder.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + if args.use_ema: + ema_unet.to(accelerator.device) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("text2image-fine-tune", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + train_loss = 0.0 + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + # Predict the noise residual and compute loss + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states, return_dict=False)[0] + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Gather the losses across all processes for logging (if we use distributed training). + avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean() + train_loss += avg_loss.item() / args.gradient_accumulation_steps + + # Backpropagate + accelerator.backward(loss) + if accelerator.sync_gradients: + accelerator.clip_grad_norm_(unet.parameters(), args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + if args.use_ema: + ema_unet.step(unet.parameters()) + progress_bar.update(1) + global_step += 1 + accelerator.log({"train_loss": train_loss}, step=global_step) + train_loss = 0.0 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"step_loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + + if global_step >= args.max_train_steps: + break + + # Create the pipeline using the trained modules and save it. + accelerator.wait_for_everyone() + if accelerator.is_main_process: + unet = accelerator.unwrap_model(unet) + if args.use_ema: + ema_unet.copy_to(unet.parameters()) + + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=text_encoder, + vae=vae, + unet=unet, + revision=args.revision, + ) + pipeline.save_pretrained(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/research_projects/onnxruntime/textual_inversion/README.md b/diffusers/examples/research_projects/onnxruntime/textual_inversion/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0ed34966e9f1836d9744edf77f46c84bb8609e97 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/textual_inversion/README.md @@ -0,0 +1,82 @@ +## Textual Inversion fine-tuning example + +[Textual inversion](https://arxiv.org/abs/2208.01618) is a method to personalize text2image models like stable diffusion on your own images using just 3-5 examples. +The `textual_inversion.py` script shows how to implement the training procedure and adapt it for stable diffusion. + +## Running on Colab + +Colab for training +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_textual_inversion_training.ipynb) + +Colab for inference +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/stable_conceptualizer_inference.ipynb) + +## Running locally with PyTorch +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + + +### Cat toy example + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-5`, so you'll need to visit [its card](https://huggingface.co/runwayml/stable-diffusion-v1-5), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. + +
+ +Now let's get our dataset.Download 3-4 images from [here](https://drive.google.com/drive/folders/1fmJMs25nxS_rSNqS5hTcRdLem_YQXbq5) and save them in a directory. This will be our training data. + +## Use ONNXRuntime to accelerate training +In order to leverage onnxruntime to accelerate training, please use textual_inversion.py + +The command to train on custom data with onnxruntime: + +```bash +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export DATA_DIR="path-to-dir-containing-images" + +accelerate launch textual_inversion.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --max_train_steps=3000 \ + --learning_rate=5.0e-04 --scale_lr \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --output_dir="textual_inversion_cat" +``` + +Please contact Prathik Rao (prathikr), Sunghoon Choi (hanbitmyths), Ashwini Khade (askhade), or Peng Wang (pengwa) on github with any questions. \ No newline at end of file diff --git a/diffusers/examples/research_projects/onnxruntime/textual_inversion/requirements.txt b/diffusers/examples/research_projects/onnxruntime/textual_inversion/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a1731c228fd4f103c2e5e32735304d0d1bbaa2d --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/textual_inversion/requirements.txt @@ -0,0 +1,6 @@ +accelerate +torchvision +transformers>=4.25.1 +ftfy +tensorboard +modelcards diff --git a/diffusers/examples/research_projects/onnxruntime/textual_inversion/textual_inversion.py b/diffusers/examples/research_projects/onnxruntime/textual_inversion/textual_inversion.py new file mode 100644 index 0000000000000000000000000000000000000000..f54e2d3e3f53d2ae6312381f6528e2a510bb0cf8 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/textual_inversion/textual_inversion.py @@ -0,0 +1,847 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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 + +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import datasets +import numpy as np +import PIL +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from onnxruntime.training.ortmodule import ORTModule + +# TODO: remove and import from diffusers.utils when the new version of diffusers is released +from packaging import version +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import ( + AutoencoderKL, + DDPMScheduler, + DiffusionPipeline, + DPMSolverMultistepScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version, is_wandb_available +from diffusers.utils.import_utils import is_xformers_available + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path): + logger.info("Saving embeddings") + learned_embeds = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[placeholder_token_id] + learned_embeds_dict = {args.placeholder_token: learned_embeds.detach().cpu()} + torch.save(learned_embeds_dict, save_path) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--save_steps", + type=int, + default=500, + help="Save learned_embeds.bin every X updates steps.", + ) + parser.add_argument( + "--only_save_embeds", + action="store_true", + default=False, + help="Save only the embeddings for the new concept.", + ) + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--train_data_dir", type=str, default=None, required=True, help="A folder containing the training data." + ) + parser.add_argument( + "--placeholder_token", + type=str, + default=None, + required=True, + help="A token to use as a placeholder for the concept.", + ) + parser.add_argument( + "--initializer_token", type=str, default=None, required=True, help="A token to use as initializer word." + ) + parser.add_argument("--learnable_property", type=str, default="object", help="Choose between 'object' and 'style'") + parser.add_argument("--repeats", type=int, default=100, help="How many times to repeat the training data.") + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution." + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=5000, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument( + "--validation_prompt", + type=str, + default=None, + help="A prompt that is used during validation to verify that the model is learning.", + ) + parser.add_argument( + "--num_validation_images", + type=int, + default=4, + help="Number of images that should be generated during validation with `validation_prompt`.", + ) + parser.add_argument( + "--validation_epochs", + type=int, + default=50, + help=( + "Run validation every X epochs. Validation consists of running the prompt" + " `args.validation_prompt` multiple times: `args.num_validation_images`" + " and logging the images." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.train_data_dir is None: + raise ValueError("You must specify a train data directory.") + + return args + + +imagenet_templates_small = [ + "a photo of a {}", + "a rendering of a {}", + "a cropped photo of the {}", + "the photo of a {}", + "a photo of a clean {}", + "a photo of a dirty {}", + "a dark photo of the {}", + "a photo of my {}", + "a photo of the cool {}", + "a close-up photo of a {}", + "a bright photo of the {}", + "a cropped photo of a {}", + "a photo of the {}", + "a good photo of the {}", + "a photo of one {}", + "a close-up photo of the {}", + "a rendition of the {}", + "a photo of the clean {}", + "a rendition of a {}", + "a photo of a nice {}", + "a good photo of a {}", + "a photo of the nice {}", + "a photo of the small {}", + "a photo of the weird {}", + "a photo of the large {}", + "a photo of a cool {}", + "a photo of a small {}", +] + +imagenet_style_templates_small = [ + "a painting in the style of {}", + "a rendering in the style of {}", + "a cropped painting in the style of {}", + "the painting in the style of {}", + "a clean painting in the style of {}", + "a dirty painting in the style of {}", + "a dark painting in the style of {}", + "a picture in the style of {}", + "a cool painting in the style of {}", + "a close-up painting in the style of {}", + "a bright painting in the style of {}", + "a cropped painting in the style of {}", + "a good painting in the style of {}", + "a close-up painting in the style of {}", + "a rendition in the style of {}", + "a nice painting in the style of {}", + "a small painting in the style of {}", + "a weird painting in the style of {}", + "a large painting in the style of {}", +] + + +class TextualInversionDataset(Dataset): + def __init__( + self, + data_root, + tokenizer, + learnable_property="object", # [object, style] + size=512, + repeats=100, + interpolation="bicubic", + flip_p=0.5, + set="train", + placeholder_token="*", + center_crop=False, + ): + self.data_root = data_root + self.tokenizer = tokenizer + self.learnable_property = learnable_property + self.size = size + self.placeholder_token = placeholder_token + self.center_crop = center_crop + self.flip_p = flip_p + + self.image_paths = [os.path.join(self.data_root, file_path) for file_path in os.listdir(self.data_root)] + + self.num_images = len(self.image_paths) + self._length = self.num_images + + if set == "train": + self._length = self.num_images * repeats + + self.interpolation = { + "linear": PIL_INTERPOLATION["linear"], + "bilinear": PIL_INTERPOLATION["bilinear"], + "bicubic": PIL_INTERPOLATION["bicubic"], + "lanczos": PIL_INTERPOLATION["lanczos"], + }[interpolation] + + self.templates = imagenet_style_templates_small if learnable_property == "style" else imagenet_templates_small + self.flip_transform = transforms.RandomHorizontalFlip(p=self.flip_p) + + def __len__(self): + return self._length + + def __getitem__(self, i): + example = {} + image = Image.open(self.image_paths[i % self.num_images]) + + if not image.mode == "RGB": + image = image.convert("RGB") + + placeholder_string = self.placeholder_token + text = random.choice(self.templates).format(placeholder_string) + + example["input_ids"] = self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids[0] + + # default to score-sde preprocessing + img = np.array(image).astype(np.uint8) + + if self.center_crop: + crop = min(img.shape[0], img.shape[1]) + ( + h, + w, + ) = ( + img.shape[0], + img.shape[1], + ) + img = img[(h - crop) // 2 : (h + crop) // 2, (w - crop) // 2 : (w + crop) // 2] + + image = Image.fromarray(img) + image = image.resize((self.size, self.size), resample=self.interpolation) + + image = self.flip_transform(image) + image = np.array(image).astype(np.uint8) + image = (image / 127.5 - 1.0).astype(np.float32) + + example["pixel_values"] = torch.from_numpy(image).permute(2, 0, 1) + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(): + args = parse_args() + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + if args.report_to == "wandb": + if not is_wandb_available(): + raise ImportError("Make sure to install wandb if you want to use it for logging during training.") + import wandb + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Load scheduler and models + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + + # Add the placeholder token in tokenizer + num_added_tokens = tokenizer.add_tokens(args.placeholder_token) + if num_added_tokens == 0: + raise ValueError( + f"The tokenizer already contains the token {args.placeholder_token}. Please pass a different" + " `placeholder_token` that is not already in the tokenizer." + ) + + # Convert the initializer_token, placeholder_token to ids + token_ids = tokenizer.encode(args.initializer_token, add_special_tokens=False) + # Check if initializer_token is a single token or a sequence of tokens + if len(token_ids) > 1: + raise ValueError("The initializer token must be a single token.") + + initializer_token_id = token_ids[0] + placeholder_token_id = tokenizer.convert_tokens_to_ids(args.placeholder_token) + + # Resize the token embeddings as we are adding new special tokens to the tokenizer + text_encoder.resize_token_embeddings(len(tokenizer)) + + # Initialise the newly added placeholder token with the embeddings of the initializer token + token_embeds = text_encoder.get_input_embeddings().weight.data + token_embeds[placeholder_token_id] = token_embeds[initializer_token_id] + + # Freeze vae and unet + vae.requires_grad_(False) + unet.requires_grad_(False) + # Freeze all parameters except for the token embeddings in text encoder + text_encoder.text_model.encoder.requires_grad_(False) + text_encoder.text_model.final_layer_norm.requires_grad_(False) + text_encoder.text_model.embeddings.position_embedding.requires_grad_(False) + + if args.gradient_checkpointing: + # Keep unet in train mode if we are using gradient checkpointing to save memory. + # The dropout cannot be != 0 so it doesn't matter if we are in eval or train mode. + unet.train() + text_encoder.gradient_checkpointing_enable() + unet.enable_gradient_checkpointing() + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + optimizer = torch.optim.AdamW( + text_encoder.get_input_embeddings().parameters(), # only optimize the embeddings + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Dataset and DataLoaders creation: + train_dataset = TextualInversionDataset( + data_root=args.train_data_dir, + tokenizer=tokenizer, + size=args.resolution, + placeholder_token=args.placeholder_token, + repeats=args.repeats, + learnable_property=args.learnable_property, + center_crop=args.center_crop, + set="train", + ) + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, num_workers=args.dataloader_num_workers + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + text_encoder, optimizer, train_dataloader, lr_scheduler + ) + + text_encoder = ORTModule(text_encoder) + + # For mixed precision training we cast the unet and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move vae and unet to device and cast to weight_dtype + unet.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("textual_inversion", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + # keep original embeddings as reference + orig_embeds_params = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight.data.clone() + + for epoch in range(first_epoch, args.num_train_epochs): + text_encoder.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(text_encoder): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample().detach() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0].to(dtype=weight_dtype) + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Let's make sure we don't update any embedding weights besides the newly added token + index_no_updates = torch.arange(len(tokenizer)) != placeholder_token_id + with torch.no_grad(): + accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[ + index_no_updates + ] = orig_embeds_params[index_no_updates] + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + if global_step % args.save_steps == 0: + save_path = os.path.join(args.output_dir, f"learned_embeds-steps-{global_step}.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + if args.validation_prompt is not None and epoch % args.validation_epochs == 0: + logger.info( + f"Running validation... \n Generating {args.num_validation_images} images with prompt:" + f" {args.validation_prompt}." + ) + # create pipeline (note: unet and vae are loaded again in float32) + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=accelerator.unwrap_model(text_encoder), + revision=args.revision, + ) + pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) + pipeline = pipeline.to(accelerator.device) + pipeline.set_progress_bar_config(disable=True) + + # run inference + generator = ( + None if args.seed is None else torch.Generator(device=accelerator.device).manual_seed(args.seed) + ) + prompt = args.num_validation_images * [args.validation_prompt] + images = pipeline(prompt, num_inference_steps=25, generator=generator).images + + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("validation", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "validation": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + del pipeline + torch.cuda.empty_cache() + + # Create the pipeline using using the trained modules and save it. + accelerator.wait_for_everyone() + if accelerator.is_main_process: + if args.push_to_hub and args.only_save_embeds: + logger.warn("Enabling full model saving because --push_to_hub=True was specified.") + save_full_model = True + else: + save_full_model = not args.only_save_embeds + if save_full_model: + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=accelerator.unwrap_model(text_encoder), + vae=vae, + unet=unet, + tokenizer=tokenizer, + ) + pipeline.save_pretrained(args.output_dir) + # Save the newly trained embeddings + save_path = os.path.join(args.output_dir, "learned_embeds.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/README.md b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..621e9a2fd69a97046230fb7561571d1484d47710 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/README.md @@ -0,0 +1,50 @@ +## Training examples + +Creating a training image set is [described in a different document](https://huggingface.co/docs/datasets/image_process#image-datasets). + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +#### Use ONNXRuntime to accelerate training + +In order to leverage onnxruntime to accelerate training, please use train_unconditional_ort.py + +The command to train a DDPM UNet model on the Oxford Flowers dataset with onnxruntime: + +```bash +accelerate launch train_unconditional_ort.py \ + --dataset_name="huggan/flowers-102-categories" \ + --resolution=64 --center_crop --random_flip \ + --output_dir="ddpm-ema-flowers-64" \ + --use_ema \ + --train_batch_size=16 \ + --num_epochs=1 \ + --gradient_accumulation_steps=1 \ + --learning_rate=1e-4 \ + --lr_warmup_steps=500 \ + --mixed_precision=fp16 + ``` + +Please contact Prathik Rao (prathikr), Sunghoon Choi (hanbitmyths), Ashwini Khade (askhade), or Peng Wang (pengwa) on github with any questions. diff --git a/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/requirements.txt b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbc6905560209d6b9c957d8c6bb61cde4462365b --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/requirements.txt @@ -0,0 +1,3 @@ +accelerate +torchvision +datasets diff --git a/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/train_unconditional.py b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/train_unconditional.py new file mode 100644 index 0000000000000000000000000000000000000000..586ed8c8d397687915ada32b107ab1e6227e1e64 --- /dev/null +++ b/diffusers/examples/research_projects/onnxruntime/unconditional_image_generation/train_unconditional.py @@ -0,0 +1,577 @@ +import argparse +import inspect +import logging +import math +import os +from pathlib import Path +from typing import Optional + +import datasets +import torch +import torch.nn.functional as F +from accelerate import Accelerator +from accelerate.logging import get_logger +from datasets import load_dataset +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from onnxruntime.training.ortmodule import ORTModule +from torchvision import transforms +from tqdm.auto import tqdm + +import diffusers +from diffusers import DDPMPipeline, DDPMScheduler, UNet2DModel +from diffusers.optimization import get_scheduler +from diffusers.training_utils import EMAModel +from diffusers.utils import check_min_version + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__, log_level="INFO") + + +def _extract_into_tensor(arr, timesteps, broadcast_shape): + """ + Extract values from a 1-D numpy array for a batch of indices. + :param arr: the 1-D numpy array. + :param timesteps: a tensor of indices into the array to extract. + :param broadcast_shape: a larger shape of K dimensions with the batch + dimension equal to the length of timesteps. + :return: a tensor of shape [batch_size, 1, ...] where the shape has K dims. + """ + if not isinstance(arr, torch.Tensor): + arr = torch.from_numpy(arr) + res = arr[timesteps].float().to(timesteps.device) + while len(res.shape) < len(broadcast_shape): + res = res[..., None] + return res.expand(broadcast_shape) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that HF Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="ddpm-model-64", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--overwrite_output_dir", action="store_true") + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument( + "--resolution", + type=int, + default=64, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + default=False, + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--eval_batch_size", type=int, default=16, help="The number of images to generate for evaluation." + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "The number of subprocesses to use for data loading. 0 means that the data will be loaded in the main" + " process." + ), + ) + parser.add_argument("--num_epochs", type=int, default=100) + parser.add_argument("--save_images_epochs", type=int, default=10, help="How often to save images during training.") + parser.add_argument( + "--save_model_epochs", type=int, default=10, help="How often to save the model during training." + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="cosine", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument("--adam_beta1", type=float, default=0.95, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument( + "--adam_weight_decay", type=float, default=1e-6, help="Weight decay magnitude for the Adam optimizer." + ) + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer.") + parser.add_argument( + "--use_ema", + action="store_true", + help="Whether to use Exponential Moving Average for the final model weights.", + ) + parser.add_argument("--ema_inv_gamma", type=float, default=1.0, help="The inverse gamma value for the EMA decay.") + parser.add_argument("--ema_power", type=float, default=3 / 4, help="The power value for the EMA decay.") + parser.add_argument("--ema_max_decay", type=float, default=0.9999, help="The maximum decay magnitude for EMA.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--hub_private_repo", action="store_true", help="Whether or not to create a private repository." + ) + parser.add_argument( + "--logger", + type=str, + default="tensorboard", + choices=["tensorboard", "wandb"], + help=( + "Whether to use [tensorboard](https://www.tensorflow.org/tensorboard) or [wandb](https://www.wandb.ai)" + " for experiment tracking and logging of model metrics and model checkpoints" + ), + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument( + "--prediction_type", + type=str, + default="epsilon", + choices=["epsilon", "sample"], + help="Whether the model should predict the 'epsilon'/noise error or directly the reconstructed image 'x0'.", + ) + parser.add_argument("--ddpm_num_steps", type=int, default=1000) + parser.add_argument("--ddpm_beta_schedule", type=str, default="linear") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("You must specify either a dataset name from the hub or a train data directory.") + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.logger, + logging_dir=logging_dir, + ) + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Initialize the model + model = UNet2DModel( + sample_size=args.resolution, + in_channels=3, + out_channels=3, + layers_per_block=2, + block_out_channels=(128, 128, 256, 256, 512, 512), + down_block_types=( + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "AttnDownBlock2D", + "DownBlock2D", + ), + up_block_types=( + "UpBlock2D", + "AttnUpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + ), + ) + + # Create EMA for the model. + if args.use_ema: + ema_model = EMAModel( + model.parameters(), + decay=args.ema_max_decay, + use_ema_warmup=True, + inv_gamma=args.ema_inv_gamma, + power=args.ema_power, + ) + + # Initialize the scheduler + accepts_prediction_type = "prediction_type" in set(inspect.signature(DDPMScheduler.__init__).parameters.keys()) + if accepts_prediction_type: + noise_scheduler = DDPMScheduler( + num_train_timesteps=args.ddpm_num_steps, + beta_schedule=args.ddpm_beta_schedule, + prediction_type=args.prediction_type, + ) + else: + noise_scheduler = DDPMScheduler(num_train_timesteps=args.ddpm_num_steps, beta_schedule=args.ddpm_beta_schedule) + + # Initialize the optimizer + optimizer = torch.optim.AdamW( + model.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + split="train", + ) + else: + dataset = load_dataset("imagefolder", data_dir=args.train_data_dir, cache_dir=args.cache_dir, split="train") + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets and DataLoaders creation. + augmentations = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def transform_images(examples): + images = [augmentations(image.convert("RGB")) for image in examples["image"]] + return {"input": images} + + logger.info(f"Dataset size: {len(dataset)}") + + dataset.set_transform(transform_images) + train_dataloader = torch.utils.data.DataLoader( + dataset, batch_size=args.train_batch_size, shuffle=True, num_workers=args.dataloader_num_workers + ) + + # Initialize the learning rate scheduler + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=(len(train_dataloader) * args.num_epochs), + ) + + # Prepare everything with our `accelerator`. + model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + model, optimizer, train_dataloader, lr_scheduler + ) + + model = ORTModule(model) + + if args.use_ema: + accelerator.register_for_checkpointing(ema_model) + ema_model.to(accelerator.device) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + run = os.path.split(__file__)[-1].split(".")[0] + accelerator.init_trackers(run) + + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + max_train_steps = args.num_epochs * num_update_steps_per_epoch + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(dataset)}") + logger.info(f" Num Epochs = {args.num_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {max_train_steps}") + + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Train! + for epoch in range(first_epoch, args.num_epochs): + model.train() + progress_bar = tqdm(total=num_update_steps_per_epoch, disable=not accelerator.is_local_main_process) + progress_bar.set_description(f"Epoch {epoch}") + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + clean_images = batch["input"] + # Sample noise that we'll add to the images + noise = torch.randn(clean_images.shape).to(clean_images.device) + bsz = clean_images.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint( + 0, noise_scheduler.config.num_train_timesteps, (bsz,), device=clean_images.device + ).long() + + # Add noise to the clean images according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_images = noise_scheduler.add_noise(clean_images, noise, timesteps) + + with accelerator.accumulate(model): + # Predict the noise residual + model_output = model(noisy_images, timesteps, return_dict=False)[0] + + if args.prediction_type == "epsilon": + loss = F.mse_loss(model_output, noise) # this could have different weights! + elif args.prediction_type == "sample": + alpha_t = _extract_into_tensor( + noise_scheduler.alphas_cumprod, timesteps, (clean_images.shape[0], 1, 1, 1) + ) + snr_weights = alpha_t / (1 - alpha_t) + loss = snr_weights * F.mse_loss( + model_output, clean_images, reduction="none" + ) # use SNR weighting from distillation paper + loss = loss.mean() + else: + raise ValueError(f"Unsupported prediction type: {args.prediction_type}") + + accelerator.backward(loss) + + if accelerator.sync_gradients: + accelerator.clip_grad_norm_(model.parameters(), 1.0) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + if args.use_ema: + ema_model.step(model.parameters()) + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0], "step": global_step} + if args.use_ema: + logs["ema_decay"] = ema_model.decay + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + progress_bar.close() + + accelerator.wait_for_everyone() + + # Generate sample images for visual inspection + if accelerator.is_main_process: + if epoch % args.save_images_epochs == 0 or epoch == args.num_epochs - 1: + unet = accelerator.unwrap_model(model) + if args.use_ema: + ema_model.copy_to(unet.parameters()) + pipeline = DDPMPipeline( + unet=unet, + scheduler=noise_scheduler, + ) + + generator = torch.Generator(device=pipeline.device).manual_seed(0) + # run pipeline in inference (sample random noise and denoise) + images = pipeline( + generator=generator, + batch_size=args.eval_batch_size, + output_type="numpy", + num_inference_steps=args.ddpm_num_steps, + ).images + + # denormalize the images and save to tensorboard + images_processed = (images * 255).round().astype("uint8") + + if args.logger == "tensorboard": + accelerator.get_tracker("tensorboard").add_images( + "test_samples", images_processed.transpose(0, 3, 1, 2), epoch + ) + + if epoch % args.save_model_epochs == 0 or epoch == args.num_epochs - 1: + # save the model + pipeline.save_pretrained(args.output_dir) + if args.push_to_hub: + repo.push_to_hub(commit_message=f"Epoch {epoch}", blocking=False) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/diffusers/examples/rl/README.md b/diffusers/examples/rl/README.md new file mode 100644 index 0000000000000000000000000000000000000000..17881d584a4043156b784a152253b0f83598ced9 --- /dev/null +++ b/diffusers/examples/rl/README.md @@ -0,0 +1,22 @@ +# Overview + +These examples show how to run [Diffuser](https://arxiv.org/abs/2205.09991) in Diffusers. +There are two ways to use the script, `run_diffuser_locomotion.py`. + +The key option is a change of the variable `n_guide_steps`. +When `n_guide_steps=0`, the trajectories are sampled from the diffusion model, but not fine-tuned to maximize reward in the environment. +By default, `n_guide_steps=2` to match the original implementation. + + +You will need some RL specific requirements to run the examples: + +``` +pip install -f https://download.pytorch.org/whl/torch_stable.html \ + free-mujoco-py \ + einops \ + gym==0.24.1 \ + protobuf==3.20.1 \ + git+https://github.com/rail-berkeley/d4rl.git \ + mediapy \ + Pillow==9.0.0 +``` diff --git a/diffusers/examples/rl/run_diffuser_locomotion.py b/diffusers/examples/rl/run_diffuser_locomotion.py new file mode 100644 index 0000000000000000000000000000000000000000..e64a20500bead6b90bc263040f837e6723f8e4b5 --- /dev/null +++ b/diffusers/examples/rl/run_diffuser_locomotion.py @@ -0,0 +1,59 @@ +import d4rl # noqa +import gym +import tqdm +from diffusers.experimental import ValueGuidedRLPipeline + + +config = dict( + n_samples=64, + horizon=32, + num_inference_steps=20, + n_guide_steps=2, # can set to 0 for faster sampling, does not use value network + scale_grad_by_std=True, + scale=0.1, + eta=0.0, + t_grad_cutoff=2, + device="cpu", +) + + +if __name__ == "__main__": + env_name = "hopper-medium-v2" + env = gym.make(env_name) + + pipeline = ValueGuidedRLPipeline.from_pretrained( + "bglick13/hopper-medium-v2-value-function-hor32", + env=env, + ) + + env.seed(0) + obs = env.reset() + total_reward = 0 + total_score = 0 + T = 1000 + rollout = [obs.copy()] + try: + for t in tqdm.tqdm(range(T)): + # call the policy + denorm_actions = pipeline(obs, planning_horizon=32) + + # execute action in environment + next_observation, reward, terminal, _ = env.step(denorm_actions) + score = env.get_normalized_score(total_reward) + + # update return + total_reward += reward + total_score += score + print( + f"Step: {t}, Reward: {reward}, Total Reward: {total_reward}, Score: {score}, Total Score:" + f" {total_score}" + ) + + # save observations for rendering + rollout.append(next_observation.copy()) + + obs = next_observation + except KeyboardInterrupt: + pass + + print(f"Total reward: {total_reward}") diff --git a/diffusers/examples/test_examples.py b/diffusers/examples/test_examples.py new file mode 100644 index 0000000000000000000000000000000000000000..f8b3b5dc532b2e22c708fcf68a83131aaf969d66 --- /dev/null +++ b/diffusers/examples/test_examples.py @@ -0,0 +1,246 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc.. +# +# 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. + + +import logging +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from typing import List + +from accelerate.utils import write_basic_config + +from diffusers import DiffusionPipeline, UNet2DConditionModel + + +logging.basicConfig(level=logging.DEBUG) + +logger = logging.getLogger() + + +# These utils relate to ensuring the right error message is received when running scripts +class SubprocessCallException(Exception): + pass + + +def run_command(command: List[str], return_stdout=False): + """ + Runs `command` with `subprocess.check_output` and will potentially return the `stdout`. Will also properly capture + if an error occurred while running `command` + """ + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT) + if return_stdout: + if hasattr(output, "decode"): + output = output.decode("utf-8") + return output + except subprocess.CalledProcessError as e: + raise SubprocessCallException( + f"Command `{' '.join(command)}` failed with the following error:\n\n{e.output.decode()}" + ) from e + + +stream_handler = logging.StreamHandler(sys.stdout) +logger.addHandler(stream_handler) + + +class ExamplesTestsAccelerate(unittest.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._tmpdir = tempfile.mkdtemp() + cls.configPath = os.path.join(cls._tmpdir, "default_config.yml") + + write_basic_config(save_location=cls.configPath) + cls._launch_args = ["accelerate", "launch", "--config_file", cls.configPath] + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + shutil.rmtree(cls._tmpdir) + + def test_train_unconditional(self): + with tempfile.TemporaryDirectory() as tmpdir: + test_args = f""" + examples/unconditional_image_generation/train_unconditional.py + --dataset_name hf-internal-testing/dummy_image_class_data + --model_config_name_or_path diffusers/ddpm_dummy + --resolution 64 + --output_dir {tmpdir} + --train_batch_size 2 + --num_epochs 1 + --gradient_accumulation_steps 1 + --ddpm_num_inference_steps 2 + --learning_rate 1e-3 + --lr_warmup_steps 5 + """.split() + + run_command(self._launch_args + test_args, return_stdout=True) + # save_pretrained smoke test + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "unet", "diffusion_pytorch_model.bin"))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "scheduler", "scheduler_config.json"))) + + def test_textual_inversion(self): + with tempfile.TemporaryDirectory() as tmpdir: + test_args = f""" + examples/textual_inversion/textual_inversion.py + --pretrained_model_name_or_path hf-internal-testing/tiny-stable-diffusion-pipe + --train_data_dir docs/source/en/imgs + --learnable_property object + --placeholder_token + --initializer_token a + --resolution 64 + --train_batch_size 1 + --gradient_accumulation_steps 1 + --max_train_steps 2 + --learning_rate 5.0e-04 + --scale_lr + --lr_scheduler constant + --lr_warmup_steps 0 + --output_dir {tmpdir} + """.split() + + run_command(self._launch_args + test_args) + # save_pretrained smoke test + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "learned_embeds.bin"))) + + def test_dreambooth(self): + with tempfile.TemporaryDirectory() as tmpdir: + test_args = f""" + examples/dreambooth/train_dreambooth.py + --pretrained_model_name_or_path hf-internal-testing/tiny-stable-diffusion-pipe + --instance_data_dir docs/source/en/imgs + --instance_prompt photo + --resolution 64 + --train_batch_size 1 + --gradient_accumulation_steps 1 + --max_train_steps 2 + --learning_rate 5.0e-04 + --scale_lr + --lr_scheduler constant + --lr_warmup_steps 0 + --output_dir {tmpdir} + """.split() + + run_command(self._launch_args + test_args) + # save_pretrained smoke test + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "unet", "diffusion_pytorch_model.bin"))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "scheduler", "scheduler_config.json"))) + + def test_dreambooth_checkpointing(self): + with tempfile.TemporaryDirectory() as tmpdir: + instance_prompt = "photo" + pretrained_model_name_or_path = "hf-internal-testing/tiny-stable-diffusion-pipe" + + # Run training script with checkpointing + # max_train_steps == 5, checkpointing_steps == 2 + # Should create checkpoints at steps 2, 4 + + initial_run_args = f""" + examples/dreambooth/train_dreambooth.py + --pretrained_model_name_or_path {pretrained_model_name_or_path} + --instance_data_dir docs/source/en/imgs + --instance_prompt {instance_prompt} + --resolution 64 + --train_batch_size 1 + --gradient_accumulation_steps 1 + --max_train_steps 5 + --learning_rate 5.0e-04 + --scale_lr + --lr_scheduler constant + --lr_warmup_steps 0 + --output_dir {tmpdir} + --checkpointing_steps=2 + --seed=0 + """.split() + + run_command(self._launch_args + initial_run_args) + + # check can run the original fully trained output pipeline + pipe = DiffusionPipeline.from_pretrained(tmpdir, safety_checker=None) + pipe(instance_prompt, num_inference_steps=2) + + # check checkpoint directories exist + self.assertTrue(os.path.isdir(os.path.join(tmpdir, "checkpoint-2"))) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, "checkpoint-4"))) + + # check can run an intermediate checkpoint + unet = UNet2DConditionModel.from_pretrained(tmpdir, subfolder="checkpoint-2/unet") + pipe = DiffusionPipeline.from_pretrained(pretrained_model_name_or_path, unet=unet, safety_checker=None) + pipe(instance_prompt, num_inference_steps=2) + + # Remove checkpoint 2 so that we can check only later checkpoints exist after resuming + shutil.rmtree(os.path.join(tmpdir, "checkpoint-2")) + + # Run training script for 7 total steps resuming from checkpoint 4 + + resume_run_args = f""" + examples/dreambooth/train_dreambooth.py + --pretrained_model_name_or_path {pretrained_model_name_or_path} + --instance_data_dir docs/source/en/imgs + --instance_prompt {instance_prompt} + --resolution 64 + --train_batch_size 1 + --gradient_accumulation_steps 1 + --max_train_steps 7 + --learning_rate 5.0e-04 + --scale_lr + --lr_scheduler constant + --lr_warmup_steps 0 + --output_dir {tmpdir} + --checkpointing_steps=2 + --resume_from_checkpoint=checkpoint-4 + --seed=0 + """.split() + + run_command(self._launch_args + resume_run_args) + + # check can run new fully trained pipeline + pipe = DiffusionPipeline.from_pretrained(tmpdir, safety_checker=None) + pipe(instance_prompt, num_inference_steps=2) + + # check old checkpoints do not exist + self.assertFalse(os.path.isdir(os.path.join(tmpdir, "checkpoint-2"))) + + # check new checkpoints exist + self.assertTrue(os.path.isdir(os.path.join(tmpdir, "checkpoint-4"))) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, "checkpoint-6"))) + + def test_text_to_image(self): + with tempfile.TemporaryDirectory() as tmpdir: + test_args = f""" + examples/text_to_image/train_text_to_image.py + --pretrained_model_name_or_path hf-internal-testing/tiny-stable-diffusion-pipe + --dataset_name hf-internal-testing/dummy_image_text_data + --resolution 64 + --center_crop + --random_flip + --train_batch_size 1 + --gradient_accumulation_steps 1 + --max_train_steps 2 + --learning_rate 5.0e-04 + --scale_lr + --lr_scheduler constant + --lr_warmup_steps 0 + --output_dir {tmpdir} + """.split() + + run_command(self._launch_args + test_args) + # save_pretrained smoke test + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "unet", "diffusion_pytorch_model.bin"))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, "scheduler", "scheduler_config.json"))) diff --git a/diffusers/examples/text_to_image/README.md b/diffusers/examples/text_to_image/README.md new file mode 100644 index 0000000000000000000000000000000000000000..92de067f7954929217b0e5c522b44a7d4dbbd29c --- /dev/null +++ b/diffusers/examples/text_to_image/README.md @@ -0,0 +1,246 @@ +# Stable Diffusion text-to-image fine-tuning + +The `train_text_to_image.py` script shows how to fine-tune stable diffusion model on your own dataset. + +___Note___: + +___This script is experimental. The script fine-tunes the whole model and often times the model overfits and runs into issues like catastrophic forgetting. It's recommended to try different hyperparamters to get the best result on your dataset.___ + + +## Running locally with PyTorch +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +### Pokemon example + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-4`, so you'll need to visit [its card](https://huggingface.co/CompVis/stable-diffusion-v1-4), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. + +
+ +#### Hardware +With `gradient_checkpointing` and `mixed_precision` it should be possible to fine tune the model on a single 24GB GPU. For higher `batch_size` and faster training it's better to use GPUs with >30GB memory. + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export dataset_name="lambdalabs/pokemon-blip-captions" + +accelerate launch --mixed_precision="fp16" train_text_to_image.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$dataset_name \ + --use_ema \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --gradient_checkpointing \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --lr_scheduler="constant" --lr_warmup_steps=0 \ + --output_dir="sd-pokemon-model" +``` + + +To run on your own training files prepare the dataset according to the format required by `datasets`, you can find the instructions for how to do that in this [document](https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder-with-metadata). +If you wish to use custom loading logic, you should modify the script, we have left pointers for that in the training script. + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export TRAIN_DIR="path_to_your_dataset" + +accelerate launch --mixed_precision="fp16" train_text_to_image.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$TRAIN_DIR \ + --use_ema \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --gradient_checkpointing \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --lr_scheduler="constant" --lr_warmup_steps=0 \ + --output_dir="sd-pokemon-model" +``` + + +Once the training is finished the model will be saved in the `output_dir` specified in the command. In this example it's `sd-pokemon-model`. To load the fine-tuned model for inference just pass that path to `StableDiffusionPipeline` + + +```python +from diffusers import StableDiffusionPipeline + +model_path = "path_to_saved_model" +pipe = StableDiffusionPipeline.from_pretrained(model_path, torch_dtype=torch.float16) +pipe.to("cuda") + +image = pipe(prompt="yoda").images[0] +image.save("yoda-pokemon.png") +``` + +## Training with LoRA + +Low-Rank Adaption of Large Language Models was first introduced by Microsoft in [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685) by *Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu, Yuanzhi Li, Shean Wang, Lu Wang, Weizhu Chen*. + +In a nutshell, LoRA allows adapting pretrained models by adding pairs of rank-decomposition matrices to existing weights and **only** training those newly added weights. This has a couple of advantages: + +- Previous pretrained weights are kept frozen so that model is not prone to [catastrophic forgetting](https://www.pnas.org/doi/10.1073/pnas.1611835114). +- Rank-decomposition matrices have significantly fewer parameters than original model, which means that trained LoRA weights are easily portable. +- LoRA attention layers allow to control to which extent the model is adapted toward new training images via a `scale` parameter. + +[cloneofsimo](https://github.com/cloneofsimo) was the first to try out LoRA training for Stable Diffusion in the popular [lora](https://github.com/cloneofsimo/lora) GitHub repository. + +With LoRA, it's possible to fine-tune Stable Diffusion on a custom image-caption pair dataset +on consumer GPUs like Tesla T4, Tesla V100. + +### Training + +First, you need to set up your development environment as is explained in the [installation section](#installing-the-dependencies). Make sure to set the `MODEL_NAME` and `DATASET_NAME` environment variables. Here, we will use [Stable Diffusion v1-4](https://hf.co/CompVis/stable-diffusion-v1-4) and the [Pokemons dataset](https://hf.colambdalabs/pokemon-blip-captions). + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +**___Note: It is quite useful to monitor the training progress by regularly generating sample images during training. [Weights and Biases](https://docs.wandb.ai/quickstart) is a nice solution to easily see generating images during training. All you need to do is to run `pip install wandb` before training to automatically log images.___** + +```bash +export MODEL_NAME="CompVis/stable-diffusion-v1-4" +export DATASET_NAME="lambdalabs/pokemon-blip-captions" +``` + +For this example we want to directly store the trained LoRA embeddings on the Hub, so +we need to be logged in and add the `--push_to_hub` flag. + +```bash +huggingface-cli login +``` + +Now we can start training! + +```bash +accelerate launch --mixed_precision="fp16" train_text_to_image_lora.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$DATASET_NAME --caption_column="text" \ + --resolution=512 --random_flip \ + --train_batch_size=1 \ + --num_train_epochs=100 --checkpointing_steps=5000 \ + --learning_rate=1e-04 --lr_scheduler="constant" --lr_warmup_steps=0 \ + --seed=42 \ + --output_dir="sd-pokemon-model-lora" \ + --validation_prompt="cute dragon creature" --report_to="wandb" +``` + +The above command will also run inference as fine-tuning progresses and log the results to Weights and Biases. + +**___Note: When using LoRA we can use a much higher learning rate compared to non-LoRA fine-tuning. Here we use *1e-4* instead of the usual *1e-5*. Also, by using LoRA, it's possible to run `train_text_to_image_lora.py` in consumer GPUs like T4 or V100.___** + +The final LoRA embedding weights have been uploaded to [sayakpaul/sd-model-finetuned-lora-t4](https://huggingface.co/sayakpaul/sd-model-finetuned-lora-t4). **___Note: [The final weights](https://huggingface.co/sayakpaul/sd-model-finetuned-lora-t4/blob/main/pytorch_lora_weights.bin) are only 3 MB in size, which is orders of magnitudes smaller than the original model.___** + +You can check some inference samples that were logged during the course of the fine-tuning process [here](https://wandb.ai/sayakpaul/text2image-fine-tune/runs/q4lc0xsw). + +### Inference + +Once you have trained a model using above command, the inference can be done simply using the `StableDiffusionPipeline` after loading the trained LoRA weights. You +need to pass the `output_dir` for loading the LoRA weights which, in this case, is `sd-pokemon-model-lora`. + +```python +from diffusers import StableDiffusionPipeline +import torch + +model_path = "sayakpaul/sd-model-finetuned-lora-t4" +pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) +pipe.unet.load_attn_procs(model_path) +pipe.to("cuda") + +prompt = "A pokemon with green eyes and red legs." +image = pipe(prompt, num_inference_steps=30, guidance_scale=7.5).images[0] +image.save("pokemon.png") +``` + +## Training with Flax/JAX + +For faster training on TPUs and GPUs you can leverage the flax training example. Follow the instructions above to get the model and dataset before running the script. + +**___Note: The flax example doesn't yet support features like gradient checkpoint, gradient accumulation etc, so to use flax for faster training we will need >30GB cards or TPU v3.___** + + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install -U -r requirements_flax.txt +``` + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export dataset_name="lambdalabs/pokemon-blip-captions" + +python train_text_to_image_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --dataset_name=$dataset_name \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --mixed_precision="fp16" \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --output_dir="sd-pokemon-model" +``` + +To run on your own training files prepare the dataset according to the format required by `datasets`, you can find the instructions for how to do that in this [document](https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder-with-metadata). +If you wish to use custom loading logic, you should modify the script, we have left pointers for that in the training script. + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export TRAIN_DIR="path_to_your_dataset" + +python train_text_to_image_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$TRAIN_DIR \ + --resolution=512 --center_crop --random_flip \ + --train_batch_size=1 \ + --mixed_precision="fp16" \ + --max_train_steps=15000 \ + --learning_rate=1e-05 \ + --max_grad_norm=1 \ + --output_dir="sd-pokemon-model" +``` + +### Training with xFormers: + +You can enable memory efficient attention by [installing xFormers](https://huggingface.co/docs/diffusers/main/en/optimization/xformers) and passing the `--enable_xformers_memory_efficient_attention` argument to the script. + +xFormers training is not available for Flax/JAX. + +**Note**: + +According to [this issue](https://github.com/huggingface/diffusers/issues/2234#issuecomment-1416931212), xFormers `v0.0.16` cannot be used for training in some GPUs. If you observe that problem, please install a development version as indicated in that comment. diff --git a/diffusers/examples/text_to_image/requirements.txt b/diffusers/examples/text_to_image/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a71be6715c15bb3fe81ad940c68e106797ba0759 --- /dev/null +++ b/diffusers/examples/text_to_image/requirements.txt @@ -0,0 +1,7 @@ +accelerate +torchvision +transformers>=4.25.1 +datasets +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/text_to_image/requirements_flax.txt b/diffusers/examples/text_to_image/requirements_flax.txt new file mode 100644 index 0000000000000000000000000000000000000000..b6eb64e254625ee8eff2ef126d67adfd5b6994dc --- /dev/null +++ b/diffusers/examples/text_to_image/requirements_flax.txt @@ -0,0 +1,9 @@ +transformers>=4.25.1 +datasets +flax +optax +torch +torchvision +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/text_to_image/train_text_to_image.py b/diffusers/examples/text_to_image/train_text_to_image.py new file mode 100644 index 0000000000000000000000000000000000000000..39089a85680f26bd6e8e441e244b0f23468fbf3c --- /dev/null +++ b/diffusers/examples/text_to_image/train_text_to_image.py @@ -0,0 +1,767 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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 + +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import accelerate +import datasets +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from datasets import load_dataset +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from packaging import version +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import AutoencoderKL, DDPMScheduler, StableDiffusionPipeline, UNet2DConditionModel +from diffusers.optimization import get_scheduler +from diffusers.training_utils import EMAModel +from diffusers.utils import check_min_version, deprecate +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__, log_level="INFO") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that 🤗 Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--image_column", type=str, default="image", help="The column of the dataset containing an image." + ) + parser.add_argument( + "--caption_column", + type=str, + default="text", + help="The column of the dataset containing a caption or a list of captions.", + ) + parser.add_argument( + "--max_train_samples", + type=int, + default=None, + help=( + "For debugging purposes or quicker training, truncate the number of training examples to this " + "value if set." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="sd-model-finetuned", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument("--use_ema", action="store_true", help="Whether to use EMA model.") + parser.add_argument( + "--non_ema_revision", + type=str, + default=None, + required=False, + help=( + "Revision of pretrained non-ema model identifier. Must be a branch, tag or git identifier of the local or" + " remote repository specified with --pretrained_model_name_or_path." + ), + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + # Sanity checks + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("Need either a dataset name or a training folder.") + + # default to using the same revision for the non-ema model if not specified + if args.non_ema_revision is None: + args.non_ema_revision = args.revision + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +dataset_name_mapping = { + "lambdalabs/pokemon-blip-captions": ("image", "text"), +} + + +def main(): + args = parse_args() + + if args.non_ema_revision is not None: + deprecate( + "non_ema_revision!=None", + "0.15.0", + message=( + "Downloading 'non_ema' weights from revision branches of the Hub is deprecated. Please make sure to" + " use `--variant=non_ema` instead." + ), + ) + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load scheduler, tokenizer and models. + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision + ) + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.non_ema_revision + ) + + # Freeze vae and text_encoder + vae.requires_grad_(False) + text_encoder.requires_grad_(False) + + # Create EMA for the unet. + if args.use_ema: + ema_unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + ema_unet = EMAModel(ema_unet.parameters()) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # `accelerate` 0.16.0 will have better support for customized saving + if version.parse(accelerate.__version__) >= version.parse("0.16.0"): + # create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format + def save_model_hook(models, weights, output_dir): + if args.use_ema: + ema_unet.save_pretrained(os.path.join(output_dir, "unet_ema")) + + for i, model in enumerate(models): + model.save_pretrained(os.path.join(output_dir, "unet")) + + # make sure to pop weight so that corresponding model is not saved again + weights.pop() + + def load_model_hook(models, input_dir): + if args.use_ema: + load_model = EMAModel.from_pretrained(os.path.join(input_dir, "unet_ema"), UNet2DConditionModel) + ema_unet.load_state_dict(load_model.state_dict()) + del load_model + + for i in range(len(models)): + # pop models so that they are not loaded again + model = models.pop() + + # load diffusers style into model + load_model = UNet2DConditionModel.from_pretrained(input_dir, subfolder="unet") + model.register_to_config(**load_model.config) + + model.load_state_dict(load_model.state_dict()) + del load_model + + accelerator.register_save_state_pre_hook(save_model_hook) + accelerator.register_load_state_pre_hook(load_model_hook) + + if args.gradient_checkpointing: + unet.enable_gradient_checkpointing() + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "Please install bitsandbytes to use 8-bit Adam. You can do so by running `pip install bitsandbytes`" + ) + + optimizer_cls = bnb.optim.AdamW8bit + else: + optimizer_cls = torch.optim.AdamW + + optimizer = optimizer_cls( + unet.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + # Downloading and loading a dataset from the hub. + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + ) + else: + data_files = {} + if args.train_data_dir is not None: + data_files["train"] = os.path.join(args.train_data_dir, "**") + dataset = load_dataset( + "imagefolder", + data_files=data_files, + cache_dir=args.cache_dir, + ) + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets. + # We need to tokenize inputs and targets. + column_names = dataset["train"].column_names + + # 6. Get the column names for input/target. + dataset_columns = dataset_name_mapping.get(args.dataset_name, None) + if args.image_column is None: + image_column = dataset_columns[0] if dataset_columns is not None else column_names[0] + else: + image_column = args.image_column + if image_column not in column_names: + raise ValueError( + f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}" + ) + if args.caption_column is None: + caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1] + else: + caption_column = args.caption_column + if caption_column not in column_names: + raise ValueError( + f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}" + ) + + # Preprocessing the datasets. + # We need to tokenize input captions and transform the images. + def tokenize_captions(examples, is_train=True): + captions = [] + for caption in examples[caption_column]: + if isinstance(caption, str): + captions.append(caption) + elif isinstance(caption, (list, np.ndarray)): + # take a random caption if there are multiple + captions.append(random.choice(caption) if is_train else caption[0]) + else: + raise ValueError( + f"Caption column `{caption_column}` should contain either strings or lists of strings." + ) + inputs = tokenizer( + captions, max_length=tokenizer.model_max_length, padding="max_length", truncation=True, return_tensors="pt" + ) + return inputs.input_ids + + # Preprocessing the datasets. + train_transforms = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def preprocess_train(examples): + images = [image.convert("RGB") for image in examples[image_column]] + examples["pixel_values"] = [train_transforms(image) for image in images] + examples["input_ids"] = tokenize_captions(examples) + return examples + + with accelerator.main_process_first(): + if args.max_train_samples is not None: + dataset["train"] = dataset["train"].shuffle(seed=args.seed).select(range(args.max_train_samples)) + # Set the training transforms + train_dataset = dataset["train"].with_transform(preprocess_train) + + def collate_fn(examples): + pixel_values = torch.stack([example["pixel_values"] for example in examples]) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + input_ids = torch.stack([example["input_ids"] for example in examples]) + return {"pixel_values": pixel_values, "input_ids": input_ids} + + # DataLoaders creation: + train_dataloader = torch.utils.data.DataLoader( + train_dataset, + shuffle=True, + collate_fn=collate_fn, + batch_size=args.train_batch_size, + num_workers=args.dataloader_num_workers, + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + unet, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + unet, optimizer, train_dataloader, lr_scheduler + ) + + if args.use_ema: + ema_unet.to(accelerator.device) + + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move text_encode and vae to gpu and cast to weight_dtype + text_encoder.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("text2image-fine-tune", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + train_loss = 0.0 + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + # Predict the noise residual and compute loss + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Gather the losses across all processes for logging (if we use distributed training). + avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean() + train_loss += avg_loss.item() / args.gradient_accumulation_steps + + # Backpropagate + accelerator.backward(loss) + if accelerator.sync_gradients: + accelerator.clip_grad_norm_(unet.parameters(), args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + if args.use_ema: + ema_unet.step(unet.parameters()) + progress_bar.update(1) + global_step += 1 + accelerator.log({"train_loss": train_loss}, step=global_step) + train_loss = 0.0 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"step_loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + + if global_step >= args.max_train_steps: + break + + # Create the pipeline using the trained modules and save it. + accelerator.wait_for_everyone() + if accelerator.is_main_process: + unet = accelerator.unwrap_model(unet) + if args.use_ema: + ema_unet.copy_to(unet.parameters()) + + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=text_encoder, + vae=vae, + unet=unet, + revision=args.revision, + ) + pipeline.save_pretrained(args.output_dir) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/text_to_image/train_text_to_image_flax.py b/diffusers/examples/text_to_image/train_text_to_image_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..d88594435e658bb1ec775026c6a7fce6400e6b7a --- /dev/null +++ b/diffusers/examples/text_to_image/train_text_to_image_flax.py @@ -0,0 +1,579 @@ +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import jax +import jax.numpy as jnp +import numpy as np +import optax +import torch +import torch.utils.checkpoint +import transformers +from datasets import load_dataset +from flax import jax_utils +from flax.training import train_state +from flax.training.common_utils import shard +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel, set_seed + +from diffusers import ( + FlaxAutoencoderKL, + FlaxDDPMScheduler, + FlaxPNDMScheduler, + FlaxStableDiffusionPipeline, + FlaxUNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion import FlaxStableDiffusionSafetyChecker +from diffusers.utils import check_min_version + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that 🤗 Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--image_column", type=str, default="image", help="The column of the dataset containing an image." + ) + parser.add_argument( + "--caption_column", + type=str, + default="text", + help="The column of the dataset containing a caption or a list of captions.", + ) + parser.add_argument( + "--max_train_samples", + type=int, + default=None, + help=( + "For debugging purposes or quicker training, truncate the number of training examples to this " + "value if set." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="sd-model-finetuned", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument("--seed", type=int, default=0, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + # Sanity checks + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("Need either a dataset name or a training folder.") + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +dataset_name_mapping = { + "lambdalabs/pokemon-blip-captions": ("image", "text"), +} + + +def get_params_to_save(params): + return jax.device_get(jax.tree_util.tree_map(lambda x: x[0], params)) + + +def main(): + args = parse_args() + + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + # Setup logging, we only want one process per machine to log things on the screen. + logger.setLevel(logging.INFO if jax.process_index() == 0 else logging.ERROR) + if jax.process_index() == 0: + transformers.utils.logging.set_verbosity_info() + else: + transformers.utils.logging.set_verbosity_error() + + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if jax.process_index() == 0: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + # Downloading and loading a dataset from the hub. + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + ) + else: + data_files = {} + if args.train_data_dir is not None: + data_files["train"] = os.path.join(args.train_data_dir, "**") + dataset = load_dataset( + "imagefolder", + data_files=data_files, + cache_dir=args.cache_dir, + ) + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets. + # We need to tokenize inputs and targets. + column_names = dataset["train"].column_names + + # 6. Get the column names for input/target. + dataset_columns = dataset_name_mapping.get(args.dataset_name, None) + if args.image_column is None: + image_column = dataset_columns[0] if dataset_columns is not None else column_names[0] + else: + image_column = args.image_column + if image_column not in column_names: + raise ValueError( + f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}" + ) + if args.caption_column is None: + caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1] + else: + caption_column = args.caption_column + if caption_column not in column_names: + raise ValueError( + f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}" + ) + + # Preprocessing the datasets. + # We need to tokenize input captions and transform the images. + def tokenize_captions(examples, is_train=True): + captions = [] + for caption in examples[caption_column]: + if isinstance(caption, str): + captions.append(caption) + elif isinstance(caption, (list, np.ndarray)): + # take a random caption if there are multiple + captions.append(random.choice(caption) if is_train else caption[0]) + else: + raise ValueError( + f"Caption column `{caption_column}` should contain either strings or lists of strings." + ) + inputs = tokenizer(captions, max_length=tokenizer.model_max_length, padding="do_not_pad", truncation=True) + input_ids = inputs.input_ids + return input_ids + + train_transforms = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def preprocess_train(examples): + images = [image.convert("RGB") for image in examples[image_column]] + examples["pixel_values"] = [train_transforms(image) for image in images] + examples["input_ids"] = tokenize_captions(examples) + + return examples + + if jax.process_index() == 0: + if args.max_train_samples is not None: + dataset["train"] = dataset["train"].shuffle(seed=args.seed).select(range(args.max_train_samples)) + # Set the training transforms + train_dataset = dataset["train"].with_transform(preprocess_train) + + def collate_fn(examples): + pixel_values = torch.stack([example["pixel_values"] for example in examples]) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + input_ids = [example["input_ids"] for example in examples] + + padded_tokens = tokenizer.pad( + {"input_ids": input_ids}, padding="max_length", max_length=tokenizer.model_max_length, return_tensors="pt" + ) + batch = { + "pixel_values": pixel_values, + "input_ids": padded_tokens.input_ids, + } + batch = {k: v.numpy() for k, v in batch.items()} + + return batch + + total_train_batch_size = args.train_batch_size * jax.local_device_count() + train_dataloader = torch.utils.data.DataLoader( + train_dataset, shuffle=True, collate_fn=collate_fn, batch_size=total_train_batch_size, drop_last=True + ) + + weight_dtype = jnp.float32 + if args.mixed_precision == "fp16": + weight_dtype = jnp.float16 + elif args.mixed_precision == "bf16": + weight_dtype = jnp.bfloat16 + + # Load models and create wrapper for stable diffusion + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + text_encoder = FlaxCLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", dtype=weight_dtype + ) + vae, vae_params = FlaxAutoencoderKL.from_pretrained( + args.pretrained_model_name_or_path, subfolder="vae", dtype=weight_dtype + ) + unet, unet_params = FlaxUNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", dtype=weight_dtype + ) + + # Optimization + if args.scale_lr: + args.learning_rate = args.learning_rate * total_train_batch_size + + constant_scheduler = optax.constant_schedule(args.learning_rate) + + adamw = optax.adamw( + learning_rate=constant_scheduler, + b1=args.adam_beta1, + b2=args.adam_beta2, + eps=args.adam_epsilon, + weight_decay=args.adam_weight_decay, + ) + + optimizer = optax.chain( + optax.clip_by_global_norm(args.max_grad_norm), + adamw, + ) + + state = train_state.TrainState.create(apply_fn=unet.__call__, params=unet_params, tx=optimizer) + + noise_scheduler = FlaxDDPMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000 + ) + noise_scheduler_state = noise_scheduler.create_state() + + # Initialize our training + rng = jax.random.PRNGKey(args.seed) + train_rngs = jax.random.split(rng, jax.local_device_count()) + + def train_step(state, text_encoder_params, vae_params, batch, train_rng): + dropout_rng, sample_rng, new_train_rng = jax.random.split(train_rng, 3) + + def compute_loss(params): + # Convert images to latent space + vae_outputs = vae.apply( + {"params": vae_params}, batch["pixel_values"], deterministic=True, method=vae.encode + ) + latents = vae_outputs.latent_dist.sample(sample_rng) + # (NHWC) -> (NCHW) + latents = jnp.transpose(latents, (0, 3, 1, 2)) + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise_rng, timestep_rng = jax.random.split(sample_rng) + noise = jax.random.normal(noise_rng, latents.shape) + # Sample a random timestep for each image + bsz = latents.shape[0] + timesteps = jax.random.randint( + timestep_rng, + (bsz,), + 0, + noise_scheduler.config.num_train_timesteps, + ) + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(noise_scheduler_state, latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder( + batch["input_ids"], + params=text_encoder_params, + train=False, + )[0] + + # Predict the noise residual and compute loss + model_pred = unet.apply( + {"params": params}, noisy_latents, timesteps, encoder_hidden_states, train=True + ).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(noise_scheduler_state, latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + loss = (target - model_pred) ** 2 + loss = loss.mean() + + return loss + + grad_fn = jax.value_and_grad(compute_loss) + loss, grad = grad_fn(state.params) + grad = jax.lax.pmean(grad, "batch") + + new_state = state.apply_gradients(grads=grad) + + metrics = {"loss": loss} + metrics = jax.lax.pmean(metrics, axis_name="batch") + + return new_state, metrics, new_train_rng + + # Create parallel version of the train step + p_train_step = jax.pmap(train_step, "batch", donate_argnums=(0,)) + + # Replicate the train state on each device + state = jax_utils.replicate(state) + text_encoder_params = jax_utils.replicate(text_encoder.params) + vae_params = jax_utils.replicate(vae_params) + + # Train! + num_update_steps_per_epoch = math.ceil(len(train_dataloader)) + + # Scheduler and math around the number of training steps. + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel & distributed) = {total_train_batch_size}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + + global_step = 0 + + epochs = tqdm(range(args.num_train_epochs), desc="Epoch ... ", position=0) + for epoch in epochs: + # ======================== Training ================================ + + train_metrics = [] + + steps_per_epoch = len(train_dataset) // total_train_batch_size + train_step_progress_bar = tqdm(total=steps_per_epoch, desc="Training...", position=1, leave=False) + # train + for batch in train_dataloader: + batch = shard(batch) + state, train_metric, train_rngs = p_train_step(state, text_encoder_params, vae_params, batch, train_rngs) + train_metrics.append(train_metric) + + train_step_progress_bar.update(1) + + global_step += 1 + if global_step >= args.max_train_steps: + break + + train_metric = jax_utils.unreplicate(train_metric) + + train_step_progress_bar.close() + epochs.write(f"Epoch... ({epoch + 1}/{args.num_train_epochs} | Loss: {train_metric['loss']})") + + # Create the pipeline using using the trained modules and save it. + if jax.process_index() == 0: + scheduler = FlaxPNDMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", skip_prk_steps=True + ) + safety_checker = FlaxStableDiffusionSafetyChecker.from_pretrained( + "CompVis/stable-diffusion-safety-checker", from_pt=True + ) + pipeline = FlaxStableDiffusionPipeline( + text_encoder=text_encoder, + vae=vae, + unet=unet, + tokenizer=tokenizer, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"), + ) + + pipeline.save_pretrained( + args.output_dir, + params={ + "text_encoder": get_params_to_save(text_encoder_params), + "vae": get_params_to_save(vae_params), + "unet": get_params_to_save(state.params), + "safety_checker": safety_checker.params, + }, + ) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/text_to_image/train_text_to_image_lora.py b/diffusers/examples/text_to_image/train_text_to_image_lora.py new file mode 100644 index 0000000000000000000000000000000000000000..abc535594d8c26d8eded86a2405a1850c087f402 --- /dev/null +++ b/diffusers/examples/text_to_image/train_text_to_image_lora.py @@ -0,0 +1,850 @@ +# coding=utf-8 +# Copyright 2023 The HuggingFace Inc. team. All rights reserved. +# +# 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. +"""Fine-tuning script for Stable Diffusion for text2image with support for LoRA.""" + +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import datasets +import numpy as np +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from datasets import load_dataset +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import AutoencoderKL, DDPMScheduler, DiffusionPipeline, UNet2DConditionModel +from diffusers.loaders import AttnProcsLayers +from diffusers.models.cross_attention import LoRACrossAttnProcessor +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version, is_wandb_available +from diffusers.utils.import_utils import is_xformers_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__, log_level="INFO") + + +def save_model_card(repo_name, images=None, base_model=str, dataset_name=str, repo_folder=None): + img_str = "" + for i, image in enumerate(images): + image.save(os.path.join(repo_folder, f"image_{i}.png")) + img_str += f"![img_{i}](./image_{i}.png)\n" + + yaml = f""" +--- +license: creativeml-openrail-m +base_model: {base_model} +tags: +- stable-diffusion +- stable-diffusion-diffusers +- text-to-image +- diffusers +- lora +inference: true +--- + """ + model_card = f""" +# LoRA text2image fine-tuning - {repo_name} +These are LoRA adaption weights for {base_model}. The weights were fine-tuned on the {dataset_name} dataset. You can find some example images in the following. \n +{img_str} +""" + with open(os.path.join(repo_folder, "README.md"), "w") as f: + f.write(yaml + model_card) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that 🤗 Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--image_column", type=str, default="image", help="The column of the dataset containing an image." + ) + parser.add_argument( + "--caption_column", + type=str, + default="text", + help="The column of the dataset containing a caption or a list of captions.", + ) + parser.add_argument( + "--validation_prompt", type=str, default=None, help="A prompt that is sampled during training for inference." + ) + parser.add_argument( + "--num_validation_images", + type=int, + default=4, + help="Number of images that should be generated during validation with `validation_prompt`.", + ) + parser.add_argument( + "--validation_epochs", + type=int, + default=1, + help=( + "Run fine-tuning validation every X epochs. The validation process consists of running the prompt" + " `args.validation_prompt` multiple times: `args.num_validation_images`." + ), + ) + parser.add_argument( + "--max_train_samples", + type=int, + default=None, + help=( + "For debugging purposes or quicker training, truncate the number of training examples to this " + "value if set." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="sd-model-finetuned-lora", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=None, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--use_8bit_adam", action="store_true", help="Whether or not to use 8-bit Adam from bitsandbytes." + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default=None, + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >=" + " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the" + " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config." + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + # Sanity checks + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("Need either a dataset name or a training folder.") + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +DATASET_NAME_MAPPING = { + "lambdalabs/pokemon-blip-captions": ("image", "text"), +} + + +def main(): + args = parse_args() + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + if args.report_to == "wandb": + if not is_wandb_available(): + raise ImportError("Make sure to install wandb if you want to use it for logging during training.") + import wandb + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + repo_name = create_repo(repo_name, exist_ok=True) + repo = Repository(args.output_dir, clone_from=repo_name) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load scheduler, tokenizer and models. + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + tokenizer = CLIPTokenizer.from_pretrained( + args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision + ) + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + # freeze parameters of models to save more memory + unet.requires_grad_(False) + vae.requires_grad_(False) + + text_encoder.requires_grad_(False) + + # For mixed precision training we cast the text_encoder and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move unet, vae and text_encoder to device and cast to weight_dtype + unet.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + text_encoder.to(accelerator.device, dtype=weight_dtype) + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # now we will add new LoRA weights to the attention layers + # It's important to realize here how many attention weights will be added and of which sizes + # The sizes of the attention layers consist only of two different variables: + # 1) - the "hidden_size", which is increased according to `unet.config.block_out_channels`. + # 2) - the "cross attention size", which is set to `unet.config.cross_attention_dim`. + + # Let's first see how many attention processors we will have to set. + # For Stable Diffusion, it should be equal to: + # - down blocks (2x attention layers) * (2x transformer layers) * (3x down blocks) = 12 + # - mid blocks (2x attention layers) * (1x transformer layers) * (1x mid blocks) = 2 + # - up blocks (2x attention layers) * (3x transformer layers) * (3x down blocks) = 18 + # => 32 layers + + # Set correct lora layers + lora_attn_procs = {} + for name in unet.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = unet.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(unet.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = unet.config.block_out_channels[block_id] + + lora_attn_procs[name] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim + ) + + unet.set_attn_processor(lora_attn_procs) + lora_layers = AttnProcsLayers(unet.attn_processors) + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + if args.use_8bit_adam: + try: + import bitsandbytes as bnb + except ImportError: + raise ImportError( + "Please install bitsandbytes to use 8-bit Adam. You can do so by running `pip install bitsandbytes`" + ) + + optimizer_cls = bnb.optim.AdamW8bit + else: + optimizer_cls = torch.optim.AdamW + + optimizer = optimizer_cls( + lora_layers.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + # Downloading and loading a dataset from the hub. + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + ) + else: + data_files = {} + if args.train_data_dir is not None: + data_files["train"] = os.path.join(args.train_data_dir, "**") + dataset = load_dataset( + "imagefolder", + data_files=data_files, + cache_dir=args.cache_dir, + ) + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets. + # We need to tokenize inputs and targets. + column_names = dataset["train"].column_names + + # 6. Get the column names for input/target. + dataset_columns = DATASET_NAME_MAPPING.get(args.dataset_name, None) + if args.image_column is None: + image_column = dataset_columns[0] if dataset_columns is not None else column_names[0] + else: + image_column = args.image_column + if image_column not in column_names: + raise ValueError( + f"--image_column' value '{args.image_column}' needs to be one of: {', '.join(column_names)}" + ) + if args.caption_column is None: + caption_column = dataset_columns[1] if dataset_columns is not None else column_names[1] + else: + caption_column = args.caption_column + if caption_column not in column_names: + raise ValueError( + f"--caption_column' value '{args.caption_column}' needs to be one of: {', '.join(column_names)}" + ) + + # Preprocessing the datasets. + # We need to tokenize input captions and transform the images. + def tokenize_captions(examples, is_train=True): + captions = [] + for caption in examples[caption_column]: + if isinstance(caption, str): + captions.append(caption) + elif isinstance(caption, (list, np.ndarray)): + # take a random caption if there are multiple + captions.append(random.choice(caption) if is_train else caption[0]) + else: + raise ValueError( + f"Caption column `{caption_column}` should contain either strings or lists of strings." + ) + inputs = tokenizer( + captions, max_length=tokenizer.model_max_length, padding="max_length", truncation=True, return_tensors="pt" + ) + return inputs.input_ids + + # Preprocessing the datasets. + train_transforms = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def preprocess_train(examples): + images = [image.convert("RGB") for image in examples[image_column]] + examples["pixel_values"] = [train_transforms(image) for image in images] + examples["input_ids"] = tokenize_captions(examples) + return examples + + with accelerator.main_process_first(): + if args.max_train_samples is not None: + dataset["train"] = dataset["train"].shuffle(seed=args.seed).select(range(args.max_train_samples)) + # Set the training transforms + train_dataset = dataset["train"].with_transform(preprocess_train) + + def collate_fn(examples): + pixel_values = torch.stack([example["pixel_values"] for example in examples]) + pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float() + input_ids = torch.stack([example["input_ids"] for example in examples]) + return {"pixel_values": pixel_values, "input_ids": input_ids} + + # DataLoaders creation: + train_dataloader = torch.utils.data.DataLoader( + train_dataset, + shuffle=True, + collate_fn=collate_fn, + batch_size=args.train_batch_size, + num_workers=args.dataloader_num_workers, + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + lora_layers, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + lora_layers, optimizer, train_dataloader, lr_scheduler + ) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("text2image-fine-tune", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + for epoch in range(first_epoch, args.num_train_epochs): + unet.train() + train_loss = 0.0 + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(unet): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0] + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + # Predict the noise residual and compute loss + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + # Gather the losses across all processes for logging (if we use distributed training). + avg_loss = accelerator.gather(loss.repeat(args.train_batch_size)).mean() + train_loss += avg_loss.item() / args.gradient_accumulation_steps + + # Backpropagate + accelerator.backward(loss) + if accelerator.sync_gradients: + params_to_clip = lora_layers.parameters() + accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + accelerator.log({"train_loss": train_loss}, step=global_step) + train_loss = 0.0 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"step_loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + + if global_step >= args.max_train_steps: + break + + if accelerator.is_main_process: + if args.validation_prompt is not None and epoch % args.validation_epochs == 0: + logger.info( + f"Running validation... \n Generating {args.num_validation_images} images with prompt:" + f" {args.validation_prompt}." + ) + # create pipeline + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + unet=accelerator.unwrap_model(unet), + revision=args.revision, + torch_dtype=weight_dtype, + ) + pipeline = pipeline.to(accelerator.device) + pipeline.set_progress_bar_config(disable=True) + + # run inference + generator = torch.Generator(device=accelerator.device).manual_seed(args.seed) + images = [] + for _ in range(args.num_validation_images): + images.append( + pipeline(args.validation_prompt, num_inference_steps=30, generator=generator).images[0] + ) + + if accelerator.is_main_process: + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("validation", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "validation": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + del pipeline + torch.cuda.empty_cache() + + # Save the lora layers + accelerator.wait_for_everyone() + if accelerator.is_main_process: + unet = unet.to(torch.float32) + unet.save_attn_procs(args.output_dir) + + if args.push_to_hub: + save_model_card( + repo_name, + images=images, + base_model=args.pretrained_model_name_or_path, + dataset_name=args.dataset_name, + repo_folder=args.output_dir, + ) + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + # Final inference + # Load previous pipeline + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, revision=args.revision, torch_dtype=weight_dtype + ) + pipeline = pipeline.to(accelerator.device) + + # load attention processors + pipeline.unet.load_attn_procs(args.output_dir) + + # run inference + generator = torch.Generator(device=accelerator.device).manual_seed(args.seed) + images = [] + for _ in range(args.num_validation_images): + images.append(pipeline(args.validation_prompt, num_inference_steps=30, generator=generator).images[0]) + + if accelerator.is_main_process: + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("test", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "test": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/textual_inversion/README.md b/diffusers/examples/textual_inversion/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3a7c96be69fbe198479a866523dc9c867a15339f --- /dev/null +++ b/diffusers/examples/textual_inversion/README.md @@ -0,0 +1,129 @@ +## Textual Inversion fine-tuning example + +[Textual inversion](https://arxiv.org/abs/2208.01618) is a method to personalize text2image models like stable diffusion on your own images using just 3-5 examples. +The `textual_inversion.py` script shows how to implement the training procedure and adapt it for stable diffusion. + +## Running on Colab + +Colab for training +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/sd_textual_inversion_training.ipynb) + +Colab for inference +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/stable_conceptualizer_inference.ipynb) + +## Running locally with PyTorch +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + + +### Cat toy example + +You need to accept the model license before downloading or using the weights. In this example we'll use model version `v1-5`, so you'll need to visit [its card](https://huggingface.co/runwayml/stable-diffusion-v1-5), read the license and tick the checkbox if you agree. + +You have to be a registered user in 🤗 Hugging Face Hub, and you'll also need to use an access token for the code to work. For more information on access tokens, please refer to [this section of the documentation](https://huggingface.co/docs/hub/security-tokens). + +Run the following command to authenticate your token + +```bash +huggingface-cli login +``` + +If you have already cloned the repo, then you won't need to go through these steps. + +
+ +Now let's get our dataset.Download 3-4 images from [here](https://drive.google.com/drive/folders/1fmJMs25nxS_rSNqS5hTcRdLem_YQXbq5) and save them in a directory. This will be our training data. + +And launch the training using + +**___Note: Change the `resolution` to 768 if you are using the [stable-diffusion-2](https://huggingface.co/stabilityai/stable-diffusion-2) 768x768 model.___** + +```bash +export MODEL_NAME="runwayml/stable-diffusion-v1-5" +export DATA_DIR="path-to-dir-containing-images" + +accelerate launch textual_inversion.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --resolution=512 \ + --train_batch_size=1 \ + --gradient_accumulation_steps=4 \ + --max_train_steps=3000 \ + --learning_rate=5.0e-04 --scale_lr \ + --lr_scheduler="constant" \ + --lr_warmup_steps=0 \ + --output_dir="textual_inversion_cat" +``` + +A full training run takes ~1 hour on one V100 GPU. + +### Inference + +Once you have trained a model using above command, the inference can be done simply using the `StableDiffusionPipeline`. Make sure to include the `placeholder_token` in your prompt. + +```python +from diffusers import StableDiffusionPipeline + +model_id = "path-to-your-trained-model" +pipe = StableDiffusionPipeline.from_pretrained(model_id,torch_dtype=torch.float16).to("cuda") + +prompt = "A backpack" + +image = pipe(prompt, num_inference_steps=50, guidance_scale=7.5).images[0] + +image.save("cat-backpack.png") +``` + + +## Training with Flax/JAX + +For faster training on TPUs and GPUs you can leverage the flax training example. Follow the instructions above to get the model and dataset before running the script. + +Before running the scripts, make sure to install the library's training dependencies: + +```bash +pip install -U -r requirements_flax.txt +``` + +```bash +export MODEL_NAME="duongna/stable-diffusion-v1-4-flax" +export DATA_DIR="path-to-dir-containing-images" + +python textual_inversion_flax.py \ + --pretrained_model_name_or_path=$MODEL_NAME \ + --train_data_dir=$DATA_DIR \ + --learnable_property="object" \ + --placeholder_token="" --initializer_token="toy" \ + --resolution=512 \ + --train_batch_size=1 \ + --max_train_steps=3000 \ + --learning_rate=5.0e-04 --scale_lr \ + --output_dir="textual_inversion_cat" +``` +It should be at least 70% faster than the PyTorch script with the same configuration. + +### Training with xformers: +You can enable memory efficient attention by [installing xFormers](https://github.com/facebookresearch/xformers#installing-xformers) and padding the `--enable_xformers_memory_efficient_attention` argument to the script. This is not available with the Flax/JAX implementation. diff --git a/diffusers/examples/textual_inversion/requirements.txt b/diffusers/examples/textual_inversion/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..7d93f3d03bd8eba09b8cab5e570d15380456b66a --- /dev/null +++ b/diffusers/examples/textual_inversion/requirements.txt @@ -0,0 +1,6 @@ +accelerate +torchvision +transformers>=4.25.1 +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/textual_inversion/requirements_flax.txt b/diffusers/examples/textual_inversion/requirements_flax.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f85ad523a3b46b65abf0138c05ecdd656e6845c --- /dev/null +++ b/diffusers/examples/textual_inversion/requirements_flax.txt @@ -0,0 +1,8 @@ +transformers>=4.25.1 +flax +optax +torch +torchvision +ftfy +tensorboard +Jinja2 diff --git a/diffusers/examples/textual_inversion/textual_inversion.py b/diffusers/examples/textual_inversion/textual_inversion.py new file mode 100644 index 0000000000000000000000000000000000000000..c61c2ae44c8a52f8b081351a3a9378563b56c868 --- /dev/null +++ b/diffusers/examples/textual_inversion/textual_inversion.py @@ -0,0 +1,848 @@ +#!/usr/bin/env python +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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 + +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import numpy as np +import PIL +import torch +import torch.nn.functional as F +import torch.utils.checkpoint +import transformers +from accelerate import Accelerator +from accelerate.logging import get_logger +from accelerate.utils import set_seed +from huggingface_hub import HfFolder, Repository, create_repo, whoami + +# TODO: remove and import from diffusers.utils when the new version of diffusers is released +from packaging import version +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPTextModel, CLIPTokenizer + +import diffusers +from diffusers import ( + AutoencoderKL, + DDPMScheduler, + DiffusionPipeline, + DPMSolverMultistepScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.optimization import get_scheduler +from diffusers.utils import check_min_version, is_wandb_available +from diffusers.utils.import_utils import is_xformers_available + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__) + + +def save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path): + logger.info("Saving embeddings") + learned_embeds = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[placeholder_token_id] + learned_embeds_dict = {args.placeholder_token: learned_embeds.detach().cpu()} + torch.save(learned_embeds_dict, save_path) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--save_steps", + type=int, + default=500, + help="Save learned_embeds.bin every X updates steps.", + ) + parser.add_argument( + "--only_save_embeds", + action="store_true", + default=False, + help="Save only the embeddings for the new concept.", + ) + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--revision", + type=str, + default=None, + required=False, + help="Revision of pretrained model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--train_data_dir", type=str, default=None, required=True, help="A folder containing the training data." + ) + parser.add_argument( + "--placeholder_token", + type=str, + default=None, + required=True, + help="A token to use as a placeholder for the concept.", + ) + parser.add_argument( + "--initializer_token", type=str, default=None, required=True, help="A token to use as initializer word." + ) + parser.add_argument("--learnable_property", type=str, default="object", help="Choose between 'object' and 'style'") + parser.add_argument("--repeats", type=int, default=100, help="How many times to repeat the training data.") + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution." + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=5000, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--gradient_checkpointing", + action="store_true", + help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=False, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process." + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument( + "--allow_tf32", + action="store_true", + help=( + "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see" + " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices" + ), + ) + parser.add_argument( + "--report_to", + type=str, + default="tensorboard", + help=( + 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`' + ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.' + ), + ) + parser.add_argument( + "--validation_prompt", + type=str, + default=None, + help="A prompt that is used during validation to verify that the model is learning.", + ) + parser.add_argument( + "--num_validation_images", + type=int, + default=4, + help="Number of images that should be generated during validation with `validation_prompt`.", + ) + parser.add_argument( + "--validation_epochs", + type=int, + default=50, + help=( + "Run validation every X epochs. Validation consists of running the prompt" + " `args.validation_prompt` multiple times: `args.num_validation_images`" + " and logging the images." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + parser.add_argument( + "--enable_xformers_memory_efficient_attention", action="store_true", help="Whether or not to use xformers." + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.train_data_dir is None: + raise ValueError("You must specify a train data directory.") + + return args + + +imagenet_templates_small = [ + "a photo of a {}", + "a rendering of a {}", + "a cropped photo of the {}", + "the photo of a {}", + "a photo of a clean {}", + "a photo of a dirty {}", + "a dark photo of the {}", + "a photo of my {}", + "a photo of the cool {}", + "a close-up photo of a {}", + "a bright photo of the {}", + "a cropped photo of a {}", + "a photo of the {}", + "a good photo of the {}", + "a photo of one {}", + "a close-up photo of the {}", + "a rendition of the {}", + "a photo of the clean {}", + "a rendition of a {}", + "a photo of a nice {}", + "a good photo of a {}", + "a photo of the nice {}", + "a photo of the small {}", + "a photo of the weird {}", + "a photo of the large {}", + "a photo of a cool {}", + "a photo of a small {}", +] + +imagenet_style_templates_small = [ + "a painting in the style of {}", + "a rendering in the style of {}", + "a cropped painting in the style of {}", + "the painting in the style of {}", + "a clean painting in the style of {}", + "a dirty painting in the style of {}", + "a dark painting in the style of {}", + "a picture in the style of {}", + "a cool painting in the style of {}", + "a close-up painting in the style of {}", + "a bright painting in the style of {}", + "a cropped painting in the style of {}", + "a good painting in the style of {}", + "a close-up painting in the style of {}", + "a rendition in the style of {}", + "a nice painting in the style of {}", + "a small painting in the style of {}", + "a weird painting in the style of {}", + "a large painting in the style of {}", +] + + +class TextualInversionDataset(Dataset): + def __init__( + self, + data_root, + tokenizer, + learnable_property="object", # [object, style] + size=512, + repeats=100, + interpolation="bicubic", + flip_p=0.5, + set="train", + placeholder_token="*", + center_crop=False, + ): + self.data_root = data_root + self.tokenizer = tokenizer + self.learnable_property = learnable_property + self.size = size + self.placeholder_token = placeholder_token + self.center_crop = center_crop + self.flip_p = flip_p + + self.image_paths = [os.path.join(self.data_root, file_path) for file_path in os.listdir(self.data_root)] + + self.num_images = len(self.image_paths) + self._length = self.num_images + + if set == "train": + self._length = self.num_images * repeats + + self.interpolation = { + "linear": PIL_INTERPOLATION["linear"], + "bilinear": PIL_INTERPOLATION["bilinear"], + "bicubic": PIL_INTERPOLATION["bicubic"], + "lanczos": PIL_INTERPOLATION["lanczos"], + }[interpolation] + + self.templates = imagenet_style_templates_small if learnable_property == "style" else imagenet_templates_small + self.flip_transform = transforms.RandomHorizontalFlip(p=self.flip_p) + + def __len__(self): + return self._length + + def __getitem__(self, i): + example = {} + image = Image.open(self.image_paths[i % self.num_images]) + + if not image.mode == "RGB": + image = image.convert("RGB") + + placeholder_string = self.placeholder_token + text = random.choice(self.templates).format(placeholder_string) + + example["input_ids"] = self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids[0] + + # default to score-sde preprocessing + img = np.array(image).astype(np.uint8) + + if self.center_crop: + crop = min(img.shape[0], img.shape[1]) + ( + h, + w, + ) = ( + img.shape[0], + img.shape[1], + ) + img = img[(h - crop) // 2 : (h + crop) // 2, (w - crop) // 2 : (w + crop) // 2] + + image = Image.fromarray(img) + image = image.resize((self.size, self.size), resample=self.interpolation) + + image = self.flip_transform(image) + image = np.array(image).astype(np.uint8) + image = (image / 127.5 - 1.0).astype(np.float32) + + example["pixel_values"] = torch.from_numpy(image).permute(2, 0, 1) + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(): + args = parse_args() + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.report_to, + logging_dir=logging_dir, + ) + + if args.report_to == "wandb": + if not is_wandb_available(): + raise ImportError("Make sure to install wandb if you want to use it for logging during training.") + import wandb + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + transformers.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + transformers.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # If passed along, set the training seed now. + if args.seed is not None: + set_seed(args.seed) + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Load tokenizer + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Load scheduler and models + noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler") + text_encoder = CLIPTextModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision + ) + vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision) + unet = UNet2DConditionModel.from_pretrained( + args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision + ) + + # Add the placeholder token in tokenizer + num_added_tokens = tokenizer.add_tokens(args.placeholder_token) + if num_added_tokens == 0: + raise ValueError( + f"The tokenizer already contains the token {args.placeholder_token}. Please pass a different" + " `placeholder_token` that is not already in the tokenizer." + ) + + # Convert the initializer_token, placeholder_token to ids + token_ids = tokenizer.encode(args.initializer_token, add_special_tokens=False) + # Check if initializer_token is a single token or a sequence of tokens + if len(token_ids) > 1: + raise ValueError("The initializer token must be a single token.") + + initializer_token_id = token_ids[0] + placeholder_token_id = tokenizer.convert_tokens_to_ids(args.placeholder_token) + + # Resize the token embeddings as we are adding new special tokens to the tokenizer + text_encoder.resize_token_embeddings(len(tokenizer)) + + # Initialise the newly added placeholder token with the embeddings of the initializer token + token_embeds = text_encoder.get_input_embeddings().weight.data + token_embeds[placeholder_token_id] = token_embeds[initializer_token_id] + + # Freeze vae and unet + vae.requires_grad_(False) + unet.requires_grad_(False) + # Freeze all parameters except for the token embeddings in text encoder + text_encoder.text_model.encoder.requires_grad_(False) + text_encoder.text_model.final_layer_norm.requires_grad_(False) + text_encoder.text_model.embeddings.position_embedding.requires_grad_(False) + + if args.gradient_checkpointing: + # Keep unet in train mode if we are using gradient checkpointing to save memory. + # The dropout cannot be != 0 so it doesn't matter if we are in eval or train mode. + unet.train() + text_encoder.gradient_checkpointing_enable() + unet.enable_gradient_checkpointing() + + if args.enable_xformers_memory_efficient_attention: + if is_xformers_available(): + unet.enable_xformers_memory_efficient_attention() + else: + raise ValueError("xformers is not available. Make sure it is installed correctly") + + # Enable TF32 for faster training on Ampere GPUs, + # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices + if args.allow_tf32: + torch.backends.cuda.matmul.allow_tf32 = True + + if args.scale_lr: + args.learning_rate = ( + args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes + ) + + # Initialize the optimizer + optimizer = torch.optim.AdamW( + text_encoder.get_input_embeddings().parameters(), # only optimize the embeddings + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Dataset and DataLoaders creation: + train_dataset = TextualInversionDataset( + data_root=args.train_data_dir, + tokenizer=tokenizer, + size=args.resolution, + placeholder_token=args.placeholder_token, + repeats=args.repeats, + learnable_property=args.learnable_property, + center_crop=args.center_crop, + set="train", + ) + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=args.train_batch_size, shuffle=True, num_workers=args.dataloader_num_workers + ) + + # Scheduler and math around the number of training steps. + overrode_max_train_steps = False + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + overrode_max_train_steps = True + + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=args.max_train_steps * args.gradient_accumulation_steps, + ) + + # Prepare everything with our `accelerator`. + text_encoder, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + text_encoder, optimizer, train_dataloader, lr_scheduler + ) + + # For mixed precision training we cast the unet and vae weights to half-precision + # as these models are only used for inference, keeping weights in full precision is not required. + weight_dtype = torch.float32 + if accelerator.mixed_precision == "fp16": + weight_dtype = torch.float16 + elif accelerator.mixed_precision == "bf16": + weight_dtype = torch.bfloat16 + + # Move vae and unet to device and cast to weight_dtype + unet.to(accelerator.device, dtype=weight_dtype) + vae.to(accelerator.device, dtype=weight_dtype) + + # We need to recalculate our total training steps as the size of the training dataloader may have changed. + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + if overrode_max_train_steps: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + # Afterwards we recalculate our number of training epochs + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + accelerator.init_trackers("textual_inversion", config=vars(args)) + + # Train! + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Only show the progress bar once on each machine. + progress_bar = tqdm(range(global_step, args.max_train_steps), disable=not accelerator.is_local_main_process) + progress_bar.set_description("Steps") + + # keep original embeddings as reference + orig_embeds_params = accelerator.unwrap_model(text_encoder).get_input_embeddings().weight.data.clone() + + for epoch in range(first_epoch, args.num_train_epochs): + text_encoder.train() + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + with accelerator.accumulate(text_encoder): + # Convert images to latent space + latents = vae.encode(batch["pixel_values"].to(dtype=weight_dtype)).latent_dist.sample().detach() + latents = latents * vae.config.scaling_factor + + # Sample noise that we'll add to the latents + noise = torch.randn_like(latents) + bsz = latents.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=latents.device) + timesteps = timesteps.long() + + # Add noise to the latents according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps) + + # Get the text embedding for conditioning + encoder_hidden_states = text_encoder(batch["input_ids"])[0].to(dtype=weight_dtype) + + # Predict the noise residual + model_pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean") + + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Let's make sure we don't update any embedding weights besides the newly added token + index_no_updates = torch.arange(len(tokenizer)) != placeholder_token_id + with torch.no_grad(): + accelerator.unwrap_model(text_encoder).get_input_embeddings().weight[ + index_no_updates + ] = orig_embeds_params[index_no_updates] + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + progress_bar.update(1) + global_step += 1 + if global_step % args.save_steps == 0: + save_path = os.path.join(args.output_dir, f"learned_embeds-steps-{global_step}.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]} + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + + if global_step >= args.max_train_steps: + break + + if args.validation_prompt is not None and epoch % args.validation_epochs == 0: + logger.info( + f"Running validation... \n Generating {args.num_validation_images} images with prompt:" + f" {args.validation_prompt}." + ) + # create pipeline (note: unet and vae are loaded again in float32) + pipeline = DiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=accelerator.unwrap_model(text_encoder), + tokenizer=tokenizer, + unet=unet, + vae=vae, + revision=args.revision, + torch_dtype=weight_dtype, + ) + pipeline.scheduler = DPMSolverMultistepScheduler.from_config(pipeline.scheduler.config) + pipeline = pipeline.to(accelerator.device) + pipeline.set_progress_bar_config(disable=True) + + # run inference + generator = ( + None if args.seed is None else torch.Generator(device=accelerator.device).manual_seed(args.seed) + ) + images = [] + for _ in range(args.num_validation_images): + with torch.autocast("cuda"): + image = pipeline(args.validation_prompt, num_inference_steps=25, generator=generator).images[0] + images.append(image) + + for tracker in accelerator.trackers: + if tracker.name == "tensorboard": + np_images = np.stack([np.asarray(img) for img in images]) + tracker.writer.add_images("validation", np_images, epoch, dataformats="NHWC") + if tracker.name == "wandb": + tracker.log( + { + "validation": [ + wandb.Image(image, caption=f"{i}: {args.validation_prompt}") + for i, image in enumerate(images) + ] + } + ) + + del pipeline + torch.cuda.empty_cache() + + # Create the pipeline using using the trained modules and save it. + accelerator.wait_for_everyone() + if accelerator.is_main_process: + if args.push_to_hub and args.only_save_embeds: + logger.warn("Enabling full model saving because --push_to_hub=True was specified.") + save_full_model = True + else: + save_full_model = not args.only_save_embeds + if save_full_model: + pipeline = StableDiffusionPipeline.from_pretrained( + args.pretrained_model_name_or_path, + text_encoder=accelerator.unwrap_model(text_encoder), + vae=vae, + unet=unet, + tokenizer=tokenizer, + ) + pipeline.save_pretrained(args.output_dir) + # Save the newly trained embeddings + save_path = os.path.join(args.output_dir, "learned_embeds.bin") + save_progress(text_encoder, placeholder_token_id, accelerator, args, save_path) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/textual_inversion/textual_inversion_flax.py b/diffusers/examples/textual_inversion/textual_inversion_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..6ed2c8d243426f623abdd977642c351afcdc34cc --- /dev/null +++ b/diffusers/examples/textual_inversion/textual_inversion_flax.py @@ -0,0 +1,668 @@ +import argparse +import logging +import math +import os +import random +from pathlib import Path +from typing import Optional + +import jax +import jax.numpy as jnp +import numpy as np +import optax +import PIL +import torch +import torch.utils.checkpoint +import transformers +from flax import jax_utils +from flax.training import train_state +from flax.training.common_utils import shard +from huggingface_hub import HfFolder, Repository, create_repo, whoami + +# TODO: remove and import from diffusers.utils when the new version of diffusers is released +from packaging import version +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms +from tqdm.auto import tqdm +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel, set_seed + +from diffusers import ( + FlaxAutoencoderKL, + FlaxDDPMScheduler, + FlaxPNDMScheduler, + FlaxStableDiffusionPipeline, + FlaxUNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion import FlaxStableDiffusionSafetyChecker +from diffusers.utils import check_min_version + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } +# ------------------------------------------------------------------------------ + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = logging.getLogger(__name__) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--pretrained_model_name_or_path", + type=str, + default=None, + required=True, + help="Path to pretrained model or model identifier from huggingface.co/models.", + ) + parser.add_argument( + "--tokenizer_name", + type=str, + default=None, + help="Pretrained tokenizer name or path if not the same as model_name", + ) + parser.add_argument( + "--train_data_dir", type=str, default=None, required=True, help="A folder containing the training data." + ) + parser.add_argument( + "--placeholder_token", + type=str, + default=None, + required=True, + help="A token to use as a placeholder for the concept.", + ) + parser.add_argument( + "--initializer_token", type=str, default=None, required=True, help="A token to use as initializer word." + ) + parser.add_argument("--learnable_property", type=str, default="object", help="Choose between 'object' and 'style'") + parser.add_argument("--repeats", type=int, default=100, help="How many times to repeat the training data.") + parser.add_argument( + "--output_dir", + type=str, + default="text-inversion-model", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--seed", type=int, default=42, help="A seed for reproducible training.") + parser.add_argument( + "--resolution", + type=int, + default=512, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", action="store_true", help="Whether to center crop images before resizing to resolution." + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument("--num_train_epochs", type=int, default=100) + parser.add_argument( + "--max_train_steps", + type=int, + default=5000, + help="Total number of training steps to perform. If provided, overrides num_train_epochs.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--scale_lr", + action="store_true", + default=True, + help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.", + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="constant", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument("--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument("--adam_weight_decay", type=float, default=1e-2, help="Weight decay to use.") + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument( + "--use_auth_token", + action="store_true", + help=( + "Will use the token generated when running `huggingface-cli login` (necessary to use this script with" + " private models)." + ), + ) + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.train_data_dir is None: + raise ValueError("You must specify a train data directory.") + + return args + + +imagenet_templates_small = [ + "a photo of a {}", + "a rendering of a {}", + "a cropped photo of the {}", + "the photo of a {}", + "a photo of a clean {}", + "a photo of a dirty {}", + "a dark photo of the {}", + "a photo of my {}", + "a photo of the cool {}", + "a close-up photo of a {}", + "a bright photo of the {}", + "a cropped photo of a {}", + "a photo of the {}", + "a good photo of the {}", + "a photo of one {}", + "a close-up photo of the {}", + "a rendition of the {}", + "a photo of the clean {}", + "a rendition of a {}", + "a photo of a nice {}", + "a good photo of a {}", + "a photo of the nice {}", + "a photo of the small {}", + "a photo of the weird {}", + "a photo of the large {}", + "a photo of a cool {}", + "a photo of a small {}", +] + +imagenet_style_templates_small = [ + "a painting in the style of {}", + "a rendering in the style of {}", + "a cropped painting in the style of {}", + "the painting in the style of {}", + "a clean painting in the style of {}", + "a dirty painting in the style of {}", + "a dark painting in the style of {}", + "a picture in the style of {}", + "a cool painting in the style of {}", + "a close-up painting in the style of {}", + "a bright painting in the style of {}", + "a cropped painting in the style of {}", + "a good painting in the style of {}", + "a close-up painting in the style of {}", + "a rendition in the style of {}", + "a nice painting in the style of {}", + "a small painting in the style of {}", + "a weird painting in the style of {}", + "a large painting in the style of {}", +] + + +class TextualInversionDataset(Dataset): + def __init__( + self, + data_root, + tokenizer, + learnable_property="object", # [object, style] + size=512, + repeats=100, + interpolation="bicubic", + flip_p=0.5, + set="train", + placeholder_token="*", + center_crop=False, + ): + self.data_root = data_root + self.tokenizer = tokenizer + self.learnable_property = learnable_property + self.size = size + self.placeholder_token = placeholder_token + self.center_crop = center_crop + self.flip_p = flip_p + + self.image_paths = [os.path.join(self.data_root, file_path) for file_path in os.listdir(self.data_root)] + + self.num_images = len(self.image_paths) + self._length = self.num_images + + if set == "train": + self._length = self.num_images * repeats + + self.interpolation = { + "linear": PIL_INTERPOLATION["linear"], + "bilinear": PIL_INTERPOLATION["bilinear"], + "bicubic": PIL_INTERPOLATION["bicubic"], + "lanczos": PIL_INTERPOLATION["lanczos"], + }[interpolation] + + self.templates = imagenet_style_templates_small if learnable_property == "style" else imagenet_templates_small + self.flip_transform = transforms.RandomHorizontalFlip(p=self.flip_p) + + def __len__(self): + return self._length + + def __getitem__(self, i): + example = {} + image = Image.open(self.image_paths[i % self.num_images]) + + if not image.mode == "RGB": + image = image.convert("RGB") + + placeholder_string = self.placeholder_token + text = random.choice(self.templates).format(placeholder_string) + + example["input_ids"] = self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ).input_ids[0] + + # default to score-sde preprocessing + img = np.array(image).astype(np.uint8) + + if self.center_crop: + crop = min(img.shape[0], img.shape[1]) + ( + h, + w, + ) = ( + img.shape[0], + img.shape[1], + ) + img = img[(h - crop) // 2 : (h + crop) // 2, (w - crop) // 2 : (w + crop) // 2] + + image = Image.fromarray(img) + image = image.resize((self.size, self.size), resample=self.interpolation) + + image = self.flip_transform(image) + image = np.array(image).astype(np.uint8) + image = (image / 127.5 - 1.0).astype(np.float32) + + example["pixel_values"] = torch.from_numpy(image).permute(2, 0, 1) + return example + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def resize_token_embeddings(model, new_num_tokens, initializer_token_id, placeholder_token_id, rng): + if model.config.vocab_size == new_num_tokens or new_num_tokens is None: + return + model.config.vocab_size = new_num_tokens + + params = model.params + old_embeddings = params["text_model"]["embeddings"]["token_embedding"]["embedding"] + old_num_tokens, emb_dim = old_embeddings.shape + + initializer = jax.nn.initializers.normal() + + new_embeddings = initializer(rng, (new_num_tokens, emb_dim)) + new_embeddings = new_embeddings.at[:old_num_tokens].set(old_embeddings) + new_embeddings = new_embeddings.at[placeholder_token_id].set(new_embeddings[initializer_token_id]) + params["text_model"]["embeddings"]["token_embedding"]["embedding"] = new_embeddings + + model.params = params + return model + + +def get_params_to_save(params): + return jax.device_get(jax.tree_util.tree_map(lambda x: x[0], params)) + + +def main(): + args = parse_args() + + if args.seed is not None: + set_seed(args.seed) + + if jax.process_index() == 0: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + # Setup logging, we only want one process per machine to log things on the screen. + logger.setLevel(logging.INFO if jax.process_index() == 0 else logging.ERROR) + if jax.process_index() == 0: + transformers.utils.logging.set_verbosity_info() + else: + transformers.utils.logging.set_verbosity_error() + + # Load the tokenizer and add the placeholder token as a additional special token + if args.tokenizer_name: + tokenizer = CLIPTokenizer.from_pretrained(args.tokenizer_name) + elif args.pretrained_model_name_or_path: + tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer") + + # Add the placeholder token in tokenizer + num_added_tokens = tokenizer.add_tokens(args.placeholder_token) + if num_added_tokens == 0: + raise ValueError( + f"The tokenizer already contains the token {args.placeholder_token}. Please pass a different" + " `placeholder_token` that is not already in the tokenizer." + ) + + # Convert the initializer_token, placeholder_token to ids + token_ids = tokenizer.encode(args.initializer_token, add_special_tokens=False) + # Check if initializer_token is a single token or a sequence of tokens + if len(token_ids) > 1: + raise ValueError("The initializer token must be a single token.") + + initializer_token_id = token_ids[0] + placeholder_token_id = tokenizer.convert_tokens_to_ids(args.placeholder_token) + + # Load models and create wrapper for stable diffusion + text_encoder = FlaxCLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder") + vae, vae_params = FlaxAutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae") + unet, unet_params = FlaxUNet2DConditionModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="unet") + + # Create sampling rng + rng = jax.random.PRNGKey(args.seed) + rng, _ = jax.random.split(rng) + # Resize the token embeddings as we are adding new special tokens to the tokenizer + text_encoder = resize_token_embeddings( + text_encoder, len(tokenizer), initializer_token_id, placeholder_token_id, rng + ) + original_token_embeds = text_encoder.params["text_model"]["embeddings"]["token_embedding"]["embedding"] + + train_dataset = TextualInversionDataset( + data_root=args.train_data_dir, + tokenizer=tokenizer, + size=args.resolution, + placeholder_token=args.placeholder_token, + repeats=args.repeats, + learnable_property=args.learnable_property, + center_crop=args.center_crop, + set="train", + ) + + def collate_fn(examples): + pixel_values = torch.stack([example["pixel_values"] for example in examples]) + input_ids = torch.stack([example["input_ids"] for example in examples]) + + batch = {"pixel_values": pixel_values, "input_ids": input_ids} + batch = {k: v.numpy() for k, v in batch.items()} + + return batch + + total_train_batch_size = args.train_batch_size * jax.local_device_count() + train_dataloader = torch.utils.data.DataLoader( + train_dataset, batch_size=total_train_batch_size, shuffle=True, drop_last=True, collate_fn=collate_fn + ) + + # Optimization + if args.scale_lr: + args.learning_rate = args.learning_rate * total_train_batch_size + + constant_scheduler = optax.constant_schedule(args.learning_rate) + + optimizer = optax.adamw( + learning_rate=constant_scheduler, + b1=args.adam_beta1, + b2=args.adam_beta2, + eps=args.adam_epsilon, + weight_decay=args.adam_weight_decay, + ) + + def create_mask(params, label_fn): + def _map(params, mask, label_fn): + for k in params: + if label_fn(k): + mask[k] = "token_embedding" + else: + if isinstance(params[k], dict): + mask[k] = {} + _map(params[k], mask[k], label_fn) + else: + mask[k] = "zero" + + mask = {} + _map(params, mask, label_fn) + return mask + + def zero_grads(): + # from https://github.com/deepmind/optax/issues/159#issuecomment-896459491 + def init_fn(_): + return () + + def update_fn(updates, state, params=None): + return jax.tree_util.tree_map(jnp.zeros_like, updates), () + + return optax.GradientTransformation(init_fn, update_fn) + + # Zero out gradients of layers other than the token embedding layer + tx = optax.multi_transform( + {"token_embedding": optimizer, "zero": zero_grads()}, + create_mask(text_encoder.params, lambda s: s == "token_embedding"), + ) + + state = train_state.TrainState.create(apply_fn=text_encoder.__call__, params=text_encoder.params, tx=tx) + + noise_scheduler = FlaxDDPMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000 + ) + noise_scheduler_state = noise_scheduler.create_state() + + # Initialize our training + train_rngs = jax.random.split(rng, jax.local_device_count()) + + # Define gradient train step fn + def train_step(state, vae_params, unet_params, batch, train_rng): + dropout_rng, sample_rng, new_train_rng = jax.random.split(train_rng, 3) + + def compute_loss(params): + vae_outputs = vae.apply( + {"params": vae_params}, batch["pixel_values"], deterministic=True, method=vae.encode + ) + latents = vae_outputs.latent_dist.sample(sample_rng) + # (NHWC) -> (NCHW) + latents = jnp.transpose(latents, (0, 3, 1, 2)) + latents = latents * vae.config.scaling_factor + + noise_rng, timestep_rng = jax.random.split(sample_rng) + noise = jax.random.normal(noise_rng, latents.shape) + bsz = latents.shape[0] + timesteps = jax.random.randint( + timestep_rng, + (bsz,), + 0, + noise_scheduler.config.num_train_timesteps, + ) + noisy_latents = noise_scheduler.add_noise(noise_scheduler_state, latents, noise, timesteps) + encoder_hidden_states = state.apply_fn( + batch["input_ids"], params=params, dropout_rng=dropout_rng, train=True + )[0] + # Predict the noise residual and compute loss + model_pred = unet.apply( + {"params": unet_params}, noisy_latents, timesteps, encoder_hidden_states, train=False + ).sample + + # Get the target for loss depending on the prediction type + if noise_scheduler.config.prediction_type == "epsilon": + target = noise + elif noise_scheduler.config.prediction_type == "v_prediction": + target = noise_scheduler.get_velocity(noise_scheduler_state, latents, noise, timesteps) + else: + raise ValueError(f"Unknown prediction type {noise_scheduler.config.prediction_type}") + + loss = (target - model_pred) ** 2 + loss = loss.mean() + + return loss + + grad_fn = jax.value_and_grad(compute_loss) + loss, grad = grad_fn(state.params) + grad = jax.lax.pmean(grad, "batch") + new_state = state.apply_gradients(grads=grad) + + # Keep the token embeddings fixed except the newly added embeddings for the concept, + # as we only want to optimize the concept embeddings + token_embeds = original_token_embeds.at[placeholder_token_id].set( + new_state.params["text_model"]["embeddings"]["token_embedding"]["embedding"][placeholder_token_id] + ) + new_state.params["text_model"]["embeddings"]["token_embedding"]["embedding"] = token_embeds + + metrics = {"loss": loss} + metrics = jax.lax.pmean(metrics, axis_name="batch") + return new_state, metrics, new_train_rng + + # Create parallel version of the train and eval step + p_train_step = jax.pmap(train_step, "batch", donate_argnums=(0,)) + + # Replicate the train state on each device + state = jax_utils.replicate(state) + vae_params = jax_utils.replicate(vae_params) + unet_params = jax_utils.replicate(unet_params) + + # Train! + num_update_steps_per_epoch = math.ceil(len(train_dataloader)) + + # Scheduler and math around the number of training steps. + if args.max_train_steps is None: + args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch + + args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch) + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(train_dataset)}") + logger.info(f" Num Epochs = {args.num_train_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel & distributed) = {total_train_batch_size}") + logger.info(f" Total optimization steps = {args.max_train_steps}") + + global_step = 0 + + epochs = tqdm(range(args.num_train_epochs), desc=f"Epoch ... (1/{args.num_train_epochs})", position=0) + for epoch in epochs: + # ======================== Training ================================ + + train_metrics = [] + + steps_per_epoch = len(train_dataset) // total_train_batch_size + train_step_progress_bar = tqdm(total=steps_per_epoch, desc="Training...", position=1, leave=False) + # train + for batch in train_dataloader: + batch = shard(batch) + state, train_metric, train_rngs = p_train_step(state, vae_params, unet_params, batch, train_rngs) + train_metrics.append(train_metric) + + train_step_progress_bar.update(1) + global_step += 1 + + if global_step >= args.max_train_steps: + break + + train_metric = jax_utils.unreplicate(train_metric) + + train_step_progress_bar.close() + epochs.write(f"Epoch... ({epoch + 1}/{args.num_train_epochs} | Loss: {train_metric['loss']})") + + # Create the pipeline using using the trained modules and save it. + if jax.process_index() == 0: + scheduler = FlaxPNDMScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", skip_prk_steps=True + ) + safety_checker = FlaxStableDiffusionSafetyChecker.from_pretrained( + "CompVis/stable-diffusion-safety-checker", from_pt=True + ) + pipeline = FlaxStableDiffusionPipeline( + text_encoder=text_encoder, + vae=vae, + unet=unet, + tokenizer=tokenizer, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=CLIPFeatureExtractor.from_pretrained("openai/clip-vit-base-patch32"), + ) + + pipeline.save_pretrained( + args.output_dir, + params={ + "text_encoder": get_params_to_save(state.params), + "vae": get_params_to_save(vae_params), + "unet": get_params_to_save(unet_params), + "safety_checker": safety_checker.params, + }, + ) + + # Also save the newly trained embeddings + learned_embeds = get_params_to_save(state.params)["text_model"]["embeddings"]["token_embedding"]["embedding"][ + placeholder_token_id + ] + learned_embeds_dict = {args.placeholder_token: learned_embeds} + jnp.save(os.path.join(args.output_dir, "learned_embeds.npy"), learned_embeds_dict) + + if args.push_to_hub: + repo.push_to_hub(commit_message="End of training", blocking=False, auto_lfs_prune=True) + + +if __name__ == "__main__": + main() diff --git a/diffusers/examples/unconditional_image_generation/README.md b/diffusers/examples/unconditional_image_generation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..db06d901168104c47d86415f42a24f3e738362e9 --- /dev/null +++ b/diffusers/examples/unconditional_image_generation/README.md @@ -0,0 +1,142 @@ +## Training examples + +Creating a training image set is [described in a different document](https://huggingface.co/docs/datasets/image_process#image-datasets). + +### Installing the dependencies + +Before running the scripts, make sure to install the library's training dependencies: + +**Important** + +To make sure you can successfully run the latest versions of the example scripts, we highly recommend **installing from source** and keeping the install up to date as we update the example scripts frequently and install some example-specific requirements. To do this, execute the following steps in a new virtual environment: +```bash +git clone https://github.com/huggingface/diffusers +cd diffusers +pip install . +``` + +Then cd in the example folder and run +```bash +pip install -r requirements.txt +``` + + +And initialize an [🤗Accelerate](https://github.com/huggingface/accelerate/) environment with: + +```bash +accelerate config +``` + +### Unconditional Flowers + +The command to train a DDPM UNet model on the Oxford Flowers dataset: + +```bash +accelerate launch train_unconditional.py \ + --dataset_name="huggan/flowers-102-categories" \ + --resolution=64 --center_crop --random_flip \ + --output_dir="ddpm-ema-flowers-64" \ + --train_batch_size=16 \ + --num_epochs=100 \ + --gradient_accumulation_steps=1 \ + --use_ema \ + --learning_rate=1e-4 \ + --lr_warmup_steps=500 \ + --mixed_precision=no \ + --push_to_hub +``` +An example trained model: https://huggingface.co/anton-l/ddpm-ema-flowers-64 + +A full training run takes 2 hours on 4xV100 GPUs. + + + + +### Unconditional Pokemon + +The command to train a DDPM UNet model on the Pokemon dataset: + +```bash +accelerate launch train_unconditional.py \ + --dataset_name="huggan/pokemon" \ + --resolution=64 --center_crop --random_flip \ + --output_dir="ddpm-ema-pokemon-64" \ + --train_batch_size=16 \ + --num_epochs=100 \ + --gradient_accumulation_steps=1 \ + --use_ema \ + --learning_rate=1e-4 \ + --lr_warmup_steps=500 \ + --mixed_precision=no \ + --push_to_hub +``` +An example trained model: https://huggingface.co/anton-l/ddpm-ema-pokemon-64 + +A full training run takes 2 hours on 4xV100 GPUs. + + + + +### Using your own data + +To use your own dataset, there are 2 ways: +- you can either provide your own folder as `--train_data_dir` +- or you can upload your dataset to the hub (possibly as a private repo, if you prefer so), and simply pass the `--dataset_name` argument. + +Below, we explain both in more detail. + +#### Provide the dataset as a folder + +If you provide your own folders with images, the script expects the following directory structure: + +```bash +data_dir/xxx.png +data_dir/xxy.png +data_dir/[...]/xxz.png +``` + +In other words, the script will take care of gathering all images inside the folder. You can then run the script like this: + +```bash +accelerate launch train_unconditional.py \ + --train_data_dir \ + +``` + +Internally, the script will use the [`ImageFolder`](https://huggingface.co/docs/datasets/v2.0.0/en/image_process#imagefolder) feature which will automatically turn the folders into 🤗 Dataset objects. + +#### Upload your data to the hub, as a (possibly private) repo + +It's very easy (and convenient) to upload your image dataset to the hub using the [`ImageFolder`](https://huggingface.co/docs/datasets/v2.0.0/en/image_process#imagefolder) feature available in 🤗 Datasets. Simply do the following: + +```python +from datasets import load_dataset + +# example 1: local folder +dataset = load_dataset("imagefolder", data_dir="path_to_your_folder") + +# example 2: local files (supported formats are tar, gzip, zip, xz, rar, zstd) +dataset = load_dataset("imagefolder", data_files="path_to_zip_file") + +# example 3: remote files (supported formats are tar, gzip, zip, xz, rar, zstd) +dataset = load_dataset("imagefolder", data_files="https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.zip") + +# example 4: providing several splits +dataset = load_dataset("imagefolder", data_files={"train": ["path/to/file1", "path/to/file2"], "test": ["path/to/file3", "path/to/file4"]}) +``` + +`ImageFolder` will create an `image` column containing the PIL-encoded images. + +Next, push it to the hub! + +```python +# assuming you have ran the huggingface-cli login command in a terminal +dataset.push_to_hub("name_of_your_dataset") + +# if you want to push to a private repo, simply pass private=True: +dataset.push_to_hub("name_of_your_dataset", private=True) +``` + +and that's it! You can now train your model by simply setting the `--dataset_name` argument to the name of your dataset on the hub. + +More on this can also be found in [this blog post](https://huggingface.co/blog/image-search-datasets). diff --git a/diffusers/examples/unconditional_image_generation/requirements.txt b/diffusers/examples/unconditional_image_generation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..bbc6905560209d6b9c957d8c6bb61cde4462365b --- /dev/null +++ b/diffusers/examples/unconditional_image_generation/requirements.txt @@ -0,0 +1,3 @@ +accelerate +torchvision +datasets diff --git a/diffusers/examples/unconditional_image_generation/train_unconditional.py b/diffusers/examples/unconditional_image_generation/train_unconditional.py new file mode 100644 index 0000000000000000000000000000000000000000..64ba126d0ccebfba74033213d204ae07ffbe74e1 --- /dev/null +++ b/diffusers/examples/unconditional_image_generation/train_unconditional.py @@ -0,0 +1,623 @@ +import argparse +import inspect +import logging +import math +import os +from pathlib import Path +from typing import Optional + +import accelerate +import datasets +import torch +import torch.nn.functional as F +from accelerate import Accelerator +from accelerate.logging import get_logger +from datasets import load_dataset +from huggingface_hub import HfFolder, Repository, create_repo, whoami +from packaging import version +from torchvision import transforms +from tqdm.auto import tqdm + +import diffusers +from diffusers import DDPMPipeline, DDPMScheduler, UNet2DModel +from diffusers.optimization import get_scheduler +from diffusers.training_utils import EMAModel +from diffusers.utils import check_min_version, is_tensorboard_available + + +# Will error if the minimal version of diffusers is not installed. Remove at your own risks. +check_min_version("0.13.0.dev0") + +logger = get_logger(__name__, log_level="INFO") + + +def _extract_into_tensor(arr, timesteps, broadcast_shape): + """ + Extract values from a 1-D numpy array for a batch of indices. + + :param arr: the 1-D numpy array. + :param timesteps: a tensor of indices into the array to extract. + :param broadcast_shape: a larger shape of K dimensions with the batch + dimension equal to the length of timesteps. + :return: a tensor of shape [batch_size, 1, ...] where the shape has K dims. + """ + if not isinstance(arr, torch.Tensor): + arr = torch.from_numpy(arr) + res = arr[timesteps].float().to(timesteps.device) + while len(res.shape) < len(broadcast_shape): + res = res[..., None] + return res.expand(broadcast_shape) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Simple example of a training script.") + parser.add_argument( + "--dataset_name", + type=str, + default=None, + help=( + "The name of the Dataset (from the HuggingFace hub) to train on (could be your own, possibly private," + " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem," + " or to a folder containing files that HF Datasets can understand." + ), + ) + parser.add_argument( + "--dataset_config_name", + type=str, + default=None, + help="The config of the Dataset, leave as None if there's only one config.", + ) + parser.add_argument( + "--model_config_name_or_path", + type=str, + default=None, + help="The config of the UNet model to train, leave as None to use standard DDPM configuration.", + ) + parser.add_argument( + "--train_data_dir", + type=str, + default=None, + help=( + "A folder containing the training data. Folder contents must follow the structure described in" + " https://huggingface.co/docs/datasets/image_dataset#imagefolder. In particular, a `metadata.jsonl` file" + " must exist to provide the captions for the images. Ignored if `dataset_name` is specified." + ), + ) + parser.add_argument( + "--output_dir", + type=str, + default="ddpm-model-64", + help="The output directory where the model predictions and checkpoints will be written.", + ) + parser.add_argument("--overwrite_output_dir", action="store_true") + parser.add_argument( + "--cache_dir", + type=str, + default=None, + help="The directory where the downloaded models and datasets will be stored.", + ) + parser.add_argument( + "--resolution", + type=int, + default=64, + help=( + "The resolution for input images, all the images in the train/validation dataset will be resized to this" + " resolution" + ), + ) + parser.add_argument( + "--center_crop", + default=False, + action="store_true", + help=( + "Whether to center crop the input images to the resolution. If not set, the images will be randomly" + " cropped. The images will be resized to the resolution first before cropping." + ), + ) + parser.add_argument( + "--random_flip", + default=False, + action="store_true", + help="whether to randomly flip images horizontally", + ) + parser.add_argument( + "--train_batch_size", type=int, default=16, help="Batch size (per device) for the training dataloader." + ) + parser.add_argument( + "--eval_batch_size", type=int, default=16, help="The number of images to generate for evaluation." + ) + parser.add_argument( + "--dataloader_num_workers", + type=int, + default=0, + help=( + "The number of subprocesses to use for data loading. 0 means that the data will be loaded in the main" + " process." + ), + ) + parser.add_argument("--num_epochs", type=int, default=100) + parser.add_argument("--save_images_epochs", type=int, default=10, help="How often to save images during training.") + parser.add_argument( + "--save_model_epochs", type=int, default=10, help="How often to save the model during training." + ) + parser.add_argument( + "--gradient_accumulation_steps", + type=int, + default=1, + help="Number of updates steps to accumulate before performing a backward/update pass.", + ) + parser.add_argument( + "--learning_rate", + type=float, + default=1e-4, + help="Initial learning rate (after the potential warmup period) to use.", + ) + parser.add_argument( + "--lr_scheduler", + type=str, + default="cosine", + help=( + 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",' + ' "constant", "constant_with_warmup"]' + ), + ) + parser.add_argument( + "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler." + ) + parser.add_argument("--adam_beta1", type=float, default=0.95, help="The beta1 parameter for the Adam optimizer.") + parser.add_argument("--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam optimizer.") + parser.add_argument( + "--adam_weight_decay", type=float, default=1e-6, help="Weight decay magnitude for the Adam optimizer." + ) + parser.add_argument("--adam_epsilon", type=float, default=1e-08, help="Epsilon value for the Adam optimizer.") + parser.add_argument( + "--use_ema", + action="store_true", + help="Whether to use Exponential Moving Average for the final model weights.", + ) + parser.add_argument("--ema_inv_gamma", type=float, default=1.0, help="The inverse gamma value for the EMA decay.") + parser.add_argument("--ema_power", type=float, default=3 / 4, help="The power value for the EMA decay.") + parser.add_argument("--ema_max_decay", type=float, default=0.9999, help="The maximum decay magnitude for EMA.") + parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.") + parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.") + parser.add_argument( + "--hub_model_id", + type=str, + default=None, + help="The name of the repository to keep in sync with the local `output_dir`.", + ) + parser.add_argument( + "--hub_private_repo", action="store_true", help="Whether or not to create a private repository." + ) + parser.add_argument( + "--logger", + type=str, + default="tensorboard", + choices=["tensorboard", "wandb"], + help=( + "Whether to use [tensorboard](https://www.tensorflow.org/tensorboard) or [wandb](https://www.wandb.ai)" + " for experiment tracking and logging of model metrics and model checkpoints" + ), + ) + parser.add_argument( + "--logging_dir", + type=str, + default="logs", + help=( + "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to" + " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***." + ), + ) + parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank") + parser.add_argument( + "--mixed_precision", + type=str, + default="no", + choices=["no", "fp16", "bf16"], + help=( + "Whether to use mixed precision. Choose" + "between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >= 1.10." + "and an Nvidia Ampere GPU." + ), + ) + parser.add_argument( + "--prediction_type", + type=str, + default="epsilon", + choices=["epsilon", "sample"], + help="Whether the model should predict the 'epsilon'/noise error or directly the reconstructed image 'x0'.", + ) + parser.add_argument("--ddpm_num_steps", type=int, default=1000) + parser.add_argument("--ddpm_num_inference_steps", type=int, default=1000) + parser.add_argument("--ddpm_beta_schedule", type=str, default="linear") + parser.add_argument( + "--checkpointing_steps", + type=int, + default=500, + help=( + "Save a checkpoint of the training state every X updates. These checkpoints are only suitable for resuming" + " training using `--resume_from_checkpoint`." + ), + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help=( + "Whether training should be resumed from a previous checkpoint. Use a path saved by" + ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.' + ), + ) + + args = parser.parse_args() + env_local_rank = int(os.environ.get("LOCAL_RANK", -1)) + if env_local_rank != -1 and env_local_rank != args.local_rank: + args.local_rank = env_local_rank + + if args.dataset_name is None and args.train_data_dir is None: + raise ValueError("You must specify either a dataset name from the hub or a train data directory.") + + return args + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def main(args): + logging_dir = os.path.join(args.output_dir, args.logging_dir) + + accelerator = Accelerator( + gradient_accumulation_steps=args.gradient_accumulation_steps, + mixed_precision=args.mixed_precision, + log_with=args.logger, + logging_dir=logging_dir, + ) + + # `accelerate` 0.16.0 will have better support for customized saving + if version.parse(accelerate.__version__) >= version.parse("0.16.0"): + # create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format + def save_model_hook(models, weights, output_dir): + if args.use_ema: + ema_model.save_pretrained(os.path.join(output_dir, "unet_ema")) + + for i, model in enumerate(models): + model.save_pretrained(os.path.join(output_dir, "unet")) + + # make sure to pop weight so that corresponding model is not saved again + weights.pop() + + def load_model_hook(models, input_dir): + if args.use_ema: + load_model = EMAModel.from_pretrained(os.path.join(input_dir, "unet_ema"), UNet2DModel) + ema_model.load_state_dict(load_model.state_dict()) + ema_model.to(accelerator.device) + del load_model + + for i in range(len(models)): + # pop models so that they are not loaded again + model = models.pop() + + # load diffusers style into model + load_model = UNet2DModel.from_pretrained(input_dir, subfolder="unet") + model.register_to_config(**load_model.config) + + model.load_state_dict(load_model.state_dict()) + del load_model + + accelerator.register_save_state_pre_hook(save_model_hook) + accelerator.register_load_state_pre_hook(load_model_hook) + + # Make one log on every process with the configuration for debugging. + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + datefmt="%m/%d/%Y %H:%M:%S", + level=logging.INFO, + ) + logger.info(accelerator.state, main_process_only=False) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + diffusers.utils.logging.set_verbosity_info() + else: + datasets.utils.logging.set_verbosity_error() + diffusers.utils.logging.set_verbosity_error() + + # Handle the repository creation + if accelerator.is_main_process: + if args.push_to_hub: + if args.hub_model_id is None: + repo_name = get_full_repo_name(Path(args.output_dir).name, token=args.hub_token) + else: + repo_name = args.hub_model_id + create_repo(repo_name, exist_ok=True, token=args.hub_token) + repo = Repository(args.output_dir, clone_from=repo_name, token=args.hub_token) + + with open(os.path.join(args.output_dir, ".gitignore"), "w+") as gitignore: + if "step_*" not in gitignore: + gitignore.write("step_*\n") + if "epoch_*" not in gitignore: + gitignore.write("epoch_*\n") + elif args.output_dir is not None: + os.makedirs(args.output_dir, exist_ok=True) + + # Initialize the model + if args.model_config_name_or_path is None: + model = UNet2DModel( + sample_size=args.resolution, + in_channels=3, + out_channels=3, + layers_per_block=2, + block_out_channels=(128, 128, 256, 256, 512, 512), + down_block_types=( + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "AttnDownBlock2D", + "DownBlock2D", + ), + up_block_types=( + "UpBlock2D", + "AttnUpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + ), + ) + else: + config = UNet2DModel.load_config(args.model_config_name_or_path) + model = UNet2DModel.from_config(config) + + # Create EMA for the model. + if args.use_ema: + ema_model = EMAModel( + model.parameters(), + decay=args.ema_max_decay, + use_ema_warmup=True, + inv_gamma=args.ema_inv_gamma, + power=args.ema_power, + model_cls=UNet2DModel, + model_config=model.config, + ) + + # Initialize the scheduler + accepts_prediction_type = "prediction_type" in set(inspect.signature(DDPMScheduler.__init__).parameters.keys()) + if accepts_prediction_type: + noise_scheduler = DDPMScheduler( + num_train_timesteps=args.ddpm_num_steps, + beta_schedule=args.ddpm_beta_schedule, + prediction_type=args.prediction_type, + ) + else: + noise_scheduler = DDPMScheduler(num_train_timesteps=args.ddpm_num_steps, beta_schedule=args.ddpm_beta_schedule) + + # Initialize the optimizer + optimizer = torch.optim.AdamW( + model.parameters(), + lr=args.learning_rate, + betas=(args.adam_beta1, args.adam_beta2), + weight_decay=args.adam_weight_decay, + eps=args.adam_epsilon, + ) + + # Get the datasets: you can either provide your own training and evaluation files (see below) + # or specify a Dataset from the hub (the dataset will be downloaded automatically from the datasets Hub). + + # In distributed training, the load_dataset function guarantees that only one local process can concurrently + # download the dataset. + if args.dataset_name is not None: + dataset = load_dataset( + args.dataset_name, + args.dataset_config_name, + cache_dir=args.cache_dir, + split="train", + ) + else: + dataset = load_dataset("imagefolder", data_dir=args.train_data_dir, cache_dir=args.cache_dir, split="train") + # See more about loading custom images at + # https://huggingface.co/docs/datasets/v2.4.0/en/image_load#imagefolder + + # Preprocessing the datasets and DataLoaders creation. + augmentations = transforms.Compose( + [ + transforms.Resize(args.resolution, interpolation=transforms.InterpolationMode.BILINEAR), + transforms.CenterCrop(args.resolution) if args.center_crop else transforms.RandomCrop(args.resolution), + transforms.RandomHorizontalFlip() if args.random_flip else transforms.Lambda(lambda x: x), + transforms.ToTensor(), + transforms.Normalize([0.5], [0.5]), + ] + ) + + def transform_images(examples): + images = [augmentations(image.convert("RGB")) for image in examples["image"]] + return {"input": images} + + logger.info(f"Dataset size: {len(dataset)}") + + dataset.set_transform(transform_images) + train_dataloader = torch.utils.data.DataLoader( + dataset, batch_size=args.train_batch_size, shuffle=True, num_workers=args.dataloader_num_workers + ) + + # Initialize the learning rate scheduler + lr_scheduler = get_scheduler( + args.lr_scheduler, + optimizer=optimizer, + num_warmup_steps=args.lr_warmup_steps * args.gradient_accumulation_steps, + num_training_steps=(len(train_dataloader) * args.num_epochs), + ) + + # Prepare everything with our `accelerator`. + model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( + model, optimizer, train_dataloader, lr_scheduler + ) + + if args.use_ema: + ema_model.to(accelerator.device) + + # We need to initialize the trackers we use, and also store our configuration. + # The trackers initializes automatically on the main process. + if accelerator.is_main_process: + run = os.path.split(__file__)[-1].split(".")[0] + accelerator.init_trackers(run) + + total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps + num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps) + max_train_steps = args.num_epochs * num_update_steps_per_epoch + + logger.info("***** Running training *****") + logger.info(f" Num examples = {len(dataset)}") + logger.info(f" Num Epochs = {args.num_epochs}") + logger.info(f" Instantaneous batch size per device = {args.train_batch_size}") + logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}") + logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}") + logger.info(f" Total optimization steps = {max_train_steps}") + + global_step = 0 + first_epoch = 0 + + # Potentially load in the weights and states from a previous save + if args.resume_from_checkpoint: + if args.resume_from_checkpoint != "latest": + path = os.path.basename(args.resume_from_checkpoint) + else: + # Get the most recent checkpoint + dirs = os.listdir(args.output_dir) + dirs = [d for d in dirs if d.startswith("checkpoint")] + dirs = sorted(dirs, key=lambda x: int(x.split("-")[1])) + path = dirs[-1] if len(dirs) > 0 else None + + if path is None: + accelerator.print( + f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run." + ) + args.resume_from_checkpoint = None + else: + accelerator.print(f"Resuming from checkpoint {path}") + accelerator.load_state(os.path.join(args.output_dir, path)) + global_step = int(path.split("-")[1]) + + resume_global_step = global_step * args.gradient_accumulation_steps + first_epoch = global_step // num_update_steps_per_epoch + resume_step = resume_global_step % (num_update_steps_per_epoch * args.gradient_accumulation_steps) + + # Train! + for epoch in range(first_epoch, args.num_epochs): + model.train() + progress_bar = tqdm(total=num_update_steps_per_epoch, disable=not accelerator.is_local_main_process) + progress_bar.set_description(f"Epoch {epoch}") + for step, batch in enumerate(train_dataloader): + # Skip steps until we reach the resumed step + if args.resume_from_checkpoint and epoch == first_epoch and step < resume_step: + if step % args.gradient_accumulation_steps == 0: + progress_bar.update(1) + continue + + clean_images = batch["input"] + # Sample noise that we'll add to the images + noise = torch.randn(clean_images.shape).to(clean_images.device) + bsz = clean_images.shape[0] + # Sample a random timestep for each image + timesteps = torch.randint( + 0, noise_scheduler.config.num_train_timesteps, (bsz,), device=clean_images.device + ).long() + + # Add noise to the clean images according to the noise magnitude at each timestep + # (this is the forward diffusion process) + noisy_images = noise_scheduler.add_noise(clean_images, noise, timesteps) + + with accelerator.accumulate(model): + # Predict the noise residual + model_output = model(noisy_images, timesteps).sample + + if args.prediction_type == "epsilon": + loss = F.mse_loss(model_output, noise) # this could have different weights! + elif args.prediction_type == "sample": + alpha_t = _extract_into_tensor( + noise_scheduler.alphas_cumprod, timesteps, (clean_images.shape[0], 1, 1, 1) + ) + snr_weights = alpha_t / (1 - alpha_t) + loss = snr_weights * F.mse_loss( + model_output, clean_images, reduction="none" + ) # use SNR weighting from distillation paper + loss = loss.mean() + else: + raise ValueError(f"Unsupported prediction type: {args.prediction_type}") + + accelerator.backward(loss) + + if accelerator.sync_gradients: + accelerator.clip_grad_norm_(model.parameters(), 1.0) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # Checks if the accelerator has performed an optimization step behind the scenes + if accelerator.sync_gradients: + if args.use_ema: + ema_model.step(model.parameters()) + progress_bar.update(1) + global_step += 1 + + if global_step % args.checkpointing_steps == 0: + if accelerator.is_main_process: + save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}") + accelerator.save_state(save_path) + logger.info(f"Saved state to {save_path}") + + logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0], "step": global_step} + if args.use_ema: + logs["ema_decay"] = ema_model.cur_decay_value + progress_bar.set_postfix(**logs) + accelerator.log(logs, step=global_step) + progress_bar.close() + + accelerator.wait_for_everyone() + + # Generate sample images for visual inspection + if accelerator.is_main_process: + if epoch % args.save_images_epochs == 0 or epoch == args.num_epochs - 1: + unet = accelerator.unwrap_model(model) + if args.use_ema: + ema_model.copy_to(unet.parameters()) + pipeline = DDPMPipeline( + unet=unet, + scheduler=noise_scheduler, + ) + + generator = torch.Generator(device=pipeline.device).manual_seed(0) + # run pipeline in inference (sample random noise and denoise) + images = pipeline( + generator=generator, + batch_size=args.eval_batch_size, + num_inference_steps=args.ddpm_num_inference_steps, + output_type="numpy", + ).images + + # denormalize the images and save to tensorboard + images_processed = (images * 255).round().astype("uint8") + + if args.logger == "tensorboard" and is_tensorboard_available(): + accelerator.get_tracker("tensorboard").add_images( + "test_samples", images_processed.transpose(0, 3, 1, 2), epoch + ) + + if epoch % args.save_model_epochs == 0 or epoch == args.num_epochs - 1: + # save the model + pipeline.save_pretrained(args.output_dir) + if args.push_to_hub: + repo.push_to_hub(commit_message=f"Epoch {epoch}", blocking=False) + + accelerator.end_training() + + +if __name__ == "__main__": + args = parse_args() + main(args) diff --git a/diffusers/pyproject.toml b/diffusers/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..5ec7ae51be1569662acca78f7ffd75e78fb34998 --- /dev/null +++ b/diffusers/pyproject.toml @@ -0,0 +1,18 @@ +[tool.black] +line-length = 119 +target-version = ['py37'] + +[tool.ruff] +# Never enforce `E501` (line length violations). +ignore = ["E501", "E741", "W605"] +select = ["E", "F", "I", "W"] +line-length = 119 + +# Ignore import violations in all `__init__.py` files. +[tool.ruff.per-file-ignores] +"__init__.py" = ["E402", "F401", "F403", "F811"] +"src/diffusers/utils/dummy_*.py" = ["F401"] + +[tool.ruff.isort] +lines-after-imports = 2 +known-first-party = ["diffusers"] diff --git a/diffusers/scripts/__init__.py b/diffusers/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/scripts/change_naming_configs_and_checkpoints.py b/diffusers/scripts/change_naming_configs_and_checkpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..685f7681a326233cec90c6de88ad57d71aae0205 --- /dev/null +++ b/diffusers/scripts/change_naming_configs_and_checkpoints.py @@ -0,0 +1,113 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the LDM checkpoints. """ + +import argparse +import json +import os + +import torch +from transformers.file_utils import has_file + +from diffusers import UNet2DConditionModel, UNet2DModel + + +do_only_config = False +do_only_weights = True +do_only_renaming = False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--repo_path", + default=None, + type=str, + required=True, + help="The config json file corresponding to the architecture.", + ) + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + args = parser.parse_args() + + config_parameters_to_change = { + "image_size": "sample_size", + "num_res_blocks": "layers_per_block", + "block_channels": "block_out_channels", + "down_blocks": "down_block_types", + "up_blocks": "up_block_types", + "downscale_freq_shift": "freq_shift", + "resnet_num_groups": "norm_num_groups", + "resnet_act_fn": "act_fn", + "resnet_eps": "norm_eps", + "num_head_channels": "attention_head_dim", + } + + key_parameters_to_change = { + "time_steps": "time_proj", + "mid": "mid_block", + "downsample_blocks": "down_blocks", + "upsample_blocks": "up_blocks", + } + + subfolder = "" if has_file(args.repo_path, "config.json") else "unet" + + with open(os.path.join(args.repo_path, subfolder, "config.json"), "r", encoding="utf-8") as reader: + text = reader.read() + config = json.loads(text) + + if do_only_config: + for key in config_parameters_to_change.keys(): + config.pop(key, None) + + if has_file(args.repo_path, "config.json"): + model = UNet2DModel(**config) + else: + class_name = UNet2DConditionModel if "ldm-text2im-large-256" in args.repo_path else UNet2DModel + model = class_name(**config) + + if do_only_config: + model.save_config(os.path.join(args.repo_path, subfolder)) + + config = dict(model.config) + + if do_only_renaming: + for key, value in config_parameters_to_change.items(): + if key in config: + config[value] = config[key] + del config[key] + + config["down_block_types"] = [k.replace("UNetRes", "") for k in config["down_block_types"]] + config["up_block_types"] = [k.replace("UNetRes", "") for k in config["up_block_types"]] + + if do_only_weights: + state_dict = torch.load(os.path.join(args.repo_path, subfolder, "diffusion_pytorch_model.bin")) + + new_state_dict = {} + for param_key, param_value in state_dict.items(): + if param_key.endswith(".op.bias") or param_key.endswith(".op.weight"): + continue + has_changed = False + for key, new_key in key_parameters_to_change.items(): + if not has_changed and param_key.split(".")[0] == key: + new_state_dict[".".join([new_key] + param_key.split(".")[1:])] = param_value + has_changed = True + if not has_changed: + new_state_dict[param_key] = param_value + + model.load_state_dict(new_state_dict) + model.save_pretrained(os.path.join(args.repo_path, subfolder)) diff --git a/diffusers/scripts/conversion_ldm_uncond.py b/diffusers/scripts/conversion_ldm_uncond.py new file mode 100644 index 0000000000000000000000000000000000000000..d2ebb3934b6696fd427c9bf09eb051cf7befe7f4 --- /dev/null +++ b/diffusers/scripts/conversion_ldm_uncond.py @@ -0,0 +1,56 @@ +import argparse + +import OmegaConf +import torch + +from diffusers import DDIMScheduler, LDMPipeline, UNetLDMModel, VQModel + + +def convert_ldm_original(checkpoint_path, config_path, output_path): + config = OmegaConf.load(config_path) + state_dict = torch.load(checkpoint_path, map_location="cpu")["model"] + keys = list(state_dict.keys()) + + # extract state_dict for VQVAE + first_stage_dict = {} + first_stage_key = "first_stage_model." + for key in keys: + if key.startswith(first_stage_key): + first_stage_dict[key.replace(first_stage_key, "")] = state_dict[key] + + # extract state_dict for UNetLDM + unet_state_dict = {} + unet_key = "model.diffusion_model." + for key in keys: + if key.startswith(unet_key): + unet_state_dict[key.replace(unet_key, "")] = state_dict[key] + + vqvae_init_args = config.model.params.first_stage_config.params + unet_init_args = config.model.params.unet_config.params + + vqvae = VQModel(**vqvae_init_args).eval() + vqvae.load_state_dict(first_stage_dict) + + unet = UNetLDMModel(**unet_init_args).eval() + unet.load_state_dict(unet_state_dict) + + noise_scheduler = DDIMScheduler( + timesteps=config.model.params.timesteps, + beta_schedule="scaled_linear", + beta_start=config.model.params.linear_start, + beta_end=config.model.params.linear_end, + clip_sample=False, + ) + + pipeline = LDMPipeline(vqvae, unet, noise_scheduler) + pipeline.save_pretrained(output_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--checkpoint_path", type=str, required=True) + parser.add_argument("--config_path", type=str, required=True) + parser.add_argument("--output_path", type=str, required=True) + args = parser.parse_args() + + convert_ldm_original(args.checkpoint_path, args.config_path, args.output_path) diff --git a/diffusers/scripts/convert_dance_diffusion_to_diffusers.py b/diffusers/scripts/convert_dance_diffusion_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..d53d1f792e89be30e26cd701c178083e94699f00 --- /dev/null +++ b/diffusers/scripts/convert_dance_diffusion_to_diffusers.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +import argparse +import math +import os +from copy import deepcopy + +import torch +from audio_diffusion.models import DiffusionAttnUnet1D +from diffusion import sampling +from torch import nn + +from diffusers import DanceDiffusionPipeline, IPNDMScheduler, UNet1DModel + + +MODELS_MAP = { + "gwf-440k": { + "url": "https://model-server.zqevans2.workers.dev/gwf-440k.ckpt", + "sample_rate": 48000, + "sample_size": 65536, + }, + "jmann-small-190k": { + "url": "https://model-server.zqevans2.workers.dev/jmann-small-190k.ckpt", + "sample_rate": 48000, + "sample_size": 65536, + }, + "jmann-large-580k": { + "url": "https://model-server.zqevans2.workers.dev/jmann-large-580k.ckpt", + "sample_rate": 48000, + "sample_size": 131072, + }, + "maestro-uncond-150k": { + "url": "https://model-server.zqevans2.workers.dev/maestro-uncond-150k.ckpt", + "sample_rate": 16000, + "sample_size": 65536, + }, + "unlocked-uncond-250k": { + "url": "https://model-server.zqevans2.workers.dev/unlocked-uncond-250k.ckpt", + "sample_rate": 16000, + "sample_size": 65536, + }, + "honk-140k": { + "url": "https://model-server.zqevans2.workers.dev/honk-140k.ckpt", + "sample_rate": 16000, + "sample_size": 65536, + }, +} + + +def alpha_sigma_to_t(alpha, sigma): + """Returns a timestep, given the scaling factors for the clean image and for + the noise.""" + return torch.atan2(sigma, alpha) / math.pi * 2 + + +def get_crash_schedule(t): + sigma = torch.sin(t * math.pi / 2) ** 2 + alpha = (1 - sigma**2) ** 0.5 + return alpha_sigma_to_t(alpha, sigma) + + +class Object(object): + pass + + +class DiffusionUncond(nn.Module): + def __init__(self, global_args): + super().__init__() + + self.diffusion = DiffusionAttnUnet1D(global_args, n_attn_layers=4) + self.diffusion_ema = deepcopy(self.diffusion) + self.rng = torch.quasirandom.SobolEngine(1, scramble=True) + + +def download(model_name): + url = MODELS_MAP[model_name]["url"] + os.system(f"wget {url} ./") + + return f"./{model_name}.ckpt" + + +DOWN_NUM_TO_LAYER = { + "1": "resnets.0", + "2": "attentions.0", + "3": "resnets.1", + "4": "attentions.1", + "5": "resnets.2", + "6": "attentions.2", +} +UP_NUM_TO_LAYER = { + "8": "resnets.0", + "9": "attentions.0", + "10": "resnets.1", + "11": "attentions.1", + "12": "resnets.2", + "13": "attentions.2", +} +MID_NUM_TO_LAYER = { + "1": "resnets.0", + "2": "attentions.0", + "3": "resnets.1", + "4": "attentions.1", + "5": "resnets.2", + "6": "attentions.2", + "8": "resnets.3", + "9": "attentions.3", + "10": "resnets.4", + "11": "attentions.4", + "12": "resnets.5", + "13": "attentions.5", +} +DEPTH_0_TO_LAYER = { + "0": "resnets.0", + "1": "resnets.1", + "2": "resnets.2", + "4": "resnets.0", + "5": "resnets.1", + "6": "resnets.2", +} + +RES_CONV_MAP = { + "skip": "conv_skip", + "main.0": "conv_1", + "main.1": "group_norm_1", + "main.3": "conv_2", + "main.4": "group_norm_2", +} + +ATTN_MAP = { + "norm": "group_norm", + "qkv_proj": ["query", "key", "value"], + "out_proj": ["proj_attn"], +} + + +def convert_resconv_naming(name): + if name.startswith("skip"): + return name.replace("skip", RES_CONV_MAP["skip"]) + + # name has to be of format main.{digit} + if not name.startswith("main."): + raise ValueError(f"ResConvBlock error with {name}") + + return name.replace(name[:6], RES_CONV_MAP[name[:6]]) + + +def convert_attn_naming(name): + for key, value in ATTN_MAP.items(): + if name.startswith(key) and not isinstance(value, list): + return name.replace(key, value) + elif name.startswith(key): + return [name.replace(key, v) for v in value] + raise ValueError(f"Attn error with {name}") + + +def rename(input_string, max_depth=13): + string = input_string + + if string.split(".")[0] == "timestep_embed": + return string.replace("timestep_embed", "time_proj") + + depth = 0 + if string.startswith("net.3."): + depth += 1 + string = string[6:] + elif string.startswith("net."): + string = string[4:] + + while string.startswith("main.7."): + depth += 1 + string = string[7:] + + if string.startswith("main."): + string = string[5:] + + # mid block + if string[:2].isdigit(): + layer_num = string[:2] + string_left = string[2:] + else: + layer_num = string[0] + string_left = string[1:] + + if depth == max_depth: + new_layer = MID_NUM_TO_LAYER[layer_num] + prefix = "mid_block" + elif depth > 0 and int(layer_num) < 7: + new_layer = DOWN_NUM_TO_LAYER[layer_num] + prefix = f"down_blocks.{depth}" + elif depth > 0 and int(layer_num) > 7: + new_layer = UP_NUM_TO_LAYER[layer_num] + prefix = f"up_blocks.{max_depth - depth - 1}" + elif depth == 0: + new_layer = DEPTH_0_TO_LAYER[layer_num] + prefix = f"up_blocks.{max_depth - 1}" if int(layer_num) > 3 else "down_blocks.0" + + if not string_left.startswith("."): + raise ValueError(f"Naming error with {input_string} and string_left: {string_left}.") + + string_left = string_left[1:] + + if "resnets" in new_layer: + string_left = convert_resconv_naming(string_left) + elif "attentions" in new_layer: + new_string_left = convert_attn_naming(string_left) + string_left = new_string_left + + if not isinstance(string_left, list): + new_string = prefix + "." + new_layer + "." + string_left + else: + new_string = [prefix + "." + new_layer + "." + s for s in string_left] + return new_string + + +def rename_orig_weights(state_dict): + new_state_dict = {} + for k, v in state_dict.items(): + if k.endswith("kernel"): + # up- and downsample layers, don't have trainable weights + continue + + new_k = rename(k) + + # check if we need to transform from Conv => Linear for attention + if isinstance(new_k, list): + new_state_dict = transform_conv_attns(new_state_dict, new_k, v) + else: + new_state_dict[new_k] = v + + return new_state_dict + + +def transform_conv_attns(new_state_dict, new_k, v): + if len(new_k) == 1: + if len(v.shape) == 3: + # weight + new_state_dict[new_k[0]] = v[:, :, 0] + else: + # bias + new_state_dict[new_k[0]] = v + else: + # qkv matrices + trippled_shape = v.shape[0] + single_shape = trippled_shape // 3 + for i in range(3): + if len(v.shape) == 3: + new_state_dict[new_k[i]] = v[i * single_shape : (i + 1) * single_shape, :, 0] + else: + new_state_dict[new_k[i]] = v[i * single_shape : (i + 1) * single_shape] + return new_state_dict + + +def main(args): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + model_name = args.model_path.split("/")[-1].split(".")[0] + if not os.path.isfile(args.model_path): + assert ( + model_name == args.model_path + ), f"Make sure to provide one of the official model names {MODELS_MAP.keys()}" + args.model_path = download(model_name) + + sample_rate = MODELS_MAP[model_name]["sample_rate"] + sample_size = MODELS_MAP[model_name]["sample_size"] + + config = Object() + config.sample_size = sample_size + config.sample_rate = sample_rate + config.latent_dim = 0 + + diffusers_model = UNet1DModel(sample_size=sample_size, sample_rate=sample_rate) + diffusers_state_dict = diffusers_model.state_dict() + + orig_model = DiffusionUncond(config) + orig_model.load_state_dict(torch.load(args.model_path, map_location=device)["state_dict"]) + orig_model = orig_model.diffusion_ema.eval() + orig_model_state_dict = orig_model.state_dict() + renamed_state_dict = rename_orig_weights(orig_model_state_dict) + + renamed_minus_diffusers = set(renamed_state_dict.keys()) - set(diffusers_state_dict.keys()) + diffusers_minus_renamed = set(diffusers_state_dict.keys()) - set(renamed_state_dict.keys()) + + assert len(renamed_minus_diffusers) == 0, f"Problem with {renamed_minus_diffusers}" + assert all(k.endswith("kernel") for k in list(diffusers_minus_renamed)), f"Problem with {diffusers_minus_renamed}" + + for key, value in renamed_state_dict.items(): + assert ( + diffusers_state_dict[key].squeeze().shape == value.squeeze().shape + ), f"Shape for {key} doesn't match. Diffusers: {diffusers_state_dict[key].shape} vs. {value.shape}" + if key == "time_proj.weight": + value = value.squeeze() + + diffusers_state_dict[key] = value + + diffusers_model.load_state_dict(diffusers_state_dict) + + steps = 100 + seed = 33 + + diffusers_scheduler = IPNDMScheduler(num_train_timesteps=steps) + + generator = torch.manual_seed(seed) + noise = torch.randn([1, 2, config.sample_size], generator=generator).to(device) + + t = torch.linspace(1, 0, steps + 1, device=device)[:-1] + step_list = get_crash_schedule(t) + + pipe = DanceDiffusionPipeline(unet=diffusers_model, scheduler=diffusers_scheduler) + + generator = torch.manual_seed(33) + audio = pipe(num_inference_steps=steps, generator=generator).audios + + generated = sampling.iplms_sample(orig_model, noise, step_list, {}) + generated = generated.clamp(-1, 1) + + diff_sum = (generated - audio).abs().sum() + diff_max = (generated - audio).abs().max() + + if args.save: + pipe.save_pretrained(args.checkpoint_path) + + print("Diff sum", diff_sum) + print("Diff max", diff_max) + + assert diff_max < 1e-3, f"Diff max: {diff_max} is too much :-/" + + print(f"Conversion for {model_name} successful!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--model_path", default=None, type=str, required=True, help="Path to the model to convert.") + parser.add_argument( + "--save", default=True, type=bool, required=False, help="Whether to save the converted model or not." + ) + parser.add_argument("--checkpoint_path", default=None, type=str, required=True, help="Path to the output model.") + args = parser.parse_args() + + main(args) diff --git a/diffusers/scripts/convert_ddpm_original_checkpoint_to_diffusers.py b/diffusers/scripts/convert_ddpm_original_checkpoint_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..4222327c23de1c55518d11fb61caac23ecd22a6b --- /dev/null +++ b/diffusers/scripts/convert_ddpm_original_checkpoint_to_diffusers.py @@ -0,0 +1,431 @@ +import argparse +import json + +import torch + +from diffusers import AutoencoderKL, DDPMPipeline, DDPMScheduler, UNet2DModel, VQModel + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + mapping = [] + for old_item in old_list: + new_item = old_item + new_item = new_item.replace("block.", "resnets.") + new_item = new_item.replace("conv_shorcut", "conv1") + new_item = new_item.replace("in_shortcut", "conv_shortcut") + new_item = new_item.replace("temb_proj", "time_emb_proj") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0, in_mid=False): + mapping = [] + for old_item in old_list: + new_item = old_item + + # In `model.mid`, the layer is called `attn`. + if not in_mid: + new_item = new_item.replace("attn", "attentions") + new_item = new_item.replace(".k.", ".key.") + new_item = new_item.replace(".v.", ".value.") + new_item = new_item.replace(".q.", ".query.") + + new_item = new_item.replace("proj_out", "proj_attn") + new_item = new_item.replace("norm", "group_norm") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + if attention_paths_to_split is not None: + if config is None: + raise ValueError("Please specify the config if setting 'attention_paths_to_split' to 'True'.") + + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config.get("num_head_channels", 1) // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape).squeeze() + checkpoint[path_map["key"]] = key.reshape(target_shape).squeeze() + checkpoint[path_map["value"]] = value.reshape(target_shape).squeeze() + + for path in paths: + new_path = path["new"] + + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + new_path = new_path.replace("down.", "down_blocks.") + new_path = new_path.replace("up.", "up_blocks.") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + if "attentions" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]].squeeze() + else: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def convert_ddpm_checkpoint(checkpoint, config): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = checkpoint["temb.dense.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = checkpoint["temb.dense.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = checkpoint["temb.dense.1.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = checkpoint["temb.dense.1.bias"] + + new_checkpoint["conv_norm_out.weight"] = checkpoint["norm_out.weight"] + new_checkpoint["conv_norm_out.bias"] = checkpoint["norm_out.bias"] + + new_checkpoint["conv_in.weight"] = checkpoint["conv_in.weight"] + new_checkpoint["conv_in.bias"] = checkpoint["conv_in.bias"] + new_checkpoint["conv_out.weight"] = checkpoint["conv_out.weight"] + new_checkpoint["conv_out.bias"] = checkpoint["conv_out.bias"] + + num_down_blocks = len({".".join(layer.split(".")[:2]) for layer in checkpoint if "down" in layer}) + down_blocks = { + layer_id: [key for key in checkpoint if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + num_up_blocks = len({".".join(layer.split(".")[:2]) for layer in checkpoint if "up" in layer}) + up_blocks = {layer_id: [key for key in checkpoint if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks)} + + for i in range(num_down_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + + if any("downsample" in layer for layer in down_blocks[i]): + new_checkpoint[f"down_blocks.{i}.downsamplers.0.conv.weight"] = checkpoint[ + f"down.{i}.downsample.op.weight" + ] + new_checkpoint[f"down_blocks.{i}.downsamplers.0.conv.bias"] = checkpoint[f"down.{i}.downsample.op.bias"] + # new_checkpoint[f'down_blocks.{i}.downsamplers.0.op.weight'] = checkpoint[f'down.{i}.downsample.conv.weight'] + # new_checkpoint[f'down_blocks.{i}.downsamplers.0.op.bias'] = checkpoint[f'down.{i}.downsample.conv.bias'] + + if any("block" in layer for layer in down_blocks[i]): + num_blocks = len( + {".".join(shave_segments(layer, 2).split(".")[:2]) for layer in down_blocks[i] if "block" in layer} + ) + blocks = { + layer_id: [key for key in down_blocks[i] if f"block.{layer_id}" in key] + for layer_id in range(num_blocks) + } + + if num_blocks > 0: + for j in range(config["layers_per_block"]): + paths = renew_resnet_paths(blocks[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint) + + if any("attn" in layer for layer in down_blocks[i]): + num_attn = len( + {".".join(shave_segments(layer, 2).split(".")[:2]) for layer in down_blocks[i] if "attn" in layer} + ) + attns = { + layer_id: [key for key in down_blocks[i] if f"attn.{layer_id}" in key] + for layer_id in range(num_blocks) + } + + if num_attn > 0: + for j in range(config["layers_per_block"]): + paths = renew_attention_paths(attns[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, config=config) + + mid_block_1_layers = [key for key in checkpoint if "mid.block_1" in key] + mid_block_2_layers = [key for key in checkpoint if "mid.block_2" in key] + mid_attn_1_layers = [key for key in checkpoint if "mid.attn_1" in key] + + # Mid new 2 + paths = renew_resnet_paths(mid_block_1_layers) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "block_1", "new": "resnets.0"}], + ) + + paths = renew_resnet_paths(mid_block_2_layers) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "block_2", "new": "resnets.1"}], + ) + + paths = renew_attention_paths(mid_attn_1_layers, in_mid=True) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "attn_1", "new": "attentions.0"}], + ) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + + if any("upsample" in layer for layer in up_blocks[i]): + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = checkpoint[ + f"up.{i}.upsample.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = checkpoint[f"up.{i}.upsample.conv.bias"] + + if any("block" in layer for layer in up_blocks[i]): + num_blocks = len( + {".".join(shave_segments(layer, 2).split(".")[:2]) for layer in up_blocks[i] if "block" in layer} + ) + blocks = { + layer_id: [key for key in up_blocks[i] if f"block.{layer_id}" in key] for layer_id in range(num_blocks) + } + + if num_blocks > 0: + for j in range(config["layers_per_block"] + 1): + replace_indices = {"old": f"up_blocks.{i}", "new": f"up_blocks.{block_id}"} + paths = renew_resnet_paths(blocks[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, additional_replacements=[replace_indices]) + + if any("attn" in layer for layer in up_blocks[i]): + num_attn = len( + {".".join(shave_segments(layer, 2).split(".")[:2]) for layer in up_blocks[i] if "attn" in layer} + ) + attns = { + layer_id: [key for key in up_blocks[i] if f"attn.{layer_id}" in key] for layer_id in range(num_blocks) + } + + if num_attn > 0: + for j in range(config["layers_per_block"] + 1): + replace_indices = {"old": f"up_blocks.{i}", "new": f"up_blocks.{block_id}"} + paths = renew_attention_paths(attns[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, additional_replacements=[replace_indices]) + + new_checkpoint = {k.replace("mid_new_2", "mid_block"): v for k, v in new_checkpoint.items()} + return new_checkpoint + + +def convert_vq_autoenc_checkpoint(checkpoint, config): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + new_checkpoint = {} + + new_checkpoint["encoder.conv_norm_out.weight"] = checkpoint["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = checkpoint["encoder.norm_out.bias"] + + new_checkpoint["encoder.conv_in.weight"] = checkpoint["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = checkpoint["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = checkpoint["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = checkpoint["encoder.conv_out.bias"] + + new_checkpoint["decoder.conv_norm_out.weight"] = checkpoint["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = checkpoint["decoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = checkpoint["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = checkpoint["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = checkpoint["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = checkpoint["decoder.conv_out.bias"] + + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in checkpoint if "down" in layer}) + down_blocks = { + layer_id: [key for key in checkpoint if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in checkpoint if "up" in layer}) + up_blocks = {layer_id: [key for key in checkpoint if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks)} + + for i in range(num_down_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + + if any("downsample" in layer for layer in down_blocks[i]): + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = checkpoint[ + f"encoder.down.{i}.downsample.conv.weight" + ] + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = checkpoint[ + f"encoder.down.{i}.downsample.conv.bias" + ] + + if any("block" in layer for layer in down_blocks[i]): + num_blocks = len( + {".".join(shave_segments(layer, 3).split(".")[:3]) for layer in down_blocks[i] if "block" in layer} + ) + blocks = { + layer_id: [key for key in down_blocks[i] if f"block.{layer_id}" in key] + for layer_id in range(num_blocks) + } + + if num_blocks > 0: + for j in range(config["layers_per_block"]): + paths = renew_resnet_paths(blocks[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint) + + if any("attn" in layer for layer in down_blocks[i]): + num_attn = len( + {".".join(shave_segments(layer, 3).split(".")[:3]) for layer in down_blocks[i] if "attn" in layer} + ) + attns = { + layer_id: [key for key in down_blocks[i] if f"attn.{layer_id}" in key] + for layer_id in range(num_blocks) + } + + if num_attn > 0: + for j in range(config["layers_per_block"]): + paths = renew_attention_paths(attns[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, config=config) + + mid_block_1_layers = [key for key in checkpoint if "mid.block_1" in key] + mid_block_2_layers = [key for key in checkpoint if "mid.block_2" in key] + mid_attn_1_layers = [key for key in checkpoint if "mid.attn_1" in key] + + # Mid new 2 + paths = renew_resnet_paths(mid_block_1_layers) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "block_1", "new": "resnets.0"}], + ) + + paths = renew_resnet_paths(mid_block_2_layers) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "block_2", "new": "resnets.1"}], + ) + + paths = renew_attention_paths(mid_attn_1_layers, in_mid=True) + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[{"old": "mid.", "new": "mid_new_2."}, {"old": "attn_1", "new": "attentions.0"}], + ) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + + if any("upsample" in layer for layer in up_blocks[i]): + new_checkpoint[f"decoder.up_blocks.{block_id}.upsamplers.0.conv.weight"] = checkpoint[ + f"decoder.up.{i}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{block_id}.upsamplers.0.conv.bias"] = checkpoint[ + f"decoder.up.{i}.upsample.conv.bias" + ] + + if any("block" in layer for layer in up_blocks[i]): + num_blocks = len( + {".".join(shave_segments(layer, 3).split(".")[:3]) for layer in up_blocks[i] if "block" in layer} + ) + blocks = { + layer_id: [key for key in up_blocks[i] if f"block.{layer_id}" in key] for layer_id in range(num_blocks) + } + + if num_blocks > 0: + for j in range(config["layers_per_block"] + 1): + replace_indices = {"old": f"up_blocks.{i}", "new": f"up_blocks.{block_id}"} + paths = renew_resnet_paths(blocks[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, additional_replacements=[replace_indices]) + + if any("attn" in layer for layer in up_blocks[i]): + num_attn = len( + {".".join(shave_segments(layer, 3).split(".")[:3]) for layer in up_blocks[i] if "attn" in layer} + ) + attns = { + layer_id: [key for key in up_blocks[i] if f"attn.{layer_id}" in key] for layer_id in range(num_blocks) + } + + if num_attn > 0: + for j in range(config["layers_per_block"] + 1): + replace_indices = {"old": f"up_blocks.{i}", "new": f"up_blocks.{block_id}"} + paths = renew_attention_paths(attns[j]) + assign_to_checkpoint(paths, new_checkpoint, checkpoint, additional_replacements=[replace_indices]) + + new_checkpoint = {k.replace("mid_new_2", "mid_block"): v for k, v in new_checkpoint.items()} + new_checkpoint["quant_conv.weight"] = checkpoint["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = checkpoint["quant_conv.bias"] + if "quantize.embedding.weight" in checkpoint: + new_checkpoint["quantize.embedding.weight"] = checkpoint["quantize.embedding.weight"] + new_checkpoint["post_quant_conv.weight"] = checkpoint["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = checkpoint["post_quant_conv.bias"] + + return new_checkpoint + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--checkpoint_path", default=None, type=str, required=True, help="Path to the checkpoint to convert." + ) + + parser.add_argument( + "--config_file", + default=None, + type=str, + required=True, + help="The config json file corresponding to the architecture.", + ) + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + args = parser.parse_args() + checkpoint = torch.load(args.checkpoint_path) + + with open(args.config_file) as f: + config = json.loads(f.read()) + + # unet case + key_prefix_set = set(key.split(".")[0] for key in checkpoint.keys()) + if "encoder" in key_prefix_set and "decoder" in key_prefix_set: + converted_checkpoint = convert_vq_autoenc_checkpoint(checkpoint, config) + else: + converted_checkpoint = convert_ddpm_checkpoint(checkpoint, config) + + if "ddpm" in config: + del config["ddpm"] + + if config["_class_name"] == "VQModel": + model = VQModel(**config) + model.load_state_dict(converted_checkpoint) + model.save_pretrained(args.dump_path) + elif config["_class_name"] == "AutoencoderKL": + model = AutoencoderKL(**config) + model.load_state_dict(converted_checkpoint) + model.save_pretrained(args.dump_path) + else: + model = UNet2DModel(**config) + model.load_state_dict(converted_checkpoint) + + scheduler = DDPMScheduler.from_config("/".join(args.checkpoint_path.split("/")[:-1])) + + pipe = DDPMPipeline(unet=model, scheduler=scheduler) + pipe.save_pretrained(args.dump_path) diff --git a/diffusers/scripts/convert_diffusers_to_original_stable_diffusion.py b/diffusers/scripts/convert_diffusers_to_original_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..9da45211551e32acf34c883c1d6c5218a7bd6dd7 --- /dev/null +++ b/diffusers/scripts/convert_diffusers_to_original_stable_diffusion.py @@ -0,0 +1,333 @@ +# Script for converting a HF Diffusers saved pipeline to a Stable Diffusion checkpoint. +# *Only* converts the UNet, VAE, and Text Encoder. +# Does not convert optimizer state or any other thing. + +import argparse +import os.path as osp +import re + +import torch +from safetensors.torch import load_file, save_file + + +# =================# +# UNet Conversion # +# =================# + +unet_conversion_map = [ + # (stable-diffusion, HF Diffusers) + ("time_embed.0.weight", "time_embedding.linear_1.weight"), + ("time_embed.0.bias", "time_embedding.linear_1.bias"), + ("time_embed.2.weight", "time_embedding.linear_2.weight"), + ("time_embed.2.bias", "time_embedding.linear_2.bias"), + ("input_blocks.0.0.weight", "conv_in.weight"), + ("input_blocks.0.0.bias", "conv_in.bias"), + ("out.0.weight", "conv_norm_out.weight"), + ("out.0.bias", "conv_norm_out.bias"), + ("out.2.weight", "conv_out.weight"), + ("out.2.bias", "conv_out.bias"), +] + +unet_conversion_map_resnet = [ + # (stable-diffusion, HF Diffusers) + ("in_layers.0", "norm1"), + ("in_layers.2", "conv1"), + ("out_layers.0", "norm2"), + ("out_layers.3", "conv2"), + ("emb_layers.1", "time_emb_proj"), + ("skip_connection", "conv_shortcut"), +] + +unet_conversion_map_layer = [] +# hardcoded number of downblocks and resnets/attentions... +# would need smarter logic for other networks. +for i in range(4): + # loop over downblocks/upblocks + + for j in range(2): + # loop over resnets/attentions for downblocks + hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}." + sd_down_res_prefix = f"input_blocks.{3*i + j + 1}.0." + unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix)) + + if i < 3: + # no attention layers in down_blocks.3 + hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}." + sd_down_atn_prefix = f"input_blocks.{3*i + j + 1}.1." + unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix)) + + for j in range(3): + # loop over resnets/attentions for upblocks + hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}." + sd_up_res_prefix = f"output_blocks.{3*i + j}.0." + unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix)) + + if i > 0: + # no attention layers in up_blocks.0 + hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}." + sd_up_atn_prefix = f"output_blocks.{3*i + j}.1." + unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix)) + + if i < 3: + # no downsample in down_blocks.3 + hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv." + sd_downsample_prefix = f"input_blocks.{3*(i+1)}.0.op." + unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix)) + + # no upsample in up_blocks.3 + hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0." + sd_upsample_prefix = f"output_blocks.{3*i + 2}.{1 if i == 0 else 2}." + unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix)) + +hf_mid_atn_prefix = "mid_block.attentions.0." +sd_mid_atn_prefix = "middle_block.1." +unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix)) + +for j in range(2): + hf_mid_res_prefix = f"mid_block.resnets.{j}." + sd_mid_res_prefix = f"middle_block.{2*j}." + unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix)) + + +def convert_unet_state_dict(unet_state_dict): + # buyer beware: this is a *brittle* function, + # and correct output requires that all of these pieces interact in + # the exact order in which I have arranged them. + mapping = {k: k for k in unet_state_dict.keys()} + for sd_name, hf_name in unet_conversion_map: + mapping[hf_name] = sd_name + for k, v in mapping.items(): + if "resnets" in k: + for sd_part, hf_part in unet_conversion_map_resnet: + v = v.replace(hf_part, sd_part) + mapping[k] = v + for k, v in mapping.items(): + for sd_part, hf_part in unet_conversion_map_layer: + v = v.replace(hf_part, sd_part) + mapping[k] = v + new_state_dict = {v: unet_state_dict[k] for k, v in mapping.items()} + return new_state_dict + + +# ================# +# VAE Conversion # +# ================# + +vae_conversion_map = [ + # (stable-diffusion, HF Diffusers) + ("nin_shortcut", "conv_shortcut"), + ("norm_out", "conv_norm_out"), + ("mid.attn_1.", "mid_block.attentions.0."), +] + +for i in range(4): + # down_blocks have two resnets + for j in range(2): + hf_down_prefix = f"encoder.down_blocks.{i}.resnets.{j}." + sd_down_prefix = f"encoder.down.{i}.block.{j}." + vae_conversion_map.append((sd_down_prefix, hf_down_prefix)) + + if i < 3: + hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0." + sd_downsample_prefix = f"down.{i}.downsample." + vae_conversion_map.append((sd_downsample_prefix, hf_downsample_prefix)) + + hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0." + sd_upsample_prefix = f"up.{3-i}.upsample." + vae_conversion_map.append((sd_upsample_prefix, hf_upsample_prefix)) + + # up_blocks have three resnets + # also, up blocks in hf are numbered in reverse from sd + for j in range(3): + hf_up_prefix = f"decoder.up_blocks.{i}.resnets.{j}." + sd_up_prefix = f"decoder.up.{3-i}.block.{j}." + vae_conversion_map.append((sd_up_prefix, hf_up_prefix)) + +# this part accounts for mid blocks in both the encoder and the decoder +for i in range(2): + hf_mid_res_prefix = f"mid_block.resnets.{i}." + sd_mid_res_prefix = f"mid.block_{i+1}." + vae_conversion_map.append((sd_mid_res_prefix, hf_mid_res_prefix)) + + +vae_conversion_map_attn = [ + # (stable-diffusion, HF Diffusers) + ("norm.", "group_norm."), + ("q.", "query."), + ("k.", "key."), + ("v.", "value."), + ("proj_out.", "proj_attn."), +] + + +def reshape_weight_for_sd(w): + # convert HF linear weights to SD conv2d weights + return w.reshape(*w.shape, 1, 1) + + +def convert_vae_state_dict(vae_state_dict): + mapping = {k: k for k in vae_state_dict.keys()} + for k, v in mapping.items(): + for sd_part, hf_part in vae_conversion_map: + v = v.replace(hf_part, sd_part) + mapping[k] = v + for k, v in mapping.items(): + if "attentions" in k: + for sd_part, hf_part in vae_conversion_map_attn: + v = v.replace(hf_part, sd_part) + mapping[k] = v + new_state_dict = {v: vae_state_dict[k] for k, v in mapping.items()} + weights_to_convert = ["q", "k", "v", "proj_out"] + for k, v in new_state_dict.items(): + for weight_name in weights_to_convert: + if f"mid.attn_1.{weight_name}.weight" in k: + print(f"Reshaping {k} for SD format") + new_state_dict[k] = reshape_weight_for_sd(v) + return new_state_dict + + +# =========================# +# Text Encoder Conversion # +# =========================# + + +textenc_conversion_lst = [ + # (stable-diffusion, HF Diffusers) + ("resblocks.", "text_model.encoder.layers."), + ("ln_1", "layer_norm1"), + ("ln_2", "layer_norm2"), + (".c_fc.", ".fc1."), + (".c_proj.", ".fc2."), + (".attn", ".self_attn"), + ("ln_final.", "transformer.text_model.final_layer_norm."), + ("token_embedding.weight", "transformer.text_model.embeddings.token_embedding.weight"), + ("positional_embedding", "transformer.text_model.embeddings.position_embedding.weight"), +] +protected = {re.escape(x[1]): x[0] for x in textenc_conversion_lst} +textenc_pattern = re.compile("|".join(protected.keys())) + +# Ordering is from https://github.com/pytorch/pytorch/blob/master/test/cpp/api/modules.cpp +code2idx = {"q": 0, "k": 1, "v": 2} + + +def convert_text_enc_state_dict_v20(text_enc_dict): + new_state_dict = {} + capture_qkv_weight = {} + capture_qkv_bias = {} + for k, v in text_enc_dict.items(): + if ( + k.endswith(".self_attn.q_proj.weight") + or k.endswith(".self_attn.k_proj.weight") + or k.endswith(".self_attn.v_proj.weight") + ): + k_pre = k[: -len(".q_proj.weight")] + k_code = k[-len("q_proj.weight")] + if k_pre not in capture_qkv_weight: + capture_qkv_weight[k_pre] = [None, None, None] + capture_qkv_weight[k_pre][code2idx[k_code]] = v + continue + + if ( + k.endswith(".self_attn.q_proj.bias") + or k.endswith(".self_attn.k_proj.bias") + or k.endswith(".self_attn.v_proj.bias") + ): + k_pre = k[: -len(".q_proj.bias")] + k_code = k[-len("q_proj.bias")] + if k_pre not in capture_qkv_bias: + capture_qkv_bias[k_pre] = [None, None, None] + capture_qkv_bias[k_pre][code2idx[k_code]] = v + continue + + relabelled_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], k) + new_state_dict[relabelled_key] = v + + for k_pre, tensors in capture_qkv_weight.items(): + if None in tensors: + raise Exception("CORRUPTED MODEL: one of the q-k-v values for the text encoder was missing") + relabelled_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], k_pre) + new_state_dict[relabelled_key + ".in_proj_weight"] = torch.cat(tensors) + + for k_pre, tensors in capture_qkv_bias.items(): + if None in tensors: + raise Exception("CORRUPTED MODEL: one of the q-k-v values for the text encoder was missing") + relabelled_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], k_pre) + new_state_dict[relabelled_key + ".in_proj_bias"] = torch.cat(tensors) + + return new_state_dict + + +def convert_text_enc_state_dict(text_enc_dict): + return text_enc_dict + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--model_path", default=None, type=str, required=True, help="Path to the model to convert.") + parser.add_argument("--checkpoint_path", default=None, type=str, required=True, help="Path to the output model.") + parser.add_argument("--half", action="store_true", help="Save weights in half precision.") + parser.add_argument( + "--use_safetensors", action="store_true", help="Save weights use safetensors, default is ckpt." + ) + + args = parser.parse_args() + + assert args.model_path is not None, "Must provide a model path!" + + assert args.checkpoint_path is not None, "Must provide a checkpoint path!" + + # Path for safetensors + unet_path = osp.join(args.model_path, "unet", "diffusion_pytorch_model.safetensors") + vae_path = osp.join(args.model_path, "vae", "diffusion_pytorch_model.safetensors") + text_enc_path = osp.join(args.model_path, "text_encoder", "model.safetensors") + + # Load models from safetensors if it exists, if it doesn't pytorch + if osp.exists(unet_path): + unet_state_dict = load_file(unet_path, device="cpu") + else: + unet_path = osp.join(args.model_path, "unet", "diffusion_pytorch_model.bin") + unet_state_dict = torch.load(unet_path, map_location="cpu") + + if osp.exists(vae_path): + vae_state_dict = load_file(vae_path, device="cpu") + else: + vae_path = osp.join(args.model_path, "vae", "diffusion_pytorch_model.bin") + vae_state_dict = torch.load(vae_path, map_location="cpu") + + if osp.exists(text_enc_path): + text_enc_dict = load_file(text_enc_path, device="cpu") + else: + text_enc_path = osp.join(args.model_path, "text_encoder", "pytorch_model.bin") + text_enc_dict = torch.load(text_enc_path, map_location="cpu") + + # Convert the UNet model + unet_state_dict = convert_unet_state_dict(unet_state_dict) + unet_state_dict = {"model.diffusion_model." + k: v for k, v in unet_state_dict.items()} + + # Convert the VAE model + vae_state_dict = convert_vae_state_dict(vae_state_dict) + vae_state_dict = {"first_stage_model." + k: v for k, v in vae_state_dict.items()} + + # Easiest way to identify v2.0 model seems to be that the text encoder (OpenCLIP) is deeper + is_v20_model = "text_model.encoder.layers.22.layer_norm2.bias" in text_enc_dict + + if is_v20_model: + # Need to add the tag 'transformer' in advance so we can knock it out from the final layer-norm + text_enc_dict = {"transformer." + k: v for k, v in text_enc_dict.items()} + text_enc_dict = convert_text_enc_state_dict_v20(text_enc_dict) + text_enc_dict = {"cond_stage_model.model." + k: v for k, v in text_enc_dict.items()} + else: + text_enc_dict = convert_text_enc_state_dict(text_enc_dict) + text_enc_dict = {"cond_stage_model.transformer." + k: v for k, v in text_enc_dict.items()} + + # Put together new checkpoint + state_dict = {**unet_state_dict, **vae_state_dict, **text_enc_dict} + if args.half: + state_dict = {k: v.half() for k, v in state_dict.items()} + + if args.use_safetensors: + save_file(state_dict, args.checkpoint_path) + else: + state_dict = {"state_dict": state_dict} + torch.save(state_dict, args.checkpoint_path) diff --git a/diffusers/scripts/convert_dit_to_diffusers.py b/diffusers/scripts/convert_dit_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..dc127f69555c260f594e70444b1540faa196e3fb --- /dev/null +++ b/diffusers/scripts/convert_dit_to_diffusers.py @@ -0,0 +1,162 @@ +import argparse +import os + +import torch +from torchvision.datasets.utils import download_url + +from diffusers import AutoencoderKL, DDIMScheduler, DiTPipeline, Transformer2DModel + + +pretrained_models = {512: "DiT-XL-2-512x512.pt", 256: "DiT-XL-2-256x256.pt"} + + +def download_model(model_name): + """ + Downloads a pre-trained DiT model from the web. + """ + local_path = f"pretrained_models/{model_name}" + if not os.path.isfile(local_path): + os.makedirs("pretrained_models", exist_ok=True) + web_path = f"https://dl.fbaipublicfiles.com/DiT/models/{model_name}" + download_url(web_path, "pretrained_models") + model = torch.load(local_path, map_location=lambda storage, loc: storage) + return model + + +def main(args): + state_dict = download_model(pretrained_models[args.image_size]) + + state_dict["pos_embed.proj.weight"] = state_dict["x_embedder.proj.weight"] + state_dict["pos_embed.proj.bias"] = state_dict["x_embedder.proj.bias"] + state_dict.pop("x_embedder.proj.weight") + state_dict.pop("x_embedder.proj.bias") + + for depth in range(28): + state_dict[f"transformer_blocks.{depth}.norm1.emb.timestep_embedder.linear_1.weight"] = state_dict[ + "t_embedder.mlp.0.weight" + ] + state_dict[f"transformer_blocks.{depth}.norm1.emb.timestep_embedder.linear_1.bias"] = state_dict[ + "t_embedder.mlp.0.bias" + ] + state_dict[f"transformer_blocks.{depth}.norm1.emb.timestep_embedder.linear_2.weight"] = state_dict[ + "t_embedder.mlp.2.weight" + ] + state_dict[f"transformer_blocks.{depth}.norm1.emb.timestep_embedder.linear_2.bias"] = state_dict[ + "t_embedder.mlp.2.bias" + ] + state_dict[f"transformer_blocks.{depth}.norm1.emb.class_embedder.embedding_table.weight"] = state_dict[ + "y_embedder.embedding_table.weight" + ] + + state_dict[f"transformer_blocks.{depth}.norm1.linear.weight"] = state_dict[ + f"blocks.{depth}.adaLN_modulation.1.weight" + ] + state_dict[f"transformer_blocks.{depth}.norm1.linear.bias"] = state_dict[ + f"blocks.{depth}.adaLN_modulation.1.bias" + ] + + q, k, v = torch.chunk(state_dict[f"blocks.{depth}.attn.qkv.weight"], 3, dim=0) + q_bias, k_bias, v_bias = torch.chunk(state_dict[f"blocks.{depth}.attn.qkv.bias"], 3, dim=0) + + state_dict[f"transformer_blocks.{depth}.attn1.to_q.weight"] = q + state_dict[f"transformer_blocks.{depth}.attn1.to_q.bias"] = q_bias + state_dict[f"transformer_blocks.{depth}.attn1.to_k.weight"] = k + state_dict[f"transformer_blocks.{depth}.attn1.to_k.bias"] = k_bias + state_dict[f"transformer_blocks.{depth}.attn1.to_v.weight"] = v + state_dict[f"transformer_blocks.{depth}.attn1.to_v.bias"] = v_bias + + state_dict[f"transformer_blocks.{depth}.attn1.to_out.0.weight"] = state_dict[ + f"blocks.{depth}.attn.proj.weight" + ] + state_dict[f"transformer_blocks.{depth}.attn1.to_out.0.bias"] = state_dict[f"blocks.{depth}.attn.proj.bias"] + + state_dict[f"transformer_blocks.{depth}.ff.net.0.proj.weight"] = state_dict[f"blocks.{depth}.mlp.fc1.weight"] + state_dict[f"transformer_blocks.{depth}.ff.net.0.proj.bias"] = state_dict[f"blocks.{depth}.mlp.fc1.bias"] + state_dict[f"transformer_blocks.{depth}.ff.net.2.weight"] = state_dict[f"blocks.{depth}.mlp.fc2.weight"] + state_dict[f"transformer_blocks.{depth}.ff.net.2.bias"] = state_dict[f"blocks.{depth}.mlp.fc2.bias"] + + state_dict.pop(f"blocks.{depth}.attn.qkv.weight") + state_dict.pop(f"blocks.{depth}.attn.qkv.bias") + state_dict.pop(f"blocks.{depth}.attn.proj.weight") + state_dict.pop(f"blocks.{depth}.attn.proj.bias") + state_dict.pop(f"blocks.{depth}.mlp.fc1.weight") + state_dict.pop(f"blocks.{depth}.mlp.fc1.bias") + state_dict.pop(f"blocks.{depth}.mlp.fc2.weight") + state_dict.pop(f"blocks.{depth}.mlp.fc2.bias") + state_dict.pop(f"blocks.{depth}.adaLN_modulation.1.weight") + state_dict.pop(f"blocks.{depth}.adaLN_modulation.1.bias") + + state_dict.pop("t_embedder.mlp.0.weight") + state_dict.pop("t_embedder.mlp.0.bias") + state_dict.pop("t_embedder.mlp.2.weight") + state_dict.pop("t_embedder.mlp.2.bias") + state_dict.pop("y_embedder.embedding_table.weight") + + state_dict["proj_out_1.weight"] = state_dict["final_layer.adaLN_modulation.1.weight"] + state_dict["proj_out_1.bias"] = state_dict["final_layer.adaLN_modulation.1.bias"] + state_dict["proj_out_2.weight"] = state_dict["final_layer.linear.weight"] + state_dict["proj_out_2.bias"] = state_dict["final_layer.linear.bias"] + + state_dict.pop("final_layer.linear.weight") + state_dict.pop("final_layer.linear.bias") + state_dict.pop("final_layer.adaLN_modulation.1.weight") + state_dict.pop("final_layer.adaLN_modulation.1.bias") + + # DiT XL/2 + transformer = Transformer2DModel( + sample_size=args.image_size // 8, + num_layers=28, + attention_head_dim=72, + in_channels=4, + out_channels=8, + patch_size=2, + attention_bias=True, + num_attention_heads=16, + activation_fn="gelu-approximate", + num_embeds_ada_norm=1000, + norm_type="ada_norm_zero", + norm_elementwise_affine=False, + ) + transformer.load_state_dict(state_dict, strict=True) + + scheduler = DDIMScheduler( + num_train_timesteps=1000, + beta_schedule="linear", + prediction_type="epsilon", + clip_sample=False, + ) + + vae = AutoencoderKL.from_pretrained(args.vae_model) + + pipeline = DiTPipeline(transformer=transformer, vae=vae, scheduler=scheduler) + + if args.save: + pipeline.save_pretrained(args.checkpoint_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--image_size", + default=256, + type=int, + required=False, + help="Image size of pretrained model, either 256 or 512.", + ) + parser.add_argument( + "--vae_model", + default="stabilityai/sd-vae-ft-ema", + type=str, + required=False, + help="Path to pretrained VAE model, either stabilityai/sd-vae-ft-mse or stabilityai/sd-vae-ft-ema.", + ) + parser.add_argument( + "--save", default=True, type=bool, required=False, help="Whether to save the converted pipeline or not." + ) + parser.add_argument( + "--checkpoint_path", default=None, type=str, required=True, help="Path to the output pipeline." + ) + + args = parser.parse_args() + main(args) diff --git a/diffusers/scripts/convert_k_upscaler_to_diffusers.py b/diffusers/scripts/convert_k_upscaler_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..62abedd737855ca0b0bc9abb75c9b6fb91d5bde2 --- /dev/null +++ b/diffusers/scripts/convert_k_upscaler_to_diffusers.py @@ -0,0 +1,297 @@ +import argparse + +import huggingface_hub +import k_diffusion as K +import torch + +from diffusers import UNet2DConditionModel + + +UPSCALER_REPO = "pcuenq/k-upscaler" + + +def resnet_to_diffusers_checkpoint(resnet, checkpoint, *, diffusers_resnet_prefix, resnet_prefix): + rv = { + # norm1 + f"{diffusers_resnet_prefix}.norm1.linear.weight": checkpoint[f"{resnet_prefix}.main.0.mapper.weight"], + f"{diffusers_resnet_prefix}.norm1.linear.bias": checkpoint[f"{resnet_prefix}.main.0.mapper.bias"], + # conv1 + f"{diffusers_resnet_prefix}.conv1.weight": checkpoint[f"{resnet_prefix}.main.2.weight"], + f"{diffusers_resnet_prefix}.conv1.bias": checkpoint[f"{resnet_prefix}.main.2.bias"], + # norm2 + f"{diffusers_resnet_prefix}.norm2.linear.weight": checkpoint[f"{resnet_prefix}.main.4.mapper.weight"], + f"{diffusers_resnet_prefix}.norm2.linear.bias": checkpoint[f"{resnet_prefix}.main.4.mapper.bias"], + # conv2 + f"{diffusers_resnet_prefix}.conv2.weight": checkpoint[f"{resnet_prefix}.main.6.weight"], + f"{diffusers_resnet_prefix}.conv2.bias": checkpoint[f"{resnet_prefix}.main.6.bias"], + } + + if resnet.conv_shortcut is not None: + rv.update( + { + f"{diffusers_resnet_prefix}.conv_shortcut.weight": checkpoint[f"{resnet_prefix}.skip.weight"], + } + ) + + return rv + + +def self_attn_to_diffusers_checkpoint(checkpoint, *, diffusers_attention_prefix, attention_prefix): + weight_q, weight_k, weight_v = checkpoint[f"{attention_prefix}.qkv_proj.weight"].chunk(3, dim=0) + bias_q, bias_k, bias_v = checkpoint[f"{attention_prefix}.qkv_proj.bias"].chunk(3, dim=0) + rv = { + # norm + f"{diffusers_attention_prefix}.norm1.linear.weight": checkpoint[f"{attention_prefix}.norm_in.mapper.weight"], + f"{diffusers_attention_prefix}.norm1.linear.bias": checkpoint[f"{attention_prefix}.norm_in.mapper.bias"], + # to_q + f"{diffusers_attention_prefix}.attn1.to_q.weight": weight_q.squeeze(-1).squeeze(-1), + f"{diffusers_attention_prefix}.attn1.to_q.bias": bias_q, + # to_k + f"{diffusers_attention_prefix}.attn1.to_k.weight": weight_k.squeeze(-1).squeeze(-1), + f"{diffusers_attention_prefix}.attn1.to_k.bias": bias_k, + # to_v + f"{diffusers_attention_prefix}.attn1.to_v.weight": weight_v.squeeze(-1).squeeze(-1), + f"{diffusers_attention_prefix}.attn1.to_v.bias": bias_v, + # to_out + f"{diffusers_attention_prefix}.attn1.to_out.0.weight": checkpoint[f"{attention_prefix}.out_proj.weight"] + .squeeze(-1) + .squeeze(-1), + f"{diffusers_attention_prefix}.attn1.to_out.0.bias": checkpoint[f"{attention_prefix}.out_proj.bias"], + } + + return rv + + +def cross_attn_to_diffusers_checkpoint( + checkpoint, *, diffusers_attention_prefix, diffusers_attention_index, attention_prefix +): + weight_k, weight_v = checkpoint[f"{attention_prefix}.kv_proj.weight"].chunk(2, dim=0) + bias_k, bias_v = checkpoint[f"{attention_prefix}.kv_proj.bias"].chunk(2, dim=0) + + rv = { + # norm2 (ada groupnorm) + f"{diffusers_attention_prefix}.norm{diffusers_attention_index}.linear.weight": checkpoint[ + f"{attention_prefix}.norm_dec.mapper.weight" + ], + f"{diffusers_attention_prefix}.norm{diffusers_attention_index}.linear.bias": checkpoint[ + f"{attention_prefix}.norm_dec.mapper.bias" + ], + # layernorm on encoder_hidden_state + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.norm_cross.weight": checkpoint[ + f"{attention_prefix}.norm_enc.weight" + ], + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.norm_cross.bias": checkpoint[ + f"{attention_prefix}.norm_enc.bias" + ], + # to_q + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_q.weight": checkpoint[ + f"{attention_prefix}.q_proj.weight" + ] + .squeeze(-1) + .squeeze(-1), + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_q.bias": checkpoint[ + f"{attention_prefix}.q_proj.bias" + ], + # to_k + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_k.weight": weight_k.squeeze(-1).squeeze(-1), + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_k.bias": bias_k, + # to_v + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_v.weight": weight_v.squeeze(-1).squeeze(-1), + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_v.bias": bias_v, + # to_out + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_out.0.weight": checkpoint[ + f"{attention_prefix}.out_proj.weight" + ] + .squeeze(-1) + .squeeze(-1), + f"{diffusers_attention_prefix}.attn{diffusers_attention_index}.to_out.0.bias": checkpoint[ + f"{attention_prefix}.out_proj.bias" + ], + } + + return rv + + +def block_to_diffusers_checkpoint(block, checkpoint, block_idx, block_type): + block_prefix = "inner_model.u_net.u_blocks" if block_type == "up" else "inner_model.u_net.d_blocks" + block_prefix = f"{block_prefix}.{block_idx}" + + diffusers_checkpoint = {} + + if not hasattr(block, "attentions"): + n = 1 # resnet only + elif not block.attentions[0].add_self_attention: + n = 2 # resnet -> cross-attention + else: + n = 3 # resnet -> self-attention -> cross-attention) + + for resnet_idx, resnet in enumerate(block.resnets): + # diffusers_resnet_prefix = f"{diffusers_up_block_prefix}.resnets.{resnet_idx}" + diffusers_resnet_prefix = f"{block_type}_blocks.{block_idx}.resnets.{resnet_idx}" + idx = n * resnet_idx if block_type == "up" else n * resnet_idx + 1 + resnet_prefix = f"{block_prefix}.{idx}" if block_type == "up" else f"{block_prefix}.{idx}" + + diffusers_checkpoint.update( + resnet_to_diffusers_checkpoint( + resnet, checkpoint, diffusers_resnet_prefix=diffusers_resnet_prefix, resnet_prefix=resnet_prefix + ) + ) + + if hasattr(block, "attentions"): + for attention_idx, attention in enumerate(block.attentions): + diffusers_attention_prefix = f"{block_type}_blocks.{block_idx}.attentions.{attention_idx}" + idx = n * attention_idx + 1 if block_type == "up" else n * attention_idx + 2 + self_attention_prefix = f"{block_prefix}.{idx}" + cross_attention_prefix = f"{block_prefix}.{idx }" + cross_attention_index = 1 if not attention.add_self_attention else 2 + idx = ( + n * attention_idx + cross_attention_index + if block_type == "up" + else n * attention_idx + cross_attention_index + 1 + ) + cross_attention_prefix = f"{block_prefix}.{idx }" + + diffusers_checkpoint.update( + cross_attn_to_diffusers_checkpoint( + checkpoint, + diffusers_attention_prefix=diffusers_attention_prefix, + diffusers_attention_index=2, + attention_prefix=cross_attention_prefix, + ) + ) + + if attention.add_self_attention is True: + diffusers_checkpoint.update( + self_attn_to_diffusers_checkpoint( + checkpoint, + diffusers_attention_prefix=diffusers_attention_prefix, + attention_prefix=self_attention_prefix, + ) + ) + + return diffusers_checkpoint + + +def unet_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + # pre-processing + diffusers_checkpoint.update( + { + "conv_in.weight": checkpoint["inner_model.proj_in.weight"], + "conv_in.bias": checkpoint["inner_model.proj_in.bias"], + } + ) + + # timestep and class embedding + diffusers_checkpoint.update( + { + "time_proj.weight": checkpoint["inner_model.timestep_embed.weight"].squeeze(-1), + "time_embedding.linear_1.weight": checkpoint["inner_model.mapping.0.weight"], + "time_embedding.linear_1.bias": checkpoint["inner_model.mapping.0.bias"], + "time_embedding.linear_2.weight": checkpoint["inner_model.mapping.2.weight"], + "time_embedding.linear_2.bias": checkpoint["inner_model.mapping.2.bias"], + "time_embedding.cond_proj.weight": checkpoint["inner_model.mapping_cond.weight"], + } + ) + + # down_blocks + for down_block_idx, down_block in enumerate(model.down_blocks): + diffusers_checkpoint.update(block_to_diffusers_checkpoint(down_block, checkpoint, down_block_idx, "down")) + + # up_blocks + for up_block_idx, up_block in enumerate(model.up_blocks): + diffusers_checkpoint.update(block_to_diffusers_checkpoint(up_block, checkpoint, up_block_idx, "up")) + + # post-processing + diffusers_checkpoint.update( + { + "conv_out.weight": checkpoint["inner_model.proj_out.weight"], + "conv_out.bias": checkpoint["inner_model.proj_out.bias"], + } + ) + + return diffusers_checkpoint + + +def unet_model_from_original_config(original_config): + in_channels = original_config["input_channels"] + original_config["unet_cond_dim"] + out_channels = original_config["input_channels"] + (1 if original_config["has_variance"] else 0) + + block_out_channels = original_config["channels"] + + assert ( + len(set(original_config["depths"])) == 1 + ), "UNet2DConditionModel currently do not support blocks with different number of layers" + layers_per_block = original_config["depths"][0] + + class_labels_dim = original_config["mapping_cond_dim"] + cross_attention_dim = original_config["cross_cond_dim"] + + attn1_types = [] + attn2_types = [] + for s, c in zip(original_config["self_attn_depths"], original_config["cross_attn_depths"]): + if s: + a1 = "self" + a2 = "cross" if c else None + elif c: + a1 = "cross" + a2 = None + else: + a1 = None + a2 = None + attn1_types.append(a1) + attn2_types.append(a2) + + unet = UNet2DConditionModel( + in_channels=in_channels, + out_channels=out_channels, + down_block_types=("KDownBlock2D", "KCrossAttnDownBlock2D", "KCrossAttnDownBlock2D", "KCrossAttnDownBlock2D"), + mid_block_type=None, + up_block_types=("KCrossAttnUpBlock2D", "KCrossAttnUpBlock2D", "KCrossAttnUpBlock2D", "KUpBlock2D"), + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + act_fn="gelu", + norm_num_groups=None, + cross_attention_dim=cross_attention_dim, + attention_head_dim=64, + time_cond_proj_dim=class_labels_dim, + resnet_time_scale_shift="scale_shift", + time_embedding_type="fourier", + timestep_post_act="gelu", + conv_in_kernel=1, + conv_out_kernel=1, + ) + + return unet + + +def main(args): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + orig_config_path = huggingface_hub.hf_hub_download(UPSCALER_REPO, "config_laion_text_cond_latent_upscaler_2.json") + orig_weights_path = huggingface_hub.hf_hub_download( + UPSCALER_REPO, "laion_text_cond_latent_upscaler_2_1_00470000_slim.pth" + ) + print(f"loading original model configuration from {orig_config_path}") + print(f"loading original model checkpoint from {orig_weights_path}") + + print("converting to diffusers unet") + orig_config = K.config.load_config(open(orig_config_path))["model"] + model = unet_model_from_original_config(orig_config) + + orig_checkpoint = torch.load(orig_weights_path, map_location=device)["model_ema"] + converted_checkpoint = unet_to_diffusers_checkpoint(model, orig_checkpoint) + + model.load_state_dict(converted_checkpoint, strict=True) + model.save_pretrained(args.dump_path) + print(f"saving converted unet model in {args.dump_path}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + args = parser.parse_args() + + main(args) diff --git a/diffusers/scripts/convert_kakao_brain_unclip_to_diffusers.py b/diffusers/scripts/convert_kakao_brain_unclip_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..85d983dea686f26d0196be94c3ef35496161eb24 --- /dev/null +++ b/diffusers/scripts/convert_kakao_brain_unclip_to_diffusers.py @@ -0,0 +1,1159 @@ +import argparse +import tempfile + +import torch +from accelerate import load_checkpoint_and_dispatch +from transformers import CLIPTextModelWithProjection, CLIPTokenizer + +from diffusers import UnCLIPPipeline, UNet2DConditionModel, UNet2DModel +from diffusers.models.prior_transformer import PriorTransformer +from diffusers.pipelines.unclip.text_proj import UnCLIPTextProjModel +from diffusers.schedulers.scheduling_unclip import UnCLIPScheduler + + +""" +Example - From the diffusers root directory: + +Download weights: +```sh +$ wget https://arena.kakaocdn.net/brainrepo/models/karlo-public/v1.0.0.alpha/efdf6206d8ed593961593dc029a8affa/decoder-ckpt-step%3D01000000-of-01000000.ckpt +$ wget https://arena.kakaocdn.net/brainrepo/models/karlo-public/v1.0.0.alpha/4226b831ae0279020d134281f3c31590/improved-sr-ckpt-step%3D1.2M.ckpt +$ wget https://arena.kakaocdn.net/brainrepo/models/karlo-public/v1.0.0.alpha/85626483eaca9f581e2a78d31ff905ca/prior-ckpt-step%3D01000000-of-01000000.ckpt +$ wget https://arena.kakaocdn.net/brainrepo/models/karlo-public/v1.0.0.alpha/0b62380a75e56f073e2844ab5199153d/ViT-L-14_stats.th +``` + +Convert the model: +```sh +$ python scripts/convert_kakao_brain_unclip_to_diffusers.py \ + --decoder_checkpoint_path ./decoder-ckpt-step\=01000000-of-01000000.ckpt \ + --super_res_unet_checkpoint_path ./improved-sr-ckpt-step\=1.2M.ckpt \ + --prior_checkpoint_path ./prior-ckpt-step\=01000000-of-01000000.ckpt \ + --clip_stat_path ./ViT-L-14_stats.th \ + --dump_path +``` +""" + + +# prior + +PRIOR_ORIGINAL_PREFIX = "model" + +# Uses default arguments +PRIOR_CONFIG = {} + + +def prior_model_from_original_config(): + model = PriorTransformer(**PRIOR_CONFIG) + + return model + + +def prior_original_checkpoint_to_diffusers_checkpoint(model, checkpoint, clip_stats_checkpoint): + diffusers_checkpoint = {} + + # .time_embed.0 -> .time_embedding.linear_1 + diffusers_checkpoint.update( + { + "time_embedding.linear_1.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.time_embed.0.weight"], + "time_embedding.linear_1.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.time_embed.0.bias"], + } + ) + + # .clip_img_proj -> .proj_in + diffusers_checkpoint.update( + { + "proj_in.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.clip_img_proj.weight"], + "proj_in.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.clip_img_proj.bias"], + } + ) + + # .text_emb_proj -> .embedding_proj + diffusers_checkpoint.update( + { + "embedding_proj.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.text_emb_proj.weight"], + "embedding_proj.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.text_emb_proj.bias"], + } + ) + + # .text_enc_proj -> .encoder_hidden_states_proj + diffusers_checkpoint.update( + { + "encoder_hidden_states_proj.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.text_enc_proj.weight"], + "encoder_hidden_states_proj.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.text_enc_proj.bias"], + } + ) + + # .positional_embedding -> .positional_embedding + diffusers_checkpoint.update({"positional_embedding": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.positional_embedding"]}) + + # .prd_emb -> .prd_embedding + diffusers_checkpoint.update({"prd_embedding": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.prd_emb"]}) + + # .time_embed.2 -> .time_embedding.linear_2 + diffusers_checkpoint.update( + { + "time_embedding.linear_2.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.time_embed.2.weight"], + "time_embedding.linear_2.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.time_embed.2.bias"], + } + ) + + # .resblocks. -> .transformer_blocks. + for idx in range(len(model.transformer_blocks)): + diffusers_transformer_prefix = f"transformer_blocks.{idx}" + original_transformer_prefix = f"{PRIOR_ORIGINAL_PREFIX}.transformer.resblocks.{idx}" + + # .attn -> .attn1 + diffusers_attention_prefix = f"{diffusers_transformer_prefix}.attn1" + original_attention_prefix = f"{original_transformer_prefix}.attn" + diffusers_checkpoint.update( + prior_attention_to_diffusers( + checkpoint, + diffusers_attention_prefix=diffusers_attention_prefix, + original_attention_prefix=original_attention_prefix, + attention_head_dim=model.attention_head_dim, + ) + ) + + # .mlp -> .ff + diffusers_ff_prefix = f"{diffusers_transformer_prefix}.ff" + original_ff_prefix = f"{original_transformer_prefix}.mlp" + diffusers_checkpoint.update( + prior_ff_to_diffusers( + checkpoint, diffusers_ff_prefix=diffusers_ff_prefix, original_ff_prefix=original_ff_prefix + ) + ) + + # .ln_1 -> .norm1 + diffusers_checkpoint.update( + { + f"{diffusers_transformer_prefix}.norm1.weight": checkpoint[ + f"{original_transformer_prefix}.ln_1.weight" + ], + f"{diffusers_transformer_prefix}.norm1.bias": checkpoint[f"{original_transformer_prefix}.ln_1.bias"], + } + ) + + # .ln_2 -> .norm3 + diffusers_checkpoint.update( + { + f"{diffusers_transformer_prefix}.norm3.weight": checkpoint[ + f"{original_transformer_prefix}.ln_2.weight" + ], + f"{diffusers_transformer_prefix}.norm3.bias": checkpoint[f"{original_transformer_prefix}.ln_2.bias"], + } + ) + + # .final_ln -> .norm_out + diffusers_checkpoint.update( + { + "norm_out.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.final_ln.weight"], + "norm_out.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.final_ln.bias"], + } + ) + + # .out_proj -> .proj_to_clip_embeddings + diffusers_checkpoint.update( + { + "proj_to_clip_embeddings.weight": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.out_proj.weight"], + "proj_to_clip_embeddings.bias": checkpoint[f"{PRIOR_ORIGINAL_PREFIX}.out_proj.bias"], + } + ) + + # clip stats + clip_mean, clip_std = clip_stats_checkpoint + clip_mean = clip_mean[None, :] + clip_std = clip_std[None, :] + + diffusers_checkpoint.update({"clip_mean": clip_mean, "clip_std": clip_std}) + + return diffusers_checkpoint + + +def prior_attention_to_diffusers( + checkpoint, *, diffusers_attention_prefix, original_attention_prefix, attention_head_dim +): + diffusers_checkpoint = {} + + # .c_qkv -> .{to_q, to_k, to_v} + [q_weight, k_weight, v_weight], [q_bias, k_bias, v_bias] = split_attentions( + weight=checkpoint[f"{original_attention_prefix}.c_qkv.weight"], + bias=checkpoint[f"{original_attention_prefix}.c_qkv.bias"], + split=3, + chunk_size=attention_head_dim, + ) + + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.to_q.weight": q_weight, + f"{diffusers_attention_prefix}.to_q.bias": q_bias, + f"{diffusers_attention_prefix}.to_k.weight": k_weight, + f"{diffusers_attention_prefix}.to_k.bias": k_bias, + f"{diffusers_attention_prefix}.to_v.weight": v_weight, + f"{diffusers_attention_prefix}.to_v.bias": v_bias, + } + ) + + # .c_proj -> .to_out.0 + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.to_out.0.weight": checkpoint[f"{original_attention_prefix}.c_proj.weight"], + f"{diffusers_attention_prefix}.to_out.0.bias": checkpoint[f"{original_attention_prefix}.c_proj.bias"], + } + ) + + return diffusers_checkpoint + + +def prior_ff_to_diffusers(checkpoint, *, diffusers_ff_prefix, original_ff_prefix): + diffusers_checkpoint = { + # .c_fc -> .net.0.proj + f"{diffusers_ff_prefix}.net.{0}.proj.weight": checkpoint[f"{original_ff_prefix}.c_fc.weight"], + f"{diffusers_ff_prefix}.net.{0}.proj.bias": checkpoint[f"{original_ff_prefix}.c_fc.bias"], + # .c_proj -> .net.2 + f"{diffusers_ff_prefix}.net.{2}.weight": checkpoint[f"{original_ff_prefix}.c_proj.weight"], + f"{diffusers_ff_prefix}.net.{2}.bias": checkpoint[f"{original_ff_prefix}.c_proj.bias"], + } + + return diffusers_checkpoint + + +# done prior + + +# decoder + +DECODER_ORIGINAL_PREFIX = "model" + +# We are hardcoding the model configuration for now. If we need to generalize to more model configurations, we can +# update then. +DECODER_CONFIG = { + "sample_size": 64, + "layers_per_block": 3, + "down_block_types": ( + "ResnetDownsampleBlock2D", + "SimpleCrossAttnDownBlock2D", + "SimpleCrossAttnDownBlock2D", + "SimpleCrossAttnDownBlock2D", + ), + "up_block_types": ( + "SimpleCrossAttnUpBlock2D", + "SimpleCrossAttnUpBlock2D", + "SimpleCrossAttnUpBlock2D", + "ResnetUpsampleBlock2D", + ), + "mid_block_type": "UNetMidBlock2DSimpleCrossAttn", + "block_out_channels": (320, 640, 960, 1280), + "in_channels": 3, + "out_channels": 6, + "cross_attention_dim": 1536, + "class_embed_type": "identity", + "attention_head_dim": 64, + "resnet_time_scale_shift": "scale_shift", +} + + +def decoder_model_from_original_config(): + model = UNet2DConditionModel(**DECODER_CONFIG) + + return model + + +def decoder_original_checkpoint_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + original_unet_prefix = DECODER_ORIGINAL_PREFIX + num_head_channels = DECODER_CONFIG["attention_head_dim"] + + diffusers_checkpoint.update(unet_time_embeddings(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_in(checkpoint, original_unet_prefix)) + + # .input_blocks -> .down_blocks + + original_down_block_idx = 1 + + for diffusers_down_block_idx in range(len(model.down_blocks)): + checkpoint_update, num_original_down_blocks = unet_downblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_down_block_idx=diffusers_down_block_idx, + original_down_block_idx=original_down_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=num_head_channels, + ) + + original_down_block_idx += num_original_down_blocks + + diffusers_checkpoint.update(checkpoint_update) + + # done .input_blocks -> .down_blocks + + diffusers_checkpoint.update( + unet_midblock_to_diffusers_checkpoint( + model, + checkpoint, + original_unet_prefix=original_unet_prefix, + num_head_channels=num_head_channels, + ) + ) + + # .output_blocks -> .up_blocks + + original_up_block_idx = 0 + + for diffusers_up_block_idx in range(len(model.up_blocks)): + checkpoint_update, num_original_up_blocks = unet_upblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_up_block_idx=diffusers_up_block_idx, + original_up_block_idx=original_up_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=num_head_channels, + ) + + original_up_block_idx += num_original_up_blocks + + diffusers_checkpoint.update(checkpoint_update) + + # done .output_blocks -> .up_blocks + + diffusers_checkpoint.update(unet_conv_norm_out(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_out(checkpoint, original_unet_prefix)) + + return diffusers_checkpoint + + +# done decoder + +# text proj + + +def text_proj_from_original_config(): + # From the conditional unet constructor where the dimension of the projected time embeddings is + # constructed + time_embed_dim = DECODER_CONFIG["block_out_channels"][0] * 4 + + cross_attention_dim = DECODER_CONFIG["cross_attention_dim"] + + model = UnCLIPTextProjModel(time_embed_dim=time_embed_dim, cross_attention_dim=cross_attention_dim) + + return model + + +# Note that the input checkpoint is the original decoder checkpoint +def text_proj_original_checkpoint_to_diffusers_checkpoint(checkpoint): + diffusers_checkpoint = { + # .text_seq_proj.0 -> .encoder_hidden_states_proj + "encoder_hidden_states_proj.weight": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_seq_proj.0.weight"], + "encoder_hidden_states_proj.bias": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_seq_proj.0.bias"], + # .text_seq_proj.1 -> .text_encoder_hidden_states_norm + "text_encoder_hidden_states_norm.weight": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_seq_proj.1.weight"], + "text_encoder_hidden_states_norm.bias": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_seq_proj.1.bias"], + # .clip_tok_proj -> .clip_extra_context_tokens_proj + "clip_extra_context_tokens_proj.weight": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.clip_tok_proj.weight"], + "clip_extra_context_tokens_proj.bias": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.clip_tok_proj.bias"], + # .text_feat_proj -> .embedding_proj + "embedding_proj.weight": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_feat_proj.weight"], + "embedding_proj.bias": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.text_feat_proj.bias"], + # .cf_param -> .learned_classifier_free_guidance_embeddings + "learned_classifier_free_guidance_embeddings": checkpoint[f"{DECODER_ORIGINAL_PREFIX}.cf_param"], + # .clip_emb -> .clip_image_embeddings_project_to_time_embeddings + "clip_image_embeddings_project_to_time_embeddings.weight": checkpoint[ + f"{DECODER_ORIGINAL_PREFIX}.clip_emb.weight" + ], + "clip_image_embeddings_project_to_time_embeddings.bias": checkpoint[ + f"{DECODER_ORIGINAL_PREFIX}.clip_emb.bias" + ], + } + + return diffusers_checkpoint + + +# done text proj + +# super res unet first steps + +SUPER_RES_UNET_FIRST_STEPS_PREFIX = "model_first_steps" + +SUPER_RES_UNET_FIRST_STEPS_CONFIG = { + "sample_size": 256, + "layers_per_block": 3, + "down_block_types": ( + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + ), + "up_block_types": ( + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + ), + "block_out_channels": (320, 640, 960, 1280), + "in_channels": 6, + "out_channels": 3, + "add_attention": False, +} + + +def super_res_unet_first_steps_model_from_original_config(): + model = UNet2DModel(**SUPER_RES_UNET_FIRST_STEPS_CONFIG) + + return model + + +def super_res_unet_first_steps_original_checkpoint_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + original_unet_prefix = SUPER_RES_UNET_FIRST_STEPS_PREFIX + + diffusers_checkpoint.update(unet_time_embeddings(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_in(checkpoint, original_unet_prefix)) + + # .input_blocks -> .down_blocks + + original_down_block_idx = 1 + + for diffusers_down_block_idx in range(len(model.down_blocks)): + checkpoint_update, num_original_down_blocks = unet_downblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_down_block_idx=diffusers_down_block_idx, + original_down_block_idx=original_down_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + + original_down_block_idx += num_original_down_blocks + + diffusers_checkpoint.update(checkpoint_update) + + diffusers_checkpoint.update( + unet_midblock_to_diffusers_checkpoint( + model, + checkpoint, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + ) + + # .output_blocks -> .up_blocks + + original_up_block_idx = 0 + + for diffusers_up_block_idx in range(len(model.up_blocks)): + checkpoint_update, num_original_up_blocks = unet_upblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_up_block_idx=diffusers_up_block_idx, + original_up_block_idx=original_up_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + + original_up_block_idx += num_original_up_blocks + + diffusers_checkpoint.update(checkpoint_update) + + # done .output_blocks -> .up_blocks + + diffusers_checkpoint.update(unet_conv_norm_out(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_out(checkpoint, original_unet_prefix)) + + return diffusers_checkpoint + + +# done super res unet first steps + +# super res unet last step + +SUPER_RES_UNET_LAST_STEP_PREFIX = "model_last_step" + +SUPER_RES_UNET_LAST_STEP_CONFIG = { + "sample_size": 256, + "layers_per_block": 3, + "down_block_types": ( + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + "ResnetDownsampleBlock2D", + ), + "up_block_types": ( + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + "ResnetUpsampleBlock2D", + ), + "block_out_channels": (320, 640, 960, 1280), + "in_channels": 6, + "out_channels": 3, + "add_attention": False, +} + + +def super_res_unet_last_step_model_from_original_config(): + model = UNet2DModel(**SUPER_RES_UNET_LAST_STEP_CONFIG) + + return model + + +def super_res_unet_last_step_original_checkpoint_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + original_unet_prefix = SUPER_RES_UNET_LAST_STEP_PREFIX + + diffusers_checkpoint.update(unet_time_embeddings(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_in(checkpoint, original_unet_prefix)) + + # .input_blocks -> .down_blocks + + original_down_block_idx = 1 + + for diffusers_down_block_idx in range(len(model.down_blocks)): + checkpoint_update, num_original_down_blocks = unet_downblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_down_block_idx=diffusers_down_block_idx, + original_down_block_idx=original_down_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + + original_down_block_idx += num_original_down_blocks + + diffusers_checkpoint.update(checkpoint_update) + + diffusers_checkpoint.update( + unet_midblock_to_diffusers_checkpoint( + model, + checkpoint, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + ) + + # .output_blocks -> .up_blocks + + original_up_block_idx = 0 + + for diffusers_up_block_idx in range(len(model.up_blocks)): + checkpoint_update, num_original_up_blocks = unet_upblock_to_diffusers_checkpoint( + model, + checkpoint, + diffusers_up_block_idx=diffusers_up_block_idx, + original_up_block_idx=original_up_block_idx, + original_unet_prefix=original_unet_prefix, + num_head_channels=None, + ) + + original_up_block_idx += num_original_up_blocks + + diffusers_checkpoint.update(checkpoint_update) + + # done .output_blocks -> .up_blocks + + diffusers_checkpoint.update(unet_conv_norm_out(checkpoint, original_unet_prefix)) + diffusers_checkpoint.update(unet_conv_out(checkpoint, original_unet_prefix)) + + return diffusers_checkpoint + + +# done super res unet last step + + +# unet utils + + +# .time_embed -> .time_embedding +def unet_time_embeddings(checkpoint, original_unet_prefix): + diffusers_checkpoint = {} + + diffusers_checkpoint.update( + { + "time_embedding.linear_1.weight": checkpoint[f"{original_unet_prefix}.time_embed.0.weight"], + "time_embedding.linear_1.bias": checkpoint[f"{original_unet_prefix}.time_embed.0.bias"], + "time_embedding.linear_2.weight": checkpoint[f"{original_unet_prefix}.time_embed.2.weight"], + "time_embedding.linear_2.bias": checkpoint[f"{original_unet_prefix}.time_embed.2.bias"], + } + ) + + return diffusers_checkpoint + + +# .input_blocks.0 -> .conv_in +def unet_conv_in(checkpoint, original_unet_prefix): + diffusers_checkpoint = {} + + diffusers_checkpoint.update( + { + "conv_in.weight": checkpoint[f"{original_unet_prefix}.input_blocks.0.0.weight"], + "conv_in.bias": checkpoint[f"{original_unet_prefix}.input_blocks.0.0.bias"], + } + ) + + return diffusers_checkpoint + + +# .out.0 -> .conv_norm_out +def unet_conv_norm_out(checkpoint, original_unet_prefix): + diffusers_checkpoint = {} + + diffusers_checkpoint.update( + { + "conv_norm_out.weight": checkpoint[f"{original_unet_prefix}.out.0.weight"], + "conv_norm_out.bias": checkpoint[f"{original_unet_prefix}.out.0.bias"], + } + ) + + return diffusers_checkpoint + + +# .out.2 -> .conv_out +def unet_conv_out(checkpoint, original_unet_prefix): + diffusers_checkpoint = {} + + diffusers_checkpoint.update( + { + "conv_out.weight": checkpoint[f"{original_unet_prefix}.out.2.weight"], + "conv_out.bias": checkpoint[f"{original_unet_prefix}.out.2.bias"], + } + ) + + return diffusers_checkpoint + + +# .input_blocks -> .down_blocks +def unet_downblock_to_diffusers_checkpoint( + model, checkpoint, *, diffusers_down_block_idx, original_down_block_idx, original_unet_prefix, num_head_channels +): + diffusers_checkpoint = {} + + diffusers_resnet_prefix = f"down_blocks.{diffusers_down_block_idx}.resnets" + original_down_block_prefix = f"{original_unet_prefix}.input_blocks" + + down_block = model.down_blocks[diffusers_down_block_idx] + + num_resnets = len(down_block.resnets) + + if down_block.downsamplers is None: + downsampler = False + else: + assert len(down_block.downsamplers) == 1 + downsampler = True + # The downsample block is also a resnet + num_resnets += 1 + + for resnet_idx_inc in range(num_resnets): + full_resnet_prefix = f"{original_down_block_prefix}.{original_down_block_idx + resnet_idx_inc}.0" + + if downsampler and resnet_idx_inc == num_resnets - 1: + # this is a downsample block + full_diffusers_resnet_prefix = f"down_blocks.{diffusers_down_block_idx}.downsamplers.0" + else: + # this is a regular resnet block + full_diffusers_resnet_prefix = f"{diffusers_resnet_prefix}.{resnet_idx_inc}" + + diffusers_checkpoint.update( + resnet_to_diffusers_checkpoint( + checkpoint, resnet_prefix=full_resnet_prefix, diffusers_resnet_prefix=full_diffusers_resnet_prefix + ) + ) + + if hasattr(down_block, "attentions"): + num_attentions = len(down_block.attentions) + diffusers_attention_prefix = f"down_blocks.{diffusers_down_block_idx}.attentions" + + for attention_idx_inc in range(num_attentions): + full_attention_prefix = f"{original_down_block_prefix}.{original_down_block_idx + attention_idx_inc}.1" + full_diffusers_attention_prefix = f"{diffusers_attention_prefix}.{attention_idx_inc}" + + diffusers_checkpoint.update( + attention_to_diffusers_checkpoint( + checkpoint, + attention_prefix=full_attention_prefix, + diffusers_attention_prefix=full_diffusers_attention_prefix, + num_head_channels=num_head_channels, + ) + ) + + num_original_down_blocks = num_resnets + + return diffusers_checkpoint, num_original_down_blocks + + +# .middle_block -> .mid_block +def unet_midblock_to_diffusers_checkpoint(model, checkpoint, *, original_unet_prefix, num_head_channels): + diffusers_checkpoint = {} + + # block 0 + + original_block_idx = 0 + + diffusers_checkpoint.update( + resnet_to_diffusers_checkpoint( + checkpoint, + diffusers_resnet_prefix="mid_block.resnets.0", + resnet_prefix=f"{original_unet_prefix}.middle_block.{original_block_idx}", + ) + ) + + original_block_idx += 1 + + # optional block 1 + + if hasattr(model.mid_block, "attentions") and model.mid_block.attentions[0] is not None: + diffusers_checkpoint.update( + attention_to_diffusers_checkpoint( + checkpoint, + diffusers_attention_prefix="mid_block.attentions.0", + attention_prefix=f"{original_unet_prefix}.middle_block.{original_block_idx}", + num_head_channels=num_head_channels, + ) + ) + original_block_idx += 1 + + # block 1 or block 2 + + diffusers_checkpoint.update( + resnet_to_diffusers_checkpoint( + checkpoint, + diffusers_resnet_prefix="mid_block.resnets.1", + resnet_prefix=f"{original_unet_prefix}.middle_block.{original_block_idx}", + ) + ) + + return diffusers_checkpoint + + +# .output_blocks -> .up_blocks +def unet_upblock_to_diffusers_checkpoint( + model, checkpoint, *, diffusers_up_block_idx, original_up_block_idx, original_unet_prefix, num_head_channels +): + diffusers_checkpoint = {} + + diffusers_resnet_prefix = f"up_blocks.{diffusers_up_block_idx}.resnets" + original_up_block_prefix = f"{original_unet_prefix}.output_blocks" + + up_block = model.up_blocks[diffusers_up_block_idx] + + num_resnets = len(up_block.resnets) + + if up_block.upsamplers is None: + upsampler = False + else: + assert len(up_block.upsamplers) == 1 + upsampler = True + # The upsample block is also a resnet + num_resnets += 1 + + has_attentions = hasattr(up_block, "attentions") + + for resnet_idx_inc in range(num_resnets): + if upsampler and resnet_idx_inc == num_resnets - 1: + # this is an upsample block + if has_attentions: + # There is a middle attention block that we skip + original_resnet_block_idx = 2 + else: + original_resnet_block_idx = 1 + + # we add the `minus 1` because the last two resnets are stuck together in the same output block + full_resnet_prefix = ( + f"{original_up_block_prefix}.{original_up_block_idx + resnet_idx_inc - 1}.{original_resnet_block_idx}" + ) + + full_diffusers_resnet_prefix = f"up_blocks.{diffusers_up_block_idx}.upsamplers.0" + else: + # this is a regular resnet block + full_resnet_prefix = f"{original_up_block_prefix}.{original_up_block_idx + resnet_idx_inc}.0" + full_diffusers_resnet_prefix = f"{diffusers_resnet_prefix}.{resnet_idx_inc}" + + diffusers_checkpoint.update( + resnet_to_diffusers_checkpoint( + checkpoint, resnet_prefix=full_resnet_prefix, diffusers_resnet_prefix=full_diffusers_resnet_prefix + ) + ) + + if has_attentions: + num_attentions = len(up_block.attentions) + diffusers_attention_prefix = f"up_blocks.{diffusers_up_block_idx}.attentions" + + for attention_idx_inc in range(num_attentions): + full_attention_prefix = f"{original_up_block_prefix}.{original_up_block_idx + attention_idx_inc}.1" + full_diffusers_attention_prefix = f"{diffusers_attention_prefix}.{attention_idx_inc}" + + diffusers_checkpoint.update( + attention_to_diffusers_checkpoint( + checkpoint, + attention_prefix=full_attention_prefix, + diffusers_attention_prefix=full_diffusers_attention_prefix, + num_head_channels=num_head_channels, + ) + ) + + num_original_down_blocks = num_resnets - 1 if upsampler else num_resnets + + return diffusers_checkpoint, num_original_down_blocks + + +def resnet_to_diffusers_checkpoint(checkpoint, *, diffusers_resnet_prefix, resnet_prefix): + diffusers_checkpoint = { + f"{diffusers_resnet_prefix}.norm1.weight": checkpoint[f"{resnet_prefix}.in_layers.0.weight"], + f"{diffusers_resnet_prefix}.norm1.bias": checkpoint[f"{resnet_prefix}.in_layers.0.bias"], + f"{diffusers_resnet_prefix}.conv1.weight": checkpoint[f"{resnet_prefix}.in_layers.2.weight"], + f"{diffusers_resnet_prefix}.conv1.bias": checkpoint[f"{resnet_prefix}.in_layers.2.bias"], + f"{diffusers_resnet_prefix}.time_emb_proj.weight": checkpoint[f"{resnet_prefix}.emb_layers.1.weight"], + f"{diffusers_resnet_prefix}.time_emb_proj.bias": checkpoint[f"{resnet_prefix}.emb_layers.1.bias"], + f"{diffusers_resnet_prefix}.norm2.weight": checkpoint[f"{resnet_prefix}.out_layers.0.weight"], + f"{diffusers_resnet_prefix}.norm2.bias": checkpoint[f"{resnet_prefix}.out_layers.0.bias"], + f"{diffusers_resnet_prefix}.conv2.weight": checkpoint[f"{resnet_prefix}.out_layers.3.weight"], + f"{diffusers_resnet_prefix}.conv2.bias": checkpoint[f"{resnet_prefix}.out_layers.3.bias"], + } + + skip_connection_prefix = f"{resnet_prefix}.skip_connection" + + if f"{skip_connection_prefix}.weight" in checkpoint: + diffusers_checkpoint.update( + { + f"{diffusers_resnet_prefix}.conv_shortcut.weight": checkpoint[f"{skip_connection_prefix}.weight"], + f"{diffusers_resnet_prefix}.conv_shortcut.bias": checkpoint[f"{skip_connection_prefix}.bias"], + } + ) + + return diffusers_checkpoint + + +def attention_to_diffusers_checkpoint(checkpoint, *, diffusers_attention_prefix, attention_prefix, num_head_channels): + diffusers_checkpoint = {} + + # .norm -> .group_norm + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.group_norm.weight": checkpoint[f"{attention_prefix}.norm.weight"], + f"{diffusers_attention_prefix}.group_norm.bias": checkpoint[f"{attention_prefix}.norm.bias"], + } + ) + + # .qkv -> .{query, key, value} + [q_weight, k_weight, v_weight], [q_bias, k_bias, v_bias] = split_attentions( + weight=checkpoint[f"{attention_prefix}.qkv.weight"][:, :, 0], + bias=checkpoint[f"{attention_prefix}.qkv.bias"], + split=3, + chunk_size=num_head_channels, + ) + + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.to_q.weight": q_weight, + f"{diffusers_attention_prefix}.to_q.bias": q_bias, + f"{diffusers_attention_prefix}.to_k.weight": k_weight, + f"{diffusers_attention_prefix}.to_k.bias": k_bias, + f"{diffusers_attention_prefix}.to_v.weight": v_weight, + f"{diffusers_attention_prefix}.to_v.bias": v_bias, + } + ) + + # .encoder_kv -> .{context_key, context_value} + [encoder_k_weight, encoder_v_weight], [encoder_k_bias, encoder_v_bias] = split_attentions( + weight=checkpoint[f"{attention_prefix}.encoder_kv.weight"][:, :, 0], + bias=checkpoint[f"{attention_prefix}.encoder_kv.bias"], + split=2, + chunk_size=num_head_channels, + ) + + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.add_k_proj.weight": encoder_k_weight, + f"{diffusers_attention_prefix}.add_k_proj.bias": encoder_k_bias, + f"{diffusers_attention_prefix}.add_v_proj.weight": encoder_v_weight, + f"{diffusers_attention_prefix}.add_v_proj.bias": encoder_v_bias, + } + ) + + # .proj_out (1d conv) -> .proj_attn (linear) + diffusers_checkpoint.update( + { + f"{diffusers_attention_prefix}.to_out.0.weight": checkpoint[f"{attention_prefix}.proj_out.weight"][ + :, :, 0 + ], + f"{diffusers_attention_prefix}.to_out.0.bias": checkpoint[f"{attention_prefix}.proj_out.bias"], + } + ) + + return diffusers_checkpoint + + +# TODO maybe document and/or can do more efficiently (build indices in for loop and extract once for each split?) +def split_attentions(*, weight, bias, split, chunk_size): + weights = [None] * split + biases = [None] * split + + weights_biases_idx = 0 + + for starting_row_index in range(0, weight.shape[0], chunk_size): + row_indices = torch.arange(starting_row_index, starting_row_index + chunk_size) + + weight_rows = weight[row_indices, :] + bias_rows = bias[row_indices] + + if weights[weights_biases_idx] is None: + assert weights[weights_biases_idx] is None + weights[weights_biases_idx] = weight_rows + biases[weights_biases_idx] = bias_rows + else: + assert weights[weights_biases_idx] is not None + weights[weights_biases_idx] = torch.concat([weights[weights_biases_idx], weight_rows]) + biases[weights_biases_idx] = torch.concat([biases[weights_biases_idx], bias_rows]) + + weights_biases_idx = (weights_biases_idx + 1) % split + + return weights, biases + + +# done unet utils + + +# Driver functions + + +def text_encoder(): + print("loading CLIP text encoder") + + clip_name = "openai/clip-vit-large-patch14" + + # sets pad_value to 0 + pad_token = "!" + + tokenizer_model = CLIPTokenizer.from_pretrained(clip_name, pad_token=pad_token, device_map="auto") + + assert tokenizer_model.convert_tokens_to_ids(pad_token) == 0 + + text_encoder_model = CLIPTextModelWithProjection.from_pretrained( + clip_name, + # `CLIPTextModel` does not support device_map="auto" + # device_map="auto" + ) + + print("done loading CLIP text encoder") + + return text_encoder_model, tokenizer_model + + +def prior(*, args, checkpoint_map_location): + print("loading prior") + + prior_checkpoint = torch.load(args.prior_checkpoint_path, map_location=checkpoint_map_location) + prior_checkpoint = prior_checkpoint["state_dict"] + + clip_stats_checkpoint = torch.load(args.clip_stat_path, map_location=checkpoint_map_location) + + prior_model = prior_model_from_original_config() + + prior_diffusers_checkpoint = prior_original_checkpoint_to_diffusers_checkpoint( + prior_model, prior_checkpoint, clip_stats_checkpoint + ) + + del prior_checkpoint + del clip_stats_checkpoint + + load_checkpoint_to_model(prior_diffusers_checkpoint, prior_model, strict=True) + + print("done loading prior") + + return prior_model + + +def decoder(*, args, checkpoint_map_location): + print("loading decoder") + + decoder_checkpoint = torch.load(args.decoder_checkpoint_path, map_location=checkpoint_map_location) + decoder_checkpoint = decoder_checkpoint["state_dict"] + + decoder_model = decoder_model_from_original_config() + + decoder_diffusers_checkpoint = decoder_original_checkpoint_to_diffusers_checkpoint( + decoder_model, decoder_checkpoint + ) + + # text proj interlude + + # The original decoder implementation includes a set of parameters that are used + # for creating the `encoder_hidden_states` which are what the U-net is conditioned + # on. The diffusers conditional unet directly takes the encoder_hidden_states. We pull + # the parameters into the UnCLIPTextProjModel class + text_proj_model = text_proj_from_original_config() + + text_proj_checkpoint = text_proj_original_checkpoint_to_diffusers_checkpoint(decoder_checkpoint) + + load_checkpoint_to_model(text_proj_checkpoint, text_proj_model, strict=True) + + # done text proj interlude + + del decoder_checkpoint + + load_checkpoint_to_model(decoder_diffusers_checkpoint, decoder_model, strict=True) + + print("done loading decoder") + + return decoder_model, text_proj_model + + +def super_res_unet(*, args, checkpoint_map_location): + print("loading super resolution unet") + + super_res_checkpoint = torch.load(args.super_res_unet_checkpoint_path, map_location=checkpoint_map_location) + super_res_checkpoint = super_res_checkpoint["state_dict"] + + # model_first_steps + + super_res_first_model = super_res_unet_first_steps_model_from_original_config() + + super_res_first_steps_checkpoint = super_res_unet_first_steps_original_checkpoint_to_diffusers_checkpoint( + super_res_first_model, super_res_checkpoint + ) + + # model_last_step + super_res_last_model = super_res_unet_last_step_model_from_original_config() + + super_res_last_step_checkpoint = super_res_unet_last_step_original_checkpoint_to_diffusers_checkpoint( + super_res_last_model, super_res_checkpoint + ) + + del super_res_checkpoint + + load_checkpoint_to_model(super_res_first_steps_checkpoint, super_res_first_model, strict=True) + + load_checkpoint_to_model(super_res_last_step_checkpoint, super_res_last_model, strict=True) + + print("done loading super resolution unet") + + return super_res_first_model, super_res_last_model + + +def load_checkpoint_to_model(checkpoint, model, strict=False): + with tempfile.NamedTemporaryFile() as file: + torch.save(checkpoint, file.name) + del checkpoint + if strict: + model.load_state_dict(torch.load(file.name), strict=True) + else: + load_checkpoint_and_dispatch(model, file.name, device_map="auto") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + parser.add_argument( + "--prior_checkpoint_path", + default=None, + type=str, + required=True, + help="Path to the prior checkpoint to convert.", + ) + + parser.add_argument( + "--decoder_checkpoint_path", + default=None, + type=str, + required=True, + help="Path to the decoder checkpoint to convert.", + ) + + parser.add_argument( + "--super_res_unet_checkpoint_path", + default=None, + type=str, + required=True, + help="Path to the super resolution checkpoint to convert.", + ) + + parser.add_argument( + "--clip_stat_path", default=None, type=str, required=True, help="Path to the clip stats checkpoint to convert." + ) + + parser.add_argument( + "--checkpoint_load_device", + default="cpu", + type=str, + required=False, + help="The device passed to `map_location` when loading checkpoints.", + ) + + parser.add_argument( + "--debug", + default=None, + type=str, + required=False, + help="Only run a specific stage of the convert script. Used for debugging", + ) + + args = parser.parse_args() + + print(f"loading checkpoints to {args.checkpoint_load_device}") + + checkpoint_map_location = torch.device(args.checkpoint_load_device) + + if args.debug is not None: + print(f"debug: only executing {args.debug}") + + if args.debug is None: + text_encoder_model, tokenizer_model = text_encoder() + + prior_model = prior(args=args, checkpoint_map_location=checkpoint_map_location) + + decoder_model, text_proj_model = decoder(args=args, checkpoint_map_location=checkpoint_map_location) + + super_res_first_model, super_res_last_model = super_res_unet( + args=args, checkpoint_map_location=checkpoint_map_location + ) + + prior_scheduler = UnCLIPScheduler( + variance_type="fixed_small_log", + prediction_type="sample", + num_train_timesteps=1000, + clip_sample_range=5.0, + ) + + decoder_scheduler = UnCLIPScheduler( + variance_type="learned_range", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + super_res_scheduler = UnCLIPScheduler( + variance_type="fixed_small_log", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + print(f"saving Kakao Brain unCLIP to {args.dump_path}") + + pipe = UnCLIPPipeline( + prior=prior_model, + decoder=decoder_model, + text_proj=text_proj_model, + tokenizer=tokenizer_model, + text_encoder=text_encoder_model, + super_res_first=super_res_first_model, + super_res_last=super_res_last_model, + prior_scheduler=prior_scheduler, + decoder_scheduler=decoder_scheduler, + super_res_scheduler=super_res_scheduler, + ) + pipe.save_pretrained(args.dump_path) + + print("done writing Kakao Brain unCLIP") + elif args.debug == "text_encoder": + text_encoder_model, tokenizer_model = text_encoder() + elif args.debug == "prior": + prior_model = prior(args=args, checkpoint_map_location=checkpoint_map_location) + elif args.debug == "decoder": + decoder_model, text_proj_model = decoder(args=args, checkpoint_map_location=checkpoint_map_location) + elif args.debug == "super_res_unet": + super_res_first_model, super_res_last_model = super_res_unet( + args=args, checkpoint_map_location=checkpoint_map_location + ) + else: + raise ValueError(f"unknown debug value : {args.debug}") diff --git a/diffusers/scripts/convert_ldm_original_checkpoint_to_diffusers.py b/diffusers/scripts/convert_ldm_original_checkpoint_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..b82df4c08dc3359f5062de9e6eafb9f3b115a8cb --- /dev/null +++ b/diffusers/scripts/convert_ldm_original_checkpoint_to_diffusers.py @@ -0,0 +1,359 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the LDM checkpoints. """ + +import argparse +import json + +import torch + +from diffusers import DDPMScheduler, LDMPipeline, UNet2DModel, VQModel + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item.replace("in_layers.0", "norm1") + new_item = new_item.replace("in_layers.2", "conv1") + + new_item = new_item.replace("out_layers.0", "norm2") + new_item = new_item.replace("out_layers.3", "conv2") + + new_item = new_item.replace("emb_layers.1", "time_emb_proj") + new_item = new_item.replace("skip_connection", "conv_shortcut") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("norm.weight", "group_norm.weight") + new_item = new_item.replace("norm.bias", "group_norm.bias") + + new_item = new_item.replace("proj_out.weight", "proj_attn.weight") + new_item = new_item.replace("proj_out.bias", "proj_attn.bias") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + """ + This does the final conversion step: take locally converted weights and apply a global renaming + to them. It splits attention layers, and takes into account additional replacements + that may arise. + + Assigns the weights to the new checkpoint. + """ + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + # Splits the attention layers into three variables. + if attention_paths_to_split is not None: + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape) + checkpoint[path_map["key"]] = key.reshape(target_shape) + checkpoint[path_map["value"]] = value.reshape(target_shape) + + for path in paths: + new_path = path["new"] + + # These have already been assigned + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + # Global renaming happens here + new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") + new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") + new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + # proj_attn.weight has to be converted from conv 1D to linear + if "proj_attn.weight" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] + else: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def convert_ldm_checkpoint(checkpoint, config): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = checkpoint["time_embed.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = checkpoint["time_embed.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = checkpoint["time_embed.2.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = checkpoint["time_embed.2.bias"] + + new_checkpoint["conv_in.weight"] = checkpoint["input_blocks.0.0.weight"] + new_checkpoint["conv_in.bias"] = checkpoint["input_blocks.0.0.bias"] + + new_checkpoint["conv_norm_out.weight"] = checkpoint["out.0.weight"] + new_checkpoint["conv_norm_out.bias"] = checkpoint["out.0.bias"] + new_checkpoint["conv_out.weight"] = checkpoint["out.2.weight"] + new_checkpoint["conv_out.bias"] = checkpoint["out.2.bias"] + + # Retrieves the keys for the input blocks only + num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in checkpoint if "input_blocks" in layer}) + input_blocks = { + layer_id: [key for key in checkpoint if f"input_blocks.{layer_id}" in key] + for layer_id in range(num_input_blocks) + } + + # Retrieves the keys for the middle blocks only + num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in checkpoint if "middle_block" in layer}) + middle_blocks = { + layer_id: [key for key in checkpoint if f"middle_block.{layer_id}" in key] + for layer_id in range(num_middle_blocks) + } + + # Retrieves the keys for the output blocks only + num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in checkpoint if "output_blocks" in layer}) + output_blocks = { + layer_id: [key for key in checkpoint if f"output_blocks.{layer_id}" in key] + for layer_id in range(num_output_blocks) + } + + for i in range(1, num_input_blocks): + block_id = (i - 1) // (config["num_res_blocks"] + 1) + layer_in_block_id = (i - 1) % (config["num_res_blocks"] + 1) + + resnets = [key for key in input_blocks[i] if f"input_blocks.{i}.0" in key] + attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] + + if f"input_blocks.{i}.0.op.weight" in checkpoint: + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = checkpoint[ + f"input_blocks.{i}.0.op.weight" + ] + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = checkpoint[ + f"input_blocks.{i}.0.op.bias" + ] + continue + + paths = renew_resnet_paths(resnets) + meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} + resnet_op = {"old": "resnets.2.op", "new": "downsamplers.0.op"} + assign_to_checkpoint( + paths, new_checkpoint, checkpoint, additional_replacements=[meta_path, resnet_op], config=config + ) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"input_blocks.{i}.1", + "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}", + } + to_split = { + f"input_blocks.{i}.1.qkv.bias": { + "key": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.key.bias", + "query": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.query.bias", + "value": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.value.bias", + }, + f"input_blocks.{i}.1.qkv.weight": { + "key": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.key.weight", + "query": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.query.weight", + "value": f"down_blocks.{block_id}.attentions.{layer_in_block_id}.value.weight", + }, + } + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[meta_path], + attention_paths_to_split=to_split, + config=config, + ) + + resnet_0 = middle_blocks[0] + attentions = middle_blocks[1] + resnet_1 = middle_blocks[2] + + resnet_0_paths = renew_resnet_paths(resnet_0) + assign_to_checkpoint(resnet_0_paths, new_checkpoint, checkpoint, config=config) + + resnet_1_paths = renew_resnet_paths(resnet_1) + assign_to_checkpoint(resnet_1_paths, new_checkpoint, checkpoint, config=config) + + attentions_paths = renew_attention_paths(attentions) + to_split = { + "middle_block.1.qkv.bias": { + "key": "mid_block.attentions.0.key.bias", + "query": "mid_block.attentions.0.query.bias", + "value": "mid_block.attentions.0.value.bias", + }, + "middle_block.1.qkv.weight": { + "key": "mid_block.attentions.0.key.weight", + "query": "mid_block.attentions.0.query.weight", + "value": "mid_block.attentions.0.value.weight", + }, + } + assign_to_checkpoint( + attentions_paths, new_checkpoint, checkpoint, attention_paths_to_split=to_split, config=config + ) + + for i in range(num_output_blocks): + block_id = i // (config["num_res_blocks"] + 1) + layer_in_block_id = i % (config["num_res_blocks"] + 1) + output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] + output_block_list = {} + + for layer in output_block_layers: + layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) + if layer_id in output_block_list: + output_block_list[layer_id].append(layer_name) + else: + output_block_list[layer_id] = [layer_name] + + if len(output_block_list) > 1: + resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] + attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] + + resnet_0_paths = renew_resnet_paths(resnets) + paths = renew_resnet_paths(resnets) + + meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint(paths, new_checkpoint, checkpoint, additional_replacements=[meta_path], config=config) + + if ["conv.weight", "conv.bias"] in output_block_list.values(): + index = list(output_block_list.values()).index(["conv.weight", "conv.bias"]) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = checkpoint[ + f"output_blocks.{i}.{index}.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = checkpoint[ + f"output_blocks.{i}.{index}.conv.bias" + ] + + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"output_blocks.{i}.1", + "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", + } + to_split = { + f"output_blocks.{i}.1.qkv.bias": { + "key": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.key.bias", + "query": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.query.bias", + "value": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.value.bias", + }, + f"output_blocks.{i}.1.qkv.weight": { + "key": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.key.weight", + "query": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.query.weight", + "value": f"up_blocks.{block_id}.attentions.{layer_in_block_id}.value.weight", + }, + } + assign_to_checkpoint( + paths, + new_checkpoint, + checkpoint, + additional_replacements=[meta_path], + attention_paths_to_split=to_split if any("qkv" in key for key in attentions) else None, + config=config, + ) + else: + resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) + for path in resnet_0_paths: + old_path = ".".join(["output_blocks", str(i), path["old"]]) + new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) + + new_checkpoint[new_path] = checkpoint[old_path] + + return new_checkpoint + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--checkpoint_path", default=None, type=str, required=True, help="Path to the checkpoint to convert." + ) + + parser.add_argument( + "--config_file", + default=None, + type=str, + required=True, + help="The config json file corresponding to the architecture.", + ) + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + args = parser.parse_args() + + checkpoint = torch.load(args.checkpoint_path) + + with open(args.config_file) as f: + config = json.loads(f.read()) + + converted_checkpoint = convert_ldm_checkpoint(checkpoint, config) + + if "ldm" in config: + del config["ldm"] + + model = UNet2DModel(**config) + model.load_state_dict(converted_checkpoint) + + try: + scheduler = DDPMScheduler.from_config("/".join(args.checkpoint_path.split("/")[:-1])) + vqvae = VQModel.from_pretrained("/".join(args.checkpoint_path.split("/")[:-1])) + + pipe = LDMPipeline(unet=model, scheduler=scheduler, vae=vqvae) + pipe.save_pretrained(args.dump_path) + except: # noqa: E722 + model.save_pretrained(args.dump_path) diff --git a/diffusers/scripts/convert_models_diffuser_to_diffusers.py b/diffusers/scripts/convert_models_diffuser_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..9475f7da93fbe6be92d52c9f856b929b8af1954c --- /dev/null +++ b/diffusers/scripts/convert_models_diffuser_to_diffusers.py @@ -0,0 +1,100 @@ +import json +import os + +import torch + +from diffusers import UNet1DModel + + +os.makedirs("hub/hopper-medium-v2/unet/hor32", exist_ok=True) +os.makedirs("hub/hopper-medium-v2/unet/hor128", exist_ok=True) + +os.makedirs("hub/hopper-medium-v2/value_function", exist_ok=True) + + +def unet(hor): + if hor == 128: + down_block_types = ("DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D") + block_out_channels = (32, 128, 256) + up_block_types = ("UpResnetBlock1D", "UpResnetBlock1D") + + elif hor == 32: + down_block_types = ("DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D") + block_out_channels = (32, 64, 128, 256) + up_block_types = ("UpResnetBlock1D", "UpResnetBlock1D", "UpResnetBlock1D") + model = torch.load(f"/Users/bglickenhaus/Documents/diffuser/temporal_unet-hopper-mediumv2-hor{hor}.torch") + state_dict = model.state_dict() + config = dict( + down_block_types=down_block_types, + block_out_channels=block_out_channels, + up_block_types=up_block_types, + layers_per_block=1, + use_timestep_embedding=True, + out_block_type="OutConv1DBlock", + norm_num_groups=8, + downsample_each_block=False, + in_channels=14, + out_channels=14, + extra_in_channels=0, + time_embedding_type="positional", + flip_sin_to_cos=False, + freq_shift=1, + sample_size=65536, + mid_block_type="MidResTemporalBlock1D", + act_fn="mish", + ) + hf_value_function = UNet1DModel(**config) + print(f"length of state dict: {len(state_dict.keys())}") + print(f"length of value function dict: {len(hf_value_function.state_dict().keys())}") + mapping = dict((k, hfk) for k, hfk in zip(model.state_dict().keys(), hf_value_function.state_dict().keys())) + for k, v in mapping.items(): + state_dict[v] = state_dict.pop(k) + hf_value_function.load_state_dict(state_dict) + + torch.save(hf_value_function.state_dict(), f"hub/hopper-medium-v2/unet/hor{hor}/diffusion_pytorch_model.bin") + with open(f"hub/hopper-medium-v2/unet/hor{hor}/config.json", "w") as f: + json.dump(config, f) + + +def value_function(): + config = dict( + in_channels=14, + down_block_types=("DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D"), + up_block_types=(), + out_block_type="ValueFunction", + mid_block_type="ValueFunctionMidBlock1D", + block_out_channels=(32, 64, 128, 256), + layers_per_block=1, + downsample_each_block=True, + sample_size=65536, + out_channels=14, + extra_in_channels=0, + time_embedding_type="positional", + use_timestep_embedding=True, + flip_sin_to_cos=False, + freq_shift=1, + norm_num_groups=8, + act_fn="mish", + ) + + model = torch.load("/Users/bglickenhaus/Documents/diffuser/value_function-hopper-mediumv2-hor32.torch") + state_dict = model + hf_value_function = UNet1DModel(**config) + print(f"length of state dict: {len(state_dict.keys())}") + print(f"length of value function dict: {len(hf_value_function.state_dict().keys())}") + + mapping = dict((k, hfk) for k, hfk in zip(state_dict.keys(), hf_value_function.state_dict().keys())) + for k, v in mapping.items(): + state_dict[v] = state_dict.pop(k) + + hf_value_function.load_state_dict(state_dict) + + torch.save(hf_value_function.state_dict(), "hub/hopper-medium-v2/value_function/diffusion_pytorch_model.bin") + with open("hub/hopper-medium-v2/value_function/config.json", "w") as f: + json.dump(config, f) + + +if __name__ == "__main__": + unet(32) + # unet(128) + value_function() diff --git a/diffusers/scripts/convert_ncsnpp_original_checkpoint_to_diffusers.py b/diffusers/scripts/convert_ncsnpp_original_checkpoint_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..144701ec19af78a6970c8818fe1c44f63101518f --- /dev/null +++ b/diffusers/scripts/convert_ncsnpp_original_checkpoint_to_diffusers.py @@ -0,0 +1,185 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the NCSNPP checkpoints. """ + +import argparse +import json + +import torch + +from diffusers import ScoreSdeVePipeline, ScoreSdeVeScheduler, UNet2DModel + + +def convert_ncsnpp_checkpoint(checkpoint, config): + """ + Takes a state dict and the path to + """ + new_model_architecture = UNet2DModel(**config) + new_model_architecture.time_proj.W.data = checkpoint["all_modules.0.W"].data + new_model_architecture.time_proj.weight.data = checkpoint["all_modules.0.W"].data + new_model_architecture.time_embedding.linear_1.weight.data = checkpoint["all_modules.1.weight"].data + new_model_architecture.time_embedding.linear_1.bias.data = checkpoint["all_modules.1.bias"].data + + new_model_architecture.time_embedding.linear_2.weight.data = checkpoint["all_modules.2.weight"].data + new_model_architecture.time_embedding.linear_2.bias.data = checkpoint["all_modules.2.bias"].data + + new_model_architecture.conv_in.weight.data = checkpoint["all_modules.3.weight"].data + new_model_architecture.conv_in.bias.data = checkpoint["all_modules.3.bias"].data + + new_model_architecture.conv_norm_out.weight.data = checkpoint[list(checkpoint.keys())[-4]].data + new_model_architecture.conv_norm_out.bias.data = checkpoint[list(checkpoint.keys())[-3]].data + new_model_architecture.conv_out.weight.data = checkpoint[list(checkpoint.keys())[-2]].data + new_model_architecture.conv_out.bias.data = checkpoint[list(checkpoint.keys())[-1]].data + + module_index = 4 + + def set_attention_weights(new_layer, old_checkpoint, index): + new_layer.query.weight.data = old_checkpoint[f"all_modules.{index}.NIN_0.W"].data.T + new_layer.key.weight.data = old_checkpoint[f"all_modules.{index}.NIN_1.W"].data.T + new_layer.value.weight.data = old_checkpoint[f"all_modules.{index}.NIN_2.W"].data.T + + new_layer.query.bias.data = old_checkpoint[f"all_modules.{index}.NIN_0.b"].data + new_layer.key.bias.data = old_checkpoint[f"all_modules.{index}.NIN_1.b"].data + new_layer.value.bias.data = old_checkpoint[f"all_modules.{index}.NIN_2.b"].data + + new_layer.proj_attn.weight.data = old_checkpoint[f"all_modules.{index}.NIN_3.W"].data.T + new_layer.proj_attn.bias.data = old_checkpoint[f"all_modules.{index}.NIN_3.b"].data + + new_layer.group_norm.weight.data = old_checkpoint[f"all_modules.{index}.GroupNorm_0.weight"].data + new_layer.group_norm.bias.data = old_checkpoint[f"all_modules.{index}.GroupNorm_0.bias"].data + + def set_resnet_weights(new_layer, old_checkpoint, index): + new_layer.conv1.weight.data = old_checkpoint[f"all_modules.{index}.Conv_0.weight"].data + new_layer.conv1.bias.data = old_checkpoint[f"all_modules.{index}.Conv_0.bias"].data + new_layer.norm1.weight.data = old_checkpoint[f"all_modules.{index}.GroupNorm_0.weight"].data + new_layer.norm1.bias.data = old_checkpoint[f"all_modules.{index}.GroupNorm_0.bias"].data + + new_layer.conv2.weight.data = old_checkpoint[f"all_modules.{index}.Conv_1.weight"].data + new_layer.conv2.bias.data = old_checkpoint[f"all_modules.{index}.Conv_1.bias"].data + new_layer.norm2.weight.data = old_checkpoint[f"all_modules.{index}.GroupNorm_1.weight"].data + new_layer.norm2.bias.data = old_checkpoint[f"all_modules.{index}.GroupNorm_1.bias"].data + + new_layer.time_emb_proj.weight.data = old_checkpoint[f"all_modules.{index}.Dense_0.weight"].data + new_layer.time_emb_proj.bias.data = old_checkpoint[f"all_modules.{index}.Dense_0.bias"].data + + if new_layer.in_channels != new_layer.out_channels or new_layer.up or new_layer.down: + new_layer.conv_shortcut.weight.data = old_checkpoint[f"all_modules.{index}.Conv_2.weight"].data + new_layer.conv_shortcut.bias.data = old_checkpoint[f"all_modules.{index}.Conv_2.bias"].data + + for i, block in enumerate(new_model_architecture.downsample_blocks): + has_attentions = hasattr(block, "attentions") + for j in range(len(block.resnets)): + set_resnet_weights(block.resnets[j], checkpoint, module_index) + module_index += 1 + if has_attentions: + set_attention_weights(block.attentions[j], checkpoint, module_index) + module_index += 1 + + if hasattr(block, "downsamplers") and block.downsamplers is not None: + set_resnet_weights(block.resnet_down, checkpoint, module_index) + module_index += 1 + block.skip_conv.weight.data = checkpoint[f"all_modules.{module_index}.Conv_0.weight"].data + block.skip_conv.bias.data = checkpoint[f"all_modules.{module_index}.Conv_0.bias"].data + module_index += 1 + + set_resnet_weights(new_model_architecture.mid_block.resnets[0], checkpoint, module_index) + module_index += 1 + set_attention_weights(new_model_architecture.mid_block.attentions[0], checkpoint, module_index) + module_index += 1 + set_resnet_weights(new_model_architecture.mid_block.resnets[1], checkpoint, module_index) + module_index += 1 + + for i, block in enumerate(new_model_architecture.up_blocks): + has_attentions = hasattr(block, "attentions") + for j in range(len(block.resnets)): + set_resnet_weights(block.resnets[j], checkpoint, module_index) + module_index += 1 + if has_attentions: + set_attention_weights( + block.attentions[0], checkpoint, module_index + ) # why can there only be a single attention layer for up? + module_index += 1 + + if hasattr(block, "resnet_up") and block.resnet_up is not None: + block.skip_norm.weight.data = checkpoint[f"all_modules.{module_index}.weight"].data + block.skip_norm.bias.data = checkpoint[f"all_modules.{module_index}.bias"].data + module_index += 1 + block.skip_conv.weight.data = checkpoint[f"all_modules.{module_index}.weight"].data + block.skip_conv.bias.data = checkpoint[f"all_modules.{module_index}.bias"].data + module_index += 1 + set_resnet_weights(block.resnet_up, checkpoint, module_index) + module_index += 1 + + new_model_architecture.conv_norm_out.weight.data = checkpoint[f"all_modules.{module_index}.weight"].data + new_model_architecture.conv_norm_out.bias.data = checkpoint[f"all_modules.{module_index}.bias"].data + module_index += 1 + new_model_architecture.conv_out.weight.data = checkpoint[f"all_modules.{module_index}.weight"].data + new_model_architecture.conv_out.bias.data = checkpoint[f"all_modules.{module_index}.bias"].data + + return new_model_architecture.state_dict() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--checkpoint_path", + default="/Users/arthurzucker/Work/diffusers/ArthurZ/diffusion_pytorch_model.bin", + type=str, + required=False, + help="Path to the checkpoint to convert.", + ) + + parser.add_argument( + "--config_file", + default="/Users/arthurzucker/Work/diffusers/ArthurZ/config.json", + type=str, + required=False, + help="The config json file corresponding to the architecture.", + ) + + parser.add_argument( + "--dump_path", + default="/Users/arthurzucker/Work/diffusers/ArthurZ/diffusion_model_new.pt", + type=str, + required=False, + help="Path to the output model.", + ) + + args = parser.parse_args() + + checkpoint = torch.load(args.checkpoint_path, map_location="cpu") + + with open(args.config_file) as f: + config = json.loads(f.read()) + + converted_checkpoint = convert_ncsnpp_checkpoint( + checkpoint, + config, + ) + + if "sde" in config: + del config["sde"] + + model = UNet2DModel(**config) + model.load_state_dict(converted_checkpoint) + + try: + scheduler = ScoreSdeVeScheduler.from_config("/".join(args.checkpoint_path.split("/")[:-1])) + + pipe = ScoreSdeVePipeline(unet=model, scheduler=scheduler) + pipe.save_pretrained(args.dump_path) + except: # noqa: E722 + model.save_pretrained(args.dump_path) diff --git a/diffusers/scripts/convert_original_stable_diffusion_to_diffusers.py b/diffusers/scripts/convert_original_stable_diffusion_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..d449f283d95e98acb65701b9904ae13412e6b172 --- /dev/null +++ b/diffusers/scripts/convert_original_stable_diffusion_to_diffusers.py @@ -0,0 +1,118 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the LDM checkpoints. """ + +import argparse + +from diffusers.pipelines.stable_diffusion.convert_from_ckpt import load_pipeline_from_original_stable_diffusion_ckpt + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--checkpoint_path", default=None, type=str, required=True, help="Path to the checkpoint to convert." + ) + # !wget https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml + parser.add_argument( + "--original_config_file", + default=None, + type=str, + help="The YAML config file corresponding to the original architecture.", + ) + parser.add_argument( + "--num_in_channels", + default=None, + type=int, + help="The number of input channels. If `None` number of input channels will be automatically inferred.", + ) + parser.add_argument( + "--scheduler_type", + default="pndm", + type=str, + help="Type of scheduler to use. Should be one of ['pndm', 'lms', 'ddim', 'euler', 'euler-ancestral', 'dpm']", + ) + parser.add_argument( + "--pipeline_type", + default=None, + type=str, + help=( + "The pipeline type. One of 'FrozenOpenCLIPEmbedder', 'FrozenCLIPEmbedder', 'PaintByExample'" + ". If `None` pipeline will be automatically inferred." + ), + ) + parser.add_argument( + "--image_size", + default=None, + type=int, + help=( + "The image size that the model was trained on. Use 512 for Stable Diffusion v1.X and Stable Siffusion v2" + " Base. Use 768 for Stable Diffusion v2." + ), + ) + parser.add_argument( + "--prediction_type", + default=None, + type=str, + help=( + "The prediction type that the model was trained on. Use 'epsilon' for Stable Diffusion v1.X and Stable" + " Diffusion v2 Base. Use 'v_prediction' for Stable Diffusion v2." + ), + ) + parser.add_argument( + "--extract_ema", + action="store_true", + help=( + "Only relevant for checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights" + " or not. Defaults to `False`. Add `--extract_ema` to extract the EMA weights. EMA weights usually yield" + " higher quality images for inference. Non-EMA weights are usually better to continue fine-tuning." + ), + ) + parser.add_argument( + "--upcast_attention", + action="store_true", + help=( + "Whether the attention computation should always be upcasted. This is necessary when running stable" + " diffusion 2.1." + ), + ) + parser.add_argument( + "--from_safetensors", + action="store_true", + help="If `--checkpoint_path` is in `safetensors` format, load checkpoint with safetensors instead of PyTorch.", + ) + parser.add_argument( + "--to_safetensors", + action="store_true", + help="Whether to store pipeline in safetensors format or not.", + ) + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + parser.add_argument("--device", type=str, help="Device to use (e.g. cpu, cuda:0, cuda:1, etc.)") + args = parser.parse_args() + + pipe = load_pipeline_from_original_stable_diffusion_ckpt( + checkpoint_path=args.checkpoint_path, + original_config_file=args.original_config_file, + image_size=args.image_size, + prediction_type=args.prediction_type, + model_type=args.pipeline_type, + extract_ema=args.extract_ema, + scheduler_type=args.scheduler_type, + num_in_channels=args.num_in_channels, + upcast_attention=args.upcast_attention, + from_safetensors=args.from_safetensors, + device=args.device, + ) + pipe.save_pretrained(args.dump_path, safe_serialization=args.to_safetensors) diff --git a/diffusers/scripts/convert_stable_diffusion_checkpoint_to_onnx.py b/diffusers/scripts/convert_stable_diffusion_checkpoint_to_onnx.py new file mode 100644 index 0000000000000000000000000000000000000000..e2d28050709eebc2be76cd4754eb015819402884 --- /dev/null +++ b/diffusers/scripts/convert_stable_diffusion_checkpoint_to_onnx.py @@ -0,0 +1,265 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import argparse +import os +import shutil +from pathlib import Path + +import onnx +import torch +from packaging import version +from torch.onnx import export + +from diffusers import OnnxRuntimeModel, OnnxStableDiffusionPipeline, StableDiffusionPipeline + + +is_torch_less_than_1_11 = version.parse(version.parse(torch.__version__).base_version) < version.parse("1.11") + + +def onnx_export( + model, + model_args: tuple, + output_path: Path, + ordered_input_names, + output_names, + dynamic_axes, + opset, + use_external_data_format=False, +): + output_path.parent.mkdir(parents=True, exist_ok=True) + # PyTorch deprecated the `enable_onnx_checker` and `use_external_data_format` arguments in v1.11, + # so we check the torch version for backwards compatibility + if is_torch_less_than_1_11: + export( + model, + model_args, + f=output_path.as_posix(), + input_names=ordered_input_names, + output_names=output_names, + dynamic_axes=dynamic_axes, + do_constant_folding=True, + use_external_data_format=use_external_data_format, + enable_onnx_checker=True, + opset_version=opset, + ) + else: + export( + model, + model_args, + f=output_path.as_posix(), + input_names=ordered_input_names, + output_names=output_names, + dynamic_axes=dynamic_axes, + do_constant_folding=True, + opset_version=opset, + ) + + +@torch.no_grad() +def convert_models(model_path: str, output_path: str, opset: int, fp16: bool = False): + dtype = torch.float16 if fp16 else torch.float32 + if fp16 and torch.cuda.is_available(): + device = "cuda" + elif fp16 and not torch.cuda.is_available(): + raise ValueError("`float16` model export is only supported on GPUs with CUDA") + else: + device = "cpu" + pipeline = StableDiffusionPipeline.from_pretrained(model_path, torch_dtype=dtype).to(device) + output_path = Path(output_path) + + # TEXT ENCODER + num_tokens = pipeline.text_encoder.config.max_position_embeddings + text_hidden_size = pipeline.text_encoder.config.hidden_size + text_input = pipeline.tokenizer( + "A sample prompt", + padding="max_length", + max_length=pipeline.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + onnx_export( + pipeline.text_encoder, + # casting to torch.int32 until the CLIP fix is released: https://github.com/huggingface/transformers/pull/18515/files + model_args=(text_input.input_ids.to(device=device, dtype=torch.int32)), + output_path=output_path / "text_encoder" / "model.onnx", + ordered_input_names=["input_ids"], + output_names=["last_hidden_state", "pooler_output"], + dynamic_axes={ + "input_ids": {0: "batch", 1: "sequence"}, + }, + opset=opset, + ) + del pipeline.text_encoder + + # UNET + unet_in_channels = pipeline.unet.config.in_channels + unet_sample_size = pipeline.unet.config.sample_size + unet_path = output_path / "unet" / "model.onnx" + onnx_export( + pipeline.unet, + model_args=( + torch.randn(2, unet_in_channels, unet_sample_size, unet_sample_size).to(device=device, dtype=dtype), + torch.randn(2).to(device=device, dtype=dtype), + torch.randn(2, num_tokens, text_hidden_size).to(device=device, dtype=dtype), + False, + ), + output_path=unet_path, + ordered_input_names=["sample", "timestep", "encoder_hidden_states", "return_dict"], + output_names=["out_sample"], # has to be different from "sample" for correct tracing + dynamic_axes={ + "sample": {0: "batch", 1: "channels", 2: "height", 3: "width"}, + "timestep": {0: "batch"}, + "encoder_hidden_states": {0: "batch", 1: "sequence"}, + }, + opset=opset, + use_external_data_format=True, # UNet is > 2GB, so the weights need to be split + ) + unet_model_path = str(unet_path.absolute().as_posix()) + unet_dir = os.path.dirname(unet_model_path) + unet = onnx.load(unet_model_path) + # clean up existing tensor files + shutil.rmtree(unet_dir) + os.mkdir(unet_dir) + # collate external tensor files into one + onnx.save_model( + unet, + unet_model_path, + save_as_external_data=True, + all_tensors_to_one_file=True, + location="weights.pb", + convert_attribute=False, + ) + del pipeline.unet + + # VAE ENCODER + vae_encoder = pipeline.vae + vae_in_channels = vae_encoder.config.in_channels + vae_sample_size = vae_encoder.config.sample_size + # need to get the raw tensor output (sample) from the encoder + vae_encoder.forward = lambda sample, return_dict: vae_encoder.encode(sample, return_dict)[0].sample() + onnx_export( + vae_encoder, + model_args=( + torch.randn(1, vae_in_channels, vae_sample_size, vae_sample_size).to(device=device, dtype=dtype), + False, + ), + output_path=output_path / "vae_encoder" / "model.onnx", + ordered_input_names=["sample", "return_dict"], + output_names=["latent_sample"], + dynamic_axes={ + "sample": {0: "batch", 1: "channels", 2: "height", 3: "width"}, + }, + opset=opset, + ) + + # VAE DECODER + vae_decoder = pipeline.vae + vae_latent_channels = vae_decoder.config.latent_channels + vae_out_channels = vae_decoder.config.out_channels + # forward only through the decoder part + vae_decoder.forward = vae_encoder.decode + onnx_export( + vae_decoder, + model_args=( + torch.randn(1, vae_latent_channels, unet_sample_size, unet_sample_size).to(device=device, dtype=dtype), + False, + ), + output_path=output_path / "vae_decoder" / "model.onnx", + ordered_input_names=["latent_sample", "return_dict"], + output_names=["sample"], + dynamic_axes={ + "latent_sample": {0: "batch", 1: "channels", 2: "height", 3: "width"}, + }, + opset=opset, + ) + del pipeline.vae + + # SAFETY CHECKER + if pipeline.safety_checker is not None: + safety_checker = pipeline.safety_checker + clip_num_channels = safety_checker.config.vision_config.num_channels + clip_image_size = safety_checker.config.vision_config.image_size + safety_checker.forward = safety_checker.forward_onnx + onnx_export( + pipeline.safety_checker, + model_args=( + torch.randn( + 1, + clip_num_channels, + clip_image_size, + clip_image_size, + ).to(device=device, dtype=dtype), + torch.randn(1, vae_sample_size, vae_sample_size, vae_out_channels).to(device=device, dtype=dtype), + ), + output_path=output_path / "safety_checker" / "model.onnx", + ordered_input_names=["clip_input", "images"], + output_names=["out_images", "has_nsfw_concepts"], + dynamic_axes={ + "clip_input": {0: "batch", 1: "channels", 2: "height", 3: "width"}, + "images": {0: "batch", 1: "height", 2: "width", 3: "channels"}, + }, + opset=opset, + ) + del pipeline.safety_checker + safety_checker = OnnxRuntimeModel.from_pretrained(output_path / "safety_checker") + feature_extractor = pipeline.feature_extractor + else: + safety_checker = None + feature_extractor = None + + onnx_pipeline = OnnxStableDiffusionPipeline( + vae_encoder=OnnxRuntimeModel.from_pretrained(output_path / "vae_encoder"), + vae_decoder=OnnxRuntimeModel.from_pretrained(output_path / "vae_decoder"), + text_encoder=OnnxRuntimeModel.from_pretrained(output_path / "text_encoder"), + tokenizer=pipeline.tokenizer, + unet=OnnxRuntimeModel.from_pretrained(output_path / "unet"), + scheduler=pipeline.scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + requires_safety_checker=safety_checker is not None, + ) + + onnx_pipeline.save_pretrained(output_path) + print("ONNX pipeline saved to", output_path) + + del pipeline + del onnx_pipeline + _ = OnnxStableDiffusionPipeline.from_pretrained(output_path, provider="CPUExecutionProvider") + print("ONNX pipeline is loadable") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--model_path", + type=str, + required=True, + help="Path to the `diffusers` checkpoint to convert (either a local directory or on the Hub).", + ) + + parser.add_argument("--output_path", type=str, required=True, help="Path to the output model.") + + parser.add_argument( + "--opset", + default=14, + type=int, + help="The version of the ONNX operator set to use.", + ) + parser.add_argument("--fp16", action="store_true", default=False, help="Export the models in `float16` mode") + + args = parser.parse_args() + + convert_models(args.model_path, args.output_path, args.opset, args.fp16) diff --git a/diffusers/scripts/convert_unclip_txt2img_to_image_variation.py b/diffusers/scripts/convert_unclip_txt2img_to_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..07f8ebf2a3d012600a533dcfa642b609c31a3d8c --- /dev/null +++ b/diffusers/scripts/convert_unclip_txt2img_to_image_variation.py @@ -0,0 +1,41 @@ +import argparse + +from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection + +from diffusers import UnCLIPImageVariationPipeline, UnCLIPPipeline + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + parser.add_argument( + "--txt2img_unclip", + default="kakaobrain/karlo-v1-alpha", + type=str, + required=False, + help="The pretrained txt2img unclip.", + ) + + args = parser.parse_args() + + txt2img = UnCLIPPipeline.from_pretrained(args.txt2img_unclip) + + feature_extractor = CLIPImageProcessor() + image_encoder = CLIPVisionModelWithProjection.from_pretrained("openai/clip-vit-large-patch14") + + img2img = UnCLIPImageVariationPipeline( + decoder=txt2img.decoder, + text_encoder=txt2img.text_encoder, + tokenizer=txt2img.tokenizer, + text_proj=txt2img.text_proj, + feature_extractor=feature_extractor, + image_encoder=image_encoder, + super_res_first=txt2img.super_res_first, + super_res_last=txt2img.super_res_last, + decoder_scheduler=txt2img.decoder_scheduler, + super_res_scheduler=txt2img.super_res_scheduler, + ) + + img2img.save_pretrained(args.dump_path) diff --git a/diffusers/scripts/convert_vae_pt_to_diffusers.py b/diffusers/scripts/convert_vae_pt_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..4762ffcf8d00dd2ec18fd1779e7eebe472392b7d --- /dev/null +++ b/diffusers/scripts/convert_vae_pt_to_diffusers.py @@ -0,0 +1,151 @@ +import argparse +import io + +import requests +import torch +from omegaconf import OmegaConf + +from diffusers import AutoencoderKL +from diffusers.pipelines.stable_diffusion.convert_from_ckpt import ( + assign_to_checkpoint, + conv_attn_to_linear, + create_vae_diffusers_config, + renew_vae_attention_paths, + renew_vae_resnet_paths, +) + + +def custom_convert_ldm_vae_checkpoint(checkpoint, config): + vae_state_dict = checkpoint + + new_checkpoint = {} + + new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] + new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] + new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] + + new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] + new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] + + # Retrieves the keys for the encoder down blocks only + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) + down_blocks = { + layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + # Retrieves the keys for the decoder up blocks only + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) + up_blocks = { + layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) + } + + for i in range(num_down_blocks): + resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] + + if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.weight" + ) + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.bias" + ) + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + resnets = [ + key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key + ] + + if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.bias" + ] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + return new_checkpoint + + +def vae_pt_to_vae_diffuser( + checkpoint_path: str, + output_path: str, +): + # Only support V1 + r = requests.get( + " https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml" + ) + io_obj = io.BytesIO(r.content) + + original_config = OmegaConf.load(io_obj) + image_size = 512 + device = "cuda" if torch.cuda.is_available() else "cpu" + checkpoint = torch.load(checkpoint_path, map_location=device) + + # Convert the VAE model. + vae_config = create_vae_diffusers_config(original_config, image_size=image_size) + converted_vae_checkpoint = custom_convert_ldm_vae_checkpoint(checkpoint["state_dict"], vae_config) + + vae = AutoencoderKL(**vae_config) + vae.load_state_dict(converted_vae_checkpoint) + vae.save_pretrained(output_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("--vae_pt_path", default=None, type=str, required=True, help="Path to the VAE.pt to convert.") + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the VAE.pt to convert.") + + args = parser.parse_args() + + vae_pt_to_vae_diffuser(args.vae_pt_path, args.dump_path) diff --git a/diffusers/scripts/convert_versatile_diffusion_to_diffusers.py b/diffusers/scripts/convert_versatile_diffusion_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..cb1ad4cc3f603b4441561b70ade52e3300827d15 --- /dev/null +++ b/diffusers/scripts/convert_versatile_diffusion_to_diffusers.py @@ -0,0 +1,791 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the Versatile Stable Diffusion checkpoints. """ + +import argparse +from argparse import Namespace + +import torch +from transformers import ( + CLIPFeatureExtractor, + CLIPTextModelWithProjection, + CLIPTokenizer, + CLIPVisionModelWithProjection, +) + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + UNet2DConditionModel, + VersatileDiffusionPipeline, +) +from diffusers.pipelines.versatile_diffusion.modeling_text_unet import UNetFlatConditionModel + + +SCHEDULER_CONFIG = Namespace( + **{ + "beta_linear_start": 0.00085, + "beta_linear_end": 0.012, + "timesteps": 1000, + "scale_factor": 0.18215, + } +) + +IMAGE_UNET_CONFIG = Namespace( + **{ + "input_channels": 4, + "model_channels": 320, + "output_channels": 4, + "num_noattn_blocks": [2, 2, 2, 2], + "channel_mult": [1, 2, 4, 4], + "with_attn": [True, True, True, False], + "num_heads": 8, + "context_dim": 768, + "use_checkpoint": True, + } +) + +TEXT_UNET_CONFIG = Namespace( + **{ + "input_channels": 768, + "model_channels": 320, + "output_channels": 768, + "num_noattn_blocks": [2, 2, 2, 2], + "channel_mult": [1, 2, 4, 4], + "second_dim": [4, 4, 4, 4], + "with_attn": [True, True, True, False], + "num_heads": 8, + "context_dim": 768, + "use_checkpoint": True, + } +) + +AUTOENCODER_CONFIG = Namespace( + **{ + "double_z": True, + "z_channels": 4, + "resolution": 256, + "in_channels": 3, + "out_ch": 3, + "ch": 128, + "ch_mult": [1, 2, 4, 4], + "num_res_blocks": 2, + "attn_resolutions": [], + "dropout": 0.0, + } +) + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item.replace("in_layers.0", "norm1") + new_item = new_item.replace("in_layers.2", "conv1") + + new_item = new_item.replace("out_layers.0", "norm2") + new_item = new_item.replace("out_layers.3", "conv2") + + new_item = new_item.replace("emb_layers.1", "time_emb_proj") + new_item = new_item.replace("skip_connection", "conv_shortcut") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("nin_shortcut", "conv_shortcut") + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + # new_item = new_item.replace('norm.weight', 'group_norm.weight') + # new_item = new_item.replace('norm.bias', 'group_norm.bias') + + # new_item = new_item.replace('proj_out.weight', 'proj_attn.weight') + # new_item = new_item.replace('proj_out.bias', 'proj_attn.bias') + + # new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("norm.weight", "group_norm.weight") + new_item = new_item.replace("norm.bias", "group_norm.bias") + + new_item = new_item.replace("q.weight", "query.weight") + new_item = new_item.replace("q.bias", "query.bias") + + new_item = new_item.replace("k.weight", "key.weight") + new_item = new_item.replace("k.bias", "key.bias") + + new_item = new_item.replace("v.weight", "value.weight") + new_item = new_item.replace("v.bias", "value.bias") + + new_item = new_item.replace("proj_out.weight", "proj_attn.weight") + new_item = new_item.replace("proj_out.bias", "proj_attn.bias") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + """ + This does the final conversion step: take locally converted weights and apply a global renaming + to them. It splits attention layers, and takes into account additional replacements + that may arise. + + Assigns the weights to the new checkpoint. + """ + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + # Splits the attention layers into three variables. + if attention_paths_to_split is not None: + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape) + checkpoint[path_map["key"]] = key.reshape(target_shape) + checkpoint[path_map["value"]] = value.reshape(target_shape) + + for path in paths: + new_path = path["new"] + + # These have already been assigned + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + # Global renaming happens here + new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") + new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") + new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + # proj_attn.weight has to be converted from conv 1D to linear + if "proj_attn.weight" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] + elif path["old"] in old_checkpoint: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def conv_attn_to_linear(checkpoint): + keys = list(checkpoint.keys()) + attn_keys = ["query.weight", "key.weight", "value.weight"] + for key in keys: + if ".".join(key.split(".")[-2:]) in attn_keys: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0, 0] + elif "proj_attn.weight" in key: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0] + + +def create_image_unet_diffusers_config(unet_params): + """ + Creates a config for the diffusers based on the config of the VD model. + """ + + block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] + + down_block_types = [] + resolution = 1 + for i in range(len(block_out_channels)): + block_type = "CrossAttnDownBlock2D" if unet_params.with_attn[i] else "DownBlock2D" + down_block_types.append(block_type) + if i != len(block_out_channels) - 1: + resolution *= 2 + + up_block_types = [] + for i in range(len(block_out_channels)): + block_type = "CrossAttnUpBlock2D" if unet_params.with_attn[-i - 1] else "UpBlock2D" + up_block_types.append(block_type) + resolution //= 2 + + if not all(n == unet_params.num_noattn_blocks[0] for n in unet_params.num_noattn_blocks): + raise ValueError("Not all num_res_blocks are equal, which is not supported in this script.") + + config = dict( + sample_size=None, + in_channels=unet_params.input_channels, + out_channels=unet_params.output_channels, + down_block_types=tuple(down_block_types), + up_block_types=tuple(up_block_types), + block_out_channels=tuple(block_out_channels), + layers_per_block=unet_params.num_noattn_blocks[0], + cross_attention_dim=unet_params.context_dim, + attention_head_dim=unet_params.num_heads, + ) + + return config + + +def create_text_unet_diffusers_config(unet_params): + """ + Creates a config for the diffusers based on the config of the VD model. + """ + + block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] + + down_block_types = [] + resolution = 1 + for i in range(len(block_out_channels)): + block_type = "CrossAttnDownBlockFlat" if unet_params.with_attn[i] else "DownBlockFlat" + down_block_types.append(block_type) + if i != len(block_out_channels) - 1: + resolution *= 2 + + up_block_types = [] + for i in range(len(block_out_channels)): + block_type = "CrossAttnUpBlockFlat" if unet_params.with_attn[-i - 1] else "UpBlockFlat" + up_block_types.append(block_type) + resolution //= 2 + + if not all(n == unet_params.num_noattn_blocks[0] for n in unet_params.num_noattn_blocks): + raise ValueError("Not all num_res_blocks are equal, which is not supported in this script.") + + config = dict( + sample_size=None, + in_channels=(unet_params.input_channels, 1, 1), + out_channels=(unet_params.output_channels, 1, 1), + down_block_types=tuple(down_block_types), + up_block_types=tuple(up_block_types), + block_out_channels=tuple(block_out_channels), + layers_per_block=unet_params.num_noattn_blocks[0], + cross_attention_dim=unet_params.context_dim, + attention_head_dim=unet_params.num_heads, + ) + + return config + + +def create_vae_diffusers_config(vae_params): + """ + Creates a config for the diffusers based on the config of the VD model. + """ + + block_out_channels = [vae_params.ch * mult for mult in vae_params.ch_mult] + down_block_types = ["DownEncoderBlock2D"] * len(block_out_channels) + up_block_types = ["UpDecoderBlock2D"] * len(block_out_channels) + + config = dict( + sample_size=vae_params.resolution, + in_channels=vae_params.in_channels, + out_channels=vae_params.out_ch, + down_block_types=tuple(down_block_types), + up_block_types=tuple(up_block_types), + block_out_channels=tuple(block_out_channels), + latent_channels=vae_params.z_channels, + layers_per_block=vae_params.num_res_blocks, + ) + return config + + +def create_diffusers_scheduler(original_config): + schedular = DDIMScheduler( + num_train_timesteps=original_config.model.params.timesteps, + beta_start=original_config.model.params.linear_start, + beta_end=original_config.model.params.linear_end, + beta_schedule="scaled_linear", + ) + return schedular + + +def convert_vd_unet_checkpoint(checkpoint, config, unet_key, extract_ema=False): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + + # extract state_dict for UNet + unet_state_dict = {} + keys = list(checkpoint.keys()) + + # at least a 100 parameters have to start with `model_ema` in order for the checkpoint to be EMA + if sum(k.startswith("model_ema") for k in keys) > 100: + print("Checkpoint has both EMA and non-EMA weights.") + if extract_ema: + print( + "In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA" + " weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag." + ) + for key in keys: + if key.startswith("model.diffusion_model"): + flat_ema_key = "model_ema." + "".join(key.split(".")[1:]) + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(flat_ema_key) + else: + print( + "In this conversion only the non-EMA weights are extracted. If you want to instead extract the EMA" + " weights (usually better for inference), please make sure to add the `--extract_ema` flag." + ) + + for key in keys: + if key.startswith(unet_key): + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(key) + + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = checkpoint["model.diffusion_model.time_embed.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = checkpoint["model.diffusion_model.time_embed.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = checkpoint["model.diffusion_model.time_embed.2.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = checkpoint["model.diffusion_model.time_embed.2.bias"] + + new_checkpoint["conv_in.weight"] = unet_state_dict["input_blocks.0.0.weight"] + new_checkpoint["conv_in.bias"] = unet_state_dict["input_blocks.0.0.bias"] + + new_checkpoint["conv_norm_out.weight"] = unet_state_dict["out.0.weight"] + new_checkpoint["conv_norm_out.bias"] = unet_state_dict["out.0.bias"] + new_checkpoint["conv_out.weight"] = unet_state_dict["out.2.weight"] + new_checkpoint["conv_out.bias"] = unet_state_dict["out.2.bias"] + + # Retrieves the keys for the input blocks only + num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "input_blocks" in layer}) + input_blocks = { + layer_id: [key for key in unet_state_dict if f"input_blocks.{layer_id}" in key] + for layer_id in range(num_input_blocks) + } + + # Retrieves the keys for the middle blocks only + num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "middle_block" in layer}) + middle_blocks = { + layer_id: [key for key in unet_state_dict if f"middle_block.{layer_id}" in key] + for layer_id in range(num_middle_blocks) + } + + # Retrieves the keys for the output blocks only + num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "output_blocks" in layer}) + output_blocks = { + layer_id: [key for key in unet_state_dict if f"output_blocks.{layer_id}" in key] + for layer_id in range(num_output_blocks) + } + + for i in range(1, num_input_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + layer_in_block_id = (i - 1) % (config["layers_per_block"] + 1) + + resnets = [ + key for key in input_blocks[i] if f"input_blocks.{i}.0" in key and f"input_blocks.{i}.0.op" not in key + ] + attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] + + if f"input_blocks.{i}.0.op.weight" in unet_state_dict: + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.weight" + ) + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.bias" + ) + elif f"input_blocks.{i}.0.weight" in unet_state_dict: + # text_unet uses linear layers in place of downsamplers + shape = unet_state_dict[f"input_blocks.{i}.0.weight"].shape + if shape[0] != shape[1]: + continue + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.weight"] = unet_state_dict.pop( + f"input_blocks.{i}.0.weight" + ) + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.bias"] = unet_state_dict.pop( + f"input_blocks.{i}.0.bias" + ) + + paths = renew_resnet_paths(resnets) + meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = {"old": f"input_blocks.{i}.1", "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + resnet_0 = middle_blocks[0] + attentions = middle_blocks[1] + resnet_1 = middle_blocks[2] + + resnet_0_paths = renew_resnet_paths(resnet_0) + assign_to_checkpoint(resnet_0_paths, new_checkpoint, unet_state_dict, config=config) + + resnet_1_paths = renew_resnet_paths(resnet_1) + assign_to_checkpoint(resnet_1_paths, new_checkpoint, unet_state_dict, config=config) + + attentions_paths = renew_attention_paths(attentions) + meta_path = {"old": "middle_block.1", "new": "mid_block.attentions.0"} + assign_to_checkpoint( + attentions_paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + for i in range(num_output_blocks): + block_id = i // (config["layers_per_block"] + 1) + layer_in_block_id = i % (config["layers_per_block"] + 1) + output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] + output_block_list = {} + + for layer in output_block_layers: + layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) + if layer_id in output_block_list: + output_block_list[layer_id].append(layer_name) + else: + output_block_list[layer_id] = [layer_name] + + if len(output_block_list) > 1: + resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] + attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] + + paths = renew_resnet_paths(resnets) + + meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + if ["conv.weight", "conv.bias"] in output_block_list.values(): + index = list(output_block_list.values()).index(["conv.weight", "conv.bias"]) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.bias" + ] + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + elif f"output_blocks.{i}.1.weight" in unet_state_dict: + # text_unet uses linear layers in place of upsamplers + shape = unet_state_dict[f"output_blocks.{i}.1.weight"].shape + if shape[0] != shape[1]: + continue + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.weight"] = unet_state_dict.pop( + f"output_blocks.{i}.1.weight" + ) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.bias"] = unet_state_dict.pop( + f"output_blocks.{i}.1.bias" + ) + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + elif f"output_blocks.{i}.2.weight" in unet_state_dict: + # text_unet uses linear layers in place of upsamplers + shape = unet_state_dict[f"output_blocks.{i}.2.weight"].shape + if shape[0] != shape[1]: + continue + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.weight"] = unet_state_dict.pop( + f"output_blocks.{i}.2.weight" + ) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.bias"] = unet_state_dict.pop( + f"output_blocks.{i}.2.bias" + ) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"output_blocks.{i}.1", + "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", + } + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + else: + resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) + for path in resnet_0_paths: + old_path = ".".join(["output_blocks", str(i), path["old"]]) + new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) + + new_checkpoint[new_path] = unet_state_dict[old_path] + + return new_checkpoint + + +def convert_vd_vae_checkpoint(checkpoint, config): + # extract state dict for VAE + vae_state_dict = {} + keys = list(checkpoint.keys()) + for key in keys: + vae_state_dict[key] = checkpoint.get(key) + + new_checkpoint = {} + + new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] + new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] + new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] + + new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] + new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] + + # Retrieves the keys for the encoder down blocks only + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) + down_blocks = { + layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + # Retrieves the keys for the decoder up blocks only + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) + up_blocks = { + layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) + } + + for i in range(num_down_blocks): + resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] + + if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.weight" + ) + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.bias" + ) + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + resnets = [ + key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key + ] + + if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.bias" + ] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + return new_checkpoint + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--unet_checkpoint_path", default=None, type=str, required=False, help="Path to the checkpoint to convert." + ) + parser.add_argument( + "--vae_checkpoint_path", default=None, type=str, required=False, help="Path to the checkpoint to convert." + ) + parser.add_argument( + "--optimus_checkpoint_path", default=None, type=str, required=False, help="Path to the checkpoint to convert." + ) + parser.add_argument( + "--scheduler_type", + default="pndm", + type=str, + help="Type of scheduler to use. Should be one of ['pndm', 'lms', 'ddim', 'euler', 'euler-ancestral', 'dpm']", + ) + parser.add_argument( + "--extract_ema", + action="store_true", + help=( + "Only relevant for checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights" + " or not. Defaults to `False`. Add `--extract_ema` to extract the EMA weights. EMA weights usually yield" + " higher quality images for inference. Non-EMA weights are usually better to continue fine-tuning." + ), + ) + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + args = parser.parse_args() + + scheduler_config = SCHEDULER_CONFIG + + num_train_timesteps = scheduler_config.timesteps + beta_start = scheduler_config.beta_linear_start + beta_end = scheduler_config.beta_linear_end + if args.scheduler_type == "pndm": + scheduler = PNDMScheduler( + beta_end=beta_end, + beta_schedule="scaled_linear", + beta_start=beta_start, + num_train_timesteps=num_train_timesteps, + skip_prk_steps=True, + steps_offset=1, + ) + elif args.scheduler_type == "lms": + scheduler = LMSDiscreteScheduler(beta_start=beta_start, beta_end=beta_end, beta_schedule="scaled_linear") + elif args.scheduler_type == "euler": + scheduler = EulerDiscreteScheduler(beta_start=beta_start, beta_end=beta_end, beta_schedule="scaled_linear") + elif args.scheduler_type == "euler-ancestral": + scheduler = EulerAncestralDiscreteScheduler( + beta_start=beta_start, beta_end=beta_end, beta_schedule="scaled_linear" + ) + elif args.scheduler_type == "dpm": + scheduler = DPMSolverMultistepScheduler( + beta_start=beta_start, beta_end=beta_end, beta_schedule="scaled_linear" + ) + elif args.scheduler_type == "ddim": + scheduler = DDIMScheduler( + beta_start=beta_start, + beta_end=beta_end, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + steps_offset=1, + ) + else: + raise ValueError(f"Scheduler of type {args.scheduler_type} doesn't exist!") + + # Convert the UNet2DConditionModel models. + if args.unet_checkpoint_path is not None: + # image UNet + image_unet_config = create_image_unet_diffusers_config(IMAGE_UNET_CONFIG) + checkpoint = torch.load(args.unet_checkpoint_path) + converted_image_unet_checkpoint = convert_vd_unet_checkpoint( + checkpoint, image_unet_config, unet_key="model.diffusion_model.unet_image.", extract_ema=args.extract_ema + ) + image_unet = UNet2DConditionModel(**image_unet_config) + image_unet.load_state_dict(converted_image_unet_checkpoint) + + # text UNet + text_unet_config = create_text_unet_diffusers_config(TEXT_UNET_CONFIG) + converted_text_unet_checkpoint = convert_vd_unet_checkpoint( + checkpoint, text_unet_config, unet_key="model.diffusion_model.unet_text.", extract_ema=args.extract_ema + ) + text_unet = UNetFlatConditionModel(**text_unet_config) + text_unet.load_state_dict(converted_text_unet_checkpoint) + + # Convert the VAE model. + if args.vae_checkpoint_path is not None: + vae_config = create_vae_diffusers_config(AUTOENCODER_CONFIG) + checkpoint = torch.load(args.vae_checkpoint_path) + converted_vae_checkpoint = convert_vd_vae_checkpoint(checkpoint, vae_config) + + vae = AutoencoderKL(**vae_config) + vae.load_state_dict(converted_vae_checkpoint) + + tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") + image_feature_extractor = CLIPFeatureExtractor.from_pretrained("openai/clip-vit-large-patch14") + text_encoder = CLIPTextModelWithProjection.from_pretrained("openai/clip-vit-large-patch14") + image_encoder = CLIPVisionModelWithProjection.from_pretrained("openai/clip-vit-large-patch14") + + pipe = VersatileDiffusionPipeline( + scheduler=scheduler, + tokenizer=tokenizer, + image_feature_extractor=image_feature_extractor, + text_encoder=text_encoder, + image_encoder=image_encoder, + image_unet=image_unet, + text_unet=text_unet, + vae=vae, + ) + pipe.save_pretrained(args.dump_path) diff --git a/diffusers/scripts/convert_vq_diffusion_to_diffusers.py b/diffusers/scripts/convert_vq_diffusion_to_diffusers.py new file mode 100644 index 0000000000000000000000000000000000000000..58ed2d93d5df4bd486b7485e1dc5e3cd255f2d99 --- /dev/null +++ b/diffusers/scripts/convert_vq_diffusion_to_diffusers.py @@ -0,0 +1,925 @@ +""" +This script ports models from VQ-diffusion (https://github.com/microsoft/VQ-Diffusion) to diffusers. + +It currently only supports porting the ITHQ dataset. + +ITHQ dataset: +```sh +# From the root directory of diffusers. + +# Download the VQVAE checkpoint +$ wget https://facevcstandard.blob.core.windows.net/v-zhictang/Improved-VQ-Diffusion_model_release/ithq_vqvae.pth?sv=2020-10-02&st=2022-05-30T15%3A17%3A18Z&se=2030-05-31T15%3A17%3A00Z&sr=b&sp=r&sig=1jVavHFPpUjDs%2FTO1V3PTezaNbPp2Nx8MxiWI7y6fEY%3D -O ithq_vqvae.pth + +# Download the VQVAE config +# NOTE that in VQ-diffusion the documented file is `configs/ithq.yaml` but the target class +# `image_synthesis.modeling.codecs.image_codec.ema_vqvae.PatchVQVAE` +# loads `OUTPUT/pretrained_model/taming_dvae/config.yaml` +$ wget https://raw.githubusercontent.com/microsoft/VQ-Diffusion/main/OUTPUT/pretrained_model/taming_dvae/config.yaml -O ithq_vqvae.yaml + +# Download the main model checkpoint +$ wget https://facevcstandard.blob.core.windows.net/v-zhictang/Improved-VQ-Diffusion_model_release/ithq_learnable.pth?sv=2020-10-02&st=2022-05-30T10%3A22%3A06Z&se=2030-05-31T10%3A22%3A00Z&sr=b&sp=r&sig=GOE%2Bza02%2FPnGxYVOOPtwrTR4RA3%2F5NVgMxdW4kjaEZ8%3D -O ithq_learnable.pth + +# Download the main model config +$ wget https://raw.githubusercontent.com/microsoft/VQ-Diffusion/main/configs/ithq.yaml -O ithq.yaml + +# run the convert script +$ python ./scripts/convert_vq_diffusion_to_diffusers.py \ + --checkpoint_path ./ithq_learnable.pth \ + --original_config_file ./ithq.yaml \ + --vqvae_checkpoint_path ./ithq_vqvae.pth \ + --vqvae_original_config_file ./ithq_vqvae.yaml \ + --dump_path +``` +""" + +import argparse +import tempfile + +import torch +import yaml +from accelerate import init_empty_weights, load_checkpoint_and_dispatch +from transformers import CLIPTextModel, CLIPTokenizer +from yaml.loader import FullLoader + +from diffusers import Transformer2DModel, VQDiffusionPipeline, VQDiffusionScheduler, VQModel +from diffusers.pipelines.vq_diffusion.pipeline_vq_diffusion import LearnedClassifierFreeSamplingEmbeddings + + +try: + from omegaconf import OmegaConf +except ImportError: + raise ImportError( + "OmegaConf is required to convert the VQ Diffusion checkpoints. Please install it with `pip install" + " OmegaConf`." + ) + +# vqvae model + +PORTED_VQVAES = ["image_synthesis.modeling.codecs.image_codec.patch_vqgan.PatchVQGAN"] + + +def vqvae_model_from_original_config(original_config): + assert original_config.target in PORTED_VQVAES, f"{original_config.target} has not yet been ported to diffusers." + + original_config = original_config.params + + original_encoder_config = original_config.encoder_config.params + original_decoder_config = original_config.decoder_config.params + + in_channels = original_encoder_config.in_channels + out_channels = original_decoder_config.out_ch + + down_block_types = get_down_block_types(original_encoder_config) + up_block_types = get_up_block_types(original_decoder_config) + + assert original_encoder_config.ch == original_decoder_config.ch + assert original_encoder_config.ch_mult == original_decoder_config.ch_mult + block_out_channels = tuple( + [original_encoder_config.ch * a_ch_mult for a_ch_mult in original_encoder_config.ch_mult] + ) + + assert original_encoder_config.num_res_blocks == original_decoder_config.num_res_blocks + layers_per_block = original_encoder_config.num_res_blocks + + assert original_encoder_config.z_channels == original_decoder_config.z_channels + latent_channels = original_encoder_config.z_channels + + num_vq_embeddings = original_config.n_embed + + # Hard coded value for ResnetBlock.GoupNorm(num_groups) in VQ-diffusion + norm_num_groups = 32 + + e_dim = original_config.embed_dim + + model = VQModel( + in_channels=in_channels, + out_channels=out_channels, + down_block_types=down_block_types, + up_block_types=up_block_types, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + latent_channels=latent_channels, + num_vq_embeddings=num_vq_embeddings, + norm_num_groups=norm_num_groups, + vq_embed_dim=e_dim, + ) + + return model + + +def get_down_block_types(original_encoder_config): + attn_resolutions = coerce_attn_resolutions(original_encoder_config.attn_resolutions) + num_resolutions = len(original_encoder_config.ch_mult) + resolution = coerce_resolution(original_encoder_config.resolution) + + curr_res = resolution + down_block_types = [] + + for _ in range(num_resolutions): + if curr_res in attn_resolutions: + down_block_type = "AttnDownEncoderBlock2D" + else: + down_block_type = "DownEncoderBlock2D" + + down_block_types.append(down_block_type) + + curr_res = [r // 2 for r in curr_res] + + return down_block_types + + +def get_up_block_types(original_decoder_config): + attn_resolutions = coerce_attn_resolutions(original_decoder_config.attn_resolutions) + num_resolutions = len(original_decoder_config.ch_mult) + resolution = coerce_resolution(original_decoder_config.resolution) + + curr_res = [r // 2 ** (num_resolutions - 1) for r in resolution] + up_block_types = [] + + for _ in reversed(range(num_resolutions)): + if curr_res in attn_resolutions: + up_block_type = "AttnUpDecoderBlock2D" + else: + up_block_type = "UpDecoderBlock2D" + + up_block_types.append(up_block_type) + + curr_res = [r * 2 for r in curr_res] + + return up_block_types + + +def coerce_attn_resolutions(attn_resolutions): + attn_resolutions = OmegaConf.to_object(attn_resolutions) + attn_resolutions_ = [] + for ar in attn_resolutions: + if isinstance(ar, (list, tuple)): + attn_resolutions_.append(list(ar)) + else: + attn_resolutions_.append([ar, ar]) + return attn_resolutions_ + + +def coerce_resolution(resolution): + resolution = OmegaConf.to_object(resolution) + if isinstance(resolution, int): + resolution = [resolution, resolution] # H, W + elif isinstance(resolution, (tuple, list)): + resolution = list(resolution) + else: + raise ValueError("Unknown type of resolution:", resolution) + return resolution + + +# done vqvae model + +# vqvae checkpoint + + +def vqvae_original_checkpoint_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + diffusers_checkpoint.update(vqvae_encoder_to_diffusers_checkpoint(model, checkpoint)) + + # quant_conv + + diffusers_checkpoint.update( + { + "quant_conv.weight": checkpoint["quant_conv.weight"], + "quant_conv.bias": checkpoint["quant_conv.bias"], + } + ) + + # quantize + diffusers_checkpoint.update({"quantize.embedding.weight": checkpoint["quantize.embedding"]}) + + # post_quant_conv + diffusers_checkpoint.update( + { + "post_quant_conv.weight": checkpoint["post_quant_conv.weight"], + "post_quant_conv.bias": checkpoint["post_quant_conv.bias"], + } + ) + + # decoder + diffusers_checkpoint.update(vqvae_decoder_to_diffusers_checkpoint(model, checkpoint)) + + return diffusers_checkpoint + + +def vqvae_encoder_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + # conv_in + diffusers_checkpoint.update( + { + "encoder.conv_in.weight": checkpoint["encoder.conv_in.weight"], + "encoder.conv_in.bias": checkpoint["encoder.conv_in.bias"], + } + ) + + # down_blocks + for down_block_idx, down_block in enumerate(model.encoder.down_blocks): + diffusers_down_block_prefix = f"encoder.down_blocks.{down_block_idx}" + down_block_prefix = f"encoder.down.{down_block_idx}" + + # resnets + for resnet_idx, resnet in enumerate(down_block.resnets): + diffusers_resnet_prefix = f"{diffusers_down_block_prefix}.resnets.{resnet_idx}" + resnet_prefix = f"{down_block_prefix}.block.{resnet_idx}" + + diffusers_checkpoint.update( + vqvae_resnet_to_diffusers_checkpoint( + resnet, checkpoint, diffusers_resnet_prefix=diffusers_resnet_prefix, resnet_prefix=resnet_prefix + ) + ) + + # downsample + + # do not include the downsample when on the last down block + # There is no downsample on the last down block + if down_block_idx != len(model.encoder.down_blocks) - 1: + # There's a single downsample in the original checkpoint but a list of downsamples + # in the diffusers model. + diffusers_downsample_prefix = f"{diffusers_down_block_prefix}.downsamplers.0.conv" + downsample_prefix = f"{down_block_prefix}.downsample.conv" + diffusers_checkpoint.update( + { + f"{diffusers_downsample_prefix}.weight": checkpoint[f"{downsample_prefix}.weight"], + f"{diffusers_downsample_prefix}.bias": checkpoint[f"{downsample_prefix}.bias"], + } + ) + + # attentions + + if hasattr(down_block, "attentions"): + for attention_idx, _ in enumerate(down_block.attentions): + diffusers_attention_prefix = f"{diffusers_down_block_prefix}.attentions.{attention_idx}" + attention_prefix = f"{down_block_prefix}.attn.{attention_idx}" + diffusers_checkpoint.update( + vqvae_attention_to_diffusers_checkpoint( + checkpoint, + diffusers_attention_prefix=diffusers_attention_prefix, + attention_prefix=attention_prefix, + ) + ) + + # mid block + + # mid block attentions + + # There is a single hardcoded attention block in the middle of the VQ-diffusion encoder + diffusers_attention_prefix = "encoder.mid_block.attentions.0" + attention_prefix = "encoder.mid.attn_1" + diffusers_checkpoint.update( + vqvae_attention_to_diffusers_checkpoint( + checkpoint, diffusers_attention_prefix=diffusers_attention_prefix, attention_prefix=attention_prefix + ) + ) + + # mid block resnets + + for diffusers_resnet_idx, resnet in enumerate(model.encoder.mid_block.resnets): + diffusers_resnet_prefix = f"encoder.mid_block.resnets.{diffusers_resnet_idx}" + + # the hardcoded prefixes to `block_` are 1 and 2 + orig_resnet_idx = diffusers_resnet_idx + 1 + # There are two hardcoded resnets in the middle of the VQ-diffusion encoder + resnet_prefix = f"encoder.mid.block_{orig_resnet_idx}" + + diffusers_checkpoint.update( + vqvae_resnet_to_diffusers_checkpoint( + resnet, checkpoint, diffusers_resnet_prefix=diffusers_resnet_prefix, resnet_prefix=resnet_prefix + ) + ) + + diffusers_checkpoint.update( + { + # conv_norm_out + "encoder.conv_norm_out.weight": checkpoint["encoder.norm_out.weight"], + "encoder.conv_norm_out.bias": checkpoint["encoder.norm_out.bias"], + # conv_out + "encoder.conv_out.weight": checkpoint["encoder.conv_out.weight"], + "encoder.conv_out.bias": checkpoint["encoder.conv_out.bias"], + } + ) + + return diffusers_checkpoint + + +def vqvae_decoder_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + # conv in + diffusers_checkpoint.update( + { + "decoder.conv_in.weight": checkpoint["decoder.conv_in.weight"], + "decoder.conv_in.bias": checkpoint["decoder.conv_in.bias"], + } + ) + + # up_blocks + + for diffusers_up_block_idx, up_block in enumerate(model.decoder.up_blocks): + # up_blocks are stored in reverse order in the VQ-diffusion checkpoint + orig_up_block_idx = len(model.decoder.up_blocks) - 1 - diffusers_up_block_idx + + diffusers_up_block_prefix = f"decoder.up_blocks.{diffusers_up_block_idx}" + up_block_prefix = f"decoder.up.{orig_up_block_idx}" + + # resnets + for resnet_idx, resnet in enumerate(up_block.resnets): + diffusers_resnet_prefix = f"{diffusers_up_block_prefix}.resnets.{resnet_idx}" + resnet_prefix = f"{up_block_prefix}.block.{resnet_idx}" + + diffusers_checkpoint.update( + vqvae_resnet_to_diffusers_checkpoint( + resnet, checkpoint, diffusers_resnet_prefix=diffusers_resnet_prefix, resnet_prefix=resnet_prefix + ) + ) + + # upsample + + # there is no up sample on the last up block + if diffusers_up_block_idx != len(model.decoder.up_blocks) - 1: + # There's a single upsample in the VQ-diffusion checkpoint but a list of downsamples + # in the diffusers model. + diffusers_downsample_prefix = f"{diffusers_up_block_prefix}.upsamplers.0.conv" + downsample_prefix = f"{up_block_prefix}.upsample.conv" + diffusers_checkpoint.update( + { + f"{diffusers_downsample_prefix}.weight": checkpoint[f"{downsample_prefix}.weight"], + f"{diffusers_downsample_prefix}.bias": checkpoint[f"{downsample_prefix}.bias"], + } + ) + + # attentions + + if hasattr(up_block, "attentions"): + for attention_idx, _ in enumerate(up_block.attentions): + diffusers_attention_prefix = f"{diffusers_up_block_prefix}.attentions.{attention_idx}" + attention_prefix = f"{up_block_prefix}.attn.{attention_idx}" + diffusers_checkpoint.update( + vqvae_attention_to_diffusers_checkpoint( + checkpoint, + diffusers_attention_prefix=diffusers_attention_prefix, + attention_prefix=attention_prefix, + ) + ) + + # mid block + + # mid block attentions + + # There is a single hardcoded attention block in the middle of the VQ-diffusion decoder + diffusers_attention_prefix = "decoder.mid_block.attentions.0" + attention_prefix = "decoder.mid.attn_1" + diffusers_checkpoint.update( + vqvae_attention_to_diffusers_checkpoint( + checkpoint, diffusers_attention_prefix=diffusers_attention_prefix, attention_prefix=attention_prefix + ) + ) + + # mid block resnets + + for diffusers_resnet_idx, resnet in enumerate(model.encoder.mid_block.resnets): + diffusers_resnet_prefix = f"decoder.mid_block.resnets.{diffusers_resnet_idx}" + + # the hardcoded prefixes to `block_` are 1 and 2 + orig_resnet_idx = diffusers_resnet_idx + 1 + # There are two hardcoded resnets in the middle of the VQ-diffusion decoder + resnet_prefix = f"decoder.mid.block_{orig_resnet_idx}" + + diffusers_checkpoint.update( + vqvae_resnet_to_diffusers_checkpoint( + resnet, checkpoint, diffusers_resnet_prefix=diffusers_resnet_prefix, resnet_prefix=resnet_prefix + ) + ) + + diffusers_checkpoint.update( + { + # conv_norm_out + "decoder.conv_norm_out.weight": checkpoint["decoder.norm_out.weight"], + "decoder.conv_norm_out.bias": checkpoint["decoder.norm_out.bias"], + # conv_out + "decoder.conv_out.weight": checkpoint["decoder.conv_out.weight"], + "decoder.conv_out.bias": checkpoint["decoder.conv_out.bias"], + } + ) + + return diffusers_checkpoint + + +def vqvae_resnet_to_diffusers_checkpoint(resnet, checkpoint, *, diffusers_resnet_prefix, resnet_prefix): + rv = { + # norm1 + f"{diffusers_resnet_prefix}.norm1.weight": checkpoint[f"{resnet_prefix}.norm1.weight"], + f"{diffusers_resnet_prefix}.norm1.bias": checkpoint[f"{resnet_prefix}.norm1.bias"], + # conv1 + f"{diffusers_resnet_prefix}.conv1.weight": checkpoint[f"{resnet_prefix}.conv1.weight"], + f"{diffusers_resnet_prefix}.conv1.bias": checkpoint[f"{resnet_prefix}.conv1.bias"], + # norm2 + f"{diffusers_resnet_prefix}.norm2.weight": checkpoint[f"{resnet_prefix}.norm2.weight"], + f"{diffusers_resnet_prefix}.norm2.bias": checkpoint[f"{resnet_prefix}.norm2.bias"], + # conv2 + f"{diffusers_resnet_prefix}.conv2.weight": checkpoint[f"{resnet_prefix}.conv2.weight"], + f"{diffusers_resnet_prefix}.conv2.bias": checkpoint[f"{resnet_prefix}.conv2.bias"], + } + + if resnet.conv_shortcut is not None: + rv.update( + { + f"{diffusers_resnet_prefix}.conv_shortcut.weight": checkpoint[f"{resnet_prefix}.nin_shortcut.weight"], + f"{diffusers_resnet_prefix}.conv_shortcut.bias": checkpoint[f"{resnet_prefix}.nin_shortcut.bias"], + } + ) + + return rv + + +def vqvae_attention_to_diffusers_checkpoint(checkpoint, *, diffusers_attention_prefix, attention_prefix): + return { + # group_norm + f"{diffusers_attention_prefix}.group_norm.weight": checkpoint[f"{attention_prefix}.norm.weight"], + f"{diffusers_attention_prefix}.group_norm.bias": checkpoint[f"{attention_prefix}.norm.bias"], + # query + f"{diffusers_attention_prefix}.query.weight": checkpoint[f"{attention_prefix}.q.weight"][:, :, 0, 0], + f"{diffusers_attention_prefix}.query.bias": checkpoint[f"{attention_prefix}.q.bias"], + # key + f"{diffusers_attention_prefix}.key.weight": checkpoint[f"{attention_prefix}.k.weight"][:, :, 0, 0], + f"{diffusers_attention_prefix}.key.bias": checkpoint[f"{attention_prefix}.k.bias"], + # value + f"{diffusers_attention_prefix}.value.weight": checkpoint[f"{attention_prefix}.v.weight"][:, :, 0, 0], + f"{diffusers_attention_prefix}.value.bias": checkpoint[f"{attention_prefix}.v.bias"], + # proj_attn + f"{diffusers_attention_prefix}.proj_attn.weight": checkpoint[f"{attention_prefix}.proj_out.weight"][ + :, :, 0, 0 + ], + f"{diffusers_attention_prefix}.proj_attn.bias": checkpoint[f"{attention_prefix}.proj_out.bias"], + } + + +# done vqvae checkpoint + +# transformer model + +PORTED_DIFFUSIONS = ["image_synthesis.modeling.transformers.diffusion_transformer.DiffusionTransformer"] +PORTED_TRANSFORMERS = ["image_synthesis.modeling.transformers.transformer_utils.Text2ImageTransformer"] +PORTED_CONTENT_EMBEDDINGS = ["image_synthesis.modeling.embeddings.dalle_mask_image_embedding.DalleMaskImageEmbedding"] + + +def transformer_model_from_original_config( + original_diffusion_config, original_transformer_config, original_content_embedding_config +): + assert ( + original_diffusion_config.target in PORTED_DIFFUSIONS + ), f"{original_diffusion_config.target} has not yet been ported to diffusers." + assert ( + original_transformer_config.target in PORTED_TRANSFORMERS + ), f"{original_transformer_config.target} has not yet been ported to diffusers." + assert ( + original_content_embedding_config.target in PORTED_CONTENT_EMBEDDINGS + ), f"{original_content_embedding_config.target} has not yet been ported to diffusers." + + original_diffusion_config = original_diffusion_config.params + original_transformer_config = original_transformer_config.params + original_content_embedding_config = original_content_embedding_config.params + + inner_dim = original_transformer_config["n_embd"] + + n_heads = original_transformer_config["n_head"] + + # VQ-Diffusion gives dimension of the multi-headed attention layers as the + # number of attention heads times the sequence length (the dimension) of a + # single head. We want to specify our attention blocks with those values + # specified separately + assert inner_dim % n_heads == 0 + d_head = inner_dim // n_heads + + depth = original_transformer_config["n_layer"] + context_dim = original_transformer_config["condition_dim"] + + num_embed = original_content_embedding_config["num_embed"] + # the number of embeddings in the transformer includes the mask embedding. + # the content embedding (the vqvae) does not include the mask embedding. + num_embed = num_embed + 1 + + height = original_transformer_config["content_spatial_size"][0] + width = original_transformer_config["content_spatial_size"][1] + + assert width == height, "width has to be equal to height" + dropout = original_transformer_config["resid_pdrop"] + num_embeds_ada_norm = original_diffusion_config["diffusion_step"] + + model_kwargs = { + "attention_bias": True, + "cross_attention_dim": context_dim, + "attention_head_dim": d_head, + "num_layers": depth, + "dropout": dropout, + "num_attention_heads": n_heads, + "num_vector_embeds": num_embed, + "num_embeds_ada_norm": num_embeds_ada_norm, + "norm_num_groups": 32, + "sample_size": width, + "activation_fn": "geglu-approximate", + } + + model = Transformer2DModel(**model_kwargs) + return model + + +# done transformer model + +# transformer checkpoint + + +def transformer_original_checkpoint_to_diffusers_checkpoint(model, checkpoint): + diffusers_checkpoint = {} + + transformer_prefix = "transformer.transformer" + + diffusers_latent_image_embedding_prefix = "latent_image_embedding" + latent_image_embedding_prefix = f"{transformer_prefix}.content_emb" + + # DalleMaskImageEmbedding + diffusers_checkpoint.update( + { + f"{diffusers_latent_image_embedding_prefix}.emb.weight": checkpoint[ + f"{latent_image_embedding_prefix}.emb.weight" + ], + f"{diffusers_latent_image_embedding_prefix}.height_emb.weight": checkpoint[ + f"{latent_image_embedding_prefix}.height_emb.weight" + ], + f"{diffusers_latent_image_embedding_prefix}.width_emb.weight": checkpoint[ + f"{latent_image_embedding_prefix}.width_emb.weight" + ], + } + ) + + # transformer blocks + for transformer_block_idx, transformer_block in enumerate(model.transformer_blocks): + diffusers_transformer_block_prefix = f"transformer_blocks.{transformer_block_idx}" + transformer_block_prefix = f"{transformer_prefix}.blocks.{transformer_block_idx}" + + # ada norm block + diffusers_ada_norm_prefix = f"{diffusers_transformer_block_prefix}.norm1" + ada_norm_prefix = f"{transformer_block_prefix}.ln1" + + diffusers_checkpoint.update( + transformer_ada_norm_to_diffusers_checkpoint( + checkpoint, diffusers_ada_norm_prefix=diffusers_ada_norm_prefix, ada_norm_prefix=ada_norm_prefix + ) + ) + + # attention block + diffusers_attention_prefix = f"{diffusers_transformer_block_prefix}.attn1" + attention_prefix = f"{transformer_block_prefix}.attn1" + + diffusers_checkpoint.update( + transformer_attention_to_diffusers_checkpoint( + checkpoint, diffusers_attention_prefix=diffusers_attention_prefix, attention_prefix=attention_prefix + ) + ) + + # ada norm block + diffusers_ada_norm_prefix = f"{diffusers_transformer_block_prefix}.norm2" + ada_norm_prefix = f"{transformer_block_prefix}.ln1_1" + + diffusers_checkpoint.update( + transformer_ada_norm_to_diffusers_checkpoint( + checkpoint, diffusers_ada_norm_prefix=diffusers_ada_norm_prefix, ada_norm_prefix=ada_norm_prefix + ) + ) + + # attention block + diffusers_attention_prefix = f"{diffusers_transformer_block_prefix}.attn2" + attention_prefix = f"{transformer_block_prefix}.attn2" + + diffusers_checkpoint.update( + transformer_attention_to_diffusers_checkpoint( + checkpoint, diffusers_attention_prefix=diffusers_attention_prefix, attention_prefix=attention_prefix + ) + ) + + # norm block + diffusers_norm_block_prefix = f"{diffusers_transformer_block_prefix}.norm3" + norm_block_prefix = f"{transformer_block_prefix}.ln2" + + diffusers_checkpoint.update( + { + f"{diffusers_norm_block_prefix}.weight": checkpoint[f"{norm_block_prefix}.weight"], + f"{diffusers_norm_block_prefix}.bias": checkpoint[f"{norm_block_prefix}.bias"], + } + ) + + # feedforward block + diffusers_feedforward_prefix = f"{diffusers_transformer_block_prefix}.ff" + feedforward_prefix = f"{transformer_block_prefix}.mlp" + + diffusers_checkpoint.update( + transformer_feedforward_to_diffusers_checkpoint( + checkpoint, + diffusers_feedforward_prefix=diffusers_feedforward_prefix, + feedforward_prefix=feedforward_prefix, + ) + ) + + # to logits + + diffusers_norm_out_prefix = "norm_out" + norm_out_prefix = f"{transformer_prefix}.to_logits.0" + + diffusers_checkpoint.update( + { + f"{diffusers_norm_out_prefix}.weight": checkpoint[f"{norm_out_prefix}.weight"], + f"{diffusers_norm_out_prefix}.bias": checkpoint[f"{norm_out_prefix}.bias"], + } + ) + + diffusers_out_prefix = "out" + out_prefix = f"{transformer_prefix}.to_logits.1" + + diffusers_checkpoint.update( + { + f"{diffusers_out_prefix}.weight": checkpoint[f"{out_prefix}.weight"], + f"{diffusers_out_prefix}.bias": checkpoint[f"{out_prefix}.bias"], + } + ) + + return diffusers_checkpoint + + +def transformer_ada_norm_to_diffusers_checkpoint(checkpoint, *, diffusers_ada_norm_prefix, ada_norm_prefix): + return { + f"{diffusers_ada_norm_prefix}.emb.weight": checkpoint[f"{ada_norm_prefix}.emb.weight"], + f"{diffusers_ada_norm_prefix}.linear.weight": checkpoint[f"{ada_norm_prefix}.linear.weight"], + f"{diffusers_ada_norm_prefix}.linear.bias": checkpoint[f"{ada_norm_prefix}.linear.bias"], + } + + +def transformer_attention_to_diffusers_checkpoint(checkpoint, *, diffusers_attention_prefix, attention_prefix): + return { + # key + f"{diffusers_attention_prefix}.to_k.weight": checkpoint[f"{attention_prefix}.key.weight"], + f"{diffusers_attention_prefix}.to_k.bias": checkpoint[f"{attention_prefix}.key.bias"], + # query + f"{diffusers_attention_prefix}.to_q.weight": checkpoint[f"{attention_prefix}.query.weight"], + f"{diffusers_attention_prefix}.to_q.bias": checkpoint[f"{attention_prefix}.query.bias"], + # value + f"{diffusers_attention_prefix}.to_v.weight": checkpoint[f"{attention_prefix}.value.weight"], + f"{diffusers_attention_prefix}.to_v.bias": checkpoint[f"{attention_prefix}.value.bias"], + # linear out + f"{diffusers_attention_prefix}.to_out.0.weight": checkpoint[f"{attention_prefix}.proj.weight"], + f"{diffusers_attention_prefix}.to_out.0.bias": checkpoint[f"{attention_prefix}.proj.bias"], + } + + +def transformer_feedforward_to_diffusers_checkpoint(checkpoint, *, diffusers_feedforward_prefix, feedforward_prefix): + return { + f"{diffusers_feedforward_prefix}.net.0.proj.weight": checkpoint[f"{feedforward_prefix}.0.weight"], + f"{diffusers_feedforward_prefix}.net.0.proj.bias": checkpoint[f"{feedforward_prefix}.0.bias"], + f"{diffusers_feedforward_prefix}.net.2.weight": checkpoint[f"{feedforward_prefix}.2.weight"], + f"{diffusers_feedforward_prefix}.net.2.bias": checkpoint[f"{feedforward_prefix}.2.bias"], + } + + +# done transformer checkpoint + + +def read_config_file(filename): + # The yaml file contains annotations that certain values should + # loaded as tuples. By default, OmegaConf will panic when reading + # these. Instead, we can manually read the yaml with the FullLoader and then + # construct the OmegaConf object. + with open(filename) as f: + original_config = yaml.load(f, FullLoader) + + return OmegaConf.create(original_config) + + +# We take separate arguments for the vqvae because the ITHQ vqvae config file +# is separate from the config file for the rest of the model. +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--vqvae_checkpoint_path", + default=None, + type=str, + required=True, + help="Path to the vqvae checkpoint to convert.", + ) + + parser.add_argument( + "--vqvae_original_config_file", + default=None, + type=str, + required=True, + help="The YAML config file corresponding to the original architecture for the vqvae.", + ) + + parser.add_argument( + "--checkpoint_path", default=None, type=str, required=True, help="Path to the checkpoint to convert." + ) + + parser.add_argument( + "--original_config_file", + default=None, + type=str, + required=True, + help="The YAML config file corresponding to the original architecture.", + ) + + parser.add_argument("--dump_path", default=None, type=str, required=True, help="Path to the output model.") + + parser.add_argument( + "--checkpoint_load_device", + default="cpu", + type=str, + required=False, + help="The device passed to `map_location` when loading checkpoints.", + ) + + # See link for how ema weights are always selected + # https://github.com/microsoft/VQ-Diffusion/blob/3c98e77f721db7c787b76304fa2c96a36c7b00af/inference_VQ_Diffusion.py#L65 + parser.add_argument( + "--no_use_ema", + action="store_true", + required=False, + help=( + "Set to not use the ema weights from the original VQ-Diffusion checkpoint. You probably do not want to set" + " it as the original VQ-Diffusion always uses the ema weights when loading models." + ), + ) + + args = parser.parse_args() + + use_ema = not args.no_use_ema + + print(f"loading checkpoints to {args.checkpoint_load_device}") + + checkpoint_map_location = torch.device(args.checkpoint_load_device) + + # vqvae_model + + print(f"loading vqvae, config: {args.vqvae_original_config_file}, checkpoint: {args.vqvae_checkpoint_path}") + + vqvae_original_config = read_config_file(args.vqvae_original_config_file).model + vqvae_checkpoint = torch.load(args.vqvae_checkpoint_path, map_location=checkpoint_map_location)["model"] + + with init_empty_weights(): + vqvae_model = vqvae_model_from_original_config(vqvae_original_config) + + vqvae_diffusers_checkpoint = vqvae_original_checkpoint_to_diffusers_checkpoint(vqvae_model, vqvae_checkpoint) + + with tempfile.NamedTemporaryFile() as vqvae_diffusers_checkpoint_file: + torch.save(vqvae_diffusers_checkpoint, vqvae_diffusers_checkpoint_file.name) + del vqvae_diffusers_checkpoint + del vqvae_checkpoint + load_checkpoint_and_dispatch(vqvae_model, vqvae_diffusers_checkpoint_file.name, device_map="auto") + + print("done loading vqvae") + + # done vqvae_model + + # transformer_model + + print( + f"loading transformer, config: {args.original_config_file}, checkpoint: {args.checkpoint_path}, use ema:" + f" {use_ema}" + ) + + original_config = read_config_file(args.original_config_file).model + + diffusion_config = original_config.params.diffusion_config + transformer_config = original_config.params.diffusion_config.params.transformer_config + content_embedding_config = original_config.params.diffusion_config.params.content_emb_config + + pre_checkpoint = torch.load(args.checkpoint_path, map_location=checkpoint_map_location) + + if use_ema: + if "ema" in pre_checkpoint: + checkpoint = {} + for k, v in pre_checkpoint["model"].items(): + checkpoint[k] = v + + for k, v in pre_checkpoint["ema"].items(): + # The ema weights are only used on the transformer. To mimic their key as if they came + # from the state_dict for the top level model, we prefix with an additional "transformer." + # See the source linked in the args.use_ema config for more information. + checkpoint[f"transformer.{k}"] = v + else: + print("attempted to load ema weights but no ema weights are specified in the loaded checkpoint.") + checkpoint = pre_checkpoint["model"] + else: + checkpoint = pre_checkpoint["model"] + + del pre_checkpoint + + with init_empty_weights(): + transformer_model = transformer_model_from_original_config( + diffusion_config, transformer_config, content_embedding_config + ) + + diffusers_transformer_checkpoint = transformer_original_checkpoint_to_diffusers_checkpoint( + transformer_model, checkpoint + ) + + # classifier free sampling embeddings interlude + + # The learned embeddings are stored on the transformer in the original VQ-diffusion. We store them on a separate + # model, so we pull them off the checkpoint before the checkpoint is deleted. + + learnable_classifier_free_sampling_embeddings = diffusion_config.params.learnable_cf + + if learnable_classifier_free_sampling_embeddings: + learned_classifier_free_sampling_embeddings_embeddings = checkpoint["transformer.empty_text_embed"] + else: + learned_classifier_free_sampling_embeddings_embeddings = None + + # done classifier free sampling embeddings interlude + + with tempfile.NamedTemporaryFile() as diffusers_transformer_checkpoint_file: + torch.save(diffusers_transformer_checkpoint, diffusers_transformer_checkpoint_file.name) + del diffusers_transformer_checkpoint + del checkpoint + load_checkpoint_and_dispatch(transformer_model, diffusers_transformer_checkpoint_file.name, device_map="auto") + + print("done loading transformer") + + # done transformer_model + + # text encoder + + print("loading CLIP text encoder") + + clip_name = "openai/clip-vit-base-patch32" + + # The original VQ-Diffusion specifies the pad value by the int used in the + # returned tokens. Each model uses `0` as the pad value. The transformers clip api + # specifies the pad value via the token before it has been tokenized. The `!` pad + # token is the same as padding with the `0` pad value. + pad_token = "!" + + tokenizer_model = CLIPTokenizer.from_pretrained(clip_name, pad_token=pad_token, device_map="auto") + + assert tokenizer_model.convert_tokens_to_ids(pad_token) == 0 + + text_encoder_model = CLIPTextModel.from_pretrained( + clip_name, + # `CLIPTextModel` does not support device_map="auto" + # device_map="auto" + ) + + print("done loading CLIP text encoder") + + # done text encoder + + # scheduler + + scheduler_model = VQDiffusionScheduler( + # the scheduler has the same number of embeddings as the transformer + num_vec_classes=transformer_model.num_vector_embeds + ) + + # done scheduler + + # learned classifier free sampling embeddings + + with init_empty_weights(): + learned_classifier_free_sampling_embeddings_model = LearnedClassifierFreeSamplingEmbeddings( + learnable_classifier_free_sampling_embeddings, + hidden_size=text_encoder_model.config.hidden_size, + length=tokenizer_model.model_max_length, + ) + + learned_classifier_free_sampling_checkpoint = { + "embeddings": learned_classifier_free_sampling_embeddings_embeddings.float() + } + + with tempfile.NamedTemporaryFile() as learned_classifier_free_sampling_checkpoint_file: + torch.save(learned_classifier_free_sampling_checkpoint, learned_classifier_free_sampling_checkpoint_file.name) + del learned_classifier_free_sampling_checkpoint + del learned_classifier_free_sampling_embeddings_embeddings + load_checkpoint_and_dispatch( + learned_classifier_free_sampling_embeddings_model, + learned_classifier_free_sampling_checkpoint_file.name, + device_map="auto", + ) + + # done learned classifier free sampling embeddings + + print(f"saving VQ diffusion model, path: {args.dump_path}") + + pipe = VQDiffusionPipeline( + vqvae=vqvae_model, + transformer=transformer_model, + tokenizer=tokenizer_model, + text_encoder=text_encoder_model, + learned_classifier_free_sampling_embeddings=learned_classifier_free_sampling_embeddings_model, + scheduler=scheduler_model, + ) + pipe.save_pretrained(args.dump_path) + + print("done writing VQ diffusion model") diff --git a/diffusers/scripts/generate_logits.py b/diffusers/scripts/generate_logits.py new file mode 100644 index 0000000000000000000000000000000000000000..89dce0e78d4ef50e060ac554ac3f7e760f55983f --- /dev/null +++ b/diffusers/scripts/generate_logits.py @@ -0,0 +1,127 @@ +import random + +import torch +from huggingface_hub import HfApi + +from diffusers import UNet2DModel + + +api = HfApi() + +results = {} +# fmt: off +results["google_ddpm_cifar10_32"] = torch.tensor([ + -0.7515, -1.6883, 0.2420, 0.0300, 0.6347, 1.3433, -1.1743, -3.7467, + 1.2342, -2.2485, 0.4636, 0.8076, -0.7991, 0.3969, 0.8498, 0.9189, + -1.8887, -3.3522, 0.7639, 0.2040, 0.6271, -2.7148, -1.6316, 3.0839, + 0.3186, 0.2721, -0.9759, -1.2461, 2.6257, 1.3557 +]) +results["google_ddpm_ema_bedroom_256"] = torch.tensor([ + -2.3639, -2.5344, 0.0054, -0.6674, 1.5990, 1.0158, 0.3124, -2.1436, + 1.8795, -2.5429, -0.1566, -0.3973, 1.2490, 2.6447, 1.2283, -0.5208, + -2.8154, -3.5119, 2.3838, 1.2033, 1.7201, -2.1256, -1.4576, 2.7948, + 2.4204, -0.9752, -1.2546, 0.8027, 3.2758, 3.1365 +]) +results["CompVis_ldm_celebahq_256"] = torch.tensor([ + -0.6531, -0.6891, -0.3172, -0.5375, -0.9140, -0.5367, -0.1175, -0.7869, + -0.3808, -0.4513, -0.2098, -0.0083, 0.3183, 0.5140, 0.2247, -0.1304, + -0.1302, -0.2802, -0.2084, -0.2025, -0.4967, -0.4873, -0.0861, 0.6925, + 0.0250, 0.1290, -0.1543, 0.6316, 1.0460, 1.4943 +]) +results["google_ncsnpp_ffhq_1024"] = torch.tensor([ + 0.0911, 0.1107, 0.0182, 0.0435, -0.0805, -0.0608, 0.0381, 0.2172, + -0.0280, 0.1327, -0.0299, -0.0255, -0.0050, -0.1170, -0.1046, 0.0309, + 0.1367, 0.1728, -0.0533, -0.0748, -0.0534, 0.1624, 0.0384, -0.1805, + -0.0707, 0.0642, 0.0220, -0.0134, -0.1333, -0.1505 +]) +results["google_ncsnpp_bedroom_256"] = torch.tensor([ + 0.1321, 0.1337, 0.0440, 0.0622, -0.0591, -0.0370, 0.0503, 0.2133, + -0.0177, 0.1415, -0.0116, -0.0112, 0.0044, -0.0980, -0.0789, 0.0395, + 0.1502, 0.1785, -0.0488, -0.0514, -0.0404, 0.1539, 0.0454, -0.1559, + -0.0665, 0.0659, 0.0383, -0.0005, -0.1266, -0.1386 +]) +results["google_ncsnpp_celebahq_256"] = torch.tensor([ + 0.1154, 0.1218, 0.0307, 0.0526, -0.0711, -0.0541, 0.0366, 0.2078, + -0.0267, 0.1317, -0.0226, -0.0193, -0.0014, -0.1055, -0.0902, 0.0330, + 0.1391, 0.1709, -0.0562, -0.0693, -0.0560, 0.1482, 0.0381, -0.1683, + -0.0681, 0.0661, 0.0331, -0.0046, -0.1268, -0.1431 +]) +results["google_ncsnpp_church_256"] = torch.tensor([ + 0.1192, 0.1240, 0.0414, 0.0606, -0.0557, -0.0412, 0.0430, 0.2042, + -0.0200, 0.1385, -0.0115, -0.0132, 0.0017, -0.0965, -0.0802, 0.0398, + 0.1433, 0.1747, -0.0458, -0.0533, -0.0407, 0.1545, 0.0419, -0.1574, + -0.0645, 0.0626, 0.0341, -0.0010, -0.1199, -0.1390 +]) +results["google_ncsnpp_ffhq_256"] = torch.tensor([ + 0.1075, 0.1074, 0.0205, 0.0431, -0.0774, -0.0607, 0.0298, 0.2042, + -0.0320, 0.1267, -0.0281, -0.0250, -0.0064, -0.1091, -0.0946, 0.0290, + 0.1328, 0.1650, -0.0580, -0.0738, -0.0586, 0.1440, 0.0337, -0.1746, + -0.0712, 0.0605, 0.0250, -0.0099, -0.1316, -0.1473 +]) +results["google_ddpm_cat_256"] = torch.tensor([ + -1.4572, -2.0481, -0.0414, -0.6005, 1.4136, 0.5848, 0.4028, -2.7330, + 1.2212, -2.1228, 0.2155, 0.4039, 0.7662, 2.0535, 0.7477, -0.3243, + -2.1758, -2.7648, 1.6947, 0.7026, 1.2338, -1.6078, -0.8682, 2.2810, + 1.8574, -0.5718, -0.5586, -0.0186, 2.3415, 2.1251]) +results["google_ddpm_celebahq_256"] = torch.tensor([ + -1.3690, -1.9720, -0.4090, -0.6966, 1.4660, 0.9938, -0.1385, -2.7324, + 0.7736, -1.8917, 0.2923, 0.4293, 0.1693, 1.4112, 1.1887, -0.3181, + -2.2160, -2.6381, 1.3170, 0.8163, 0.9240, -1.6544, -0.6099, 2.5259, + 1.6430, -0.9090, -0.9392, -0.0126, 2.4268, 2.3266 +]) +results["google_ddpm_ema_celebahq_256"] = torch.tensor([ + -1.3525, -1.9628, -0.3956, -0.6860, 1.4664, 1.0014, -0.1259, -2.7212, + 0.7772, -1.8811, 0.2996, 0.4388, 0.1704, 1.4029, 1.1701, -0.3027, + -2.2053, -2.6287, 1.3350, 0.8131, 0.9274, -1.6292, -0.6098, 2.5131, + 1.6505, -0.8958, -0.9298, -0.0151, 2.4257, 2.3355 +]) +results["google_ddpm_church_256"] = torch.tensor([ + -2.0585, -2.7897, -0.2850, -0.8940, 1.9052, 0.5702, 0.6345, -3.8959, + 1.5932, -3.2319, 0.1974, 0.0287, 1.7566, 2.6543, 0.8387, -0.5351, + -3.2736, -4.3375, 2.9029, 1.6390, 1.4640, -2.1701, -1.9013, 2.9341, + 3.4981, -0.6255, -1.1644, -0.1591, 3.7097, 3.2066 +]) +results["google_ddpm_bedroom_256"] = torch.tensor([ + -2.3139, -2.5594, -0.0197, -0.6785, 1.7001, 1.1606, 0.3075, -2.1740, + 1.8071, -2.5630, -0.0926, -0.3811, 1.2116, 2.6246, 1.2731, -0.5398, + -2.8153, -3.6140, 2.3893, 1.3262, 1.6258, -2.1856, -1.3267, 2.8395, + 2.3779, -1.0623, -1.2468, 0.8959, 3.3367, 3.2243 +]) +results["google_ddpm_ema_church_256"] = torch.tensor([ + -2.0628, -2.7667, -0.2089, -0.8263, 2.0539, 0.5992, 0.6495, -3.8336, + 1.6025, -3.2817, 0.1721, -0.0633, 1.7516, 2.7039, 0.8100, -0.5908, + -3.2113, -4.4343, 2.9257, 1.3632, 1.5562, -2.1489, -1.9894, 3.0560, + 3.3396, -0.7328, -1.0417, 0.0383, 3.7093, 3.2343 +]) +results["google_ddpm_ema_cat_256"] = torch.tensor([ + -1.4574, -2.0569, -0.0473, -0.6117, 1.4018, 0.5769, 0.4129, -2.7344, + 1.2241, -2.1397, 0.2000, 0.3937, 0.7616, 2.0453, 0.7324, -0.3391, + -2.1746, -2.7744, 1.6963, 0.6921, 1.2187, -1.6172, -0.8877, 2.2439, + 1.8471, -0.5839, -0.5605, -0.0464, 2.3250, 2.1219 +]) +# fmt: on + +models = api.list_models(filter="diffusers") +for mod in models: + if "google" in mod.author or mod.modelId == "CompVis/ldm-celebahq-256": + local_checkpoint = "/home/patrick/google_checkpoints/" + mod.modelId.split("/")[-1] + + print(f"Started running {mod.modelId}!!!") + + if mod.modelId.startswith("CompVis"): + model = UNet2DModel.from_pretrained(local_checkpoint, subfolder="unet") + else: + model = UNet2DModel.from_pretrained(local_checkpoint) + + torch.manual_seed(0) + random.seed(0) + + noise = torch.randn(1, model.config.in_channels, model.config.sample_size, model.config.sample_size) + time_step = torch.tensor([10] * noise.shape[0]) + with torch.no_grad(): + logits = model(noise, time_step).sample + + assert torch.allclose( + logits[0, 0, 0, :30], results["_".join("_".join(mod.modelId.split("/")).split("-"))], atol=1e-3 + ) + print(f"{mod.modelId} has passed successfully!!!") diff --git a/diffusers/setup.cfg b/diffusers/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..fe555d61c69ae01d96d862039ca1867cfffdd6f5 --- /dev/null +++ b/diffusers/setup.cfg @@ -0,0 +1,20 @@ +[isort] +default_section = FIRSTPARTY +ensure_newline_before_comments = True +force_grid_wrap = 0 +include_trailing_comma = True +known_first_party = accelerate +known_third_party = + numpy + torch + torch_xla + +line_length = 119 +lines_after_imports = 2 +multi_line_output = 3 +use_parentheses = True + +[flake8] +ignore = E203, E722, E501, E741, W503, W605 +max-line-length = 119 +per-file-ignores = __init__.py:F401 diff --git a/diffusers/setup.py b/diffusers/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..5e38ab6ef90112a030ec2983e45c4d0d333931f6 --- /dev/null +++ b/diffusers/setup.py @@ -0,0 +1,272 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +""" +Simple check list from AllenNLP repo: https://github.com/allenai/allennlp/blob/main/setup.py + +To create the package for pypi. + +1. Run `make pre-release` (or `make pre-patch` for a patch release) then run `make fix-copies` to fix the index of the + documentation. + + If releasing on a special branch, copy the updated README.md on the main branch for your the commit you will make + for the post-release and run `make fix-copies` on the main branch as well. + +2. Run Tests for Amazon Sagemaker. The documentation is located in `./tests/sagemaker/README.md`, otherwise @philschmid. + +3. Unpin specific versions from setup.py that use a git install. + +4. Checkout the release branch (v-release, for example v4.19-release), and commit these changes with the + message: "Release: " and push. + +5. Wait for the tests on main to be completed and be green (otherwise revert and fix bugs) + +6. Add a tag in git to mark the release: "git tag v -m 'Adds tag v for pypi' " + Push the tag to git: git push --tags origin v-release + +7. Build both the sources and the wheel. Do not change anything in setup.py between + creating the wheel and the source distribution (obviously). + + For the wheel, run: "python setup.py bdist_wheel" in the top level directory. + (this will build a wheel for the python version you use to build it). + + For the sources, run: "python setup.py sdist" + You should now have a /dist directory with both .whl and .tar.gz source versions. + +8. Check that everything looks correct by uploading the package to the pypi test server: + + twine upload dist/* -r pypitest + (pypi suggest using twine as other methods upload files via plaintext.) + You may have to specify the repository url, use the following command then: + twine upload dist/* -r pypitest --repository-url=https://test.pypi.org/legacy/ + + Check that you can install it in a virtualenv by running: + pip install -i https://testpypi.python.org/pypi diffusers + + Check you can run the following commands: + python -c "from diffusers import pipeline; classifier = pipeline('text-classification'); print(classifier('What a nice release'))" + python -c "from diffusers import *" + +9. Upload the final version to actual pypi: + twine upload dist/* -r pypi + +10. Copy the release notes from RELEASE.md to the tag in github once everything is looking hunky-dory. + +11. Run `make post-release` (or, for a patch release, `make post-patch`). If you were on a branch for the release, + you need to go back to main before executing this. +""" + +import os +import re +from distutils.core import Command + +from setuptools import find_packages, setup + + +# IMPORTANT: +# 1. all dependencies should be listed here with their version requirements if any +# 2. once modified, run: `make deps_table_update` to update src/diffusers/dependency_versions_table.py +_deps = [ + "Pillow", # keep the PIL.Image.Resampling deprecation away + "accelerate>=0.11.0", + "black~=23.1", + "datasets", + "filelock", + "flax>=0.4.1", + "hf-doc-builder>=0.3.0", + "huggingface-hub>=0.10.0", + "importlib_metadata", + "isort>=5.5.4", + "jax>=0.2.8,!=0.3.2", + "jaxlib>=0.1.65", + "Jinja2", + "k-diffusion>=0.0.12", + "librosa", + "numpy", + "parameterized", + "pytest", + "pytest-timeout", + "pytest-xdist", + "ruff>=0.0.241", + "safetensors", + "sentencepiece>=0.1.91,!=0.1.92", + "scipy", + "regex!=2019.12.17", + "requests", + "tensorboard", + "torch>=1.4", + "torchvision", + "transformers>=4.25.1", +] + +# this is a lookup table with items like: +# +# tokenizers: "huggingface-hub==0.8.0" +# packaging: "packaging" +# +# some of the values are versioned whereas others aren't. +deps = {b: a for a, b in (re.findall(r"^(([^!=<>~]+)(?:[!=<>~].*)?$)", x)[0] for x in _deps)} + +# since we save this data in src/diffusers/dependency_versions_table.py it can be easily accessed from +# anywhere. If you need to quickly access the data from this table in a shell, you can do so easily with: +# +# python -c 'import sys; from diffusers.dependency_versions_table import deps; \ +# print(" ".join([ deps[x] for x in sys.argv[1:]]))' tokenizers datasets +# +# Just pass the desired package names to that script as it's shown with 2 packages above. +# +# If diffusers is not yet installed and the work is done from the cloned repo remember to add `PYTHONPATH=src` to the script above +# +# You can then feed this for example to `pip`: +# +# pip install -U $(python -c 'import sys; from diffusers.dependency_versions_table import deps; \ +# print(" ".join([ deps[x] for x in sys.argv[1:]]))' tokenizers datasets) +# + + +def deps_list(*pkgs): + return [deps[pkg] for pkg in pkgs] + + +class DepsTableUpdateCommand(Command): + """ + A custom distutils command that updates the dependency table. + usage: python setup.py deps_table_update + """ + + description = "build runtime dependency table" + user_options = [ + # format: (long option, short option, description). + ("dep-table-update", None, "updates src/diffusers/dependency_versions_table.py"), + ] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + entries = "\n".join([f' "{k}": "{v}",' for k, v in deps.items()]) + content = [ + "# THIS FILE HAS BEEN AUTOGENERATED. To update:", + "# 1. modify the `_deps` dict in setup.py", + "# 2. run `make deps_table_update``", + "deps = {", + entries, + "}", + "", + ] + target = "src/diffusers/dependency_versions_table.py" + print(f"updating {target}") + with open(target, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(content)) + + +extras = {} + + +extras = {} +extras["quality"] = deps_list("black", "isort", "ruff", "hf-doc-builder") +extras["docs"] = deps_list("hf-doc-builder") +extras["training"] = deps_list("accelerate", "datasets", "tensorboard", "Jinja2") +extras["test"] = deps_list( + "datasets", + "Jinja2", + "k-diffusion", + "librosa", + "parameterized", + "pytest", + "pytest-timeout", + "pytest-xdist", + "safetensors", + "sentencepiece", + "scipy", + "torchvision", + "transformers", +) +extras["torch"] = deps_list("torch", "accelerate") + +if os.name == "nt": # windows + extras["flax"] = [] # jax is not supported on windows +else: + extras["flax"] = deps_list("jax", "jaxlib", "flax") + +extras["dev"] = ( + extras["quality"] + extras["test"] + extras["training"] + extras["docs"] + extras["torch"] + extras["flax"] +) + +install_requires = [ + deps["importlib_metadata"], + deps["filelock"], + deps["huggingface-hub"], + deps["numpy"], + deps["regex"], + deps["requests"], + deps["Pillow"], +] + +setup( + name="diffusers", + version="0.13.0.dev0", # expected format is one of x.y.z.dev0, or x.y.z.rc1 or x.y.z (no to dashes, yes to dots) + description="Diffusers", + long_description=open("README.md", "r", encoding="utf-8").read(), + long_description_content_type="text/markdown", + keywords="deep learning", + license="Apache", + author="The HuggingFace team", + author_email="patrick@huggingface.co", + url="https://github.com/huggingface/diffusers", + package_dir={"": "src"}, + packages=find_packages("src"), + include_package_data=True, + python_requires=">=3.7.0", + install_requires=install_requires, + extras_require=extras, + entry_points={"console_scripts": ["diffusers-cli=diffusers.commands.diffusers_cli:main"]}, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + ], + cmdclass={"deps_table_update": DepsTableUpdateCommand}, +) + +# Release checklist +# 1. Change the version in __init__.py and setup.py. +# 2. Commit these changes with the message: "Release: Release" +# 3. Add a tag in git to mark the release: "git tag RELEASE -m 'Adds tag RELEASE for pypi' " +# Push the tag to git: git push --tags origin main +# 4. Run the following commands in the top-level directory: +# python setup.py bdist_wheel +# python setup.py sdist +# 5. Upload the package to the pypi test server first: +# twine upload dist/* -r pypitest +# twine upload dist/* -r pypitest --repository-url=https://test.pypi.org/legacy/ +# 6. Check that you can install it in a virtualenv by running: +# pip install -i https://testpypi.python.org/pypi diffusers +# diffusers env +# diffusers test +# 7. Upload the final version to actual pypi: +# twine upload dist/* -r pypi +# 8. Add release notes to the tag in github once everything is looking hunky-dory. +# 9. Update the version in __init__.py, setup.py to the new version "-dev" and push to master diff --git a/diffusers/src/diffusers/__init__.py b/diffusers/src/diffusers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6057eaf2da26184a96dd18b843f6ddad5d9902 --- /dev/null +++ b/diffusers/src/diffusers/__init__.py @@ -0,0 +1,193 @@ +__version__ = "0.13.0.dev0" + +from .configuration_utils import ConfigMixin +from .utils import ( + OptionalDependencyNotAvailable, + is_flax_available, + is_inflect_available, + is_k_diffusion_available, + is_k_diffusion_version, + is_librosa_available, + is_onnx_available, + is_scipy_available, + is_torch_available, + is_transformers_available, + is_transformers_version, + is_unidecode_available, + logging, +) + + +try: + if not is_onnx_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_onnx_objects import * # noqa F403 +else: + from .pipelines import OnnxRuntimeModel + +try: + if not is_torch_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_pt_objects import * # noqa F403 +else: + from .models import ( + AutoencoderKL, + ModelMixin, + PriorTransformer, + Transformer2DModel, + UNet1DModel, + UNet2DConditionModel, + UNet2DModel, + VQModel, + ) + from .optimization import ( + get_constant_schedule, + get_constant_schedule_with_warmup, + get_cosine_schedule_with_warmup, + get_cosine_with_hard_restarts_schedule_with_warmup, + get_linear_schedule_with_warmup, + get_polynomial_decay_schedule_with_warmup, + get_scheduler, + ) + from .pipelines import ( + AudioPipelineOutput, + DanceDiffusionPipeline, + DDIMPipeline, + DDPMPipeline, + DiffusionPipeline, + DiTPipeline, + ImagePipelineOutput, + KarrasVePipeline, + LDMPipeline, + LDMSuperResolutionPipeline, + PNDMPipeline, + RePaintPipeline, + ScoreSdeVePipeline, + ) + from .schedulers import ( + DDIMScheduler, + DDPMScheduler, + DEISMultistepScheduler, + DPMSolverMultistepScheduler, + DPMSolverSinglestepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + HeunDiscreteScheduler, + IPNDMScheduler, + KarrasVeScheduler, + KDPM2AncestralDiscreteScheduler, + KDPM2DiscreteScheduler, + PNDMScheduler, + RePaintScheduler, + SchedulerMixin, + ScoreSdeVeScheduler, + UnCLIPScheduler, + VQDiffusionScheduler, + ) + from .training_utils import EMAModel + +try: + if not (is_torch_available() and is_scipy_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_torch_and_scipy_objects import * # noqa F403 +else: + from .schedulers import LMSDiscreteScheduler + + +try: + if not (is_torch_available() and is_transformers_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_torch_and_transformers_objects import * # noqa F403 +else: + from .pipelines import ( + AltDiffusionImg2ImgPipeline, + AltDiffusionPipeline, + CycleDiffusionPipeline, + LDMTextToImagePipeline, + PaintByExamplePipeline, + StableDiffusionDepth2ImgPipeline, + StableDiffusionImageVariationPipeline, + StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipeline, + StableDiffusionInpaintPipelineLegacy, + StableDiffusionInstructPix2PixPipeline, + StableDiffusionLatentUpscalePipeline, + StableDiffusionPipeline, + StableDiffusionPipelineSafe, + StableDiffusionUpscalePipeline, + UnCLIPImageVariationPipeline, + UnCLIPPipeline, + VersatileDiffusionDualGuidedPipeline, + VersatileDiffusionImageVariationPipeline, + VersatileDiffusionPipeline, + VersatileDiffusionTextToImagePipeline, + VQDiffusionPipeline, + ) + +try: + if not (is_torch_available() and is_transformers_available() and is_k_diffusion_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_torch_and_transformers_and_k_diffusion_objects import * # noqa F403 +else: + from .pipelines import StableDiffusionKDiffusionPipeline + +try: + if not (is_torch_available() and is_transformers_available() and is_onnx_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_torch_and_transformers_and_onnx_objects import * # noqa F403 +else: + from .pipelines import ( + OnnxStableDiffusionImg2ImgPipeline, + OnnxStableDiffusionInpaintPipeline, + OnnxStableDiffusionInpaintPipelineLegacy, + OnnxStableDiffusionPipeline, + StableDiffusionOnnxPipeline, + ) + +try: + if not (is_torch_available() and is_librosa_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_torch_and_librosa_objects import * # noqa F403 +else: + from .pipelines import AudioDiffusionPipeline, Mel + +try: + if not is_flax_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_flax_objects import * # noqa F403 +else: + from .models.modeling_flax_utils import FlaxModelMixin + from .models.unet_2d_condition_flax import FlaxUNet2DConditionModel + from .models.vae_flax import FlaxAutoencoderKL + from .pipelines import FlaxDiffusionPipeline + from .schedulers import ( + FlaxDDIMScheduler, + FlaxDDPMScheduler, + FlaxDPMSolverMultistepScheduler, + FlaxKarrasVeScheduler, + FlaxLMSDiscreteScheduler, + FlaxPNDMScheduler, + FlaxSchedulerMixin, + FlaxScoreSdeVeScheduler, + ) + + +try: + if not (is_flax_available() and is_transformers_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from .utils.dummy_flax_and_transformers_objects import * # noqa F403 +else: + from .pipelines import ( + FlaxStableDiffusionImg2ImgPipeline, + FlaxStableDiffusionInpaintPipeline, + FlaxStableDiffusionPipeline, + ) diff --git a/diffusers/src/diffusers/commands/__init__.py b/diffusers/src/diffusers/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..902bd46cedc6f2df785c1dc5d2e6bd8ef7c69ca6 --- /dev/null +++ b/diffusers/src/diffusers/commands/__init__.py @@ -0,0 +1,27 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from abc import ABC, abstractmethod +from argparse import ArgumentParser + + +class BaseDiffusersCLICommand(ABC): + @staticmethod + @abstractmethod + def register_subcommand(parser: ArgumentParser): + raise NotImplementedError() + + @abstractmethod + def run(self): + raise NotImplementedError() diff --git a/diffusers/src/diffusers/commands/diffusers_cli.py b/diffusers/src/diffusers/commands/diffusers_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..30084e55ba4eeec79c87a99eae3e60a6233dc556 --- /dev/null +++ b/diffusers/src/diffusers/commands/diffusers_cli.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from argparse import ArgumentParser + +from .env import EnvironmentCommand + + +def main(): + parser = ArgumentParser("Diffusers CLI tool", usage="diffusers-cli []") + commands_parser = parser.add_subparsers(help="diffusers-cli command helpers") + + # Register commands + EnvironmentCommand.register_subcommand(commands_parser) + + # Let's go + args = parser.parse_args() + + if not hasattr(args, "func"): + parser.print_help() + exit(1) + + # Run + service = args.func(args) + service.run() + + +if __name__ == "__main__": + main() diff --git a/diffusers/src/diffusers/commands/env.py b/diffusers/src/diffusers/commands/env.py new file mode 100644 index 0000000000000000000000000000000000000000..711761495dbddfea4a16ba1d1d7b6835d10eceac --- /dev/null +++ b/diffusers/src/diffusers/commands/env.py @@ -0,0 +1,84 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import platform +from argparse import ArgumentParser + +import huggingface_hub + +from .. import __version__ as version +from ..utils import is_accelerate_available, is_torch_available, is_transformers_available, is_xformers_available +from . import BaseDiffusersCLICommand + + +def info_command_factory(_): + return EnvironmentCommand() + + +class EnvironmentCommand(BaseDiffusersCLICommand): + @staticmethod + def register_subcommand(parser: ArgumentParser): + download_parser = parser.add_parser("env") + download_parser.set_defaults(func=info_command_factory) + + def run(self): + hub_version = huggingface_hub.__version__ + + pt_version = "not installed" + pt_cuda_available = "NA" + if is_torch_available(): + import torch + + pt_version = torch.__version__ + pt_cuda_available = torch.cuda.is_available() + + transformers_version = "not installed" + if is_transformers_available(): + import transformers + + transformers_version = transformers.__version__ + + accelerate_version = "not installed" + if is_accelerate_available(): + import accelerate + + accelerate_version = accelerate.__version__ + + xformers_version = "not installed" + if is_xformers_available(): + import xformers + + xformers_version = xformers.__version__ + + info = { + "`diffusers` version": version, + "Platform": platform.platform(), + "Python version": platform.python_version(), + "PyTorch version (GPU?)": f"{pt_version} ({pt_cuda_available})", + "Huggingface_hub version": hub_version, + "Transformers version": transformers_version, + "Accelerate version": accelerate_version, + "xFormers version": xformers_version, + "Using GPU in script?": "", + "Using distributed or parallel set-up in script?": "", + } + + print("\nCopy-and-paste the text below in your GitHub issue and FILL OUT the two last points.\n") + print(self.format_dict(info)) + + return info + + @staticmethod + def format_dict(d): + return "\n".join([f"- {prop}: {val}" for prop, val in d.items()]) + "\n" diff --git a/diffusers/src/diffusers/configuration_utils.py b/diffusers/src/diffusers/configuration_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4191aa0b56a6d3e17495a4af23fd1228b3c1b6aa --- /dev/null +++ b/diffusers/src/diffusers/configuration_utils.py @@ -0,0 +1,615 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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. +""" ConfigMixin base class and utilities.""" +import dataclasses +import functools +import importlib +import inspect +import json +import os +import re +from collections import OrderedDict +from pathlib import PosixPath +from typing import Any, Dict, Tuple, Union + +import numpy as np +from huggingface_hub import hf_hub_download +from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError +from requests import HTTPError + +from . import __version__ +from .utils import DIFFUSERS_CACHE, HUGGINGFACE_CO_RESOLVE_ENDPOINT, DummyObject, deprecate, logging + + +logger = logging.get_logger(__name__) + +_re_configuration_file = re.compile(r"config\.(.*)\.json") + + +class FrozenDict(OrderedDict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for key, value in self.items(): + setattr(self, key, value) + + self.__frozen = True + + def __delitem__(self, *args, **kwargs): + raise Exception(f"You cannot use ``__delitem__`` on a {self.__class__.__name__} instance.") + + def setdefault(self, *args, **kwargs): + raise Exception(f"You cannot use ``setdefault`` on a {self.__class__.__name__} instance.") + + def pop(self, *args, **kwargs): + raise Exception(f"You cannot use ``pop`` on a {self.__class__.__name__} instance.") + + def update(self, *args, **kwargs): + raise Exception(f"You cannot use ``update`` on a {self.__class__.__name__} instance.") + + def __setattr__(self, name, value): + if hasattr(self, "__frozen") and self.__frozen: + raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.") + super().__setattr__(name, value) + + def __setitem__(self, name, value): + if hasattr(self, "__frozen") and self.__frozen: + raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.") + super().__setitem__(name, value) + + +class ConfigMixin: + r""" + Base class for all configuration classes. Stores all configuration parameters under `self.config` Also handles all + methods for loading/downloading/saving classes inheriting from [`ConfigMixin`] with + - [`~ConfigMixin.from_config`] + - [`~ConfigMixin.save_config`] + + Class attributes: + - **config_name** (`str`) -- A filename under which the config should stored when calling + [`~ConfigMixin.save_config`] (should be overridden by parent class). + - **ignore_for_config** (`List[str]`) -- A list of attributes that should not be saved in the config (should be + overridden by subclass). + - **has_compatibles** (`bool`) -- Whether the class has compatible classes (should be overridden by subclass). + - **_deprecated_kwargs** (`List[str]`) -- Keyword arguments that are deprecated. Note that the init function + should only have a `kwargs` argument if at least one argument is deprecated (should be overridden by + subclass). + """ + config_name = None + ignore_for_config = [] + has_compatibles = False + + _deprecated_kwargs = [] + + def register_to_config(self, **kwargs): + if self.config_name is None: + raise NotImplementedError(f"Make sure that {self.__class__} has defined a class name `config_name`") + # Special case for `kwargs` used in deprecation warning added to schedulers + # TODO: remove this when we remove the deprecation warning, and the `kwargs` argument, + # or solve in a more general way. + kwargs.pop("kwargs", None) + for key, value in kwargs.items(): + try: + setattr(self, key, value) + except AttributeError as err: + logger.error(f"Can't set {key} with value {value} for {self}") + raise err + + if not hasattr(self, "_internal_dict"): + internal_dict = kwargs + else: + previous_dict = dict(self._internal_dict) + internal_dict = {**self._internal_dict, **kwargs} + logger.debug(f"Updating config from {previous_dict} to {internal_dict}") + + self._internal_dict = FrozenDict(internal_dict) + + def save_config(self, save_directory: Union[str, os.PathLike], push_to_hub: bool = False, **kwargs): + """ + Save a configuration object to the directory `save_directory`, so that it can be re-loaded using the + [`~ConfigMixin.from_config`] class method. + + Args: + save_directory (`str` or `os.PathLike`): + Directory where the configuration JSON file will be saved (will be created if it does not exist). + """ + if os.path.isfile(save_directory): + raise AssertionError(f"Provided path ({save_directory}) should be a directory, not a file") + + os.makedirs(save_directory, exist_ok=True) + + # If we save using the predefined names, we can load using `from_config` + output_config_file = os.path.join(save_directory, self.config_name) + + self.to_json_file(output_config_file) + logger.info(f"Configuration saved in {output_config_file}") + + @classmethod + def from_config(cls, config: Union[FrozenDict, Dict[str, Any]] = None, return_unused_kwargs=False, **kwargs): + r""" + Instantiate a Python class from a config dictionary + + Parameters: + config (`Dict[str, Any]`): + A config dictionary from which the Python class will be instantiated. Make sure to only load + configuration files of compatible classes. + return_unused_kwargs (`bool`, *optional*, defaults to `False`): + Whether kwargs that are not consumed by the Python class should be returned or not. + + kwargs (remaining dictionary of keyword arguments, *optional*): + Can be used to update the configuration object (after it being loaded) and initiate the Python class. + `**kwargs` will be directly passed to the underlying scheduler/model's `__init__` method and eventually + overwrite same named arguments of `config`. + + Examples: + + ```python + >>> from diffusers import DDPMScheduler, DDIMScheduler, PNDMScheduler + + >>> # Download scheduler from huggingface.co and cache. + >>> scheduler = DDPMScheduler.from_pretrained("google/ddpm-cifar10-32") + + >>> # Instantiate DDIM scheduler class with same config as DDPM + >>> scheduler = DDIMScheduler.from_config(scheduler.config) + + >>> # Instantiate PNDM scheduler class with same config as DDPM + >>> scheduler = PNDMScheduler.from_config(scheduler.config) + ``` + """ + # <===== TO BE REMOVED WITH DEPRECATION + # TODO(Patrick) - make sure to remove the following lines when config=="model_path" is deprecated + if "pretrained_model_name_or_path" in kwargs: + config = kwargs.pop("pretrained_model_name_or_path") + + if config is None: + raise ValueError("Please make sure to provide a config as the first positional argument.") + # ======> + + if not isinstance(config, dict): + deprecation_message = "It is deprecated to pass a pretrained model name or path to `from_config`." + if "Scheduler" in cls.__name__: + deprecation_message += ( + f"If you were trying to load a scheduler, please use {cls}.from_pretrained(...) instead." + " Otherwise, please make sure to pass a configuration dictionary instead. This functionality will" + " be removed in v1.0.0." + ) + elif "Model" in cls.__name__: + deprecation_message += ( + f"If you were trying to load a model, please use {cls}.load_config(...) followed by" + f" {cls}.from_config(...) instead. Otherwise, please make sure to pass a configuration dictionary" + " instead. This functionality will be removed in v1.0.0." + ) + deprecate("config-passed-as-path", "1.0.0", deprecation_message, standard_warn=False) + config, kwargs = cls.load_config(pretrained_model_name_or_path=config, return_unused_kwargs=True, **kwargs) + + init_dict, unused_kwargs, hidden_dict = cls.extract_init_dict(config, **kwargs) + + # Allow dtype to be specified on initialization + if "dtype" in unused_kwargs: + init_dict["dtype"] = unused_kwargs.pop("dtype") + + # add possible deprecated kwargs + for deprecated_kwarg in cls._deprecated_kwargs: + if deprecated_kwarg in unused_kwargs: + init_dict[deprecated_kwarg] = unused_kwargs.pop(deprecated_kwarg) + + # Return model and optionally state and/or unused_kwargs + model = cls(**init_dict) + + # make sure to also save config parameters that might be used for compatible classes + model.register_to_config(**hidden_dict) + + # add hidden kwargs of compatible classes to unused_kwargs + unused_kwargs = {**unused_kwargs, **hidden_dict} + + if return_unused_kwargs: + return (model, unused_kwargs) + else: + return model + + @classmethod + def get_config_dict(cls, *args, **kwargs): + deprecation_message = ( + f" The function get_config_dict is deprecated. Please use {cls}.load_config instead. This function will be" + " removed in version v1.0.0" + ) + deprecate("get_config_dict", "1.0.0", deprecation_message, standard_warn=False) + return cls.load_config(*args, **kwargs) + + @classmethod + def load_config( + cls, pretrained_model_name_or_path: Union[str, os.PathLike], return_unused_kwargs=False, **kwargs + ) -> Tuple[Dict[str, Any], Dict[str, Any]]: + r""" + Instantiate a Python class from a config dictionary + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *model id* of a model repo on huggingface.co. Valid model ids should have an + organization name, like `google/ddpm-celebahq-256`. + - A path to a *directory* containing model weights saved using [`~ConfigMixin.save_config`], e.g., + `./my_model_directory/`. + + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `transformers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + subfolder (`str`, *optional*, defaults to `""`): + In case the relevant files are located inside a subfolder of the model repo (either remote in + huggingface.co or downloaded locally), you can specify the folder name here. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + + + Activate the special ["offline-mode"](https://huggingface.co/transformers/installation.html#offline-mode) to + use this method in a firewalled environment. + + + """ + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + force_download = kwargs.pop("force_download", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + use_auth_token = kwargs.pop("use_auth_token", None) + local_files_only = kwargs.pop("local_files_only", False) + revision = kwargs.pop("revision", None) + _ = kwargs.pop("mirror", None) + subfolder = kwargs.pop("subfolder", None) + + user_agent = {"file_type": "config"} + + pretrained_model_name_or_path = str(pretrained_model_name_or_path) + + if cls.config_name is None: + raise ValueError( + "`self.config_name` is not defined. Note that one should not load a config from " + "`ConfigMixin`. Please make sure to define `config_name` in a class inheriting from `ConfigMixin`" + ) + + if os.path.isfile(pretrained_model_name_or_path): + config_file = pretrained_model_name_or_path + elif os.path.isdir(pretrained_model_name_or_path): + if os.path.isfile(os.path.join(pretrained_model_name_or_path, cls.config_name)): + # Load from a PyTorch checkpoint + config_file = os.path.join(pretrained_model_name_or_path, cls.config_name) + elif subfolder is not None and os.path.isfile( + os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name) + ): + config_file = os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name) + else: + raise EnvironmentError( + f"Error no file named {cls.config_name} found in directory {pretrained_model_name_or_path}." + ) + else: + try: + # Load from URL or cache if already cached + config_file = hf_hub_download( + pretrained_model_name_or_path, + filename=cls.config_name, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + user_agent=user_agent, + subfolder=subfolder, + revision=revision, + ) + + except RepositoryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier" + " listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a" + " token having permission to this repo with `use_auth_token` or log in with `huggingface-cli" + " login`." + ) + except RevisionNotFoundError: + raise EnvironmentError( + f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for" + " this model name. Check the model page at" + f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions." + ) + except EntryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}." + ) + except HTTPError as err: + raise EnvironmentError( + "There was a specific connection error when trying to load" + f" {pretrained_model_name_or_path}:\n{err}" + ) + except ValueError: + raise EnvironmentError( + f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it" + f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a" + f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to" + " run the library in offline mode at" + " 'https://huggingface.co/docs/diffusers/installation#offline-mode'." + ) + except EnvironmentError: + raise EnvironmentError( + f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from " + "'https://huggingface.co/models', make sure you don't have a local directory with the same name. " + f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory " + f"containing a {cls.config_name} file" + ) + + try: + # Load config dict + config_dict = cls._dict_from_json_file(config_file) + except (json.JSONDecodeError, UnicodeDecodeError): + raise EnvironmentError(f"It looks like the config file at '{config_file}' is not a valid JSON file.") + + if return_unused_kwargs: + return config_dict, kwargs + + return config_dict + + @staticmethod + def _get_init_keys(cls): + return set(dict(inspect.signature(cls.__init__).parameters).keys()) + + @classmethod + def extract_init_dict(cls, config_dict, **kwargs): + # 0. Copy origin config dict + original_dict = {k: v for k, v in config_dict.items()} + + # 1. Retrieve expected config attributes from __init__ signature + expected_keys = cls._get_init_keys(cls) + expected_keys.remove("self") + # remove general kwargs if present in dict + if "kwargs" in expected_keys: + expected_keys.remove("kwargs") + # remove flax internal keys + if hasattr(cls, "_flax_internal_args"): + for arg in cls._flax_internal_args: + expected_keys.remove(arg) + + # 2. Remove attributes that cannot be expected from expected config attributes + # remove keys to be ignored + if len(cls.ignore_for_config) > 0: + expected_keys = expected_keys - set(cls.ignore_for_config) + + # load diffusers library to import compatible and original scheduler + diffusers_library = importlib.import_module(__name__.split(".")[0]) + + if cls.has_compatibles: + compatible_classes = [c for c in cls._get_compatibles() if not isinstance(c, DummyObject)] + else: + compatible_classes = [] + + expected_keys_comp_cls = set() + for c in compatible_classes: + expected_keys_c = cls._get_init_keys(c) + expected_keys_comp_cls = expected_keys_comp_cls.union(expected_keys_c) + expected_keys_comp_cls = expected_keys_comp_cls - cls._get_init_keys(cls) + config_dict = {k: v for k, v in config_dict.items() if k not in expected_keys_comp_cls} + + # remove attributes from orig class that cannot be expected + orig_cls_name = config_dict.pop("_class_name", cls.__name__) + if orig_cls_name != cls.__name__ and hasattr(diffusers_library, orig_cls_name): + orig_cls = getattr(diffusers_library, orig_cls_name) + unexpected_keys_from_orig = cls._get_init_keys(orig_cls) - expected_keys + config_dict = {k: v for k, v in config_dict.items() if k not in unexpected_keys_from_orig} + + # remove private attributes + config_dict = {k: v for k, v in config_dict.items() if not k.startswith("_")} + + # 3. Create keyword arguments that will be passed to __init__ from expected keyword arguments + init_dict = {} + for key in expected_keys: + # if config param is passed to kwarg and is present in config dict + # it should overwrite existing config dict key + if key in kwargs and key in config_dict: + config_dict[key] = kwargs.pop(key) + + if key in kwargs: + # overwrite key + init_dict[key] = kwargs.pop(key) + elif key in config_dict: + # use value from config dict + init_dict[key] = config_dict.pop(key) + + # 4. Give nice warning if unexpected values have been passed + if len(config_dict) > 0: + logger.warning( + f"The config attributes {config_dict} were passed to {cls.__name__}, " + "but are not expected and will be ignored. Please verify your " + f"{cls.config_name} configuration file." + ) + + # 5. Give nice info if config attributes are initiliazed to default because they have not been passed + passed_keys = set(init_dict.keys()) + if len(expected_keys - passed_keys) > 0: + logger.info( + f"{expected_keys - passed_keys} was not found in config. Values will be initialized to default values." + ) + + # 6. Define unused keyword arguments + unused_kwargs = {**config_dict, **kwargs} + + # 7. Define "hidden" config parameters that were saved for compatible classes + hidden_config_dict = {k: v for k, v in original_dict.items() if k not in init_dict} + + return init_dict, unused_kwargs, hidden_config_dict + + @classmethod + def _dict_from_json_file(cls, json_file: Union[str, os.PathLike]): + with open(json_file, "r", encoding="utf-8") as reader: + text = reader.read() + return json.loads(text) + + def __repr__(self): + return f"{self.__class__.__name__} {self.to_json_string()}" + + @property + def config(self) -> Dict[str, Any]: + """ + Returns the config of the class as a frozen dictionary + + Returns: + `Dict[str, Any]`: Config of the class. + """ + return self._internal_dict + + def to_json_string(self) -> str: + """ + Serializes this instance to a JSON string. + + Returns: + `str`: String containing all the attributes that make up this configuration instance in JSON format. + """ + config_dict = self._internal_dict if hasattr(self, "_internal_dict") else {} + config_dict["_class_name"] = self.__class__.__name__ + config_dict["_diffusers_version"] = __version__ + + def to_json_saveable(value): + if isinstance(value, np.ndarray): + value = value.tolist() + elif isinstance(value, PosixPath): + value = str(value) + return value + + config_dict = {k: to_json_saveable(v) for k, v in config_dict.items()} + return json.dumps(config_dict, indent=2, sort_keys=True) + "\n" + + def to_json_file(self, json_file_path: Union[str, os.PathLike]): + """ + Save this instance to a JSON file. + + Args: + json_file_path (`str` or `os.PathLike`): + Path to the JSON file in which this configuration instance's parameters will be saved. + """ + with open(json_file_path, "w", encoding="utf-8") as writer: + writer.write(self.to_json_string()) + + +def register_to_config(init): + r""" + Decorator to apply on the init of classes inheriting from [`ConfigMixin`] so that all the arguments are + automatically sent to `self.register_for_config`. To ignore a specific argument accepted by the init but that + shouldn't be registered in the config, use the `ignore_for_config` class variable + + Warning: Once decorated, all private arguments (beginning with an underscore) are trashed and not sent to the init! + """ + + @functools.wraps(init) + def inner_init(self, *args, **kwargs): + # Ignore private kwargs in the init. + init_kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")} + config_init_kwargs = {k: v for k, v in kwargs.items() if k.startswith("_")} + if not isinstance(self, ConfigMixin): + raise RuntimeError( + f"`@register_for_config` was applied to {self.__class__.__name__} init method, but this class does " + "not inherit from `ConfigMixin`." + ) + + ignore = getattr(self, "ignore_for_config", []) + # Get positional arguments aligned with kwargs + new_kwargs = {} + signature = inspect.signature(init) + parameters = { + name: p.default for i, (name, p) in enumerate(signature.parameters.items()) if i > 0 and name not in ignore + } + for arg, name in zip(args, parameters.keys()): + new_kwargs[name] = arg + + # Then add all kwargs + new_kwargs.update( + { + k: init_kwargs.get(k, default) + for k, default in parameters.items() + if k not in ignore and k not in new_kwargs + } + ) + new_kwargs = {**config_init_kwargs, **new_kwargs} + getattr(self, "register_to_config")(**new_kwargs) + init(self, *args, **init_kwargs) + + return inner_init + + +def flax_register_to_config(cls): + original_init = cls.__init__ + + @functools.wraps(original_init) + def init(self, *args, **kwargs): + if not isinstance(self, ConfigMixin): + raise RuntimeError( + f"`@register_for_config` was applied to {self.__class__.__name__} init method, but this class does " + "not inherit from `ConfigMixin`." + ) + + # Ignore private kwargs in the init. Retrieve all passed attributes + init_kwargs = {k: v for k, v in kwargs.items()} + + # Retrieve default values + fields = dataclasses.fields(self) + default_kwargs = {} + for field in fields: + # ignore flax specific attributes + if field.name in self._flax_internal_args: + continue + if type(field.default) == dataclasses._MISSING_TYPE: + default_kwargs[field.name] = None + else: + default_kwargs[field.name] = getattr(self, field.name) + + # Make sure init_kwargs override default kwargs + new_kwargs = {**default_kwargs, **init_kwargs} + # dtype should be part of `init_kwargs`, but not `new_kwargs` + if "dtype" in new_kwargs: + new_kwargs.pop("dtype") + + # Get positional arguments aligned with kwargs + for i, arg in enumerate(args): + name = fields[i].name + new_kwargs[name] = arg + + getattr(self, "register_to_config")(**new_kwargs) + original_init(self, *args, **kwargs) + + cls.__init__ = init + return cls diff --git a/diffusers/src/diffusers/dependency_versions_check.py b/diffusers/src/diffusers/dependency_versions_check.py new file mode 100644 index 0000000000000000000000000000000000000000..c6330299700c1848703aed6c9336c00fc905fea4 --- /dev/null +++ b/diffusers/src/diffusers/dependency_versions_check.py @@ -0,0 +1,47 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import sys + +from .dependency_versions_table import deps +from .utils.versions import require_version, require_version_core + + +# define which module versions we always want to check at run time +# (usually the ones defined in `install_requires` in setup.py) +# +# order specific notes: +# - tqdm must be checked before tokenizers + +pkgs_to_check_at_runtime = "python tqdm regex requests packaging filelock numpy tokenizers".split() +if sys.version_info < (3, 7): + pkgs_to_check_at_runtime.append("dataclasses") +if sys.version_info < (3, 8): + pkgs_to_check_at_runtime.append("importlib_metadata") + +for pkg in pkgs_to_check_at_runtime: + if pkg in deps: + if pkg == "tokenizers": + # must be loaded here, or else tqdm check may fail + from .utils import is_tokenizers_available + + if not is_tokenizers_available(): + continue # not required, check version only if installed + + require_version_core(deps[pkg]) + else: + raise ValueError(f"can't find {pkg} in {deps.keys()}, check dependency_versions_table.py") + + +def dep_version_check(pkg, hint=None): + require_version(deps[pkg], hint) diff --git a/diffusers/src/diffusers/dependency_versions_table.py b/diffusers/src/diffusers/dependency_versions_table.py new file mode 100644 index 0000000000000000000000000000000000000000..a84d33706ef8a69ab4f01b73b204c9e06e956c9d --- /dev/null +++ b/diffusers/src/diffusers/dependency_versions_table.py @@ -0,0 +1,35 @@ +# THIS FILE HAS BEEN AUTOGENERATED. To update: +# 1. modify the `_deps` dict in setup.py +# 2. run `make deps_table_update`` +deps = { + "Pillow": "Pillow", + "accelerate": "accelerate>=0.11.0", + "black": "black~=23.1", + "datasets": "datasets", + "filelock": "filelock", + "flax": "flax>=0.4.1", + "hf-doc-builder": "hf-doc-builder>=0.3.0", + "huggingface-hub": "huggingface-hub>=0.10.0", + "importlib_metadata": "importlib_metadata", + "isort": "isort>=5.5.4", + "jax": "jax>=0.2.8,!=0.3.2", + "jaxlib": "jaxlib>=0.1.65", + "Jinja2": "Jinja2", + "k-diffusion": "k-diffusion>=0.0.12", + "librosa": "librosa", + "numpy": "numpy", + "parameterized": "parameterized", + "pytest": "pytest", + "pytest-timeout": "pytest-timeout", + "pytest-xdist": "pytest-xdist", + "ruff": "ruff>=0.0.241", + "safetensors": "safetensors", + "sentencepiece": "sentencepiece>=0.1.91,!=0.1.92", + "scipy": "scipy", + "regex": "regex!=2019.12.17", + "requests": "requests", + "tensorboard": "tensorboard", + "torch": "torch>=1.4", + "torchvision": "torchvision", + "transformers": "transformers>=4.25.1", +} diff --git a/diffusers/src/diffusers/experimental/README.md b/diffusers/src/diffusers/experimental/README.md new file mode 100644 index 0000000000000000000000000000000000000000..81a9de81c73728ea41eb6e8617a5429c3c9645ff --- /dev/null +++ b/diffusers/src/diffusers/experimental/README.md @@ -0,0 +1,5 @@ +# 🧨 Diffusers Experimental + +We are adding experimental code to support novel applications and usages of the Diffusers library. +Currently, the following experiments are supported: +* Reinforcement learning via an implementation of the [Diffuser](https://arxiv.org/abs/2205.09991) model. \ No newline at end of file diff --git a/diffusers/src/diffusers/experimental/__init__.py b/diffusers/src/diffusers/experimental/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ebc8155403016dfd8ad7fb78d246f9da9098ac50 --- /dev/null +++ b/diffusers/src/diffusers/experimental/__init__.py @@ -0,0 +1 @@ +from .rl import ValueGuidedRLPipeline diff --git a/diffusers/src/diffusers/experimental/rl/__init__.py b/diffusers/src/diffusers/experimental/rl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7b338d3173e12d478b6b6d6fd0e50650a0ab5a4c --- /dev/null +++ b/diffusers/src/diffusers/experimental/rl/__init__.py @@ -0,0 +1 @@ +from .value_guided_sampling import ValueGuidedRLPipeline diff --git a/diffusers/src/diffusers/experimental/rl/value_guided_sampling.py b/diffusers/src/diffusers/experimental/rl/value_guided_sampling.py new file mode 100644 index 0000000000000000000000000000000000000000..d10062da6025f7105dbde44a4dec25215e4af348 --- /dev/null +++ b/diffusers/src/diffusers/experimental/rl/value_guided_sampling.py @@ -0,0 +1,152 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import numpy as np +import torch +import tqdm + +from ...models.unet_1d import UNet1DModel +from ...pipelines import DiffusionPipeline +from ...utils import randn_tensor +from ...utils.dummy_pt_objects import DDPMScheduler + + +class ValueGuidedRLPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + Pipeline for sampling actions from a diffusion model trained to predict sequences of states. + + Original implementation inspired by this repository: https://github.com/jannerm/diffuser. + + Parameters: + value_function ([`UNet1DModel`]): A specialized UNet for fine-tuning trajectories base on reward. + unet ([`UNet1DModel`]): U-Net architecture to denoise the encoded trajectories. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded trajectories. Default for this + application is [`DDPMScheduler`]. + env: An environment following the OpenAI gym API to act in. For now only Hopper has pretrained models. + """ + + def __init__( + self, + value_function: UNet1DModel, + unet: UNet1DModel, + scheduler: DDPMScheduler, + env, + ): + super().__init__() + self.value_function = value_function + self.unet = unet + self.scheduler = scheduler + self.env = env + self.data = env.get_dataset() + self.means = dict() + for key in self.data.keys(): + try: + self.means[key] = self.data[key].mean() + except: # noqa: E722 + pass + self.stds = dict() + for key in self.data.keys(): + try: + self.stds[key] = self.data[key].std() + except: # noqa: E722 + pass + self.state_dim = env.observation_space.shape[0] + self.action_dim = env.action_space.shape[0] + + def normalize(self, x_in, key): + return (x_in - self.means[key]) / self.stds[key] + + def de_normalize(self, x_in, key): + return x_in * self.stds[key] + self.means[key] + + def to_torch(self, x_in): + if type(x_in) is dict: + return {k: self.to_torch(v) for k, v in x_in.items()} + elif torch.is_tensor(x_in): + return x_in.to(self.unet.device) + return torch.tensor(x_in, device=self.unet.device) + + def reset_x0(self, x_in, cond, act_dim): + for key, val in cond.items(): + x_in[:, key, act_dim:] = val.clone() + return x_in + + def run_diffusion(self, x, conditions, n_guide_steps, scale): + batch_size = x.shape[0] + y = None + for i in tqdm.tqdm(self.scheduler.timesteps): + # create batch of timesteps to pass into model + timesteps = torch.full((batch_size,), i, device=self.unet.device, dtype=torch.long) + for _ in range(n_guide_steps): + with torch.enable_grad(): + x.requires_grad_() + + # permute to match dimension for pre-trained models + y = self.value_function(x.permute(0, 2, 1), timesteps).sample + grad = torch.autograd.grad([y.sum()], [x])[0] + + posterior_variance = self.scheduler._get_variance(i) + model_std = torch.exp(0.5 * posterior_variance) + grad = model_std * grad + + grad[timesteps < 2] = 0 + x = x.detach() + x = x + scale * grad + x = self.reset_x0(x, conditions, self.action_dim) + + prev_x = self.unet(x.permute(0, 2, 1), timesteps).sample.permute(0, 2, 1) + + # TODO: verify deprecation of this kwarg + x = self.scheduler.step(prev_x, i, x, predict_epsilon=False)["prev_sample"] + + # apply conditions to the trajectory (set the initial state) + x = self.reset_x0(x, conditions, self.action_dim) + x = self.to_torch(x) + return x, y + + def __call__(self, obs, batch_size=64, planning_horizon=32, n_guide_steps=2, scale=0.1): + # normalize the observations and create batch dimension + obs = self.normalize(obs, "observations") + obs = obs[None].repeat(batch_size, axis=0) + + conditions = {0: self.to_torch(obs)} + shape = (batch_size, planning_horizon, self.state_dim + self.action_dim) + + # generate initial noise and apply our conditions (to make the trajectories start at current state) + x1 = randn_tensor(shape, device=self.unet.device) + x = self.reset_x0(x1, conditions, self.action_dim) + x = self.to_torch(x) + + # run the diffusion process + x, y = self.run_diffusion(x, conditions, n_guide_steps, scale) + + # sort output trajectories by value + sorted_idx = y.argsort(0, descending=True).squeeze() + sorted_values = x[sorted_idx] + actions = sorted_values[:, :, : self.action_dim] + actions = actions.detach().cpu().numpy() + denorm_actions = self.de_normalize(actions, key="actions") + + # select the action with the highest value + if y is not None: + selected_index = 0 + else: + # if we didn't run value guiding, select a random action + selected_index = np.random.randint(0, batch_size) + + denorm_actions = denorm_actions[selected_index, 0] + return denorm_actions diff --git a/diffusers/src/diffusers/loaders.py b/diffusers/src/diffusers/loaders.py new file mode 100644 index 0000000000000000000000000000000000000000..860c43ea43e597f047a0b921c6fc6bcce04d0d34 --- /dev/null +++ b/diffusers/src/diffusers/loaders.py @@ -0,0 +1,243 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import os +from collections import defaultdict +from typing import Callable, Dict, Union + +import torch + +from .models.cross_attention import LoRACrossAttnProcessor +from .models.modeling_utils import _get_model_file +from .utils import DIFFUSERS_CACHE, HF_HUB_OFFLINE, logging + + +logger = logging.get_logger(__name__) + + +LORA_WEIGHT_NAME = "pytorch_lora_weights.bin" + + +class AttnProcsLayers(torch.nn.Module): + def __init__(self, state_dict: Dict[str, torch.Tensor]): + super().__init__() + self.layers = torch.nn.ModuleList(state_dict.values()) + self.mapping = {k: v for k, v in enumerate(state_dict.keys())} + self.rev_mapping = {v: k for k, v in enumerate(state_dict.keys())} + + # we add a hook to state_dict() and load_state_dict() so that the + # naming fits with `unet.attn_processors` + def map_to(module, state_dict, *args, **kwargs): + new_state_dict = {} + for key, value in state_dict.items(): + num = int(key.split(".")[1]) # 0 is always "layers" + new_key = key.replace(f"layers.{num}", module.mapping[num]) + new_state_dict[new_key] = value + + return new_state_dict + + def map_from(module, state_dict, *args, **kwargs): + all_keys = list(state_dict.keys()) + for key in all_keys: + replace_key = key.split(".processor")[0] + ".processor" + new_key = key.replace(replace_key, f"layers.{module.rev_mapping[replace_key]}") + state_dict[new_key] = state_dict[key] + del state_dict[key] + + self._register_state_dict_hook(map_to) + self._register_load_state_dict_pre_hook(map_from, with_module=True) + + +class UNet2DConditionLoadersMixin: + def load_attn_procs(self, pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]], **kwargs): + r""" + Load pretrained attention processor layers into `UNet2DConditionModel`. Attention processor layers have to be + defined in + [cross_attention.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py) + and be a `torch.nn.Module` class. + + + + This function is experimental and might change in the future. + + + + Parameters: + pretrained_model_name_or_path_or_dict (`str` or `os.PathLike` or `dict`): + Can be either: + + - A string, the *model id* of a pretrained model hosted inside a model repo on huggingface.co. + Valid model ids should have an organization name, like `google/ddpm-celebahq-256`. + - A path to a *directory* containing model weights saved using [`~ModelMixin.save_config`], e.g., + `./my_model_directory/`. + - A [torch state + dict](https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict). + + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `diffusers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + subfolder (`str`, *optional*, defaults to `""`): + In case the relevant files are located inside a subfolder of the model repo (either remote in + huggingface.co or downloaded locally), you can specify the folder name here. + + mirror (`str`, *optional*): + Mirror source to accelerate downloads in China. If you are from China and have an accessibility + problem, you can set this option to resolve it. Note that we do not guarantee the timeliness or safety. + Please refer to the mirror site for more information. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + + + Activate the special ["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use + this method in a firewalled environment. + + + """ + + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + force_download = kwargs.pop("force_download", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", HF_HUB_OFFLINE) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + subfolder = kwargs.pop("subfolder", None) + weight_name = kwargs.pop("weight_name", LORA_WEIGHT_NAME) + + user_agent = { + "file_type": "attn_procs_weights", + "framework": "pytorch", + } + + if not isinstance(pretrained_model_name_or_path_or_dict, dict): + model_file = _get_model_file( + pretrained_model_name_or_path_or_dict, + weights_name=weight_name, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + state_dict = torch.load(model_file, map_location="cpu") + else: + state_dict = pretrained_model_name_or_path_or_dict + + # fill attn processors + attn_processors = {} + + is_lora = all("lora" in k for k in state_dict.keys()) + + if is_lora: + lora_grouped_dict = defaultdict(dict) + for key, value in state_dict.items(): + attn_processor_key, sub_key = ".".join(key.split(".")[:-3]), ".".join(key.split(".")[-3:]) + lora_grouped_dict[attn_processor_key][sub_key] = value + + for key, value_dict in lora_grouped_dict.items(): + rank = value_dict["to_k_lora.down.weight"].shape[0] + cross_attention_dim = value_dict["to_k_lora.down.weight"].shape[1] + hidden_size = value_dict["to_k_lora.up.weight"].shape[0] + + attn_processors[key] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim, rank=rank + ) + attn_processors[key].load_state_dict(value_dict) + + else: + raise ValueError(f"{model_file} does not seem to be in the correct format expected by LoRA training.") + + # set correct dtype & device + attn_processors = {k: v.to(device=self.device, dtype=self.dtype) for k, v in attn_processors.items()} + + # set layers + self.set_attn_processor(attn_processors) + + def save_attn_procs( + self, + save_directory: Union[str, os.PathLike], + is_main_process: bool = True, + weights_name: str = LORA_WEIGHT_NAME, + save_function: Callable = None, + ): + r""" + Save an attention processor to a directory, so that it can be re-loaded using the + `[`~loaders.UNet2DConditionLoadersMixin.load_attn_procs`]` method. + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + is_main_process (`bool`, *optional*, defaults to `True`): + Whether the process calling this is the main process or not. Useful when in distributed training like + TPUs and need to call this function on all processes. In this case, set `is_main_process=True` only on + the main process to avoid race conditions. + save_function (`Callable`): + The function to use to save the state dictionary. Useful on distributed training like TPUs when one + need to replace `torch.save` by another method. Can be configured with the environment variable + `DIFFUSERS_SAVE_MODE`. + """ + if os.path.isfile(save_directory): + logger.error(f"Provided path ({save_directory}) should be a directory, not a file") + return + + if save_function is None: + save_function = torch.save + + os.makedirs(save_directory, exist_ok=True) + + model_to_save = AttnProcsLayers(self.attn_processors) + + # Save the model + state_dict = model_to_save.state_dict() + + # Clean the folder from a previous save + for filename in os.listdir(save_directory): + full_filename = os.path.join(save_directory, filename) + # If we have a shard file that is not going to be replaced, we delete it, but only from the main process + # in distributed settings to avoid race conditions. + weights_no_suffix = weights_name.replace(".bin", "") + if filename.startswith(weights_no_suffix) and os.path.isfile(full_filename) and is_main_process: + os.remove(full_filename) + + # Save the model + save_function(state_dict, os.path.join(save_directory, weights_name)) + + logger.info(f"Model weights saved in {os.path.join(save_directory, weights_name)}") diff --git a/diffusers/src/diffusers/models/README.md b/diffusers/src/diffusers/models/README.md new file mode 100644 index 0000000000000000000000000000000000000000..80fe0bc381406457665d632816891fe364efd71f --- /dev/null +++ b/diffusers/src/diffusers/models/README.md @@ -0,0 +1,3 @@ +# Models + +For more detail on the models, please refer to the [docs](https://huggingface.co/docs/diffusers/api/models). \ No newline at end of file diff --git a/diffusers/src/diffusers/models/__init__.py b/diffusers/src/diffusers/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..474b8412560ea1a83887dc1aae2ec43b812c4b5d --- /dev/null +++ b/diffusers/src/diffusers/models/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from ..utils import is_flax_available, is_torch_available + + +if is_torch_available(): + from .autoencoder_kl import AutoencoderKL + from .dual_transformer_2d import DualTransformer2DModel + from .modeling_utils import ModelMixin + from .prior_transformer import PriorTransformer + from .transformer_2d import Transformer2DModel + from .unet_1d import UNet1DModel + from .unet_2d import UNet2DModel + from .unet_2d_condition import UNet2DConditionModel + from .vq_model import VQModel + +if is_flax_available(): + from .unet_2d_condition_flax import FlaxUNet2DConditionModel + from .vae_flax import FlaxAutoencoderKL diff --git a/diffusers/src/diffusers/models/attention.py b/diffusers/src/diffusers/models/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..3cdc7177a4113215db818767f7940a2952215c2b --- /dev/null +++ b/diffusers/src/diffusers/models/attention.py @@ -0,0 +1,517 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import math +from typing import Callable, Optional + +import torch +import torch.nn.functional as F +from torch import nn + +from ..utils.import_utils import is_xformers_available +from .cross_attention import CrossAttention +from .embeddings import CombinedTimestepLabelEmbeddings + + +if is_xformers_available(): + import xformers + import xformers.ops +else: + xformers = None + + +class AttentionBlock(nn.Module): + """ + An attention block that allows spatial positions to attend to each other. Originally ported from here, but adapted + to the N-d case. + https://github.com/hojonathanho/diffusion/blob/1e0dceb3b3495bbe19116a5e1b3596cd0706c543/diffusion_tf/models/unet.py#L66. + Uses three q, k, v linear layers to compute attention. + + Parameters: + channels (`int`): The number of channels in the input and output. + num_head_channels (`int`, *optional*): + The number of channels in each head. If None, then `num_heads` = 1. + norm_num_groups (`int`, *optional*, defaults to 32): The number of groups to use for group norm. + rescale_output_factor (`float`, *optional*, defaults to 1.0): The factor to rescale the output by. + eps (`float`, *optional*, defaults to 1e-5): The epsilon value to use for group norm. + """ + + # IMPORTANT;TODO(Patrick, William) - this class will be deprecated soon. Do not use it anymore + + def __init__( + self, + channels: int, + num_head_channels: Optional[int] = None, + norm_num_groups: int = 32, + rescale_output_factor: float = 1.0, + eps: float = 1e-5, + ): + super().__init__() + self.channels = channels + + self.num_heads = channels // num_head_channels if num_head_channels is not None else 1 + self.num_head_size = num_head_channels + self.group_norm = nn.GroupNorm(num_channels=channels, num_groups=norm_num_groups, eps=eps, affine=True) + + # define q,k,v as linear layers + self.query = nn.Linear(channels, channels) + self.key = nn.Linear(channels, channels) + self.value = nn.Linear(channels, channels) + + self.rescale_output_factor = rescale_output_factor + self.proj_attn = nn.Linear(channels, channels, 1) + + self._use_memory_efficient_attention_xformers = False + self._attention_op = None + + def reshape_heads_to_batch_dim(self, tensor): + batch_size, seq_len, dim = tensor.shape + head_size = self.num_heads + tensor = tensor.reshape(batch_size, seq_len, head_size, dim // head_size) + tensor = tensor.permute(0, 2, 1, 3).reshape(batch_size * head_size, seq_len, dim // head_size) + return tensor + + def reshape_batch_dim_to_heads(self, tensor): + batch_size, seq_len, dim = tensor.shape + head_size = self.num_heads + tensor = tensor.reshape(batch_size // head_size, head_size, seq_len, dim) + tensor = tensor.permute(0, 2, 1, 3).reshape(batch_size // head_size, seq_len, dim * head_size) + return tensor + + def set_use_memory_efficient_attention_xformers( + self, use_memory_efficient_attention_xformers: bool, attention_op: Optional[Callable] = None + ): + if use_memory_efficient_attention_xformers: + if not is_xformers_available(): + raise ModuleNotFoundError( + ( + "Refer to https://github.com/facebookresearch/xformers for more information on how to install" + " xformers" + ), + name="xformers", + ) + elif not torch.cuda.is_available(): + raise ValueError( + "torch.cuda.is_available() should be True but is False. xformers' memory efficient attention is" + " only available for GPU " + ) + else: + try: + # Make sure we can run the memory efficient attention + _ = xformers.ops.memory_efficient_attention( + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + ) + except Exception as e: + raise e + self._use_memory_efficient_attention_xformers = use_memory_efficient_attention_xformers + self._attention_op = attention_op + + def forward(self, hidden_states): + residual = hidden_states + batch, channel, height, width = hidden_states.shape + + # norm + hidden_states = self.group_norm(hidden_states) + + hidden_states = hidden_states.view(batch, channel, height * width).transpose(1, 2) + + # proj to q, k, v + query_proj = self.query(hidden_states) + key_proj = self.key(hidden_states) + value_proj = self.value(hidden_states) + + scale = 1 / math.sqrt(self.channels / self.num_heads) + + query_proj = self.reshape_heads_to_batch_dim(query_proj) + key_proj = self.reshape_heads_to_batch_dim(key_proj) + value_proj = self.reshape_heads_to_batch_dim(value_proj) + + if self._use_memory_efficient_attention_xformers: + # Memory efficient attention + hidden_states = xformers.ops.memory_efficient_attention( + query_proj, key_proj, value_proj, attn_bias=None, op=self._attention_op + ) + hidden_states = hidden_states.to(query_proj.dtype) + else: + attention_scores = torch.baddbmm( + torch.empty( + query_proj.shape[0], + query_proj.shape[1], + key_proj.shape[1], + dtype=query_proj.dtype, + device=query_proj.device, + ), + query_proj, + key_proj.transpose(-1, -2), + beta=0, + alpha=scale, + ) + attention_probs = torch.softmax(attention_scores.float(), dim=-1).type(attention_scores.dtype) + hidden_states = torch.bmm(attention_probs, value_proj) + + # reshape hidden_states + hidden_states = self.reshape_batch_dim_to_heads(hidden_states) + + # compute next hidden_states + hidden_states = self.proj_attn(hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(batch, channel, height, width) + + # res connect and rescale + hidden_states = (hidden_states + residual) / self.rescale_output_factor + return hidden_states + + +class BasicTransformerBlock(nn.Module): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm (: + obj: `int`, *optional*): The number of diffusion steps used during training. See `Transformer2DModel`. + attention_bias (: + obj: `bool`, *optional*, defaults to `False`): Configure if the attentions should contain a bias parameter. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout=0.0, + cross_attention_dim: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + attention_bias: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + norm_elementwise_affine: bool = True, + norm_type: str = "layer_norm", + final_dropout: bool = False, + ): + super().__init__() + self.only_cross_attention = only_cross_attention + + self.use_ada_layer_norm_zero = (num_embeds_ada_norm is not None) and norm_type == "ada_norm_zero" + self.use_ada_layer_norm = (num_embeds_ada_norm is not None) and norm_type == "ada_norm" + + if norm_type in ("ada_norm", "ada_norm_zero") and num_embeds_ada_norm is None: + raise ValueError( + f"`norm_type` is set to {norm_type}, but `num_embeds_ada_norm` is not defined. Please make sure to" + f" define `num_embeds_ada_norm` if setting `norm_type` to {norm_type}." + ) + + # 1. Self-Attn + self.attn1 = CrossAttention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=cross_attention_dim if only_cross_attention else None, + upcast_attention=upcast_attention, + ) + + self.ff = FeedForward(dim, dropout=dropout, activation_fn=activation_fn, final_dropout=final_dropout) + + # 2. Cross-Attn + if cross_attention_dim is not None: + self.attn2 = CrossAttention( + query_dim=dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + ) # is self-attn if encoder_hidden_states is none + else: + self.attn2 = None + + if self.use_ada_layer_norm: + self.norm1 = AdaLayerNorm(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_zero: + self.norm1 = AdaLayerNormZero(dim, num_embeds_ada_norm) + else: + self.norm1 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine) + + if cross_attention_dim is not None: + # We currently only use AdaLayerNormZero for self attention where there will only be one attention block. + # I.e. the number of returned modulation chunks from AdaLayerZero would not make sense if returned during + # the second cross attention block. + self.norm2 = ( + AdaLayerNorm(dim, num_embeds_ada_norm) + if self.use_ada_layer_norm + else nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine) + ) + else: + self.norm2 = None + + # 3. Feed-forward + self.norm3 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine) + + def forward( + self, + hidden_states, + encoder_hidden_states=None, + timestep=None, + attention_mask=None, + cross_attention_kwargs=None, + class_labels=None, + ): + if self.use_ada_layer_norm: + norm_hidden_states = self.norm1(hidden_states, timestep) + elif self.use_ada_layer_norm_zero: + norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1( + hidden_states, timestep, class_labels, hidden_dtype=hidden_states.dtype + ) + else: + norm_hidden_states = self.norm1(hidden_states) + + # 1. Self-Attention + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states if self.only_cross_attention else None, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + if self.use_ada_layer_norm_zero: + attn_output = gate_msa.unsqueeze(1) * attn_output + hidden_states = attn_output + hidden_states + + if self.attn2 is not None: + norm_hidden_states = ( + self.norm2(hidden_states, timestep) if self.use_ada_layer_norm else self.norm2(hidden_states) + ) + + # 2. Cross-Attention + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + hidden_states = attn_output + hidden_states + + # 3. Feed-forward + norm_hidden_states = self.norm3(hidden_states) + + if self.use_ada_layer_norm_zero: + norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None] + + ff_output = self.ff(norm_hidden_states) + + if self.use_ada_layer_norm_zero: + ff_output = gate_mlp.unsqueeze(1) * ff_output + + hidden_states = ff_output + hidden_states + + return hidden_states + + +class FeedForward(nn.Module): + r""" + A feed-forward layer. + + Parameters: + dim (`int`): The number of channels in the input. + dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`. + mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + final_dropout (`bool` *optional*, defaults to False): Apply a final dropout. + """ + + def __init__( + self, + dim: int, + dim_out: Optional[int] = None, + mult: int = 4, + dropout: float = 0.0, + activation_fn: str = "geglu", + final_dropout: bool = False, + ): + super().__init__() + inner_dim = int(dim * mult) + dim_out = dim_out if dim_out is not None else dim + + if activation_fn == "gelu": + act_fn = GELU(dim, inner_dim) + if activation_fn == "gelu-approximate": + act_fn = GELU(dim, inner_dim, approximate="tanh") + elif activation_fn == "geglu": + act_fn = GEGLU(dim, inner_dim) + elif activation_fn == "geglu-approximate": + act_fn = ApproximateGELU(dim, inner_dim) + + self.net = nn.ModuleList([]) + # project in + self.net.append(act_fn) + # project dropout + self.net.append(nn.Dropout(dropout)) + # project out + self.net.append(nn.Linear(inner_dim, dim_out)) + # FF as used in Vision Transformer, MLP-Mixer, etc. have a final dropout + if final_dropout: + self.net.append(nn.Dropout(dropout)) + + def forward(self, hidden_states): + for module in self.net: + hidden_states = module(hidden_states) + return hidden_states + + +class GELU(nn.Module): + r""" + GELU activation function with tanh approximation support with `approximate="tanh"`. + """ + + def __init__(self, dim_in: int, dim_out: int, approximate: str = "none"): + super().__init__() + self.proj = nn.Linear(dim_in, dim_out) + self.approximate = approximate + + def gelu(self, gate): + if gate.device.type != "mps": + return F.gelu(gate, approximate=self.approximate) + # mps: gelu is not implemented for float16 + return F.gelu(gate.to(dtype=torch.float32), approximate=self.approximate).to(dtype=gate.dtype) + + def forward(self, hidden_states): + hidden_states = self.proj(hidden_states) + hidden_states = self.gelu(hidden_states) + return hidden_states + + +class GEGLU(nn.Module): + r""" + A variant of the gated linear unit activation function from https://arxiv.org/abs/2002.05202. + + Parameters: + dim_in (`int`): The number of channels in the input. + dim_out (`int`): The number of channels in the output. + """ + + def __init__(self, dim_in: int, dim_out: int): + super().__init__() + self.proj = nn.Linear(dim_in, dim_out * 2) + + def gelu(self, gate): + if gate.device.type != "mps": + return F.gelu(gate) + # mps: gelu is not implemented for float16 + return F.gelu(gate.to(dtype=torch.float32)).to(dtype=gate.dtype) + + def forward(self, hidden_states): + hidden_states, gate = self.proj(hidden_states).chunk(2, dim=-1) + return hidden_states * self.gelu(gate) + + +class ApproximateGELU(nn.Module): + """ + The approximate form of Gaussian Error Linear Unit (GELU) + + For more details, see section 2: https://arxiv.org/abs/1606.08415 + """ + + def __init__(self, dim_in: int, dim_out: int): + super().__init__() + self.proj = nn.Linear(dim_in, dim_out) + + def forward(self, x): + x = self.proj(x) + return x * torch.sigmoid(1.702 * x) + + +class AdaLayerNorm(nn.Module): + """ + Norm layer modified to incorporate timestep embeddings. + """ + + def __init__(self, embedding_dim, num_embeddings): + super().__init__() + self.emb = nn.Embedding(num_embeddings, embedding_dim) + self.silu = nn.SiLU() + self.linear = nn.Linear(embedding_dim, embedding_dim * 2) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False) + + def forward(self, x, timestep): + emb = self.linear(self.silu(self.emb(timestep))) + scale, shift = torch.chunk(emb, 2) + x = self.norm(x) * (1 + scale) + shift + return x + + +class AdaLayerNormZero(nn.Module): + """ + Norm layer adaptive layer norm zero (adaLN-Zero). + """ + + def __init__(self, embedding_dim, num_embeddings): + super().__init__() + + self.emb = CombinedTimestepLabelEmbeddings(num_embeddings, embedding_dim) + + self.silu = nn.SiLU() + self.linear = nn.Linear(embedding_dim, 6 * embedding_dim, bias=True) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False, eps=1e-6) + + def forward(self, x, timestep, class_labels, hidden_dtype=None): + emb = self.linear(self.silu(self.emb(timestep, class_labels, hidden_dtype=hidden_dtype))) + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = emb.chunk(6, dim=1) + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x, gate_msa, shift_mlp, scale_mlp, gate_mlp + + +class AdaGroupNorm(nn.Module): + """ + GroupNorm layer modified to incorporate timestep embeddings. + """ + + def __init__( + self, embedding_dim: int, out_dim: int, num_groups: int, act_fn: Optional[str] = None, eps: float = 1e-5 + ): + super().__init__() + self.num_groups = num_groups + self.eps = eps + self.act = None + if act_fn == "swish": + self.act = lambda x: F.silu(x) + elif act_fn == "mish": + self.act = nn.Mish() + elif act_fn == "silu": + self.act = nn.SiLU() + elif act_fn == "gelu": + self.act = nn.GELU() + + self.linear = nn.Linear(embedding_dim, out_dim * 2) + + def forward(self, x, emb): + if self.act: + emb = self.act(emb) + emb = self.linear(emb) + emb = emb[:, :, None, None] + scale, shift = emb.chunk(2, dim=1) + + x = F.group_norm(x, self.num_groups, eps=self.eps) + x = x * (1 + scale) + shift + return x diff --git a/diffusers/src/diffusers/models/attention_flax.py b/diffusers/src/diffusers/models/attention_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..71106e05452cc7525cfbb81f2ac52926887313ec --- /dev/null +++ b/diffusers/src/diffusers/models/attention_flax.py @@ -0,0 +1,298 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import flax.linen as nn +import jax.numpy as jnp + + +class FlaxAttentionBlock(nn.Module): + r""" + A Flax multi-head attention module as described in: https://arxiv.org/abs/1706.03762 + + Parameters: + query_dim (:obj:`int`): + Input hidden states dimension + heads (:obj:`int`, *optional*, defaults to 8): + Number of heads + dim_head (:obj:`int`, *optional*, defaults to 64): + Hidden states dimension inside each head + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + + """ + query_dim: int + heads: int = 8 + dim_head: int = 64 + dropout: float = 0.0 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + inner_dim = self.dim_head * self.heads + self.scale = self.dim_head**-0.5 + + # Weights were exported with old names {to_q, to_k, to_v, to_out} + self.query = nn.Dense(inner_dim, use_bias=False, dtype=self.dtype, name="to_q") + self.key = nn.Dense(inner_dim, use_bias=False, dtype=self.dtype, name="to_k") + self.value = nn.Dense(inner_dim, use_bias=False, dtype=self.dtype, name="to_v") + + self.proj_attn = nn.Dense(self.query_dim, dtype=self.dtype, name="to_out_0") + + def reshape_heads_to_batch_dim(self, tensor): + batch_size, seq_len, dim = tensor.shape + head_size = self.heads + tensor = tensor.reshape(batch_size, seq_len, head_size, dim // head_size) + tensor = jnp.transpose(tensor, (0, 2, 1, 3)) + tensor = tensor.reshape(batch_size * head_size, seq_len, dim // head_size) + return tensor + + def reshape_batch_dim_to_heads(self, tensor): + batch_size, seq_len, dim = tensor.shape + head_size = self.heads + tensor = tensor.reshape(batch_size // head_size, head_size, seq_len, dim) + tensor = jnp.transpose(tensor, (0, 2, 1, 3)) + tensor = tensor.reshape(batch_size // head_size, seq_len, dim * head_size) + return tensor + + def __call__(self, hidden_states, context=None, deterministic=True): + context = hidden_states if context is None else context + + query_proj = self.query(hidden_states) + key_proj = self.key(context) + value_proj = self.value(context) + + query_states = self.reshape_heads_to_batch_dim(query_proj) + key_states = self.reshape_heads_to_batch_dim(key_proj) + value_states = self.reshape_heads_to_batch_dim(value_proj) + + # compute attentions + attention_scores = jnp.einsum("b i d, b j d->b i j", query_states, key_states) + attention_scores = attention_scores * self.scale + attention_probs = nn.softmax(attention_scores, axis=2) + + # attend to values + hidden_states = jnp.einsum("b i j, b j d -> b i d", attention_probs, value_states) + hidden_states = self.reshape_batch_dim_to_heads(hidden_states) + hidden_states = self.proj_attn(hidden_states) + return hidden_states + + +class FlaxBasicTransformerBlock(nn.Module): + r""" + A Flax transformer block layer with `GLU` (Gated Linear Unit) activation function as described in: + https://arxiv.org/abs/1706.03762 + + + Parameters: + dim (:obj:`int`): + Inner hidden states dimension + n_heads (:obj:`int`): + Number of heads + d_head (:obj:`int`): + Hidden states dimension inside each head + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + only_cross_attention (`bool`, defaults to `False`): + Whether to only apply cross attention. + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + dim: int + n_heads: int + d_head: int + dropout: float = 0.0 + only_cross_attention: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + # self attention (or cross_attention if only_cross_attention is True) + self.attn1 = FlaxAttentionBlock(self.dim, self.n_heads, self.d_head, self.dropout, dtype=self.dtype) + # cross attention + self.attn2 = FlaxAttentionBlock(self.dim, self.n_heads, self.d_head, self.dropout, dtype=self.dtype) + self.ff = FlaxGluFeedForward(dim=self.dim, dropout=self.dropout, dtype=self.dtype) + self.norm1 = nn.LayerNorm(epsilon=1e-5, dtype=self.dtype) + self.norm2 = nn.LayerNorm(epsilon=1e-5, dtype=self.dtype) + self.norm3 = nn.LayerNorm(epsilon=1e-5, dtype=self.dtype) + + def __call__(self, hidden_states, context, deterministic=True): + # self attention + residual = hidden_states + if self.only_cross_attention: + hidden_states = self.attn1(self.norm1(hidden_states), context, deterministic=deterministic) + else: + hidden_states = self.attn1(self.norm1(hidden_states), deterministic=deterministic) + hidden_states = hidden_states + residual + + # cross attention + residual = hidden_states + hidden_states = self.attn2(self.norm2(hidden_states), context, deterministic=deterministic) + hidden_states = hidden_states + residual + + # feed forward + residual = hidden_states + hidden_states = self.ff(self.norm3(hidden_states), deterministic=deterministic) + hidden_states = hidden_states + residual + + return hidden_states + + +class FlaxTransformer2DModel(nn.Module): + r""" + A Spatial Transformer layer with Gated Linear Unit (GLU) activation function as described in: + https://arxiv.org/pdf/1506.02025.pdf + + + Parameters: + in_channels (:obj:`int`): + Input number of channels + n_heads (:obj:`int`): + Number of heads + d_head (:obj:`int`): + Hidden states dimension inside each head + depth (:obj:`int`, *optional*, defaults to 1): + Number of transformers block + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + use_linear_projection (`bool`, defaults to `False`): tbd + only_cross_attention (`bool`, defaults to `False`): tbd + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + n_heads: int + d_head: int + depth: int = 1 + dropout: float = 0.0 + use_linear_projection: bool = False + only_cross_attention: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.norm = nn.GroupNorm(num_groups=32, epsilon=1e-5) + + inner_dim = self.n_heads * self.d_head + if self.use_linear_projection: + self.proj_in = nn.Dense(inner_dim, dtype=self.dtype) + else: + self.proj_in = nn.Conv( + inner_dim, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + + self.transformer_blocks = [ + FlaxBasicTransformerBlock( + inner_dim, + self.n_heads, + self.d_head, + dropout=self.dropout, + only_cross_attention=self.only_cross_attention, + dtype=self.dtype, + ) + for _ in range(self.depth) + ] + + if self.use_linear_projection: + self.proj_out = nn.Dense(inner_dim, dtype=self.dtype) + else: + self.proj_out = nn.Conv( + inner_dim, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + + def __call__(self, hidden_states, context, deterministic=True): + batch, height, width, channels = hidden_states.shape + residual = hidden_states + hidden_states = self.norm(hidden_states) + if self.use_linear_projection: + hidden_states = hidden_states.reshape(batch, height * width, channels) + hidden_states = self.proj_in(hidden_states) + else: + hidden_states = self.proj_in(hidden_states) + hidden_states = hidden_states.reshape(batch, height * width, channels) + + for transformer_block in self.transformer_blocks: + hidden_states = transformer_block(hidden_states, context, deterministic=deterministic) + + if self.use_linear_projection: + hidden_states = self.proj_out(hidden_states) + hidden_states = hidden_states.reshape(batch, height, width, channels) + else: + hidden_states = hidden_states.reshape(batch, height, width, channels) + hidden_states = self.proj_out(hidden_states) + + hidden_states = hidden_states + residual + return hidden_states + + +class FlaxGluFeedForward(nn.Module): + r""" + Flax module that encapsulates two Linear layers separated by a gated linear unit activation from: + https://arxiv.org/abs/2002.05202 + + Parameters: + dim (:obj:`int`): + Inner hidden states dimension + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + dim: int + dropout: float = 0.0 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + # The second linear layer needs to be called + # net_2 for now to match the index of the Sequential layer + self.net_0 = FlaxGEGLU(self.dim, self.dropout, self.dtype) + self.net_2 = nn.Dense(self.dim, dtype=self.dtype) + + def __call__(self, hidden_states, deterministic=True): + hidden_states = self.net_0(hidden_states) + hidden_states = self.net_2(hidden_states) + return hidden_states + + +class FlaxGEGLU(nn.Module): + r""" + Flax implementation of a Linear layer followed by the variant of the gated linear unit activation function from + https://arxiv.org/abs/2002.05202. + + Parameters: + dim (:obj:`int`): + Input hidden states dimension + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + dim: int + dropout: float = 0.0 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + inner_dim = self.dim * 4 + self.proj = nn.Dense(inner_dim * 2, dtype=self.dtype) + + def __call__(self, hidden_states, deterministic=True): + hidden_states = self.proj(hidden_states) + hidden_linear, hidden_gelu = jnp.split(hidden_states, 2, axis=2) + return hidden_linear * nn.gelu(hidden_gelu) diff --git a/diffusers/src/diffusers/models/autoencoder_kl.py b/diffusers/src/diffusers/models/autoencoder_kl.py new file mode 100644 index 0000000000000000000000000000000000000000..18e7eb39c8e929eb28c0b977feadf0a18777d015 --- /dev/null +++ b/diffusers/src/diffusers/models/autoencoder_kl.py @@ -0,0 +1,185 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import torch +import torch.nn as nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .modeling_utils import ModelMixin +from .vae import Decoder, DecoderOutput, DiagonalGaussianDistribution, Encoder + + +@dataclass +class AutoencoderKLOutput(BaseOutput): + """ + Output of AutoencoderKL encoding method. + + Args: + latent_dist (`DiagonalGaussianDistribution`): + Encoded outputs of `Encoder` represented as the mean and logvar of `DiagonalGaussianDistribution`. + `DiagonalGaussianDistribution` allows for sampling latents from the distribution. + """ + + latent_dist: "DiagonalGaussianDistribution" + + +class AutoencoderKL(ModelMixin, ConfigMixin): + r"""Variational Autoencoder (VAE) model with KL loss from the paper Auto-Encoding Variational Bayes by Diederik P. Kingma + and Max Welling. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the model (such as downloading or saving, etc.) + + Parameters: + in_channels (int, *optional*, defaults to 3): Number of channels in the input image. + out_channels (int, *optional*, defaults to 3): Number of channels in the output. + down_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("DownEncoderBlock2D",)`): Tuple of downsample block types. + up_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("UpDecoderBlock2D",)`): Tuple of upsample block types. + block_out_channels (`Tuple[int]`, *optional*, defaults to : + obj:`(64,)`): Tuple of block output channels. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + latent_channels (`int`, *optional*, defaults to 4): Number of channels in the latent space. + sample_size (`int`, *optional*, defaults to `32`): TODO + scaling_factor (`float`, *optional*, defaults to 0.18215): + The component-wise standard deviation of the trained latent space computed using the first batch of the + training set. This is used to scale the latent space to have unit variance when training the diffusion + model. The latents are scaled with the formula `z = z * scaling_factor` before being passed to the + diffusion model. When decoding, the latents are scaled back to the original scale with the formula: `z = 1 + / scaling_factor * z`. For more details, refer to sections 4.3.2 and D.1 of the [High-Resolution Image + Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) paper. + """ + + @register_to_config + def __init__( + self, + in_channels: int = 3, + out_channels: int = 3, + down_block_types: Tuple[str] = ("DownEncoderBlock2D",), + up_block_types: Tuple[str] = ("UpDecoderBlock2D",), + block_out_channels: Tuple[int] = (64,), + layers_per_block: int = 1, + act_fn: str = "silu", + latent_channels: int = 4, + norm_num_groups: int = 32, + sample_size: int = 32, + scaling_factor: float = 0.18215, + ): + super().__init__() + + # pass init params to Encoder + self.encoder = Encoder( + in_channels=in_channels, + out_channels=latent_channels, + down_block_types=down_block_types, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + act_fn=act_fn, + norm_num_groups=norm_num_groups, + double_z=True, + ) + + # pass init params to Decoder + self.decoder = Decoder( + in_channels=latent_channels, + out_channels=out_channels, + up_block_types=up_block_types, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + norm_num_groups=norm_num_groups, + act_fn=act_fn, + ) + + self.quant_conv = nn.Conv2d(2 * latent_channels, 2 * latent_channels, 1) + self.post_quant_conv = nn.Conv2d(latent_channels, latent_channels, 1) + self.use_slicing = False + + def encode(self, x: torch.FloatTensor, return_dict: bool = True) -> AutoencoderKLOutput: + h = self.encoder(x) + moments = self.quant_conv(h) + posterior = DiagonalGaussianDistribution(moments) + + if not return_dict: + return (posterior,) + + return AutoencoderKLOutput(latent_dist=posterior) + + def _decode(self, z: torch.FloatTensor, return_dict: bool = True) -> Union[DecoderOutput, torch.FloatTensor]: + z = self.post_quant_conv(z) + dec = self.decoder(z) + + if not return_dict: + return (dec,) + + return DecoderOutput(sample=dec) + + def enable_slicing(self): + r""" + Enable sliced VAE decoding. + + When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several + steps. This is useful to save some memory and allow larger batch sizes. + """ + self.use_slicing = True + + def disable_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_slicing` was previously invoked, this method will go back to computing + decoding in one step. + """ + self.use_slicing = False + + def decode(self, z: torch.FloatTensor, return_dict: bool = True) -> Union[DecoderOutput, torch.FloatTensor]: + if self.use_slicing and z.shape[0] > 1: + decoded_slices = [self._decode(z_slice).sample for z_slice in z.split(1)] + decoded = torch.cat(decoded_slices) + else: + decoded = self._decode(z).sample + + if not return_dict: + return (decoded,) + + return DecoderOutput(sample=decoded) + + def forward( + self, + sample: torch.FloatTensor, + sample_posterior: bool = False, + return_dict: bool = True, + generator: Optional[torch.Generator] = None, + ) -> Union[DecoderOutput, torch.FloatTensor]: + r""" + Args: + sample (`torch.FloatTensor`): Input sample. + sample_posterior (`bool`, *optional*, defaults to `False`): + Whether to sample from the posterior. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`DecoderOutput`] instead of a plain tuple. + """ + x = sample + posterior = self.encode(x).latent_dist + if sample_posterior: + z = posterior.sample(generator=generator) + else: + z = posterior.mode() + dec = self.decode(z).sample + + if not return_dict: + return (dec,) + + return DecoderOutput(sample=dec) diff --git a/diffusers/src/diffusers/models/cross_attention.py b/diffusers/src/diffusers/models/cross_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..baccdd83f20215f1b07e2b3a2bdf0b162d044f92 --- /dev/null +++ b/diffusers/src/diffusers/models/cross_attention.py @@ -0,0 +1,634 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from typing import Callable, Optional, Union + +import torch +import torch.nn.functional as F +from torch import nn + +from ..utils import deprecate, logging +from ..utils.import_utils import is_xformers_available + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +if is_xformers_available(): + import xformers + import xformers.ops +else: + xformers = None + + +class CrossAttention(nn.Module): + r""" + A cross attention layer. + + Parameters: + query_dim (`int`): The number of channels in the query. + cross_attention_dim (`int`, *optional*): + The number of channels in the encoder_hidden_states. If not given, defaults to `query_dim`. + heads (`int`, *optional*, defaults to 8): The number of heads to use for multi-head attention. + dim_head (`int`, *optional*, defaults to 64): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + bias (`bool`, *optional*, defaults to False): + Set to `True` for the query, key, and value linear layers to contain a bias parameter. + """ + + def __init__( + self, + query_dim: int, + cross_attention_dim: Optional[int] = None, + heads: int = 8, + dim_head: int = 64, + dropout: float = 0.0, + bias=False, + upcast_attention: bool = False, + upcast_softmax: bool = False, + cross_attention_norm: bool = False, + added_kv_proj_dim: Optional[int] = None, + norm_num_groups: Optional[int] = None, + processor: Optional["AttnProcessor"] = None, + ): + super().__init__() + inner_dim = dim_head * heads + cross_attention_dim = cross_attention_dim if cross_attention_dim is not None else query_dim + self.upcast_attention = upcast_attention + self.upcast_softmax = upcast_softmax + self.cross_attention_norm = cross_attention_norm + + self.scale = dim_head**-0.5 + + self.heads = heads + # for slice_size > 0 the attention score computation + # is split across the batch axis to save memory + # You can set slice_size with `set_attention_slice` + self.sliceable_head_dim = heads + + self.added_kv_proj_dim = added_kv_proj_dim + + if norm_num_groups is not None: + self.group_norm = nn.GroupNorm(num_channels=inner_dim, num_groups=norm_num_groups, eps=1e-5, affine=True) + else: + self.group_norm = None + + if cross_attention_norm: + self.norm_cross = nn.LayerNorm(cross_attention_dim) + + self.to_q = nn.Linear(query_dim, inner_dim, bias=bias) + self.to_k = nn.Linear(cross_attention_dim, inner_dim, bias=bias) + self.to_v = nn.Linear(cross_attention_dim, inner_dim, bias=bias) + + if self.added_kv_proj_dim is not None: + self.add_k_proj = nn.Linear(added_kv_proj_dim, cross_attention_dim) + self.add_v_proj = nn.Linear(added_kv_proj_dim, cross_attention_dim) + + self.to_out = nn.ModuleList([]) + self.to_out.append(nn.Linear(inner_dim, query_dim)) + self.to_out.append(nn.Dropout(dropout)) + + # set attention processor + processor = processor if processor is not None else CrossAttnProcessor() + self.set_processor(processor) + + def set_use_memory_efficient_attention_xformers( + self, use_memory_efficient_attention_xformers: bool, attention_op: Optional[Callable] = None + ): + is_lora = hasattr(self, "processor") and isinstance( + self.processor, (LoRACrossAttnProcessor, LoRAXFormersCrossAttnProcessor) + ) + + if use_memory_efficient_attention_xformers: + if self.added_kv_proj_dim is not None: + # TODO(Anton, Patrick, Suraj, William) - currently xformers doesn't work for UnCLIP + # which uses this type of cross attention ONLY because the attention mask of format + # [0, ..., -10.000, ..., 0, ...,] is not supported + raise NotImplementedError( + "Memory efficient attention with `xformers` is currently not supported when" + " `self.added_kv_proj_dim` is defined." + ) + elif not is_xformers_available(): + raise ModuleNotFoundError( + ( + "Refer to https://github.com/facebookresearch/xformers for more information on how to install" + " xformers" + ), + name="xformers", + ) + elif not torch.cuda.is_available(): + raise ValueError( + "torch.cuda.is_available() should be True but is False. xformers' memory efficient attention is" + " only available for GPU " + ) + else: + try: + # Make sure we can run the memory efficient attention + _ = xformers.ops.memory_efficient_attention( + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + torch.randn((1, 2, 40), device="cuda"), + ) + except Exception as e: + raise e + + if is_lora: + processor = LoRAXFormersCrossAttnProcessor( + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + rank=self.processor.rank, + attention_op=attention_op, + ) + processor.load_state_dict(self.processor.state_dict()) + processor.to(self.processor.to_q_lora.up.weight.device) + else: + processor = XFormersCrossAttnProcessor(attention_op=attention_op) + else: + if is_lora: + processor = LoRACrossAttnProcessor( + hidden_size=self.processor.hidden_size, + cross_attention_dim=self.processor.cross_attention_dim, + rank=self.processor.rank, + ) + processor.load_state_dict(self.processor.state_dict()) + processor.to(self.processor.to_q_lora.up.weight.device) + else: + processor = CrossAttnProcessor() + + self.set_processor(processor) + + def set_attention_slice(self, slice_size): + if slice_size is not None and slice_size > self.sliceable_head_dim: + raise ValueError(f"slice_size {slice_size} has to be smaller or equal to {self.sliceable_head_dim}.") + + if slice_size is not None and self.added_kv_proj_dim is not None: + processor = SlicedAttnAddedKVProcessor(slice_size) + elif slice_size is not None: + processor = SlicedAttnProcessor(slice_size) + elif self.added_kv_proj_dim is not None: + processor = CrossAttnAddedKVProcessor() + else: + processor = CrossAttnProcessor() + + self.set_processor(processor) + + def set_processor(self, processor: "AttnProcessor"): + # if current processor is in `self._modules` and if passed `processor` is not, we need to + # pop `processor` from `self._modules` + if ( + hasattr(self, "processor") + and isinstance(self.processor, torch.nn.Module) + and not isinstance(processor, torch.nn.Module) + ): + logger.info(f"You are removing possibly trained weights of {self.processor} with {processor}") + self._modules.pop("processor") + + self.processor = processor + + def forward(self, hidden_states, encoder_hidden_states=None, attention_mask=None, **cross_attention_kwargs): + # The `CrossAttention` class can call different attention processors / attention functions + # here we simply pass along all tensors to the selected processor class + # For standard processors that are defined here, `**cross_attention_kwargs` is empty + return self.processor( + self, + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + def batch_to_head_dim(self, tensor): + head_size = self.heads + batch_size, seq_len, dim = tensor.shape + tensor = tensor.reshape(batch_size // head_size, head_size, seq_len, dim) + tensor = tensor.permute(0, 2, 1, 3).reshape(batch_size // head_size, seq_len, dim * head_size) + return tensor + + def head_to_batch_dim(self, tensor): + head_size = self.heads + batch_size, seq_len, dim = tensor.shape + tensor = tensor.reshape(batch_size, seq_len, head_size, dim // head_size) + tensor = tensor.permute(0, 2, 1, 3).reshape(batch_size * head_size, seq_len, dim // head_size) + return tensor + + def get_attention_scores(self, query, key, attention_mask=None): + dtype = query.dtype + if self.upcast_attention: + query = query.float() + key = key.float() + + if attention_mask is None: + baddbmm_input = torch.empty( + query.shape[0], query.shape[1], key.shape[1], dtype=query.dtype, device=query.device + ) + beta = 0 + else: + baddbmm_input = attention_mask + beta = 1 + + attention_scores = torch.baddbmm( + baddbmm_input, + query, + key.transpose(-1, -2), + beta=beta, + alpha=self.scale, + ) + + if self.upcast_softmax: + attention_scores = attention_scores.float() + + attention_probs = attention_scores.softmax(dim=-1) + attention_probs = attention_probs.to(dtype) + + return attention_probs + + def prepare_attention_mask(self, attention_mask, target_length, batch_size=None): + if batch_size is None: + deprecate( + "batch_size=None", + "0.0.15", + message=( + "Not passing the `batch_size` parameter to `prepare_attention_mask` can lead to incorrect" + " attention mask preparation and is deprecated behavior. Please make sure to pass `batch_size` to" + " `prepare_attention_mask` when preparing the attention_mask." + ), + ) + batch_size = 1 + + head_size = self.heads + if attention_mask is None: + return attention_mask + + if attention_mask.shape[-1] != target_length: + if attention_mask.device.type == "mps": + # HACK: MPS: Does not support padding by greater than dimension of input tensor. + # Instead, we can manually construct the padding tensor. + padding_shape = (attention_mask.shape[0], attention_mask.shape[1], target_length) + padding = torch.zeros(padding_shape, dtype=attention_mask.dtype, device=attention_mask.device) + attention_mask = torch.concat([attention_mask, padding], dim=2) + else: + attention_mask = F.pad(attention_mask, (0, target_length), value=0.0) + + if attention_mask.shape[0] < batch_size * head_size: + attention_mask = attention_mask.repeat_interleave(head_size, dim=0) + return attention_mask + + +class CrossAttnProcessor: + def __call__( + self, + attn: CrossAttention, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + ): + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.cross_attention_norm: + encoder_hidden_states = attn.norm_cross(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class LoRALinearLayer(nn.Module): + def __init__(self, in_features, out_features, rank=4): + super().__init__() + + if rank > min(in_features, out_features): + raise ValueError(f"LoRA rank {rank} must be less or equal than {min(in_features, out_features)}") + + self.down = nn.Linear(in_features, rank, bias=False) + self.up = nn.Linear(rank, out_features, bias=False) + + nn.init.normal_(self.down.weight, std=1 / rank) + nn.init.zeros_(self.up.weight) + + def forward(self, hidden_states): + orig_dtype = hidden_states.dtype + dtype = self.down.weight.dtype + + down_hidden_states = self.down(hidden_states.to(dtype)) + up_hidden_states = self.up(down_hidden_states) + + return up_hidden_states.to(orig_dtype) + + +class LoRACrossAttnProcessor(nn.Module): + def __init__(self, hidden_size, cross_attention_dim=None, rank=4): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + + self.to_q_lora = LoRALinearLayer(hidden_size, hidden_size, rank) + self.to_k_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank) + self.to_v_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank) + self.to_out_lora = LoRALinearLayer(hidden_size, hidden_size, rank) + + def __call__( + self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None, scale=1.0 + ): + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + query = attn.to_q(hidden_states) + scale * self.to_q_lora(hidden_states) + query = attn.head_to_batch_dim(query) + + encoder_hidden_states = encoder_hidden_states if encoder_hidden_states is not None else hidden_states + + key = attn.to_k(encoder_hidden_states) + scale * self.to_k_lora(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + scale * self.to_v_lora(encoder_hidden_states) + + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + scale * self.to_out_lora(hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class CrossAttnAddedKVProcessor: + def __call__(self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None): + residual = hidden_states + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + batch_size, sequence_length, _ = hidden_states.shape + encoder_hidden_states = encoder_hidden_states.transpose(1, 2) + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + query = attn.head_to_batch_dim(query) + + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states) + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj) + + key = torch.concat([encoder_hidden_states_key_proj, key], dim=1) + value = torch.concat([encoder_hidden_states_value_proj, value], dim=1) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +class XFormersCrossAttnProcessor: + def __init__(self, attention_op: Optional[Callable] = None): + self.attention_op = attention_op + + def __call__(self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None): + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + query = attn.to_q(hidden_states) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.cross_attention_norm: + encoder_hidden_states = attn.norm_cross(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query).contiguous() + key = attn.head_to_batch_dim(key).contiguous() + value = attn.head_to_batch_dim(value).contiguous() + + hidden_states = xformers.ops.memory_efficient_attention( + query, key, value, attn_bias=attention_mask, op=self.attention_op + ) + hidden_states = hidden_states.to(query.dtype) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + return hidden_states + + +class LoRAXFormersCrossAttnProcessor(nn.Module): + def __init__(self, hidden_size, cross_attention_dim, rank=4, attention_op: Optional[Callable] = None): + super().__init__() + + self.hidden_size = hidden_size + self.cross_attention_dim = cross_attention_dim + self.rank = rank + self.attention_op = attention_op + + self.to_q_lora = LoRALinearLayer(hidden_size, hidden_size, rank) + self.to_k_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank) + self.to_v_lora = LoRALinearLayer(cross_attention_dim or hidden_size, hidden_size, rank) + self.to_out_lora = LoRALinearLayer(hidden_size, hidden_size, rank) + + def __call__( + self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None, scale=1.0 + ): + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + query = attn.to_q(hidden_states) + scale * self.to_q_lora(hidden_states) + query = attn.head_to_batch_dim(query).contiguous() + + encoder_hidden_states = encoder_hidden_states if encoder_hidden_states is not None else hidden_states + + key = attn.to_k(encoder_hidden_states) + scale * self.to_k_lora(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + scale * self.to_v_lora(encoder_hidden_states) + + key = attn.head_to_batch_dim(key).contiguous() + value = attn.head_to_batch_dim(value).contiguous() + + hidden_states = xformers.ops.memory_efficient_attention( + query, key, value, attn_bias=attention_mask, op=self.attention_op + ) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + scale * self.to_out_lora(hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class SlicedAttnProcessor: + def __init__(self, slice_size): + self.slice_size = slice_size + + def __call__(self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None): + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + query = attn.to_q(hidden_states) + dim = query.shape[-1] + query = attn.head_to_batch_dim(query) + + if encoder_hidden_states is None: + encoder_hidden_states = hidden_states + elif attn.cross_attention_norm: + encoder_hidden_states = attn.norm_cross(encoder_hidden_states) + + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + batch_size_attention = query.shape[0] + hidden_states = torch.zeros( + (batch_size_attention, sequence_length, dim // attn.heads), device=query.device, dtype=query.dtype + ) + + for i in range(hidden_states.shape[0] // self.slice_size): + start_idx = i * self.slice_size + end_idx = (i + 1) * self.slice_size + + query_slice = query[start_idx:end_idx] + key_slice = key[start_idx:end_idx] + attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None + + attn_slice = attn.get_attention_scores(query_slice, key_slice, attn_mask_slice) + + attn_slice = torch.bmm(attn_slice, value[start_idx:end_idx]) + + hidden_states[start_idx:end_idx] = attn_slice + + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + return hidden_states + + +class SlicedAttnAddedKVProcessor: + def __init__(self, slice_size): + self.slice_size = slice_size + + def __call__(self, attn: "CrossAttention", hidden_states, encoder_hidden_states=None, attention_mask=None): + residual = hidden_states + hidden_states = hidden_states.view(hidden_states.shape[0], hidden_states.shape[1], -1).transpose(1, 2) + encoder_hidden_states = encoder_hidden_states.transpose(1, 2) + + batch_size, sequence_length, _ = hidden_states.shape + + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + dim = query.shape[-1] + query = attn.head_to_batch_dim(query) + + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + encoder_hidden_states_key_proj = attn.add_k_proj(encoder_hidden_states) + encoder_hidden_states_value_proj = attn.add_v_proj(encoder_hidden_states) + + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + encoder_hidden_states_key_proj = attn.head_to_batch_dim(encoder_hidden_states_key_proj) + encoder_hidden_states_value_proj = attn.head_to_batch_dim(encoder_hidden_states_value_proj) + + key = torch.concat([encoder_hidden_states_key_proj, key], dim=1) + value = torch.concat([encoder_hidden_states_value_proj, value], dim=1) + + batch_size_attention = query.shape[0] + hidden_states = torch.zeros( + (batch_size_attention, sequence_length, dim // attn.heads), device=query.device, dtype=query.dtype + ) + + for i in range(hidden_states.shape[0] // self.slice_size): + start_idx = i * self.slice_size + end_idx = (i + 1) * self.slice_size + + query_slice = query[start_idx:end_idx] + key_slice = key[start_idx:end_idx] + attn_mask_slice = attention_mask[start_idx:end_idx] if attention_mask is not None else None + + attn_slice = attn.get_attention_scores(query_slice, key_slice, attn_mask_slice) + + attn_slice = torch.bmm(attn_slice, value[start_idx:end_idx]) + + hidden_states[start_idx:end_idx] = attn_slice + + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states = hidden_states.transpose(-1, -2).reshape(residual.shape) + hidden_states = hidden_states + residual + + return hidden_states + + +AttnProcessor = Union[ + CrossAttnProcessor, + XFormersCrossAttnProcessor, + SlicedAttnProcessor, + CrossAttnAddedKVProcessor, + SlicedAttnAddedKVProcessor, + LoRACrossAttnProcessor, + LoRAXFormersCrossAttnProcessor, +] diff --git a/diffusers/src/diffusers/models/dual_transformer_2d.py b/diffusers/src/diffusers/models/dual_transformer_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..25735605797d1209f1d99966a6b7857454740094 --- /dev/null +++ b/diffusers/src/diffusers/models/dual_transformer_2d.py @@ -0,0 +1,151 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from typing import Optional + +from torch import nn + +from .transformer_2d import Transformer2DModel, Transformer2DModelOutput + + +class DualTransformer2DModel(nn.Module): + """ + Dual transformer wrapper that combines two `Transformer2DModel`s for mixed inference. + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 16): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 88): The number of channels in each head. + in_channels (`int`, *optional*): + Pass if the input is continuous. The number of channels in the input and output. + num_layers (`int`, *optional*, defaults to 1): The number of layers of Transformer blocks to use. + dropout (`float`, *optional*, defaults to 0.1): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The number of encoder_hidden_states dimensions to use. + sample_size (`int`, *optional*): Pass if the input is discrete. The width of the latent images. + Note that this is fixed at training time as it is used for learning a number of position embeddings. See + `ImagePositionalEmbeddings`. + num_vector_embeds (`int`, *optional*): + Pass if the input is discrete. The number of classes of the vector embeddings of the latent pixels. + Includes the class for the masked latent pixel. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm ( `int`, *optional*): Pass if at least one of the norm_layers is `AdaLayerNorm`. + The number of diffusion steps used during training. Note that this is fixed at training time as it is used + to learn a number of embeddings that are added to the hidden states. During inference, you can denoise for + up to but not more than steps than `num_embeds_ada_norm`. + attention_bias (`bool`, *optional*): + Configure if the TransformerBlocks' attention should contain a bias parameter. + """ + + def __init__( + self, + num_attention_heads: int = 16, + attention_head_dim: int = 88, + in_channels: Optional[int] = None, + num_layers: int = 1, + dropout: float = 0.0, + norm_num_groups: int = 32, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + sample_size: Optional[int] = None, + num_vector_embeds: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + ): + super().__init__() + self.transformers = nn.ModuleList( + [ + Transformer2DModel( + num_attention_heads=num_attention_heads, + attention_head_dim=attention_head_dim, + in_channels=in_channels, + num_layers=num_layers, + dropout=dropout, + norm_num_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attention_bias=attention_bias, + sample_size=sample_size, + num_vector_embeds=num_vector_embeds, + activation_fn=activation_fn, + num_embeds_ada_norm=num_embeds_ada_norm, + ) + for _ in range(2) + ] + ) + + # Variables that can be set by a pipeline: + + # The ratio of transformer1 to transformer2's output states to be combined during inference + self.mix_ratio = 0.5 + + # The shape of `encoder_hidden_states` is expected to be + # `(batch_size, condition_lengths[0]+condition_lengths[1], num_features)` + self.condition_lengths = [77, 257] + + # Which transformer to use to encode which condition. + # E.g. `(1, 0)` means that we'll use `transformers[1](conditions[0])` and `transformers[0](conditions[1])` + self.transformer_index_for_condition = [1, 0] + + def forward( + self, + hidden_states, + encoder_hidden_states, + timestep=None, + attention_mask=None, + cross_attention_kwargs=None, + return_dict: bool = True, + ): + """ + Args: + hidden_states ( When discrete, `torch.LongTensor` of shape `(batch size, num latent pixels)`. + When continuous, `torch.FloatTensor` of shape `(batch size, channel, height, width)`): Input + hidden_states + encoder_hidden_states ( `torch.LongTensor` of shape `(batch size, encoder_hidden_states dim)`, *optional*): + Conditional embeddings for cross attention layer. If not given, cross-attention defaults to + self-attention. + timestep ( `torch.long`, *optional*): + Optional timestep to be applied as an embedding in AdaLayerNorm's. Used to indicate denoising step. + attention_mask (`torch.FloatTensor`, *optional*): + Optional attention mask to be applied in CrossAttention + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain tuple. + + Returns: + [`~models.transformer_2d.Transformer2DModelOutput`] or `tuple`: + [`~models.transformer_2d.Transformer2DModelOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + input_states = hidden_states + + encoded_states = [] + tokens_start = 0 + # attention_mask is not used yet + for i in range(2): + # for each of the two transformers, pass the corresponding condition tokens + condition_state = encoder_hidden_states[:, tokens_start : tokens_start + self.condition_lengths[i]] + transformer_index = self.transformer_index_for_condition[i] + encoded_state = self.transformers[transformer_index]( + input_states, + encoder_hidden_states=condition_state, + timestep=timestep, + cross_attention_kwargs=cross_attention_kwargs, + return_dict=False, + )[0] + encoded_states.append(encoded_state - input_states) + tokens_start += self.condition_lengths[i] + + output_states = encoded_states[0] * self.mix_ratio + encoded_states[1] * (1 - self.mix_ratio) + output_states = output_states + input_states + + if not return_dict: + return (output_states,) + + return Transformer2DModelOutput(sample=output_states) diff --git a/diffusers/src/diffusers/models/embeddings.py b/diffusers/src/diffusers/models/embeddings.py new file mode 100644 index 0000000000000000000000000000000000000000..28a67d7f075a2654a329848a133f260e1b401172 --- /dev/null +++ b/diffusers/src/diffusers/models/embeddings.py @@ -0,0 +1,379 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import math +from typing import Optional + +import numpy as np +import torch +from torch import nn + + +def get_timestep_embedding( + timesteps: torch.Tensor, + embedding_dim: int, + flip_sin_to_cos: bool = False, + downscale_freq_shift: float = 1, + scale: float = 1, + max_period: int = 10000, +): + """ + This matches the implementation in Denoising Diffusion Probabilistic Models: Create sinusoidal timestep embeddings. + + :param timesteps: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param embedding_dim: the dimension of the output. :param max_period: controls the minimum frequency of the + embeddings. :return: an [N x dim] Tensor of positional embeddings. + """ + assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array" + + half_dim = embedding_dim // 2 + exponent = -math.log(max_period) * torch.arange( + start=0, end=half_dim, dtype=torch.float32, device=timesteps.device + ) + exponent = exponent / (half_dim - downscale_freq_shift) + + emb = torch.exp(exponent) + emb = timesteps[:, None].float() * emb[None, :] + + # scale embeddings + emb = scale * emb + + # concat sine and cosine embeddings + emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1) + + # flip sine and cosine embeddings + if flip_sin_to_cos: + emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1) + + # zero pad + if embedding_dim % 2 == 1: + emb = torch.nn.functional.pad(emb, (0, 1, 0, 0)) + return emb + + +def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0): + """ + grid_size: int of the grid height and width return: pos_embed: [grid_size*grid_size, embed_dim] or + [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token) + """ + grid_h = np.arange(grid_size, dtype=np.float32) + grid_w = np.arange(grid_size, dtype=np.float32) + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + + grid = grid.reshape([2, 1, grid_size, grid_size]) + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token and extra_tokens > 0: + pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0) + return pos_embed + + +def get_2d_sincos_pos_embed_from_grid(embed_dim, grid): + if embed_dim % 2 != 0: + raise ValueError("embed_dim must be divisible by 2") + + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2) + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2) + + emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D) + return emb + + +def get_1d_sincos_pos_embed_from_grid(embed_dim, pos): + """ + embed_dim: output dimension for each position pos: a list of positions to be encoded: size (M,) out: (M, D) + """ + if embed_dim % 2 != 0: + raise ValueError("embed_dim must be divisible by 2") + + omega = np.arange(embed_dim // 2, dtype=np.float64) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # (D/2,) + + pos = pos.reshape(-1) # (M,) + out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product + + emb_sin = np.sin(out) # (M, D/2) + emb_cos = np.cos(out) # (M, D/2) + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D) + return emb + + +class PatchEmbed(nn.Module): + """2D Image to Patch Embedding""" + + def __init__( + self, + height=224, + width=224, + patch_size=16, + in_channels=3, + embed_dim=768, + layer_norm=False, + flatten=True, + bias=True, + ): + super().__init__() + + num_patches = (height // patch_size) * (width // patch_size) + self.flatten = flatten + self.layer_norm = layer_norm + + self.proj = nn.Conv2d( + in_channels, embed_dim, kernel_size=(patch_size, patch_size), stride=patch_size, bias=bias + ) + if layer_norm: + self.norm = nn.LayerNorm(embed_dim, elementwise_affine=False, eps=1e-6) + else: + self.norm = None + + pos_embed = get_2d_sincos_pos_embed(embed_dim, int(num_patches**0.5)) + self.register_buffer("pos_embed", torch.from_numpy(pos_embed).float().unsqueeze(0), persistent=False) + + def forward(self, latent): + latent = self.proj(latent) + if self.flatten: + latent = latent.flatten(2).transpose(1, 2) # BCHW -> BNC + if self.layer_norm: + latent = self.norm(latent) + return latent + self.pos_embed + + +class TimestepEmbedding(nn.Module): + def __init__( + self, + in_channels: int, + time_embed_dim: int, + act_fn: str = "silu", + out_dim: int = None, + post_act_fn: Optional[str] = None, + cond_proj_dim=None, + ): + super().__init__() + + self.linear_1 = nn.Linear(in_channels, time_embed_dim) + + if cond_proj_dim is not None: + self.cond_proj = nn.Linear(cond_proj_dim, in_channels, bias=False) + else: + self.cond_proj = None + + if act_fn == "silu": + self.act = nn.SiLU() + elif act_fn == "mish": + self.act = nn.Mish() + elif act_fn == "gelu": + self.act = nn.GELU() + else: + raise ValueError(f"{act_fn} does not exist. Make sure to define one of 'silu', 'mish', or 'gelu'") + + if out_dim is not None: + time_embed_dim_out = out_dim + else: + time_embed_dim_out = time_embed_dim + self.linear_2 = nn.Linear(time_embed_dim, time_embed_dim_out) + + if post_act_fn is None: + self.post_act = None + elif post_act_fn == "silu": + self.post_act = nn.SiLU() + elif post_act_fn == "mish": + self.post_act = nn.Mish() + elif post_act_fn == "gelu": + self.post_act = nn.GELU() + else: + raise ValueError(f"{post_act_fn} does not exist. Make sure to define one of 'silu', 'mish', or 'gelu'") + + def forward(self, sample, condition=None): + if condition is not None: + sample = sample + self.cond_proj(condition) + sample = self.linear_1(sample) + + if self.act is not None: + sample = self.act(sample) + + sample = self.linear_2(sample) + + if self.post_act is not None: + sample = self.post_act(sample) + return sample + + +class Timesteps(nn.Module): + def __init__(self, num_channels: int, flip_sin_to_cos: bool, downscale_freq_shift: float): + super().__init__() + self.num_channels = num_channels + self.flip_sin_to_cos = flip_sin_to_cos + self.downscale_freq_shift = downscale_freq_shift + + def forward(self, timesteps): + t_emb = get_timestep_embedding( + timesteps, + self.num_channels, + flip_sin_to_cos=self.flip_sin_to_cos, + downscale_freq_shift=self.downscale_freq_shift, + ) + return t_emb + + +class GaussianFourierProjection(nn.Module): + """Gaussian Fourier embeddings for noise levels.""" + + def __init__( + self, embedding_size: int = 256, scale: float = 1.0, set_W_to_weight=True, log=True, flip_sin_to_cos=False + ): + super().__init__() + self.weight = nn.Parameter(torch.randn(embedding_size) * scale, requires_grad=False) + self.log = log + self.flip_sin_to_cos = flip_sin_to_cos + + if set_W_to_weight: + # to delete later + self.W = nn.Parameter(torch.randn(embedding_size) * scale, requires_grad=False) + + self.weight = self.W + + def forward(self, x): + if self.log: + x = torch.log(x) + + x_proj = x[:, None] * self.weight[None, :] * 2 * np.pi + + if self.flip_sin_to_cos: + out = torch.cat([torch.cos(x_proj), torch.sin(x_proj)], dim=-1) + else: + out = torch.cat([torch.sin(x_proj), torch.cos(x_proj)], dim=-1) + return out + + +class ImagePositionalEmbeddings(nn.Module): + """ + Converts latent image classes into vector embeddings. Sums the vector embeddings with positional embeddings for the + height and width of the latent space. + + For more details, see figure 10 of the dall-e paper: https://arxiv.org/abs/2102.12092 + + For VQ-diffusion: + + Output vector embeddings are used as input for the transformer. + + Note that the vector embeddings for the transformer are different than the vector embeddings from the VQVAE. + + Args: + num_embed (`int`): + Number of embeddings for the latent pixels embeddings. + height (`int`): + Height of the latent image i.e. the number of height embeddings. + width (`int`): + Width of the latent image i.e. the number of width embeddings. + embed_dim (`int`): + Dimension of the produced vector embeddings. Used for the latent pixel, height, and width embeddings. + """ + + def __init__( + self, + num_embed: int, + height: int, + width: int, + embed_dim: int, + ): + super().__init__() + + self.height = height + self.width = width + self.num_embed = num_embed + self.embed_dim = embed_dim + + self.emb = nn.Embedding(self.num_embed, embed_dim) + self.height_emb = nn.Embedding(self.height, embed_dim) + self.width_emb = nn.Embedding(self.width, embed_dim) + + def forward(self, index): + emb = self.emb(index) + + height_emb = self.height_emb(torch.arange(self.height, device=index.device).view(1, self.height)) + + # 1 x H x D -> 1 x H x 1 x D + height_emb = height_emb.unsqueeze(2) + + width_emb = self.width_emb(torch.arange(self.width, device=index.device).view(1, self.width)) + + # 1 x W x D -> 1 x 1 x W x D + width_emb = width_emb.unsqueeze(1) + + pos_emb = height_emb + width_emb + + # 1 x H x W x D -> 1 x L xD + pos_emb = pos_emb.view(1, self.height * self.width, -1) + + emb = emb + pos_emb[:, : emb.shape[1], :] + + return emb + + +class LabelEmbedding(nn.Module): + """ + Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance. + + Args: + num_classes (`int`): The number of classes. + hidden_size (`int`): The size of the vector embeddings. + dropout_prob (`float`): The probability of dropping a label. + """ + + def __init__(self, num_classes, hidden_size, dropout_prob): + super().__init__() + use_cfg_embedding = dropout_prob > 0 + self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size) + self.num_classes = num_classes + self.dropout_prob = dropout_prob + + def token_drop(self, labels, force_drop_ids=None): + """ + Drops labels to enable classifier-free guidance. + """ + if force_drop_ids is None: + drop_ids = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob + else: + drop_ids = torch.tensor(force_drop_ids == 1) + labels = torch.where(drop_ids, self.num_classes, labels) + return labels + + def forward(self, labels, force_drop_ids=None): + use_dropout = self.dropout_prob > 0 + if (self.training and use_dropout) or (force_drop_ids is not None): + labels = self.token_drop(labels, force_drop_ids) + embeddings = self.embedding_table(labels) + return embeddings + + +class CombinedTimestepLabelEmbeddings(nn.Module): + def __init__(self, num_classes, embedding_dim, class_dropout_prob=0.1): + super().__init__() + + self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=1) + self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim) + self.class_embedder = LabelEmbedding(num_classes, embedding_dim, class_dropout_prob) + + def forward(self, timestep, class_labels, hidden_dtype=None): + timesteps_proj = self.time_proj(timestep) + timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_dtype)) # (N, D) + + class_labels = self.class_embedder(class_labels) # (N, D) + + conditioning = timesteps_emb + class_labels # (N, D) + + return conditioning diff --git a/diffusers/src/diffusers/models/embeddings_flax.py b/diffusers/src/diffusers/models/embeddings_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..42d18cbac59293d24cfa04acc55e95dc6c56411c --- /dev/null +++ b/diffusers/src/diffusers/models/embeddings_flax.py @@ -0,0 +1,95 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import math + +import flax.linen as nn +import jax.numpy as jnp + + +def get_sinusoidal_embeddings( + timesteps: jnp.ndarray, + embedding_dim: int, + freq_shift: float = 1, + min_timescale: float = 1, + max_timescale: float = 1.0e4, + flip_sin_to_cos: bool = False, + scale: float = 1.0, +) -> jnp.ndarray: + """Returns the positional encoding (same as Tensor2Tensor). + + Args: + timesteps: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + embedding_dim: The number of output channels. + min_timescale: The smallest time unit (should probably be 0.0). + max_timescale: The largest time unit. + Returns: + a Tensor of timing signals [N, num_channels] + """ + assert timesteps.ndim == 1, "Timesteps should be a 1d-array" + assert embedding_dim % 2 == 0, f"Embedding dimension {embedding_dim} should be even" + num_timescales = float(embedding_dim // 2) + log_timescale_increment = math.log(max_timescale / min_timescale) / (num_timescales - freq_shift) + inv_timescales = min_timescale * jnp.exp(jnp.arange(num_timescales, dtype=jnp.float32) * -log_timescale_increment) + emb = jnp.expand_dims(timesteps, 1) * jnp.expand_dims(inv_timescales, 0) + + # scale embeddings + scaled_time = scale * emb + + if flip_sin_to_cos: + signal = jnp.concatenate([jnp.cos(scaled_time), jnp.sin(scaled_time)], axis=1) + else: + signal = jnp.concatenate([jnp.sin(scaled_time), jnp.cos(scaled_time)], axis=1) + signal = jnp.reshape(signal, [jnp.shape(timesteps)[0], embedding_dim]) + return signal + + +class FlaxTimestepEmbedding(nn.Module): + r""" + Time step Embedding Module. Learns embeddings for input time steps. + + Args: + time_embed_dim (`int`, *optional*, defaults to `32`): + Time step embedding dimension + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + time_embed_dim: int = 32 + dtype: jnp.dtype = jnp.float32 + + @nn.compact + def __call__(self, temb): + temb = nn.Dense(self.time_embed_dim, dtype=self.dtype, name="linear_1")(temb) + temb = nn.silu(temb) + temb = nn.Dense(self.time_embed_dim, dtype=self.dtype, name="linear_2")(temb) + return temb + + +class FlaxTimesteps(nn.Module): + r""" + Wrapper Module for sinusoidal Time step Embeddings as described in https://arxiv.org/abs/2006.11239 + + Args: + dim (`int`, *optional*, defaults to `32`): + Time step embedding dimension + """ + dim: int = 32 + flip_sin_to_cos: bool = False + freq_shift: float = 1 + + @nn.compact + def __call__(self, timesteps): + return get_sinusoidal_embeddings( + timesteps, embedding_dim=self.dim, flip_sin_to_cos=self.flip_sin_to_cos, freq_shift=self.freq_shift + ) diff --git a/diffusers/src/diffusers/models/modeling_flax_pytorch_utils.py b/diffusers/src/diffusers/models/modeling_flax_pytorch_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e75e0419c4d4c1a1229e8c8df9572cd273e1d4db --- /dev/null +++ b/diffusers/src/diffusers/models/modeling_flax_pytorch_utils.py @@ -0,0 +1,118 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" PyTorch - Flax general utilities.""" +import re + +import jax.numpy as jnp +from flax.traverse_util import flatten_dict, unflatten_dict +from jax.random import PRNGKey + +from ..utils import logging + + +logger = logging.get_logger(__name__) + + +def rename_key(key): + regex = r"\w+[.]\d+" + pats = re.findall(regex, key) + for pat in pats: + key = key.replace(pat, "_".join(pat.split("."))) + return key + + +##################### +# PyTorch => Flax # +##################### + + +# Adapted from https://github.com/huggingface/transformers/blob/c603c80f46881ae18b2ca50770ef65fa4033eacd/src/transformers/modeling_flax_pytorch_utils.py#L69 +# and https://github.com/patil-suraj/stable-diffusion-jax/blob/main/stable_diffusion_jax/convert_diffusers_to_jax.py +def rename_key_and_reshape_tensor(pt_tuple_key, pt_tensor, random_flax_state_dict): + """Rename PT weight names to corresponding Flax weight names and reshape tensor if necessary""" + + # conv norm or layer norm + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("scale",) + if ( + any("norm" in str_ for str_ in pt_tuple_key) + and (pt_tuple_key[-1] == "bias") + and (pt_tuple_key[:-1] + ("bias",) not in random_flax_state_dict) + and (pt_tuple_key[:-1] + ("scale",) in random_flax_state_dict) + ): + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("scale",) + return renamed_pt_tuple_key, pt_tensor + elif pt_tuple_key[-1] in ["weight", "gamma"] and pt_tuple_key[:-1] + ("scale",) in random_flax_state_dict: + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("scale",) + return renamed_pt_tuple_key, pt_tensor + + # embedding + if pt_tuple_key[-1] == "weight" and pt_tuple_key[:-1] + ("embedding",) in random_flax_state_dict: + pt_tuple_key = pt_tuple_key[:-1] + ("embedding",) + return renamed_pt_tuple_key, pt_tensor + + # conv layer + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("kernel",) + if pt_tuple_key[-1] == "weight" and pt_tensor.ndim == 4: + pt_tensor = pt_tensor.transpose(2, 3, 1, 0) + return renamed_pt_tuple_key, pt_tensor + + # linear layer + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("kernel",) + if pt_tuple_key[-1] == "weight": + pt_tensor = pt_tensor.T + return renamed_pt_tuple_key, pt_tensor + + # old PyTorch layer norm weight + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("weight",) + if pt_tuple_key[-1] == "gamma": + return renamed_pt_tuple_key, pt_tensor + + # old PyTorch layer norm bias + renamed_pt_tuple_key = pt_tuple_key[:-1] + ("bias",) + if pt_tuple_key[-1] == "beta": + return renamed_pt_tuple_key, pt_tensor + + return pt_tuple_key, pt_tensor + + +def convert_pytorch_state_dict_to_flax(pt_state_dict, flax_model, init_key=42): + # Step 1: Convert pytorch tensor to numpy + pt_state_dict = {k: v.numpy() for k, v in pt_state_dict.items()} + + # Step 2: Since the model is stateless, get random Flax params + random_flax_params = flax_model.init_weights(PRNGKey(init_key)) + + random_flax_state_dict = flatten_dict(random_flax_params) + flax_state_dict = {} + + # Need to change some parameters name to match Flax names + for pt_key, pt_tensor in pt_state_dict.items(): + renamed_pt_key = rename_key(pt_key) + pt_tuple_key = tuple(renamed_pt_key.split(".")) + + # Correctly rename weight parameters + flax_key, flax_tensor = rename_key_and_reshape_tensor(pt_tuple_key, pt_tensor, random_flax_state_dict) + + if flax_key in random_flax_state_dict: + if flax_tensor.shape != random_flax_state_dict[flax_key].shape: + raise ValueError( + f"PyTorch checkpoint seems to be incorrect. Weight {pt_key} was expected to be of shape " + f"{random_flax_state_dict[flax_key].shape}, but is {flax_tensor.shape}." + ) + + # also add unexpected weight so that warning is thrown + flax_state_dict[flax_key] = jnp.asarray(flax_tensor) + + return unflatten_dict(flax_state_dict) diff --git a/diffusers/src/diffusers/models/modeling_flax_utils.py b/diffusers/src/diffusers/models/modeling_flax_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..aeeeccad674b55ef885a89ffa433acd5dab0fca9 --- /dev/null +++ b/diffusers/src/diffusers/models/modeling_flax_utils.py @@ -0,0 +1,526 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import os +from pickle import UnpicklingError +from typing import Any, Dict, Union + +import jax +import jax.numpy as jnp +import msgpack.exceptions +from flax.core.frozen_dict import FrozenDict, unfreeze +from flax.serialization import from_bytes, to_bytes +from flax.traverse_util import flatten_dict, unflatten_dict +from huggingface_hub import hf_hub_download +from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError +from requests import HTTPError + +from .. import __version__, is_torch_available +from ..utils import ( + CONFIG_NAME, + DIFFUSERS_CACHE, + FLAX_WEIGHTS_NAME, + HUGGINGFACE_CO_RESOLVE_ENDPOINT, + WEIGHTS_NAME, + logging, +) +from .modeling_flax_pytorch_utils import convert_pytorch_state_dict_to_flax + + +logger = logging.get_logger(__name__) + + +class FlaxModelMixin: + r""" + Base class for all flax models. + + [`FlaxModelMixin`] takes care of storing the configuration of the models and handles methods for loading, + downloading and saving models. + """ + config_name = CONFIG_NAME + _automatically_saved_args = ["_diffusers_version", "_class_name", "_name_or_path"] + _flax_internal_args = ["name", "parent", "dtype"] + + @classmethod + def _from_config(cls, config, **kwargs): + """ + All context managers that the model should be initialized under go here. + """ + return cls(config, **kwargs) + + def _cast_floating_to(self, params: Union[Dict, FrozenDict], dtype: jnp.dtype, mask: Any = None) -> Any: + """ + Helper method to cast floating-point values of given parameter `PyTree` to given `dtype`. + """ + + # taken from https://github.com/deepmind/jmp/blob/3a8318abc3292be38582794dbf7b094e6583b192/jmp/_src/policy.py#L27 + def conditional_cast(param): + if isinstance(param, jnp.ndarray) and jnp.issubdtype(param.dtype, jnp.floating): + param = param.astype(dtype) + return param + + if mask is None: + return jax.tree_map(conditional_cast, params) + + flat_params = flatten_dict(params) + flat_mask, _ = jax.tree_flatten(mask) + + for masked, key in zip(flat_mask, flat_params.keys()): + if masked: + param = flat_params[key] + flat_params[key] = conditional_cast(param) + + return unflatten_dict(flat_params) + + def to_bf16(self, params: Union[Dict, FrozenDict], mask: Any = None): + r""" + Cast the floating-point `params` to `jax.numpy.bfloat16`. This returns a new `params` tree and does not cast + the `params` in place. + + This method can be used on TPU to explicitly convert the model parameters to bfloat16 precision to do full + half-precision training or to save weights in bfloat16 for inference in order to save memory and improve speed. + + Arguments: + params (`Union[Dict, FrozenDict]`): + A `PyTree` of model parameters. + mask (`Union[Dict, FrozenDict]`): + A `PyTree` with same structure as the `params` tree. The leaves should be booleans, `True` for params + you want to cast, and should be `False` for those you want to skip. + + Examples: + + ```python + >>> from diffusers import FlaxUNet2DConditionModel + + >>> # load model + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> # By default, the model parameters will be in fp32 precision, to cast these to bfloat16 precision + >>> params = model.to_bf16(params) + >>> # If you don't want to cast certain parameters (for example layer norm bias and scale) + >>> # then pass the mask as follows + >>> from flax import traverse_util + + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> flat_params = traverse_util.flatten_dict(params) + >>> mask = { + ... path: (path[-2] != ("LayerNorm", "bias") and path[-2:] != ("LayerNorm", "scale")) + ... for path in flat_params + ... } + >>> mask = traverse_util.unflatten_dict(mask) + >>> params = model.to_bf16(params, mask) + ```""" + return self._cast_floating_to(params, jnp.bfloat16, mask) + + def to_fp32(self, params: Union[Dict, FrozenDict], mask: Any = None): + r""" + Cast the floating-point `params` to `jax.numpy.float32`. This method can be used to explicitly convert the + model parameters to fp32 precision. This returns a new `params` tree and does not cast the `params` in place. + + Arguments: + params (`Union[Dict, FrozenDict]`): + A `PyTree` of model parameters. + mask (`Union[Dict, FrozenDict]`): + A `PyTree` with same structure as the `params` tree. The leaves should be booleans, `True` for params + you want to cast, and should be `False` for those you want to skip + + Examples: + + ```python + >>> from diffusers import FlaxUNet2DConditionModel + + >>> # Download model and configuration from huggingface.co + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> # By default, the model params will be in fp32, to illustrate the use of this method, + >>> # we'll first cast to fp16 and back to fp32 + >>> params = model.to_f16(params) + >>> # now cast back to fp32 + >>> params = model.to_fp32(params) + ```""" + return self._cast_floating_to(params, jnp.float32, mask) + + def to_fp16(self, params: Union[Dict, FrozenDict], mask: Any = None): + r""" + Cast the floating-point `params` to `jax.numpy.float16`. This returns a new `params` tree and does not cast the + `params` in place. + + This method can be used on GPU to explicitly convert the model parameters to float16 precision to do full + half-precision training or to save weights in float16 for inference in order to save memory and improve speed. + + Arguments: + params (`Union[Dict, FrozenDict]`): + A `PyTree` of model parameters. + mask (`Union[Dict, FrozenDict]`): + A `PyTree` with same structure as the `params` tree. The leaves should be booleans, `True` for params + you want to cast, and should be `False` for those you want to skip + + Examples: + + ```python + >>> from diffusers import FlaxUNet2DConditionModel + + >>> # load model + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> # By default, the model params will be in fp32, to cast these to float16 + >>> params = model.to_fp16(params) + >>> # If you want don't want to cast certain parameters (for example layer norm bias and scale) + >>> # then pass the mask as follows + >>> from flax import traverse_util + + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> flat_params = traverse_util.flatten_dict(params) + >>> mask = { + ... path: (path[-2] != ("LayerNorm", "bias") and path[-2:] != ("LayerNorm", "scale")) + ... for path in flat_params + ... } + >>> mask = traverse_util.unflatten_dict(mask) + >>> params = model.to_fp16(params, mask) + ```""" + return self._cast_floating_to(params, jnp.float16, mask) + + def init_weights(self, rng: jax.random.KeyArray) -> Dict: + raise NotImplementedError(f"init_weights method has to be implemented for {self}") + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: Union[str, os.PathLike], + dtype: jnp.dtype = jnp.float32, + *model_args, + **kwargs, + ): + r""" + Instantiate a pretrained flax model from a pre-trained model configuration. + + The warning *Weights from XXX not initialized from pretrained model* means that the weights of XXX do not come + pretrained with the rest of the model. It is up to you to train those weights with a downstream fine-tuning + task. + + The warning *Weights from XXX not used in YYY* means that the layer XXX is not used by YYY, therefore those + weights are discarded. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`): + Can be either: + + - A string, the *model id* of a pretrained model hosted inside a model repo on huggingface.co. + Valid model ids are namespaced under a user or organization name, like + `runwayml/stable-diffusion-v1-5`. + - A path to a *directory* containing model weights saved using [`~ModelMixin.save_pretrained`], + e.g., `./my_model_directory/`. + dtype (`jax.numpy.dtype`, *optional*, defaults to `jax.numpy.float32`): + The data type of the computation. Can be one of `jax.numpy.float32`, `jax.numpy.float16` (on GPUs) and + `jax.numpy.bfloat16` (on TPUs). + + This can be used to enable mixed-precision training or half-precision inference on GPUs or TPUs. If + specified all the computation will be performed with the given `dtype`. + + **Note that this only specifies the dtype of the computation and does not influence the dtype of model + parameters.** + + If you wish to change the dtype of the model parameters, see [`~ModelMixin.to_fp16`] and + [`~ModelMixin.to_bf16`]. + model_args (sequence of positional arguments, *optional*): + All remaining positional arguments will be passed to the underlying model's `__init__` method. + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + from_pt (`bool`, *optional*, defaults to `False`): + Load the model weights from a PyTorch checkpoint save file. + kwargs (remaining dictionary of keyword arguments, *optional*): + Can be used to update the configuration object (after it being loaded) and initiate the model (e.g., + `output_attentions=True`). Behaves differently depending on whether a `config` is provided or + automatically loaded: + + - If a configuration is provided with `config`, `**kwargs` will be directly passed to the + underlying model's `__init__` method (we assume all relevant updates to the configuration have + already been done) + - If a configuration is not provided, `kwargs` will be first passed to the configuration class + initialization function ([`~ConfigMixin.from_config`]). Each key of `kwargs` that corresponds to + a configuration attribute will be used to override said attribute with the supplied `kwargs` + value. Remaining keys that do not correspond to any configuration attribute will be passed to the + underlying model's `__init__` function. + + Examples: + + ```python + >>> from diffusers import FlaxUNet2DConditionModel + + >>> # Download model and configuration from huggingface.co and cache. + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> # Model was saved using *save_pretrained('./test/saved_model/')* (for example purposes, not runnable). + >>> model, params = FlaxUNet2DConditionModel.from_pretrained("./test/saved_model/") + ```""" + config = kwargs.pop("config", None) + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + force_download = kwargs.pop("force_download", False) + from_pt = kwargs.pop("from_pt", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", False) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + subfolder = kwargs.pop("subfolder", None) + + user_agent = { + "diffusers": __version__, + "file_type": "model", + "framework": "flax", + } + + # Load config if we don't provide a configuration + config_path = config if config is not None else pretrained_model_name_or_path + model, model_kwargs = cls.from_config( + config_path, + cache_dir=cache_dir, + return_unused_kwargs=True, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + # model args + dtype=dtype, + **kwargs, + ) + + # Load model + pretrained_path_with_subfolder = ( + pretrained_model_name_or_path + if subfolder is None + else os.path.join(pretrained_model_name_or_path, subfolder) + ) + if os.path.isdir(pretrained_path_with_subfolder): + if from_pt: + if not os.path.isfile(os.path.join(pretrained_path_with_subfolder, WEIGHTS_NAME)): + raise EnvironmentError( + f"Error no file named {WEIGHTS_NAME} found in directory {pretrained_path_with_subfolder} " + ) + model_file = os.path.join(pretrained_path_with_subfolder, WEIGHTS_NAME) + elif os.path.isfile(os.path.join(pretrained_path_with_subfolder, FLAX_WEIGHTS_NAME)): + # Load from a Flax checkpoint + model_file = os.path.join(pretrained_path_with_subfolder, FLAX_WEIGHTS_NAME) + # Check if pytorch weights exist instead + elif os.path.isfile(os.path.join(pretrained_path_with_subfolder, WEIGHTS_NAME)): + raise EnvironmentError( + f"{WEIGHTS_NAME} file found in directory {pretrained_path_with_subfolder}. Please load the model" + " using `from_pt=True`." + ) + else: + raise EnvironmentError( + f"Error no file named {FLAX_WEIGHTS_NAME} or {WEIGHTS_NAME} found in directory " + f"{pretrained_path_with_subfolder}." + ) + else: + try: + model_file = hf_hub_download( + pretrained_model_name_or_path, + filename=FLAX_WEIGHTS_NAME if not from_pt else WEIGHTS_NAME, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + user_agent=user_agent, + subfolder=subfolder, + revision=revision, + ) + + except RepositoryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier " + "listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a " + "token having permission to this repo with `use_auth_token` or log in with `huggingface-cli " + "login`." + ) + except RevisionNotFoundError: + raise EnvironmentError( + f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for " + "this model name. Check the model page at " + f"'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions." + ) + except EntryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} does not appear to have a file named {FLAX_WEIGHTS_NAME}." + ) + except HTTPError as err: + raise EnvironmentError( + f"There was a specific connection error when trying to load {pretrained_model_name_or_path}:\n" + f"{err}" + ) + except ValueError: + raise EnvironmentError( + f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it" + f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a" + f" directory containing a file named {FLAX_WEIGHTS_NAME} or {WEIGHTS_NAME}.\nCheckout your" + " internet connection or see how to run the library in offline mode at" + " 'https://huggingface.co/docs/transformers/installation#offline-mode'." + ) + except EnvironmentError: + raise EnvironmentError( + f"Can't load the model for '{pretrained_model_name_or_path}'. If you were trying to load it from " + "'https://huggingface.co/models', make sure you don't have a local directory with the same name. " + f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory " + f"containing a file named {FLAX_WEIGHTS_NAME} or {WEIGHTS_NAME}." + ) + + if from_pt: + if is_torch_available(): + from .modeling_utils import load_state_dict + else: + raise EnvironmentError( + "Can't load the model in PyTorch format because PyTorch is not installed. " + "Please, install PyTorch or use native Flax weights." + ) + + # Step 1: Get the pytorch file + pytorch_model_file = load_state_dict(model_file) + + # Step 2: Convert the weights + state = convert_pytorch_state_dict_to_flax(pytorch_model_file, model) + else: + try: + with open(model_file, "rb") as state_f: + state = from_bytes(cls, state_f.read()) + except (UnpicklingError, msgpack.exceptions.ExtraData) as e: + try: + with open(model_file) as f: + if f.read().startswith("version"): + raise OSError( + "You seem to have cloned a repository without having git-lfs installed. Please" + " install git-lfs and run `git lfs install` followed by `git lfs pull` in the" + " folder you cloned." + ) + else: + raise ValueError from e + except (UnicodeDecodeError, ValueError): + raise EnvironmentError(f"Unable to convert {model_file} to Flax deserializable object. ") + # make sure all arrays are stored as jnp.ndarray + # NOTE: This is to prevent a bug this will be fixed in Flax >= v0.3.4: + # https://github.com/google/flax/issues/1261 + state = jax.tree_util.tree_map(lambda x: jax.device_put(x, jax.devices("cpu")[0]), state) + + # flatten dicts + state = flatten_dict(state) + + params_shape_tree = jax.eval_shape(model.init_weights, rng=jax.random.PRNGKey(0)) + required_params = set(flatten_dict(unfreeze(params_shape_tree)).keys()) + + shape_state = flatten_dict(unfreeze(params_shape_tree)) + + missing_keys = required_params - set(state.keys()) + unexpected_keys = set(state.keys()) - required_params + + if missing_keys: + logger.warning( + f"The checkpoint {pretrained_model_name_or_path} is missing required keys: {missing_keys}. " + "Make sure to call model.init_weights to initialize the missing weights." + ) + cls._missing_keys = missing_keys + + for key in state.keys(): + if key in shape_state and state[key].shape != shape_state[key].shape: + raise ValueError( + f"Trying to load the pretrained weight for {key} failed: checkpoint has shape " + f"{state[key].shape} which is incompatible with the model shape {shape_state[key].shape}. " + ) + + # remove unexpected keys to not be saved again + for unexpected_key in unexpected_keys: + del state[unexpected_key] + + if len(unexpected_keys) > 0: + logger.warning( + f"Some weights of the model checkpoint at {pretrained_model_name_or_path} were not used when" + f" initializing {model.__class__.__name__}: {unexpected_keys}\n- This IS expected if you are" + f" initializing {model.__class__.__name__} from the checkpoint of a model trained on another task or" + " with another architecture." + ) + else: + logger.info(f"All model checkpoint weights were used when initializing {model.__class__.__name__}.\n") + + if len(missing_keys) > 0: + logger.warning( + f"Some weights of {model.__class__.__name__} were not initialized from the model checkpoint at" + f" {pretrained_model_name_or_path} and are newly initialized: {missing_keys}\nYou should probably" + " TRAIN this model on a down-stream task to be able to use it for predictions and inference." + ) + else: + logger.info( + f"All the weights of {model.__class__.__name__} were initialized from the model checkpoint at" + f" {pretrained_model_name_or_path}.\nIf your task is similar to the task the model of the checkpoint" + f" was trained on, you can already use {model.__class__.__name__} for predictions without further" + " training." + ) + + return model, unflatten_dict(state) + + def save_pretrained( + self, + save_directory: Union[str, os.PathLike], + params: Union[Dict, FrozenDict], + is_main_process: bool = True, + ): + """ + Save a model and its configuration file to a directory, so that it can be re-loaded using the + `[`~FlaxModelMixin.from_pretrained`]` class method + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + params (`Union[Dict, FrozenDict]`): + A `PyTree` of model parameters. + is_main_process (`bool`, *optional*, defaults to `True`): + Whether the process calling this is the main process or not. Useful when in distributed training like + TPUs and need to call this function on all processes. In this case, set `is_main_process=True` only on + the main process to avoid race conditions. + """ + if os.path.isfile(save_directory): + logger.error(f"Provided path ({save_directory}) should be a directory, not a file") + return + + os.makedirs(save_directory, exist_ok=True) + + model_to_save = self + + # Attach architecture to the config + # Save the config + if is_main_process: + model_to_save.save_config(save_directory) + + # save model + output_model_file = os.path.join(save_directory, FLAX_WEIGHTS_NAME) + with open(output_model_file, "wb") as f: + model_bytes = to_bytes(params) + f.write(model_bytes) + + logger.info(f"Model weights saved in {output_model_file}") diff --git a/diffusers/src/diffusers/models/modeling_pytorch_flax_utils.py b/diffusers/src/diffusers/models/modeling_pytorch_flax_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8ada7e69977d5c6f996efe99f98895ab16a36d33 --- /dev/null +++ b/diffusers/src/diffusers/models/modeling_pytorch_flax_utils.py @@ -0,0 +1,155 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" PyTorch - Flax general utilities.""" + +from pickle import UnpicklingError + +import jax +import jax.numpy as jnp +import numpy as np +from flax.serialization import from_bytes +from flax.traverse_util import flatten_dict + +from ..utils import logging + + +logger = logging.get_logger(__name__) + + +##################### +# Flax => PyTorch # +##################### + + +# from https://github.com/huggingface/transformers/blob/main/src/transformers/modeling_flax_pytorch_utils.py#L224-L352 +def load_flax_checkpoint_in_pytorch_model(pt_model, model_file): + try: + with open(model_file, "rb") as flax_state_f: + flax_state = from_bytes(None, flax_state_f.read()) + except UnpicklingError as e: + try: + with open(model_file) as f: + if f.read().startswith("version"): + raise OSError( + "You seem to have cloned a repository without having git-lfs installed. Please" + " install git-lfs and run `git lfs install` followed by `git lfs pull` in the" + " folder you cloned." + ) + else: + raise ValueError from e + except (UnicodeDecodeError, ValueError): + raise EnvironmentError(f"Unable to convert {model_file} to Flax deserializable object. ") + + return load_flax_weights_in_pytorch_model(pt_model, flax_state) + + +def load_flax_weights_in_pytorch_model(pt_model, flax_state): + """Load flax checkpoints in a PyTorch model""" + + try: + import torch # noqa: F401 + except ImportError: + logger.error( + "Loading Flax weights in PyTorch requires both PyTorch and Flax to be installed. Please see" + " https://pytorch.org/ and https://flax.readthedocs.io/en/latest/installation.html for installation" + " instructions." + ) + raise + + # check if we have bf16 weights + is_type_bf16 = flatten_dict(jax.tree_util.tree_map(lambda x: x.dtype == jnp.bfloat16, flax_state)).values() + if any(is_type_bf16): + # convert all weights to fp32 if they are bf16 since torch.from_numpy can-not handle bf16 + + # and bf16 is not fully supported in PT yet. + logger.warning( + "Found ``bfloat16`` weights in Flax model. Casting all ``bfloat16`` weights to ``float32`` " + "before loading those in PyTorch model." + ) + flax_state = jax.tree_util.tree_map( + lambda params: params.astype(np.float32) if params.dtype == jnp.bfloat16 else params, flax_state + ) + + pt_model.base_model_prefix = "" + + flax_state_dict = flatten_dict(flax_state, sep=".") + pt_model_dict = pt_model.state_dict() + + # keep track of unexpected & missing keys + unexpected_keys = [] + missing_keys = set(pt_model_dict.keys()) + + for flax_key_tuple, flax_tensor in flax_state_dict.items(): + flax_key_tuple_array = flax_key_tuple.split(".") + + if flax_key_tuple_array[-1] == "kernel" and flax_tensor.ndim == 4: + flax_key_tuple_array = flax_key_tuple_array[:-1] + ["weight"] + flax_tensor = jnp.transpose(flax_tensor, (3, 2, 0, 1)) + elif flax_key_tuple_array[-1] == "kernel": + flax_key_tuple_array = flax_key_tuple_array[:-1] + ["weight"] + flax_tensor = flax_tensor.T + elif flax_key_tuple_array[-1] == "scale": + flax_key_tuple_array = flax_key_tuple_array[:-1] + ["weight"] + + if "time_embedding" not in flax_key_tuple_array: + for i, flax_key_tuple_string in enumerate(flax_key_tuple_array): + flax_key_tuple_array[i] = ( + flax_key_tuple_string.replace("_0", ".0") + .replace("_1", ".1") + .replace("_2", ".2") + .replace("_3", ".3") + ) + + flax_key = ".".join(flax_key_tuple_array) + + if flax_key in pt_model_dict: + if flax_tensor.shape != pt_model_dict[flax_key].shape: + raise ValueError( + f"Flax checkpoint seems to be incorrect. Weight {flax_key_tuple} was expected " + f"to be of shape {pt_model_dict[flax_key].shape}, but is {flax_tensor.shape}." + ) + else: + # add weight to pytorch dict + flax_tensor = np.asarray(flax_tensor) if not isinstance(flax_tensor, np.ndarray) else flax_tensor + pt_model_dict[flax_key] = torch.from_numpy(flax_tensor) + # remove from missing keys + missing_keys.remove(flax_key) + else: + # weight is not expected by PyTorch model + unexpected_keys.append(flax_key) + + pt_model.load_state_dict(pt_model_dict) + + # re-transform missing_keys to list + missing_keys = list(missing_keys) + + if len(unexpected_keys) > 0: + logger.warning( + "Some weights of the Flax model were not used when initializing the PyTorch model" + f" {pt_model.__class__.__name__}: {unexpected_keys}\n- This IS expected if you are initializing" + f" {pt_model.__class__.__name__} from a Flax model trained on another task or with another architecture" + " (e.g. initializing a BertForSequenceClassification model from a FlaxBertForPreTraining model).\n- This" + f" IS NOT expected if you are initializing {pt_model.__class__.__name__} from a Flax model that you expect" + " to be exactly identical (e.g. initializing a BertForSequenceClassification model from a" + " FlaxBertForSequenceClassification model)." + ) + if len(missing_keys) > 0: + logger.warning( + f"Some weights of {pt_model.__class__.__name__} were not initialized from the Flax model and are newly" + f" initialized: {missing_keys}\nYou should probably TRAIN this model on a down-stream task to be able to" + " use it for predictions and inference." + ) + + return pt_model diff --git a/diffusers/src/diffusers/models/modeling_utils.py b/diffusers/src/diffusers/models/modeling_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f71d1d769699427177149fe001e3a70b75a71c00 --- /dev/null +++ b/diffusers/src/diffusers/models/modeling_utils.py @@ -0,0 +1,855 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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. + +import inspect +import os +from functools import partial +from typing import Callable, List, Optional, Tuple, Union + +import torch +from huggingface_hub import hf_hub_download +from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError +from requests import HTTPError +from torch import Tensor, device + +from .. import __version__ +from ..utils import ( + CONFIG_NAME, + DIFFUSERS_CACHE, + FLAX_WEIGHTS_NAME, + HF_HUB_OFFLINE, + HUGGINGFACE_CO_RESOLVE_ENDPOINT, + SAFETENSORS_WEIGHTS_NAME, + WEIGHTS_NAME, + is_accelerate_available, + is_safetensors_available, + is_torch_version, + logging, +) + + +logger = logging.get_logger(__name__) + + +if is_torch_version(">=", "1.9.0"): + _LOW_CPU_MEM_USAGE_DEFAULT = True +else: + _LOW_CPU_MEM_USAGE_DEFAULT = False + + +if is_accelerate_available(): + import accelerate + from accelerate.utils import set_module_tensor_to_device + from accelerate.utils.versions import is_torch_version + +if is_safetensors_available(): + import safetensors + + +def get_parameter_device(parameter: torch.nn.Module): + try: + return next(parameter.parameters()).device + except StopIteration: + # For torch.nn.DataParallel compatibility in PyTorch 1.5 + + def find_tensor_attributes(module: torch.nn.Module) -> List[Tuple[str, Tensor]]: + tuples = [(k, v) for k, v in module.__dict__.items() if torch.is_tensor(v)] + return tuples + + gen = parameter._named_members(get_members_fn=find_tensor_attributes) + first_tuple = next(gen) + return first_tuple[1].device + + +def get_parameter_dtype(parameter: torch.nn.Module): + try: + return next(parameter.parameters()).dtype + except StopIteration: + # For torch.nn.DataParallel compatibility in PyTorch 1.5 + + def find_tensor_attributes(module: torch.nn.Module) -> List[Tuple[str, Tensor]]: + tuples = [(k, v) for k, v in module.__dict__.items() if torch.is_tensor(v)] + return tuples + + gen = parameter._named_members(get_members_fn=find_tensor_attributes) + first_tuple = next(gen) + return first_tuple[1].dtype + + +def load_state_dict(checkpoint_file: Union[str, os.PathLike]): + """ + Reads a checkpoint file, returning properly formatted errors if they arise. + """ + try: + if os.path.basename(checkpoint_file) == WEIGHTS_NAME: + return torch.load(checkpoint_file, map_location="cpu") + else: + return safetensors.torch.load_file(checkpoint_file, device="cpu") + except Exception as e: + try: + with open(checkpoint_file) as f: + if f.read().startswith("version"): + raise OSError( + "You seem to have cloned a repository without having git-lfs installed. Please install " + "git-lfs and run `git lfs install` followed by `git lfs pull` in the folder " + "you cloned." + ) + else: + raise ValueError( + f"Unable to locate the file {checkpoint_file} which is necessary to load this pretrained " + "model. Make sure you have saved the model properly." + ) from e + except (UnicodeDecodeError, ValueError): + raise OSError( + f"Unable to load weights from checkpoint file for '{checkpoint_file}' " + f"at '{checkpoint_file}'. " + "If you tried to load a PyTorch model from a TF 2.0 checkpoint, please set from_tf=True." + ) + + +def _load_state_dict_into_model(model_to_load, state_dict): + # Convert old format to new format if needed from a PyTorch state_dict + # copy state_dict so _load_from_state_dict can modify it + state_dict = state_dict.copy() + error_msgs = [] + + # PyTorch's `_load_from_state_dict` does not copy parameters in a module's descendants + # so we need to apply the function recursively. + def load(module: torch.nn.Module, prefix=""): + args = (state_dict, prefix, {}, True, [], [], error_msgs) + module._load_from_state_dict(*args) + + for name, child in module._modules.items(): + if child is not None: + load(child, prefix + name + ".") + + load(model_to_load) + + return error_msgs + + +class ModelMixin(torch.nn.Module): + r""" + Base class for all models. + + [`ModelMixin`] takes care of storing the configuration of the models and handles methods for loading, downloading + and saving models. + + - **config_name** ([`str`]) -- A filename under which the model should be stored when calling + [`~models.ModelMixin.save_pretrained`]. + """ + config_name = CONFIG_NAME + _automatically_saved_args = ["_diffusers_version", "_class_name", "_name_or_path"] + _supports_gradient_checkpointing = False + + def __init__(self): + super().__init__() + + @property + def is_gradient_checkpointing(self) -> bool: + """ + Whether gradient checkpointing is activated for this model or not. + + Note that in other frameworks this feature can be referred to as "activation checkpointing" or "checkpoint + activations". + """ + return any(hasattr(m, "gradient_checkpointing") and m.gradient_checkpointing for m in self.modules()) + + def enable_gradient_checkpointing(self): + """ + Activates gradient checkpointing for the current model. + + Note that in other frameworks this feature can be referred to as "activation checkpointing" or "checkpoint + activations". + """ + if not self._supports_gradient_checkpointing: + raise ValueError(f"{self.__class__.__name__} does not support gradient checkpointing.") + self.apply(partial(self._set_gradient_checkpointing, value=True)) + + def disable_gradient_checkpointing(self): + """ + Deactivates gradient checkpointing for the current model. + + Note that in other frameworks this feature can be referred to as "activation checkpointing" or "checkpoint + activations". + """ + if self._supports_gradient_checkpointing: + self.apply(partial(self._set_gradient_checkpointing, value=False)) + + def set_use_memory_efficient_attention_xformers( + self, valid: bool, attention_op: Optional[Callable] = None + ) -> None: + # Recursively walk through all the children. + # Any children which exposes the set_use_memory_efficient_attention_xformers method + # gets the message + def fn_recursive_set_mem_eff(module: torch.nn.Module): + if hasattr(module, "set_use_memory_efficient_attention_xformers"): + module.set_use_memory_efficient_attention_xformers(valid, attention_op) + + for child in module.children(): + fn_recursive_set_mem_eff(child) + + for module in self.children(): + if isinstance(module, torch.nn.Module): + fn_recursive_set_mem_eff(module) + + def enable_xformers_memory_efficient_attention(self, attention_op: Optional[Callable] = None): + r""" + Enable memory efficient attention as implemented in xformers. + + When this option is enabled, you should observe lower GPU memory usage and a potential speed up at inference + time. Speed up at training time is not guaranteed. + + Warning: When Memory Efficient Attention and Sliced attention are both enabled, the Memory Efficient Attention + is used. + + Parameters: + attention_op (`Callable`, *optional*): + Override the default `None` operator for use as `op` argument to the + [`memory_efficient_attention()`](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.memory_efficient_attention) + function of xFormers. + + Examples: + + ```py + >>> import torch + >>> from diffusers import UNet2DConditionModel + >>> from xformers.ops import MemoryEfficientAttentionFlashAttentionOp + + >>> model = UNet2DConditionModel.from_pretrained( + ... "stabilityai/stable-diffusion-2-1", subfolder="unet", torch_dtype=torch.float16 + ... ) + >>> model = model.to("cuda") + >>> model.enable_xformers_memory_efficient_attention(attention_op=MemoryEfficientAttentionFlashAttentionOp) + ``` + """ + self.set_use_memory_efficient_attention_xformers(True, attention_op) + + def disable_xformers_memory_efficient_attention(self): + r""" + Disable memory efficient attention as implemented in xformers. + """ + self.set_use_memory_efficient_attention_xformers(False) + + def save_pretrained( + self, + save_directory: Union[str, os.PathLike], + is_main_process: bool = True, + save_function: Callable = None, + safe_serialization: bool = False, + ): + """ + Save a model and its configuration file to a directory, so that it can be re-loaded using the + `[`~models.ModelMixin.from_pretrained`]` class method. + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + is_main_process (`bool`, *optional*, defaults to `True`): + Whether the process calling this is the main process or not. Useful when in distributed training like + TPUs and need to call this function on all processes. In this case, set `is_main_process=True` only on + the main process to avoid race conditions. + save_function (`Callable`): + The function to use to save the state dictionary. Useful on distributed training like TPUs when one + need to replace `torch.save` by another method. Can be configured with the environment variable + `DIFFUSERS_SAVE_MODE`. + safe_serialization (`bool`, *optional*, defaults to `False`): + Whether to save the model using `safetensors` or the traditional PyTorch way (that uses `pickle`). + """ + if safe_serialization and not is_safetensors_available(): + raise ImportError("`safe_serialization` requires the `safetensors library: `pip install safetensors`.") + + if os.path.isfile(save_directory): + logger.error(f"Provided path ({save_directory}) should be a directory, not a file") + return + + if save_function is None: + save_function = safetensors.torch.save_file if safe_serialization else torch.save + + os.makedirs(save_directory, exist_ok=True) + + model_to_save = self + + # Attach architecture to the config + # Save the config + if is_main_process: + model_to_save.save_config(save_directory) + + # Save the model + state_dict = model_to_save.state_dict() + + weights_name = SAFETENSORS_WEIGHTS_NAME if safe_serialization else WEIGHTS_NAME + + # Save the model + save_function(state_dict, os.path.join(save_directory, weights_name)) + + logger.info(f"Model weights saved in {os.path.join(save_directory, weights_name)}") + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.PathLike]], **kwargs): + r""" + Instantiate a pretrained pytorch model from a pre-trained model configuration. + + The model is set in evaluation mode by default using `model.eval()` (Dropout modules are deactivated). To train + the model, you should first set it back in training mode with `model.train()`. + + The warning *Weights from XXX not initialized from pretrained model* means that the weights of XXX do not come + pretrained with the rest of the model. It is up to you to train those weights with a downstream fine-tuning + task. + + The warning *Weights from XXX not used in YYY* means that the layer XXX is not used by YYY, therefore those + weights are discarded. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *model id* of a pretrained model hosted inside a model repo on huggingface.co. + Valid model ids should have an organization name, like `google/ddpm-celebahq-256`. + - A path to a *directory* containing model weights saved using [`~ModelMixin.save_config`], e.g., + `./my_model_directory/`. + + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + torch_dtype (`str` or `torch.dtype`, *optional*): + Override the default `torch.dtype` and load the model under this dtype. If `"auto"` is passed the dtype + will be automatically derived from the model's weights. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `diffusers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + from_flax (`bool`, *optional*, defaults to `False`): + Load the model weights from a Flax checkpoint save file. + subfolder (`str`, *optional*, defaults to `""`): + In case the relevant files are located inside a subfolder of the model repo (either remote in + huggingface.co or downloaded locally), you can specify the folder name here. + + mirror (`str`, *optional*): + Mirror source to accelerate downloads in China. If you are from China and have an accessibility + problem, you can set this option to resolve it. Note that we do not guarantee the timeliness or safety. + Please refer to the mirror site for more information. + device_map (`str` or `Dict[str, Union[int, str, torch.device]]`, *optional*): + A map that specifies where each submodule should go. It doesn't need to be refined to each + parameter/buffer name, once a given module name is inside, every submodule of it will be sent to the + same device. + + To have Accelerate compute the most optimized `device_map` automatically, set `device_map="auto"`. For + more information about each option see [designing a device + map](https://hf.co/docs/accelerate/main/en/usage_guides/big_modeling#designing-a-device-map). + low_cpu_mem_usage (`bool`, *optional*, defaults to `True` if torch version >= 1.9.0 else `False`): + Speed up model loading by not initializing the weights and only loading the pre-trained weights. This + also tries to not use more than 1x model size in CPU memory (including peak memory) while loading the + model. This is only supported when torch version >= 1.9.0. If you are using an older version of torch, + setting this argument to `True` will raise an error. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + + + Activate the special ["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use + this method in a firewalled environment. + + + + """ + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + ignore_mismatched_sizes = kwargs.pop("ignore_mismatched_sizes", False) + force_download = kwargs.pop("force_download", False) + from_flax = kwargs.pop("from_flax", False) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + output_loading_info = kwargs.pop("output_loading_info", False) + local_files_only = kwargs.pop("local_files_only", HF_HUB_OFFLINE) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + torch_dtype = kwargs.pop("torch_dtype", None) + subfolder = kwargs.pop("subfolder", None) + device_map = kwargs.pop("device_map", None) + low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", _LOW_CPU_MEM_USAGE_DEFAULT) + + if low_cpu_mem_usage and not is_accelerate_available(): + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if device_map is not None and not is_accelerate_available(): + raise NotImplementedError( + "Loading and dispatching requires `accelerate`. Please make sure to install accelerate or set" + " `device_map=None`. You can install accelerate with `pip install accelerate`." + ) + + # Check if we can handle device_map and dispatching the weights + if device_map is not None and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Loading and dispatching requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `device_map=None`." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + if low_cpu_mem_usage is False and device_map is not None: + raise ValueError( + f"You cannot set `low_cpu_mem_usage` to `False` while using device_map={device_map} for loading and" + " dispatching. Please make sure to set `low_cpu_mem_usage=True`." + ) + + user_agent = { + "diffusers": __version__, + "file_type": "model", + "framework": "pytorch", + } + + # Load config if we don't provide a configuration + config_path = pretrained_model_name_or_path + + # This variable will flag if we're loading a sharded checkpoint. In this case the archive file is just the + # Load model + + model_file = None + if from_flax: + model_file = _get_model_file( + pretrained_model_name_or_path, + weights_name=FLAX_WEIGHTS_NAME, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + config, unused_kwargs = cls.load_config( + config_path, + cache_dir=cache_dir, + return_unused_kwargs=True, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + device_map=device_map, + **kwargs, + ) + model = cls.from_config(config, **unused_kwargs) + + # Convert the weights + from .modeling_pytorch_flax_utils import load_flax_checkpoint_in_pytorch_model + + model = load_flax_checkpoint_in_pytorch_model(model, model_file) + else: + if is_safetensors_available(): + try: + model_file = _get_model_file( + pretrained_model_name_or_path, + weights_name=SAFETENSORS_WEIGHTS_NAME, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + except: # noqa: E722 + pass + if model_file is None: + model_file = _get_model_file( + pretrained_model_name_or_path, + weights_name=WEIGHTS_NAME, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + user_agent=user_agent, + ) + + if low_cpu_mem_usage: + # Instantiate model with empty weights + with accelerate.init_empty_weights(): + config, unused_kwargs = cls.load_config( + config_path, + cache_dir=cache_dir, + return_unused_kwargs=True, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + device_map=device_map, + **kwargs, + ) + model = cls.from_config(config, **unused_kwargs) + + # if device_map is None, load the state dict and move the params from meta device to the cpu + if device_map is None: + param_device = "cpu" + state_dict = load_state_dict(model_file) + # move the params from meta device to cpu + missing_keys = set(model.state_dict().keys()) - set(state_dict.keys()) + if len(missing_keys) > 0: + raise ValueError( + f"Cannot load {cls} from {pretrained_model_name_or_path} because the following keys are" + f" missing: \n {', '.join(missing_keys)}. \n Please make sure to pass" + " `low_cpu_mem_usage=False` and `device_map=None` if you want to randomely initialize" + " those weights or else make sure your checkpoint file is correct." + ) + + for param_name, param in state_dict.items(): + accepts_dtype = "dtype" in set( + inspect.signature(set_module_tensor_to_device).parameters.keys() + ) + if accepts_dtype: + set_module_tensor_to_device( + model, param_name, param_device, value=param, dtype=torch_dtype + ) + else: + set_module_tensor_to_device(model, param_name, param_device, value=param) + else: # else let accelerate handle loading and dispatching. + # Load weights and dispatch according to the device_map + # by deafult the device_map is None and the weights are loaded on the CPU + accelerate.load_checkpoint_and_dispatch(model, model_file, device_map, dtype=torch_dtype) + + loading_info = { + "missing_keys": [], + "unexpected_keys": [], + "mismatched_keys": [], + "error_msgs": [], + } + else: + config, unused_kwargs = cls.load_config( + config_path, + cache_dir=cache_dir, + return_unused_kwargs=True, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + subfolder=subfolder, + device_map=device_map, + **kwargs, + ) + model = cls.from_config(config, **unused_kwargs) + + state_dict = load_state_dict(model_file) + + model, missing_keys, unexpected_keys, mismatched_keys, error_msgs = cls._load_pretrained_model( + model, + state_dict, + model_file, + pretrained_model_name_or_path, + ignore_mismatched_sizes=ignore_mismatched_sizes, + ) + + loading_info = { + "missing_keys": missing_keys, + "unexpected_keys": unexpected_keys, + "mismatched_keys": mismatched_keys, + "error_msgs": error_msgs, + } + + if torch_dtype is not None and not isinstance(torch_dtype, torch.dtype): + raise ValueError( + f"{torch_dtype} needs to be of type `torch.dtype`, e.g. `torch.float16`, but is {type(torch_dtype)}." + ) + elif torch_dtype is not None: + model = model.to(torch_dtype) + + model.register_to_config(_name_or_path=pretrained_model_name_or_path) + + # Set model in evaluation mode to deactivate DropOut modules by default + model.eval() + if output_loading_info: + return model, loading_info + + return model + + @classmethod + def _load_pretrained_model( + cls, + model, + state_dict, + resolved_archive_file, + pretrained_model_name_or_path, + ignore_mismatched_sizes=False, + ): + # Retrieve missing & unexpected_keys + model_state_dict = model.state_dict() + loaded_keys = [k for k in state_dict.keys()] + + expected_keys = list(model_state_dict.keys()) + + original_loaded_keys = loaded_keys + + missing_keys = list(set(expected_keys) - set(loaded_keys)) + unexpected_keys = list(set(loaded_keys) - set(expected_keys)) + + # Make sure we are able to load base models as well as derived models (with heads) + model_to_load = model + + def _find_mismatched_keys( + state_dict, + model_state_dict, + loaded_keys, + ignore_mismatched_sizes, + ): + mismatched_keys = [] + if ignore_mismatched_sizes: + for checkpoint_key in loaded_keys: + model_key = checkpoint_key + + if ( + model_key in model_state_dict + and state_dict[checkpoint_key].shape != model_state_dict[model_key].shape + ): + mismatched_keys.append( + (checkpoint_key, state_dict[checkpoint_key].shape, model_state_dict[model_key].shape) + ) + del state_dict[checkpoint_key] + return mismatched_keys + + if state_dict is not None: + # Whole checkpoint + mismatched_keys = _find_mismatched_keys( + state_dict, + model_state_dict, + original_loaded_keys, + ignore_mismatched_sizes, + ) + error_msgs = _load_state_dict_into_model(model_to_load, state_dict) + + if len(error_msgs) > 0: + error_msg = "\n\t".join(error_msgs) + if "size mismatch" in error_msg: + error_msg += ( + "\n\tYou may consider adding `ignore_mismatched_sizes=True` in the model `from_pretrained` method." + ) + raise RuntimeError(f"Error(s) in loading state_dict for {model.__class__.__name__}:\n\t{error_msg}") + + if len(unexpected_keys) > 0: + logger.warning( + f"Some weights of the model checkpoint at {pretrained_model_name_or_path} were not used when" + f" initializing {model.__class__.__name__}: {unexpected_keys}\n- This IS expected if you are" + f" initializing {model.__class__.__name__} from the checkpoint of a model trained on another task" + " or with another architecture (e.g. initializing a BertForSequenceClassification model from a" + " BertForPreTraining model).\n- This IS NOT expected if you are initializing" + f" {model.__class__.__name__} from the checkpoint of a model that you expect to be exactly" + " identical (initializing a BertForSequenceClassification model from a" + " BertForSequenceClassification model)." + ) + else: + logger.info(f"All model checkpoint weights were used when initializing {model.__class__.__name__}.\n") + if len(missing_keys) > 0: + logger.warning( + f"Some weights of {model.__class__.__name__} were not initialized from the model checkpoint at" + f" {pretrained_model_name_or_path} and are newly initialized: {missing_keys}\nYou should probably" + " TRAIN this model on a down-stream task to be able to use it for predictions and inference." + ) + elif len(mismatched_keys) == 0: + logger.info( + f"All the weights of {model.__class__.__name__} were initialized from the model checkpoint at" + f" {pretrained_model_name_or_path}.\nIf your task is similar to the task the model of the" + f" checkpoint was trained on, you can already use {model.__class__.__name__} for predictions" + " without further training." + ) + if len(mismatched_keys) > 0: + mismatched_warning = "\n".join( + [ + f"- {key}: found shape {shape1} in the checkpoint and {shape2} in the model instantiated" + for key, shape1, shape2 in mismatched_keys + ] + ) + logger.warning( + f"Some weights of {model.__class__.__name__} were not initialized from the model checkpoint at" + f" {pretrained_model_name_or_path} and are newly initialized because the shapes did not" + f" match:\n{mismatched_warning}\nYou should probably TRAIN this model on a down-stream task to be" + " able to use it for predictions and inference." + ) + + return model, missing_keys, unexpected_keys, mismatched_keys, error_msgs + + @property + def device(self) -> device: + """ + `torch.device`: The device on which the module is (assuming that all the module parameters are on the same + device). + """ + return get_parameter_device(self) + + @property + def dtype(self) -> torch.dtype: + """ + `torch.dtype`: The dtype of the module (assuming that all the module parameters have the same dtype). + """ + return get_parameter_dtype(self) + + def num_parameters(self, only_trainable: bool = False, exclude_embeddings: bool = False) -> int: + """ + Get number of (optionally, trainable or non-embeddings) parameters in the module. + + Args: + only_trainable (`bool`, *optional*, defaults to `False`): + Whether or not to return only the number of trainable parameters + + exclude_embeddings (`bool`, *optional*, defaults to `False`): + Whether or not to return only the number of non-embeddings parameters + + Returns: + `int`: The number of parameters. + """ + + if exclude_embeddings: + embedding_param_names = [ + f"{name}.weight" + for name, module_type in self.named_modules() + if isinstance(module_type, torch.nn.Embedding) + ] + non_embedding_parameters = [ + parameter for name, parameter in self.named_parameters() if name not in embedding_param_names + ] + return sum(p.numel() for p in non_embedding_parameters if p.requires_grad or not only_trainable) + else: + return sum(p.numel() for p in self.parameters() if p.requires_grad or not only_trainable) + + +def _get_model_file( + pretrained_model_name_or_path, + *, + weights_name, + subfolder, + cache_dir, + force_download, + proxies, + resume_download, + local_files_only, + use_auth_token, + user_agent, + revision, +): + pretrained_model_name_or_path = str(pretrained_model_name_or_path) + if os.path.isfile(pretrained_model_name_or_path): + return pretrained_model_name_or_path + elif os.path.isdir(pretrained_model_name_or_path): + if os.path.isfile(os.path.join(pretrained_model_name_or_path, weights_name)): + # Load from a PyTorch checkpoint + model_file = os.path.join(pretrained_model_name_or_path, weights_name) + return model_file + elif subfolder is not None and os.path.isfile( + os.path.join(pretrained_model_name_or_path, subfolder, weights_name) + ): + model_file = os.path.join(pretrained_model_name_or_path, subfolder, weights_name) + return model_file + else: + raise EnvironmentError( + f"Error no file named {weights_name} found in directory {pretrained_model_name_or_path}." + ) + else: + try: + # Load from URL or cache if already cached + model_file = hf_hub_download( + pretrained_model_name_or_path, + filename=weights_name, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + user_agent=user_agent, + subfolder=subfolder, + revision=revision, + ) + return model_file + + except RepositoryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier " + "listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a " + "token having permission to this repo with `use_auth_token` or log in with `huggingface-cli " + "login`." + ) + except RevisionNotFoundError: + raise EnvironmentError( + f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for " + "this model name. Check the model page at " + f"'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions." + ) + except EntryNotFoundError: + raise EnvironmentError( + f"{pretrained_model_name_or_path} does not appear to have a file named {weights_name}." + ) + except HTTPError as err: + raise EnvironmentError( + f"There was a specific connection error when trying to load {pretrained_model_name_or_path}:\n{err}" + ) + except ValueError: + raise EnvironmentError( + f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it" + f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a" + f" directory containing a file named {weights_name} or" + " \nCheckout your internet connection or see how to run the library in" + " offline mode at 'https://huggingface.co/docs/diffusers/installation#offline-mode'." + ) + except EnvironmentError: + raise EnvironmentError( + f"Can't load the model for '{pretrained_model_name_or_path}'. If you were trying to load it from " + "'https://huggingface.co/models', make sure you don't have a local directory with the same name. " + f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory " + f"containing a file named {weights_name}" + ) diff --git a/diffusers/src/diffusers/models/prior_transformer.py b/diffusers/src/diffusers/models/prior_transformer.py new file mode 100644 index 0000000000000000000000000000000000000000..b245612e6fc16800cd6f0cb2560d681f1360d60b --- /dev/null +++ b/diffusers/src/diffusers/models/prior_transformer.py @@ -0,0 +1,194 @@ +from dataclasses import dataclass +from typing import Optional, Union + +import torch +import torch.nn.functional as F +from torch import nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .attention import BasicTransformerBlock +from .embeddings import TimestepEmbedding, Timesteps +from .modeling_utils import ModelMixin + + +@dataclass +class PriorTransformerOutput(BaseOutput): + """ + Args: + predicted_image_embedding (`torch.FloatTensor` of shape `(batch_size, embedding_dim)`): + The predicted CLIP image embedding conditioned on the CLIP text embedding input. + """ + + predicted_image_embedding: torch.FloatTensor + + +class PriorTransformer(ModelMixin, ConfigMixin): + """ + The prior transformer from unCLIP is used to predict CLIP image embeddings from CLIP text embeddings. Note that the + transformer predicts the image embeddings through a denoising diffusion process. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the models (such as downloading or saving, etc.) + + For more details, see the original paper: https://arxiv.org/abs/2204.06125 + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 32): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 64): The number of channels in each head. + num_layers (`int`, *optional*, defaults to 20): The number of layers of Transformer blocks to use. + embedding_dim (`int`, *optional*, defaults to 768): The dimension of the CLIP embeddings. Note that CLIP + image embeddings and text embeddings are both the same dimension. + num_embeddings (`int`, *optional*, defaults to 77): The max number of clip embeddings allowed. I.e. the + length of the prompt after it has been tokenized. + additional_embeddings (`int`, *optional*, defaults to 4): The number of additional tokens appended to the + projected hidden_states. The actual length of the used hidden_states is `num_embeddings + + additional_embeddings`. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + + """ + + @register_to_config + def __init__( + self, + num_attention_heads: int = 32, + attention_head_dim: int = 64, + num_layers: int = 20, + embedding_dim: int = 768, + num_embeddings=77, + additional_embeddings=4, + dropout: float = 0.0, + ): + super().__init__() + self.num_attention_heads = num_attention_heads + self.attention_head_dim = attention_head_dim + inner_dim = num_attention_heads * attention_head_dim + self.additional_embeddings = additional_embeddings + + self.time_proj = Timesteps(inner_dim, True, 0) + self.time_embedding = TimestepEmbedding(inner_dim, inner_dim) + + self.proj_in = nn.Linear(embedding_dim, inner_dim) + + self.embedding_proj = nn.Linear(embedding_dim, inner_dim) + self.encoder_hidden_states_proj = nn.Linear(embedding_dim, inner_dim) + + self.positional_embedding = nn.Parameter(torch.zeros(1, num_embeddings + additional_embeddings, inner_dim)) + + self.prd_embedding = nn.Parameter(torch.zeros(1, 1, inner_dim)) + + self.transformer_blocks = nn.ModuleList( + [ + BasicTransformerBlock( + inner_dim, + num_attention_heads, + attention_head_dim, + dropout=dropout, + activation_fn="gelu", + attention_bias=True, + ) + for d in range(num_layers) + ] + ) + + self.norm_out = nn.LayerNorm(inner_dim) + self.proj_to_clip_embeddings = nn.Linear(inner_dim, embedding_dim) + + causal_attention_mask = torch.full( + [num_embeddings + additional_embeddings, num_embeddings + additional_embeddings], -10000.0 + ) + causal_attention_mask.triu_(1) + causal_attention_mask = causal_attention_mask[None, ...] + self.register_buffer("causal_attention_mask", causal_attention_mask, persistent=False) + + self.clip_mean = nn.Parameter(torch.zeros(1, embedding_dim)) + self.clip_std = nn.Parameter(torch.zeros(1, embedding_dim)) + + def forward( + self, + hidden_states, + timestep: Union[torch.Tensor, float, int], + proj_embedding: torch.FloatTensor, + encoder_hidden_states: torch.FloatTensor, + attention_mask: Optional[torch.BoolTensor] = None, + return_dict: bool = True, + ): + """ + Args: + hidden_states (`torch.FloatTensor` of shape `(batch_size, embedding_dim)`): + x_t, the currently predicted image embeddings. + timestep (`torch.long`): + Current denoising step. + proj_embedding (`torch.FloatTensor` of shape `(batch_size, embedding_dim)`): + Projected embedding vector the denoising process is conditioned on. + encoder_hidden_states (`torch.FloatTensor` of shape `(batch_size, num_embeddings, embedding_dim)`): + Hidden states of the text embeddings the denoising process is conditioned on. + attention_mask (`torch.BoolTensor` of shape `(batch_size, num_embeddings)`): + Text mask for the text embeddings. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.prior_transformer.PriorTransformerOutput`] instead of a plain + tuple. + + Returns: + [`~models.prior_transformer.PriorTransformerOutput`] or `tuple`: + [`~models.prior_transformer.PriorTransformerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + batch_size = hidden_states.shape[0] + + timesteps = timestep + if not torch.is_tensor(timesteps): + timesteps = torch.tensor([timesteps], dtype=torch.long, device=hidden_states.device) + elif torch.is_tensor(timesteps) and len(timesteps.shape) == 0: + timesteps = timesteps[None].to(hidden_states.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps * torch.ones(batch_size, dtype=timesteps.dtype, device=timesteps.device) + + timesteps_projected = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might be fp16, so we need to cast here. + timesteps_projected = timesteps_projected.to(dtype=self.dtype) + time_embeddings = self.time_embedding(timesteps_projected) + + proj_embeddings = self.embedding_proj(proj_embedding) + encoder_hidden_states = self.encoder_hidden_states_proj(encoder_hidden_states) + hidden_states = self.proj_in(hidden_states) + prd_embedding = self.prd_embedding.to(hidden_states.dtype).expand(batch_size, -1, -1) + positional_embeddings = self.positional_embedding.to(hidden_states.dtype) + + hidden_states = torch.cat( + [ + encoder_hidden_states, + proj_embeddings[:, None, :], + time_embeddings[:, None, :], + hidden_states[:, None, :], + prd_embedding, + ], + dim=1, + ) + + hidden_states = hidden_states + positional_embeddings + + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(hidden_states.dtype)) * -10000.0 + attention_mask = F.pad(attention_mask, (0, self.additional_embeddings), value=0.0) + attention_mask = (attention_mask[:, None, :] + self.causal_attention_mask).to(hidden_states.dtype) + attention_mask = attention_mask.repeat_interleave(self.config.num_attention_heads, dim=0) + + for block in self.transformer_blocks: + hidden_states = block(hidden_states, attention_mask=attention_mask) + + hidden_states = self.norm_out(hidden_states) + hidden_states = hidden_states[:, -1] + predicted_image_embedding = self.proj_to_clip_embeddings(hidden_states) + + if not return_dict: + return (predicted_image_embedding,) + + return PriorTransformerOutput(predicted_image_embedding=predicted_image_embedding) + + def post_process_latents(self, prior_latents): + prior_latents = (prior_latents * self.clip_std) + self.clip_mean + return prior_latents diff --git a/diffusers/src/diffusers/models/resnet.py b/diffusers/src/diffusers/models/resnet.py new file mode 100644 index 0000000000000000000000000000000000000000..7c14a7c4832dc399fc653fff9182ed3ebf3f6045 --- /dev/null +++ b/diffusers/src/diffusers/models/resnet.py @@ -0,0 +1,766 @@ +from functools import partial +from typing import Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .attention import AdaGroupNorm + + +class Upsample1D(nn.Module): + """ + An upsampling layer with an optional convolution. + + Parameters: + channels: channels in the inputs and outputs. + use_conv: a bool determining if a convolution is applied. + use_conv_transpose: + out_channels: + """ + + def __init__(self, channels, use_conv=False, use_conv_transpose=False, out_channels=None, name="conv"): + super().__init__() + self.channels = channels + self.out_channels = out_channels or channels + self.use_conv = use_conv + self.use_conv_transpose = use_conv_transpose + self.name = name + + self.conv = None + if use_conv_transpose: + self.conv = nn.ConvTranspose1d(channels, self.out_channels, 4, 2, 1) + elif use_conv: + self.conv = nn.Conv1d(self.channels, self.out_channels, 3, padding=1) + + def forward(self, x): + assert x.shape[1] == self.channels + if self.use_conv_transpose: + return self.conv(x) + + x = F.interpolate(x, scale_factor=2.0, mode="nearest") + + if self.use_conv: + x = self.conv(x) + + return x + + +class Downsample1D(nn.Module): + """ + A downsampling layer with an optional convolution. + + Parameters: + channels: channels in the inputs and outputs. + use_conv: a bool determining if a convolution is applied. + out_channels: + padding: + """ + + def __init__(self, channels, use_conv=False, out_channels=None, padding=1, name="conv"): + super().__init__() + self.channels = channels + self.out_channels = out_channels or channels + self.use_conv = use_conv + self.padding = padding + stride = 2 + self.name = name + + if use_conv: + self.conv = nn.Conv1d(self.channels, self.out_channels, 3, stride=stride, padding=padding) + else: + assert self.channels == self.out_channels + self.conv = nn.AvgPool1d(kernel_size=stride, stride=stride) + + def forward(self, x): + assert x.shape[1] == self.channels + return self.conv(x) + + +class Upsample2D(nn.Module): + """ + An upsampling layer with an optional convolution. + + Parameters: + channels: channels in the inputs and outputs. + use_conv: a bool determining if a convolution is applied. + use_conv_transpose: + out_channels: + """ + + def __init__(self, channels, use_conv=False, use_conv_transpose=False, out_channels=None, name="conv"): + super().__init__() + self.channels = channels + self.out_channels = out_channels or channels + self.use_conv = use_conv + self.use_conv_transpose = use_conv_transpose + self.name = name + + conv = None + if use_conv_transpose: + conv = nn.ConvTranspose2d(channels, self.out_channels, 4, 2, 1) + elif use_conv: + conv = nn.Conv2d(self.channels, self.out_channels, 3, padding=1) + + # TODO(Suraj, Patrick) - clean up after weight dicts are correctly renamed + if name == "conv": + self.conv = conv + else: + self.Conv2d_0 = conv + + def forward(self, hidden_states, output_size=None): + assert hidden_states.shape[1] == self.channels + + if self.use_conv_transpose: + return self.conv(hidden_states) + + # Cast to float32 to as 'upsample_nearest2d_out_frame' op does not support bfloat16 + # TODO(Suraj): Remove this cast once the issue is fixed in PyTorch + # https://github.com/pytorch/pytorch/issues/86679 + dtype = hidden_states.dtype + if dtype == torch.bfloat16: + hidden_states = hidden_states.to(torch.float32) + + # upsample_nearest_nhwc fails with large batch sizes. see https://github.com/huggingface/diffusers/issues/984 + if hidden_states.shape[0] >= 64: + hidden_states = hidden_states.contiguous() + + # if `output_size` is passed we force the interpolation output + # size and do not make use of `scale_factor=2` + if output_size is None: + hidden_states = F.interpolate(hidden_states, scale_factor=2.0, mode="nearest") + else: + hidden_states = F.interpolate(hidden_states, size=output_size, mode="nearest") + + # If the input is bfloat16, we cast back to bfloat16 + if dtype == torch.bfloat16: + hidden_states = hidden_states.to(dtype) + + # TODO(Suraj, Patrick) - clean up after weight dicts are correctly renamed + if self.use_conv: + if self.name == "conv": + hidden_states = self.conv(hidden_states) + else: + hidden_states = self.Conv2d_0(hidden_states) + + return hidden_states + + +class Downsample2D(nn.Module): + """ + A downsampling layer with an optional convolution. + + Parameters: + channels: channels in the inputs and outputs. + use_conv: a bool determining if a convolution is applied. + out_channels: + padding: + """ + + def __init__(self, channels, use_conv=False, out_channels=None, padding=1, name="conv"): + super().__init__() + self.channels = channels + self.out_channels = out_channels or channels + self.use_conv = use_conv + self.padding = padding + stride = 2 + self.name = name + + if use_conv: + conv = nn.Conv2d(self.channels, self.out_channels, 3, stride=stride, padding=padding) + else: + assert self.channels == self.out_channels + conv = nn.AvgPool2d(kernel_size=stride, stride=stride) + + # TODO(Suraj, Patrick) - clean up after weight dicts are correctly renamed + if name == "conv": + self.Conv2d_0 = conv + self.conv = conv + elif name == "Conv2d_0": + self.conv = conv + else: + self.conv = conv + + def forward(self, hidden_states): + assert hidden_states.shape[1] == self.channels + if self.use_conv and self.padding == 0: + pad = (0, 1, 0, 1) + hidden_states = F.pad(hidden_states, pad, mode="constant", value=0) + + assert hidden_states.shape[1] == self.channels + hidden_states = self.conv(hidden_states) + + return hidden_states + + +class FirUpsample2D(nn.Module): + def __init__(self, channels=None, out_channels=None, use_conv=False, fir_kernel=(1, 3, 3, 1)): + super().__init__() + out_channels = out_channels if out_channels else channels + if use_conv: + self.Conv2d_0 = nn.Conv2d(channels, out_channels, kernel_size=3, stride=1, padding=1) + self.use_conv = use_conv + self.fir_kernel = fir_kernel + self.out_channels = out_channels + + def _upsample_2d(self, hidden_states, weight=None, kernel=None, factor=2, gain=1): + """Fused `upsample_2d()` followed by `Conv2d()`. + + Padding is performed only once at the beginning, not between the operations. The fused op is considerably more + efficient than performing the same calculation using standard TensorFlow ops. It supports gradients of + arbitrary order. + + Args: + hidden_states: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. + weight: Weight tensor of the shape `[filterH, filterW, inChannels, + outChannels]`. Grouped convolution can be performed by `inChannels = x.shape[0] // numGroups`. + kernel: FIR filter of the shape `[firH, firW]` or `[firN]` + (separable). The default is `[1] * factor`, which corresponds to nearest-neighbor upsampling. + factor: Integer upsampling factor (default: 2). + gain: Scaling factor for signal magnitude (default: 1.0). + + Returns: + output: Tensor of the shape `[N, C, H * factor, W * factor]` or `[N, H * factor, W * factor, C]`, and same + datatype as `hidden_states`. + """ + + assert isinstance(factor, int) and factor >= 1 + + # Setup filter kernel. + if kernel is None: + kernel = [1] * factor + + # setup kernel + kernel = torch.tensor(kernel, dtype=torch.float32) + if kernel.ndim == 1: + kernel = torch.outer(kernel, kernel) + kernel /= torch.sum(kernel) + + kernel = kernel * (gain * (factor**2)) + + if self.use_conv: + convH = weight.shape[2] + convW = weight.shape[3] + inC = weight.shape[1] + + pad_value = (kernel.shape[0] - factor) - (convW - 1) + + stride = (factor, factor) + # Determine data dimensions. + output_shape = ( + (hidden_states.shape[2] - 1) * factor + convH, + (hidden_states.shape[3] - 1) * factor + convW, + ) + output_padding = ( + output_shape[0] - (hidden_states.shape[2] - 1) * stride[0] - convH, + output_shape[1] - (hidden_states.shape[3] - 1) * stride[1] - convW, + ) + assert output_padding[0] >= 0 and output_padding[1] >= 0 + num_groups = hidden_states.shape[1] // inC + + # Transpose weights. + weight = torch.reshape(weight, (num_groups, -1, inC, convH, convW)) + weight = torch.flip(weight, dims=[3, 4]).permute(0, 2, 1, 3, 4) + weight = torch.reshape(weight, (num_groups * inC, -1, convH, convW)) + + inverse_conv = F.conv_transpose2d( + hidden_states, weight, stride=stride, output_padding=output_padding, padding=0 + ) + + output = upfirdn2d_native( + inverse_conv, + torch.tensor(kernel, device=inverse_conv.device), + pad=((pad_value + 1) // 2 + factor - 1, pad_value // 2 + 1), + ) + else: + pad_value = kernel.shape[0] - factor + output = upfirdn2d_native( + hidden_states, + torch.tensor(kernel, device=hidden_states.device), + up=factor, + pad=((pad_value + 1) // 2 + factor - 1, pad_value // 2), + ) + + return output + + def forward(self, hidden_states): + if self.use_conv: + height = self._upsample_2d(hidden_states, self.Conv2d_0.weight, kernel=self.fir_kernel) + height = height + self.Conv2d_0.bias.reshape(1, -1, 1, 1) + else: + height = self._upsample_2d(hidden_states, kernel=self.fir_kernel, factor=2) + + return height + + +class FirDownsample2D(nn.Module): + def __init__(self, channels=None, out_channels=None, use_conv=False, fir_kernel=(1, 3, 3, 1)): + super().__init__() + out_channels = out_channels if out_channels else channels + if use_conv: + self.Conv2d_0 = nn.Conv2d(channels, out_channels, kernel_size=3, stride=1, padding=1) + self.fir_kernel = fir_kernel + self.use_conv = use_conv + self.out_channels = out_channels + + def _downsample_2d(self, hidden_states, weight=None, kernel=None, factor=2, gain=1): + """Fused `Conv2d()` followed by `downsample_2d()`. + Padding is performed only once at the beginning, not between the operations. The fused op is considerably more + efficient than performing the same calculation using standard TensorFlow ops. It supports gradients of + arbitrary order. + + Args: + hidden_states: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. + weight: + Weight tensor of the shape `[filterH, filterW, inChannels, outChannels]`. Grouped convolution can be + performed by `inChannels = x.shape[0] // numGroups`. + kernel: FIR filter of the shape `[firH, firW]` or `[firN]` (separable). The default is `[1] * + factor`, which corresponds to average pooling. + factor: Integer downsampling factor (default: 2). + gain: Scaling factor for signal magnitude (default: 1.0). + + Returns: + output: Tensor of the shape `[N, C, H // factor, W // factor]` or `[N, H // factor, W // factor, C]`, and + same datatype as `x`. + """ + + assert isinstance(factor, int) and factor >= 1 + if kernel is None: + kernel = [1] * factor + + # setup kernel + kernel = torch.tensor(kernel, dtype=torch.float32) + if kernel.ndim == 1: + kernel = torch.outer(kernel, kernel) + kernel /= torch.sum(kernel) + + kernel = kernel * gain + + if self.use_conv: + _, _, convH, convW = weight.shape + pad_value = (kernel.shape[0] - factor) + (convW - 1) + stride_value = [factor, factor] + upfirdn_input = upfirdn2d_native( + hidden_states, + torch.tensor(kernel, device=hidden_states.device), + pad=((pad_value + 1) // 2, pad_value // 2), + ) + output = F.conv2d(upfirdn_input, weight, stride=stride_value, padding=0) + else: + pad_value = kernel.shape[0] - factor + output = upfirdn2d_native( + hidden_states, + torch.tensor(kernel, device=hidden_states.device), + down=factor, + pad=((pad_value + 1) // 2, pad_value // 2), + ) + + return output + + def forward(self, hidden_states): + if self.use_conv: + downsample_input = self._downsample_2d(hidden_states, weight=self.Conv2d_0.weight, kernel=self.fir_kernel) + hidden_states = downsample_input + self.Conv2d_0.bias.reshape(1, -1, 1, 1) + else: + hidden_states = self._downsample_2d(hidden_states, kernel=self.fir_kernel, factor=2) + + return hidden_states + + +# downsample/upsample layer used in k-upscaler, might be able to use FirDownsample2D/DirUpsample2D instead +class KDownsample2D(nn.Module): + def __init__(self, pad_mode="reflect"): + super().__init__() + self.pad_mode = pad_mode + kernel_1d = torch.tensor([[1 / 8, 3 / 8, 3 / 8, 1 / 8]]) + self.pad = kernel_1d.shape[1] // 2 - 1 + self.register_buffer("kernel", kernel_1d.T @ kernel_1d, persistent=False) + + def forward(self, x): + x = F.pad(x, (self.pad,) * 4, self.pad_mode) + weight = x.new_zeros([x.shape[1], x.shape[1], self.kernel.shape[0], self.kernel.shape[1]]) + indices = torch.arange(x.shape[1], device=x.device) + weight[indices, indices] = self.kernel.to(weight) + return F.conv2d(x, weight, stride=2) + + +class KUpsample2D(nn.Module): + def __init__(self, pad_mode="reflect"): + super().__init__() + self.pad_mode = pad_mode + kernel_1d = torch.tensor([[1 / 8, 3 / 8, 3 / 8, 1 / 8]]) * 2 + self.pad = kernel_1d.shape[1] // 2 - 1 + self.register_buffer("kernel", kernel_1d.T @ kernel_1d, persistent=False) + + def forward(self, x): + x = F.pad(x, ((self.pad + 1) // 2,) * 4, self.pad_mode) + weight = x.new_zeros([x.shape[1], x.shape[1], self.kernel.shape[0], self.kernel.shape[1]]) + indices = torch.arange(x.shape[1], device=x.device) + weight[indices, indices] = self.kernel.to(weight) + return F.conv_transpose2d(x, weight, stride=2, padding=self.pad * 2 + 1) + + +class ResnetBlock2D(nn.Module): + r""" + A Resnet block. + + Parameters: + in_channels (`int`): The number of channels in the input. + out_channels (`int`, *optional*, default to be `None`): + The number of output channels for the first conv2d layer. If None, same as `in_channels`. + dropout (`float`, *optional*, defaults to `0.0`): The dropout probability to use. + temb_channels (`int`, *optional*, default to `512`): the number of channels in timestep embedding. + groups (`int`, *optional*, default to `32`): The number of groups to use for the first normalization layer. + groups_out (`int`, *optional*, default to None): + The number of groups to use for the second normalization layer. if set to None, same as `groups`. + eps (`float`, *optional*, defaults to `1e-6`): The epsilon to use for the normalization. + non_linearity (`str`, *optional*, default to `"swish"`): the activation function to use. + time_embedding_norm (`str`, *optional*, default to `"default"` ): Time scale shift config. + By default, apply timestep embedding conditioning with a simple shift mechanism. Choose "scale_shift" or + "ada_group" for a stronger conditioning with scale and shift. + kernal (`torch.FloatTensor`, optional, default to None): FIR filter, see + [`~models.resnet.FirUpsample2D`] and [`~models.resnet.FirDownsample2D`]. + output_scale_factor (`float`, *optional*, default to be `1.0`): the scale factor to use for the output. + use_in_shortcut (`bool`, *optional*, default to `True`): + If `True`, add a 1x1 nn.conv2d layer for skip-connection. + up (`bool`, *optional*, default to `False`): If `True`, add an upsample layer. + down (`bool`, *optional*, default to `False`): If `True`, add a downsample layer. + conv_shortcut_bias (`bool`, *optional*, default to `True`): If `True`, adds a learnable bias to the + `conv_shortcut` output. + conv_2d_out_channels (`int`, *optional*, default to `None`): the number of channels in the output. + If None, same as `out_channels`. + """ + + def __init__( + self, + *, + in_channels, + out_channels=None, + conv_shortcut=False, + dropout=0.0, + temb_channels=512, + groups=32, + groups_out=None, + pre_norm=True, + eps=1e-6, + non_linearity="swish", + time_embedding_norm="default", # default, scale_shift, ada_group + kernel=None, + output_scale_factor=1.0, + use_in_shortcut=None, + up=False, + down=False, + conv_shortcut_bias: bool = True, + conv_2d_out_channels: Optional[int] = None, + ): + super().__init__() + self.pre_norm = pre_norm + self.pre_norm = True + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + self.use_conv_shortcut = conv_shortcut + self.up = up + self.down = down + self.output_scale_factor = output_scale_factor + self.time_embedding_norm = time_embedding_norm + + if groups_out is None: + groups_out = groups + + if self.time_embedding_norm == "ada_group": + self.norm1 = AdaGroupNorm(temb_channels, in_channels, groups, eps=eps) + else: + self.norm1 = torch.nn.GroupNorm(num_groups=groups, num_channels=in_channels, eps=eps, affine=True) + + self.conv1 = torch.nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1) + + if temb_channels is not None: + if self.time_embedding_norm == "default": + self.time_emb_proj = torch.nn.Linear(temb_channels, out_channels) + elif self.time_embedding_norm == "scale_shift": + self.time_emb_proj = torch.nn.Linear(temb_channels, 2 * out_channels) + elif self.time_embedding_norm == "ada_group": + self.time_emb_proj = None + else: + raise ValueError(f"unknown time_embedding_norm : {self.time_embedding_norm} ") + else: + self.time_emb_proj = None + + if self.time_embedding_norm == "ada_group": + self.norm2 = AdaGroupNorm(temb_channels, out_channels, groups_out, eps=eps) + else: + self.norm2 = torch.nn.GroupNorm(num_groups=groups_out, num_channels=out_channels, eps=eps, affine=True) + + self.dropout = torch.nn.Dropout(dropout) + conv_2d_out_channels = conv_2d_out_channels or out_channels + self.conv2 = torch.nn.Conv2d(out_channels, conv_2d_out_channels, kernel_size=3, stride=1, padding=1) + + if non_linearity == "swish": + self.nonlinearity = lambda x: F.silu(x) + elif non_linearity == "mish": + self.nonlinearity = nn.Mish() + elif non_linearity == "silu": + self.nonlinearity = nn.SiLU() + elif non_linearity == "gelu": + self.nonlinearity = nn.GELU() + + self.upsample = self.downsample = None + if self.up: + if kernel == "fir": + fir_kernel = (1, 3, 3, 1) + self.upsample = lambda x: upsample_2d(x, kernel=fir_kernel) + elif kernel == "sde_vp": + self.upsample = partial(F.interpolate, scale_factor=2.0, mode="nearest") + else: + self.upsample = Upsample2D(in_channels, use_conv=False) + elif self.down: + if kernel == "fir": + fir_kernel = (1, 3, 3, 1) + self.downsample = lambda x: downsample_2d(x, kernel=fir_kernel) + elif kernel == "sde_vp": + self.downsample = partial(F.avg_pool2d, kernel_size=2, stride=2) + else: + self.downsample = Downsample2D(in_channels, use_conv=False, padding=1, name="op") + + self.use_in_shortcut = self.in_channels != conv_2d_out_channels if use_in_shortcut is None else use_in_shortcut + + self.conv_shortcut = None + if self.use_in_shortcut: + self.conv_shortcut = torch.nn.Conv2d( + in_channels, conv_2d_out_channels, kernel_size=1, stride=1, padding=0, bias=conv_shortcut_bias + ) + + def forward(self, input_tensor, temb): + hidden_states = input_tensor + + if self.time_embedding_norm == "ada_group": + hidden_states = self.norm1(hidden_states, temb) + else: + hidden_states = self.norm1(hidden_states) + + hidden_states = self.nonlinearity(hidden_states) + + if self.upsample is not None: + # upsample_nearest_nhwc fails with large batch sizes. see https://github.com/huggingface/diffusers/issues/984 + if hidden_states.shape[0] >= 64: + input_tensor = input_tensor.contiguous() + hidden_states = hidden_states.contiguous() + input_tensor = self.upsample(input_tensor) + hidden_states = self.upsample(hidden_states) + elif self.downsample is not None: + input_tensor = self.downsample(input_tensor) + hidden_states = self.downsample(hidden_states) + + hidden_states = self.conv1(hidden_states) + + if self.time_emb_proj is not None: + temb = self.time_emb_proj(self.nonlinearity(temb))[:, :, None, None] + + if temb is not None and self.time_embedding_norm == "default": + hidden_states = hidden_states + temb + + if self.time_embedding_norm == "ada_group": + hidden_states = self.norm2(hidden_states, temb) + else: + hidden_states = self.norm2(hidden_states) + + if temb is not None and self.time_embedding_norm == "scale_shift": + scale, shift = torch.chunk(temb, 2, dim=1) + hidden_states = hidden_states * (1 + scale) + shift + + hidden_states = self.nonlinearity(hidden_states) + + hidden_states = self.dropout(hidden_states) + hidden_states = self.conv2(hidden_states) + + if self.conv_shortcut is not None: + input_tensor = self.conv_shortcut(input_tensor) + + output_tensor = (input_tensor + hidden_states) / self.output_scale_factor + + return output_tensor + + +class Mish(torch.nn.Module): + def forward(self, hidden_states): + return hidden_states * torch.tanh(torch.nn.functional.softplus(hidden_states)) + + +# unet_rl.py +def rearrange_dims(tensor): + if len(tensor.shape) == 2: + return tensor[:, :, None] + if len(tensor.shape) == 3: + return tensor[:, :, None, :] + elif len(tensor.shape) == 4: + return tensor[:, :, 0, :] + else: + raise ValueError(f"`len(tensor)`: {len(tensor)} has to be 2, 3 or 4.") + + +class Conv1dBlock(nn.Module): + """ + Conv1d --> GroupNorm --> Mish + """ + + def __init__(self, inp_channels, out_channels, kernel_size, n_groups=8): + super().__init__() + + self.conv1d = nn.Conv1d(inp_channels, out_channels, kernel_size, padding=kernel_size // 2) + self.group_norm = nn.GroupNorm(n_groups, out_channels) + self.mish = nn.Mish() + + def forward(self, x): + x = self.conv1d(x) + x = rearrange_dims(x) + x = self.group_norm(x) + x = rearrange_dims(x) + x = self.mish(x) + return x + + +# unet_rl.py +class ResidualTemporalBlock1D(nn.Module): + def __init__(self, inp_channels, out_channels, embed_dim, kernel_size=5): + super().__init__() + self.conv_in = Conv1dBlock(inp_channels, out_channels, kernel_size) + self.conv_out = Conv1dBlock(out_channels, out_channels, kernel_size) + + self.time_emb_act = nn.Mish() + self.time_emb = nn.Linear(embed_dim, out_channels) + + self.residual_conv = ( + nn.Conv1d(inp_channels, out_channels, 1) if inp_channels != out_channels else nn.Identity() + ) + + def forward(self, x, t): + """ + Args: + x : [ batch_size x inp_channels x horizon ] + t : [ batch_size x embed_dim ] + + returns: + out : [ batch_size x out_channels x horizon ] + """ + t = self.time_emb_act(t) + t = self.time_emb(t) + out = self.conv_in(x) + rearrange_dims(t) + out = self.conv_out(out) + return out + self.residual_conv(x) + + +def upsample_2d(hidden_states, kernel=None, factor=2, gain=1): + r"""Upsample2D a batch of 2D images with the given filter. + Accepts a batch of 2D images of the shape `[N, C, H, W]` or `[N, H, W, C]` and upsamples each image with the given + filter. The filter is normalized so that if the input pixels are constant, they will be scaled by the specified + `gain`. Pixels outside the image are assumed to be zero, and the filter is padded with zeros so that its shape is + a: multiple of the upsampling factor. + + Args: + hidden_states: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. + kernel: FIR filter of the shape `[firH, firW]` or `[firN]` + (separable). The default is `[1] * factor`, which corresponds to nearest-neighbor upsampling. + factor: Integer upsampling factor (default: 2). + gain: Scaling factor for signal magnitude (default: 1.0). + + Returns: + output: Tensor of the shape `[N, C, H * factor, W * factor]` + """ + assert isinstance(factor, int) and factor >= 1 + if kernel is None: + kernel = [1] * factor + + kernel = torch.tensor(kernel, dtype=torch.float32) + if kernel.ndim == 1: + kernel = torch.outer(kernel, kernel) + kernel /= torch.sum(kernel) + + kernel = kernel * (gain * (factor**2)) + pad_value = kernel.shape[0] - factor + output = upfirdn2d_native( + hidden_states, + kernel.to(device=hidden_states.device), + up=factor, + pad=((pad_value + 1) // 2 + factor - 1, pad_value // 2), + ) + return output + + +def downsample_2d(hidden_states, kernel=None, factor=2, gain=1): + r"""Downsample2D a batch of 2D images with the given filter. + Accepts a batch of 2D images of the shape `[N, C, H, W]` or `[N, H, W, C]` and downsamples each image with the + given filter. The filter is normalized so that if the input pixels are constant, they will be scaled by the + specified `gain`. Pixels outside the image are assumed to be zero, and the filter is padded with zeros so that its + shape is a multiple of the downsampling factor. + + Args: + hidden_states: Input tensor of the shape `[N, C, H, W]` or `[N, H, W, C]`. + kernel: FIR filter of the shape `[firH, firW]` or `[firN]` + (separable). The default is `[1] * factor`, which corresponds to average pooling. + factor: Integer downsampling factor (default: 2). + gain: Scaling factor for signal magnitude (default: 1.0). + + Returns: + output: Tensor of the shape `[N, C, H // factor, W // factor]` + """ + + assert isinstance(factor, int) and factor >= 1 + if kernel is None: + kernel = [1] * factor + + kernel = torch.tensor(kernel, dtype=torch.float32) + if kernel.ndim == 1: + kernel = torch.outer(kernel, kernel) + kernel /= torch.sum(kernel) + + kernel = kernel * gain + pad_value = kernel.shape[0] - factor + output = upfirdn2d_native( + hidden_states, kernel.to(device=hidden_states.device), down=factor, pad=((pad_value + 1) // 2, pad_value // 2) + ) + return output + + +def upfirdn2d_native(tensor, kernel, up=1, down=1, pad=(0, 0)): + up_x = up_y = up + down_x = down_y = down + pad_x0 = pad_y0 = pad[0] + pad_x1 = pad_y1 = pad[1] + + _, channel, in_h, in_w = tensor.shape + tensor = tensor.reshape(-1, in_h, in_w, 1) + + _, in_h, in_w, minor = tensor.shape + kernel_h, kernel_w = kernel.shape + + out = tensor.view(-1, in_h, 1, in_w, 1, minor) + out = F.pad(out, [0, 0, 0, up_x - 1, 0, 0, 0, up_y - 1]) + out = out.view(-1, in_h * up_y, in_w * up_x, minor) + + out = F.pad(out, [0, 0, max(pad_x0, 0), max(pad_x1, 0), max(pad_y0, 0), max(pad_y1, 0)]) + out = out.to(tensor.device) # Move back to mps if necessary + out = out[ + :, + max(-pad_y0, 0) : out.shape[1] - max(-pad_y1, 0), + max(-pad_x0, 0) : out.shape[2] - max(-pad_x1, 0), + :, + ] + + out = out.permute(0, 3, 1, 2) + out = out.reshape([-1, 1, in_h * up_y + pad_y0 + pad_y1, in_w * up_x + pad_x0 + pad_x1]) + w = torch.flip(kernel, [0, 1]).view(1, 1, kernel_h, kernel_w) + out = F.conv2d(out, w) + out = out.reshape( + -1, + minor, + in_h * up_y + pad_y0 + pad_y1 - kernel_h + 1, + in_w * up_x + pad_x0 + pad_x1 - kernel_w + 1, + ) + out = out.permute(0, 2, 3, 1) + out = out[:, ::down_y, ::down_x, :] + + out_h = (in_h * up_y + pad_y0 + pad_y1 - kernel_h) // down_y + 1 + out_w = (in_w * up_x + pad_x0 + pad_x1 - kernel_w) // down_x + 1 + + return out.view(-1, channel, out_h, out_w) diff --git a/diffusers/src/diffusers/models/resnet_flax.py b/diffusers/src/diffusers/models/resnet_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..632780378ee0e8fa49404ecae470146250270ce5 --- /dev/null +++ b/diffusers/src/diffusers/models/resnet_flax.py @@ -0,0 +1,124 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import flax.linen as nn +import jax +import jax.numpy as jnp + + +class FlaxUpsample2D(nn.Module): + out_channels: int + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.conv = nn.Conv( + self.out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + def __call__(self, hidden_states): + batch, height, width, channels = hidden_states.shape + hidden_states = jax.image.resize( + hidden_states, + shape=(batch, height * 2, width * 2, channels), + method="nearest", + ) + hidden_states = self.conv(hidden_states) + return hidden_states + + +class FlaxDownsample2D(nn.Module): + out_channels: int + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.conv = nn.Conv( + self.out_channels, + kernel_size=(3, 3), + strides=(2, 2), + padding=((1, 1), (1, 1)), # padding="VALID", + dtype=self.dtype, + ) + + def __call__(self, hidden_states): + # pad = ((0, 0), (0, 1), (0, 1), (0, 0)) # pad height and width dim + # hidden_states = jnp.pad(hidden_states, pad_width=pad) + hidden_states = self.conv(hidden_states) + return hidden_states + + +class FlaxResnetBlock2D(nn.Module): + in_channels: int + out_channels: int = None + dropout_prob: float = 0.0 + use_nin_shortcut: bool = None + dtype: jnp.dtype = jnp.float32 + + def setup(self): + out_channels = self.in_channels if self.out_channels is None else self.out_channels + + self.norm1 = nn.GroupNorm(num_groups=32, epsilon=1e-5) + self.conv1 = nn.Conv( + out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + self.time_emb_proj = nn.Dense(out_channels, dtype=self.dtype) + + self.norm2 = nn.GroupNorm(num_groups=32, epsilon=1e-5) + self.dropout = nn.Dropout(self.dropout_prob) + self.conv2 = nn.Conv( + out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + use_nin_shortcut = self.in_channels != out_channels if self.use_nin_shortcut is None else self.use_nin_shortcut + + self.conv_shortcut = None + if use_nin_shortcut: + self.conv_shortcut = nn.Conv( + out_channels, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + + def __call__(self, hidden_states, temb, deterministic=True): + residual = hidden_states + hidden_states = self.norm1(hidden_states) + hidden_states = nn.swish(hidden_states) + hidden_states = self.conv1(hidden_states) + + temb = self.time_emb_proj(nn.swish(temb)) + temb = jnp.expand_dims(jnp.expand_dims(temb, 1), 1) + hidden_states = hidden_states + temb + + hidden_states = self.norm2(hidden_states) + hidden_states = nn.swish(hidden_states) + hidden_states = self.dropout(hidden_states, deterministic) + hidden_states = self.conv2(hidden_states) + + if self.conv_shortcut is not None: + residual = self.conv_shortcut(residual) + + return hidden_states + residual diff --git a/diffusers/src/diffusers/models/transformer_2d.py b/diffusers/src/diffusers/models/transformer_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..2fdf70ce76b902040b17a2c007c51ea3a14de8f1 --- /dev/null +++ b/diffusers/src/diffusers/models/transformer_2d.py @@ -0,0 +1,313 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Optional + +import torch +import torch.nn.functional as F +from torch import nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..models.embeddings import ImagePositionalEmbeddings +from ..utils import BaseOutput, deprecate +from .attention import BasicTransformerBlock +from .embeddings import PatchEmbed +from .modeling_utils import ModelMixin + + +@dataclass +class Transformer2DModelOutput(BaseOutput): + """ + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` or `(batch size, num_vector_embeds - 1, num_latent_pixels)` if [`Transformer2DModel`] is discrete): + Hidden states conditioned on `encoder_hidden_states` input. If discrete, returns probability distributions + for the unnoised latent pixels. + """ + + sample: torch.FloatTensor + + +class Transformer2DModel(ModelMixin, ConfigMixin): + """ + Transformer model for image-like data. Takes either discrete (classes of vector embeddings) or continuous (actual + embeddings) inputs. + + When input is continuous: First, project the input (aka embedding) and reshape to b, t, d. Then apply standard + transformer action. Finally, reshape to image. + + When input is discrete: First, input (classes of latent pixels) is converted to embeddings and has positional + embeddings applied, see `ImagePositionalEmbeddings`. Then apply standard transformer action. Finally, predict + classes of unnoised image. + + Note that it is assumed one of the input classes is the masked latent pixel. The predicted classes of the unnoised + image do not contain a prediction for the masked pixel as the unnoised image cannot be masked. + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 16): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 88): The number of channels in each head. + in_channels (`int`, *optional*): + Pass if the input is continuous. The number of channels in the input and output. + num_layers (`int`, *optional*, defaults to 1): The number of layers of Transformer blocks to use. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The number of encoder_hidden_states dimensions to use. + sample_size (`int`, *optional*): Pass if the input is discrete. The width of the latent images. + Note that this is fixed at training time as it is used for learning a number of position embeddings. See + `ImagePositionalEmbeddings`. + num_vector_embeds (`int`, *optional*): + Pass if the input is discrete. The number of classes of the vector embeddings of the latent pixels. + Includes the class for the masked latent pixel. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm ( `int`, *optional*): Pass if at least one of the norm_layers is `AdaLayerNorm`. + The number of diffusion steps used during training. Note that this is fixed at training time as it is used + to learn a number of embeddings that are added to the hidden states. During inference, you can denoise for + up to but not more than steps than `num_embeds_ada_norm`. + attention_bias (`bool`, *optional*): + Configure if the TransformerBlocks' attention should contain a bias parameter. + """ + + @register_to_config + def __init__( + self, + num_attention_heads: int = 16, + attention_head_dim: int = 88, + in_channels: Optional[int] = None, + out_channels: Optional[int] = None, + num_layers: int = 1, + dropout: float = 0.0, + norm_num_groups: int = 32, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + sample_size: Optional[int] = None, + num_vector_embeds: Optional[int] = None, + patch_size: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + norm_type: str = "layer_norm", + norm_elementwise_affine: bool = True, + ): + super().__init__() + self.use_linear_projection = use_linear_projection + self.num_attention_heads = num_attention_heads + self.attention_head_dim = attention_head_dim + inner_dim = num_attention_heads * attention_head_dim + + # 1. Transformer2DModel can process both standard continous images of shape `(batch_size, num_channels, width, height)` as well as quantized image embeddings of shape `(batch_size, num_image_vectors)` + # Define whether input is continuous or discrete depending on configuration + self.is_input_continuous = (in_channels is not None) and (patch_size is None) + self.is_input_vectorized = num_vector_embeds is not None + self.is_input_patches = in_channels is not None and patch_size is not None + + if norm_type == "layer_norm" and num_embeds_ada_norm is not None: + deprecation_message = ( + f"The configuration file of this model: {self.__class__} is outdated. `norm_type` is either not set or" + " incorrectly set to `'layer_norm'`.Make sure to set `norm_type` to `'ada_norm'` in the config." + " Please make sure to update the config accordingly as leaving `norm_type` might led to incorrect" + " results in future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it" + " would be very nice if you could open a Pull request for the `transformer/config.json` file" + ) + deprecate("norm_type!=num_embeds_ada_norm", "1.0.0", deprecation_message, standard_warn=False) + norm_type = "ada_norm" + + if self.is_input_continuous and self.is_input_vectorized: + raise ValueError( + f"Cannot define both `in_channels`: {in_channels} and `num_vector_embeds`: {num_vector_embeds}. Make" + " sure that either `in_channels` or `num_vector_embeds` is None." + ) + elif self.is_input_vectorized and self.is_input_patches: + raise ValueError( + f"Cannot define both `num_vector_embeds`: {num_vector_embeds} and `patch_size`: {patch_size}. Make" + " sure that either `num_vector_embeds` or `num_patches` is None." + ) + elif not self.is_input_continuous and not self.is_input_vectorized and not self.is_input_patches: + raise ValueError( + f"Has to define `in_channels`: {in_channels}, `num_vector_embeds`: {num_vector_embeds}, or patch_size:" + f" {patch_size}. Make sure that `in_channels`, `num_vector_embeds` or `num_patches` is not None." + ) + + # 2. Define input layers + if self.is_input_continuous: + self.in_channels = in_channels + + self.norm = torch.nn.GroupNorm(num_groups=norm_num_groups, num_channels=in_channels, eps=1e-6, affine=True) + if use_linear_projection: + self.proj_in = nn.Linear(in_channels, inner_dim) + else: + self.proj_in = nn.Conv2d(in_channels, inner_dim, kernel_size=1, stride=1, padding=0) + elif self.is_input_vectorized: + assert sample_size is not None, "Transformer2DModel over discrete input must provide sample_size" + assert num_vector_embeds is not None, "Transformer2DModel over discrete input must provide num_embed" + + self.height = sample_size + self.width = sample_size + self.num_vector_embeds = num_vector_embeds + self.num_latent_pixels = self.height * self.width + + self.latent_image_embedding = ImagePositionalEmbeddings( + num_embed=num_vector_embeds, embed_dim=inner_dim, height=self.height, width=self.width + ) + elif self.is_input_patches: + assert sample_size is not None, "Transformer2DModel over patched input must provide sample_size" + + self.height = sample_size + self.width = sample_size + + self.patch_size = patch_size + self.pos_embed = PatchEmbed( + height=sample_size, + width=sample_size, + patch_size=patch_size, + in_channels=in_channels, + embed_dim=inner_dim, + ) + + # 3. Define transformers blocks + self.transformer_blocks = nn.ModuleList( + [ + BasicTransformerBlock( + inner_dim, + num_attention_heads, + attention_head_dim, + dropout=dropout, + cross_attention_dim=cross_attention_dim, + activation_fn=activation_fn, + num_embeds_ada_norm=num_embeds_ada_norm, + attention_bias=attention_bias, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + norm_type=norm_type, + norm_elementwise_affine=norm_elementwise_affine, + ) + for d in range(num_layers) + ] + ) + + # 4. Define output layers + self.out_channels = in_channels if out_channels is None else out_channels + if self.is_input_continuous: + # TODO: should use out_channels for continous projections + if use_linear_projection: + self.proj_out = nn.Linear(inner_dim, in_channels) + else: + self.proj_out = nn.Conv2d(inner_dim, in_channels, kernel_size=1, stride=1, padding=0) + elif self.is_input_vectorized: + self.norm_out = nn.LayerNorm(inner_dim) + self.out = nn.Linear(inner_dim, self.num_vector_embeds - 1) + elif self.is_input_patches: + self.norm_out = nn.LayerNorm(inner_dim, elementwise_affine=False, eps=1e-6) + self.proj_out_1 = nn.Linear(inner_dim, 2 * inner_dim) + self.proj_out_2 = nn.Linear(inner_dim, patch_size * patch_size * self.out_channels) + + def forward( + self, + hidden_states, + encoder_hidden_states=None, + timestep=None, + class_labels=None, + cross_attention_kwargs=None, + return_dict: bool = True, + ): + """ + Args: + hidden_states ( When discrete, `torch.LongTensor` of shape `(batch size, num latent pixels)`. + When continous, `torch.FloatTensor` of shape `(batch size, channel, height, width)`): Input + hidden_states + encoder_hidden_states ( `torch.LongTensor` of shape `(batch size, encoder_hidden_states dim)`, *optional*): + Conditional embeddings for cross attention layer. If not given, cross-attention defaults to + self-attention. + timestep ( `torch.long`, *optional*): + Optional timestep to be applied as an embedding in AdaLayerNorm's. Used to indicate denoising step. + class_labels ( `torch.LongTensor` of shape `(batch size, num classes)`, *optional*): + Optional class labels to be applied as an embedding in AdaLayerZeroNorm. Used to indicate class labels + conditioning. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain tuple. + + Returns: + [`~models.transformer_2d.Transformer2DModelOutput`] or `tuple`: + [`~models.transformer_2d.Transformer2DModelOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + # 1. Input + if self.is_input_continuous: + batch, _, height, width = hidden_states.shape + residual = hidden_states + + hidden_states = self.norm(hidden_states) + if not self.use_linear_projection: + hidden_states = self.proj_in(hidden_states) + inner_dim = hidden_states.shape[1] + hidden_states = hidden_states.permute(0, 2, 3, 1).reshape(batch, height * width, inner_dim) + else: + inner_dim = hidden_states.shape[1] + hidden_states = hidden_states.permute(0, 2, 3, 1).reshape(batch, height * width, inner_dim) + hidden_states = self.proj_in(hidden_states) + elif self.is_input_vectorized: + hidden_states = self.latent_image_embedding(hidden_states) + elif self.is_input_patches: + hidden_states = self.pos_embed(hidden_states) + + # 2. Blocks + for block in self.transformer_blocks: + hidden_states = block( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + timestep=timestep, + cross_attention_kwargs=cross_attention_kwargs, + class_labels=class_labels, + ) + + # 3. Output + if self.is_input_continuous: + if not self.use_linear_projection: + hidden_states = hidden_states.reshape(batch, height, width, inner_dim).permute(0, 3, 1, 2).contiguous() + hidden_states = self.proj_out(hidden_states) + else: + hidden_states = self.proj_out(hidden_states) + hidden_states = hidden_states.reshape(batch, height, width, inner_dim).permute(0, 3, 1, 2).contiguous() + + output = hidden_states + residual + elif self.is_input_vectorized: + hidden_states = self.norm_out(hidden_states) + logits = self.out(hidden_states) + # (batch, self.num_vector_embeds - 1, self.num_latent_pixels) + logits = logits.permute(0, 2, 1) + + # log(p(x_0)) + output = F.log_softmax(logits.double(), dim=1).float() + elif self.is_input_patches: + # TODO: cleanup! + conditioning = self.transformer_blocks[0].norm1.emb( + timestep, class_labels, hidden_dtype=hidden_states.dtype + ) + shift, scale = self.proj_out_1(F.silu(conditioning)).chunk(2, dim=1) + hidden_states = self.norm_out(hidden_states) * (1 + scale[:, None]) + shift[:, None] + hidden_states = self.proj_out_2(hidden_states) + + # unpatchify + height = width = int(hidden_states.shape[1] ** 0.5) + hidden_states = hidden_states.reshape( + shape=(-1, height, width, self.patch_size, self.patch_size, self.out_channels) + ) + hidden_states = torch.einsum("nhwpqc->nchpwq", hidden_states) + output = hidden_states.reshape( + shape=(-1, self.out_channels, height * self.patch_size, width * self.patch_size) + ) + + if not return_dict: + return (output,) + + return Transformer2DModelOutput(sample=output) diff --git a/diffusers/src/diffusers/models/unet_1d.py b/diffusers/src/diffusers/models/unet_1d.py new file mode 100644 index 0000000000000000000000000000000000000000..2ab6c69da72bf4cb503abd171a38595ce0c524f0 --- /dev/null +++ b/diffusers/src/diffusers/models/unet_1d.py @@ -0,0 +1,246 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import torch +import torch.nn as nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .embeddings import GaussianFourierProjection, TimestepEmbedding, Timesteps +from .modeling_utils import ModelMixin +from .unet_1d_blocks import get_down_block, get_mid_block, get_out_block, get_up_block + + +@dataclass +class UNet1DOutput(BaseOutput): + """ + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, sample_size)`): + Hidden states output. Output of last layer of model. + """ + + sample: torch.FloatTensor + + +class UNet1DModel(ModelMixin, ConfigMixin): + r""" + UNet1DModel is a 1D UNet model that takes in a noisy sample and a timestep and returns sample shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the model (such as downloading or saving, etc.) + + Parameters: + sample_size (`int`, *optional*): Default length of sample. Should be adaptable at runtime. + in_channels (`int`, *optional*, defaults to 2): Number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 2): Number of channels in the output. + time_embedding_type (`str`, *optional*, defaults to `"fourier"`): Type of time embedding to use. + freq_shift (`float`, *optional*, defaults to 0.0): Frequency shift for fourier time embedding. + flip_sin_to_cos (`bool`, *optional*, defaults to : + obj:`False`): Whether to flip sin to cos for fourier time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("DownBlock1D", "DownBlock1DNoSkip", "AttnDownBlock1D")`): Tuple of downsample block types. + up_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("UpBlock1D", "UpBlock1DNoSkip", "AttnUpBlock1D")`): Tuple of upsample block types. + block_out_channels (`Tuple[int]`, *optional*, defaults to : + obj:`(32, 32, 64)`): Tuple of block output channels. + mid_block_type (`str`, *optional*, defaults to "UNetMidBlock1D"): block type for middle of UNet. + out_block_type (`str`, *optional*, defaults to `None`): optional output processing of UNet. + act_fn (`str`, *optional*, defaults to None): optional activitation function in UNet blocks. + norm_num_groups (`int`, *optional*, defaults to 8): group norm member count in UNet blocks. + layers_per_block (`int`, *optional*, defaults to 1): added number of layers in a UNet block. + downsample_each_block (`int`, *optional*, defaults to False: + experimental feature for using a UNet without upsampling. + """ + + @register_to_config + def __init__( + self, + sample_size: int = 65536, + sample_rate: Optional[int] = None, + in_channels: int = 2, + out_channels: int = 2, + extra_in_channels: int = 0, + time_embedding_type: str = "fourier", + flip_sin_to_cos: bool = True, + use_timestep_embedding: bool = False, + freq_shift: float = 0.0, + down_block_types: Tuple[str] = ("DownBlock1DNoSkip", "DownBlock1D", "AttnDownBlock1D"), + up_block_types: Tuple[str] = ("AttnUpBlock1D", "UpBlock1D", "UpBlock1DNoSkip"), + mid_block_type: Tuple[str] = "UNetMidBlock1D", + out_block_type: str = None, + block_out_channels: Tuple[int] = (32, 32, 64), + act_fn: str = None, + norm_num_groups: int = 8, + layers_per_block: int = 1, + downsample_each_block: bool = False, + ): + super().__init__() + self.sample_size = sample_size + + # time + if time_embedding_type == "fourier": + self.time_proj = GaussianFourierProjection( + embedding_size=8, set_W_to_weight=False, log=False, flip_sin_to_cos=flip_sin_to_cos + ) + timestep_input_dim = 2 * block_out_channels[0] + elif time_embedding_type == "positional": + self.time_proj = Timesteps( + block_out_channels[0], flip_sin_to_cos=flip_sin_to_cos, downscale_freq_shift=freq_shift + ) + timestep_input_dim = block_out_channels[0] + + if use_timestep_embedding: + time_embed_dim = block_out_channels[0] * 4 + self.time_mlp = TimestepEmbedding( + in_channels=timestep_input_dim, + time_embed_dim=time_embed_dim, + act_fn=act_fn, + out_dim=block_out_channels[0], + ) + + self.down_blocks = nn.ModuleList([]) + self.mid_block = None + self.up_blocks = nn.ModuleList([]) + self.out_block = None + + # down + output_channel = in_channels + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + + if i == 0: + input_channel += extra_in_channels + + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=block_out_channels[0], + add_downsample=not is_final_block or downsample_each_block, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = get_mid_block( + mid_block_type, + in_channels=block_out_channels[-1], + mid_channels=block_out_channels[-1], + out_channels=block_out_channels[-1], + embed_dim=block_out_channels[0], + num_layers=layers_per_block, + add_downsample=downsample_each_block, + ) + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + if out_block_type is None: + final_upsample_channels = out_channels + else: + final_upsample_channels = block_out_channels[0] + + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = ( + reversed_block_out_channels[i + 1] if i < len(up_block_types) - 1 else final_upsample_channels + ) + + is_final_block = i == len(block_out_channels) - 1 + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block, + in_channels=prev_output_channel, + out_channels=output_channel, + temb_channels=block_out_channels[0], + add_upsample=not is_final_block, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + num_groups_out = norm_num_groups if norm_num_groups is not None else min(block_out_channels[0] // 4, 32) + self.out_block = get_out_block( + out_block_type=out_block_type, + num_groups_out=num_groups_out, + embed_dim=block_out_channels[0], + out_channels=out_channels, + act_fn=act_fn, + fc_dim=block_out_channels[-1] // 4, + ) + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + return_dict: bool = True, + ) -> Union[UNet1DOutput, Tuple]: + r""" + Args: + sample (`torch.FloatTensor`): `(batch_size, sample_size, num_channels)` noisy inputs tensor + timestep (`torch.FloatTensor` or `float` or `int): (batch) timesteps + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unet_1d.UNet1DOutput`] instead of a plain tuple. + + Returns: + [`~models.unet_1d.UNet1DOutput`] or `tuple`: [`~models.unet_1d.UNet1DOutput`] if `return_dict` is True, + otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + """ + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + timesteps = torch.tensor([timesteps], dtype=torch.long, device=sample.device) + elif torch.is_tensor(timesteps) and len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + timestep_embed = self.time_proj(timesteps) + if self.config.use_timestep_embedding: + timestep_embed = self.time_mlp(timestep_embed) + else: + timestep_embed = timestep_embed[..., None] + timestep_embed = timestep_embed.repeat([1, 1, sample.shape[2]]).to(sample.dtype) + timestep_embed = timestep_embed.broadcast_to((sample.shape[:1] + timestep_embed.shape[1:])) + + # 2. down + down_block_res_samples = () + for downsample_block in self.down_blocks: + sample, res_samples = downsample_block(hidden_states=sample, temb=timestep_embed) + down_block_res_samples += res_samples + + # 3. mid + if self.mid_block: + sample = self.mid_block(sample, timestep_embed) + + # 4. up + for i, upsample_block in enumerate(self.up_blocks): + res_samples = down_block_res_samples[-1:] + down_block_res_samples = down_block_res_samples[:-1] + sample = upsample_block(sample, res_hidden_states_tuple=res_samples, temb=timestep_embed) + + # 5. post-process + if self.out_block: + sample = self.out_block(sample, timestep_embed) + + if not return_dict: + return (sample,) + + return UNet1DOutput(sample=sample) diff --git a/diffusers/src/diffusers/models/unet_1d_blocks.py b/diffusers/src/diffusers/models/unet_1d_blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..fc758ebbb044644e921c7e66089e052981a82e1e --- /dev/null +++ b/diffusers/src/diffusers/models/unet_1d_blocks.py @@ -0,0 +1,668 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import math + +import torch +import torch.nn.functional as F +from torch import nn + +from .resnet import Downsample1D, ResidualTemporalBlock1D, Upsample1D, rearrange_dims + + +class DownResnetBlock1D(nn.Module): + def __init__( + self, + in_channels, + out_channels=None, + num_layers=1, + conv_shortcut=False, + temb_channels=32, + groups=32, + groups_out=None, + non_linearity=None, + time_embedding_norm="default", + output_scale_factor=1.0, + add_downsample=True, + ): + super().__init__() + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + self.use_conv_shortcut = conv_shortcut + self.time_embedding_norm = time_embedding_norm + self.add_downsample = add_downsample + self.output_scale_factor = output_scale_factor + + if groups_out is None: + groups_out = groups + + # there will always be at least one resnet + resnets = [ResidualTemporalBlock1D(in_channels, out_channels, embed_dim=temb_channels)] + + for _ in range(num_layers): + resnets.append(ResidualTemporalBlock1D(out_channels, out_channels, embed_dim=temb_channels)) + + self.resnets = nn.ModuleList(resnets) + + if non_linearity == "swish": + self.nonlinearity = lambda x: F.silu(x) + elif non_linearity == "mish": + self.nonlinearity = nn.Mish() + elif non_linearity == "silu": + self.nonlinearity = nn.SiLU() + else: + self.nonlinearity = None + + self.downsample = None + if add_downsample: + self.downsample = Downsample1D(out_channels, use_conv=True, padding=1) + + def forward(self, hidden_states, temb=None): + output_states = () + + hidden_states = self.resnets[0](hidden_states, temb) + for resnet in self.resnets[1:]: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.nonlinearity is not None: + hidden_states = self.nonlinearity(hidden_states) + + if self.downsample is not None: + hidden_states = self.downsample(hidden_states) + + return hidden_states, output_states + + +class UpResnetBlock1D(nn.Module): + def __init__( + self, + in_channels, + out_channels=None, + num_layers=1, + temb_channels=32, + groups=32, + groups_out=None, + non_linearity=None, + time_embedding_norm="default", + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + self.time_embedding_norm = time_embedding_norm + self.add_upsample = add_upsample + self.output_scale_factor = output_scale_factor + + if groups_out is None: + groups_out = groups + + # there will always be at least one resnet + resnets = [ResidualTemporalBlock1D(2 * in_channels, out_channels, embed_dim=temb_channels)] + + for _ in range(num_layers): + resnets.append(ResidualTemporalBlock1D(out_channels, out_channels, embed_dim=temb_channels)) + + self.resnets = nn.ModuleList(resnets) + + if non_linearity == "swish": + self.nonlinearity = lambda x: F.silu(x) + elif non_linearity == "mish": + self.nonlinearity = nn.Mish() + elif non_linearity == "silu": + self.nonlinearity = nn.SiLU() + else: + self.nonlinearity = None + + self.upsample = None + if add_upsample: + self.upsample = Upsample1D(out_channels, use_conv_transpose=True) + + def forward(self, hidden_states, res_hidden_states_tuple=None, temb=None): + if res_hidden_states_tuple is not None: + res_hidden_states = res_hidden_states_tuple[-1] + hidden_states = torch.cat((hidden_states, res_hidden_states), dim=1) + + hidden_states = self.resnets[0](hidden_states, temb) + for resnet in self.resnets[1:]: + hidden_states = resnet(hidden_states, temb) + + if self.nonlinearity is not None: + hidden_states = self.nonlinearity(hidden_states) + + if self.upsample is not None: + hidden_states = self.upsample(hidden_states) + + return hidden_states + + +class ValueFunctionMidBlock1D(nn.Module): + def __init__(self, in_channels, out_channels, embed_dim): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.embed_dim = embed_dim + + self.res1 = ResidualTemporalBlock1D(in_channels, in_channels // 2, embed_dim=embed_dim) + self.down1 = Downsample1D(out_channels // 2, use_conv=True) + self.res2 = ResidualTemporalBlock1D(in_channels // 2, in_channels // 4, embed_dim=embed_dim) + self.down2 = Downsample1D(out_channels // 4, use_conv=True) + + def forward(self, x, temb=None): + x = self.res1(x, temb) + x = self.down1(x) + x = self.res2(x, temb) + x = self.down2(x) + return x + + +class MidResTemporalBlock1D(nn.Module): + def __init__( + self, + in_channels, + out_channels, + embed_dim, + num_layers: int = 1, + add_downsample: bool = False, + add_upsample: bool = False, + non_linearity=None, + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.add_downsample = add_downsample + + # there will always be at least one resnet + resnets = [ResidualTemporalBlock1D(in_channels, out_channels, embed_dim=embed_dim)] + + for _ in range(num_layers): + resnets.append(ResidualTemporalBlock1D(out_channels, out_channels, embed_dim=embed_dim)) + + self.resnets = nn.ModuleList(resnets) + + if non_linearity == "swish": + self.nonlinearity = lambda x: F.silu(x) + elif non_linearity == "mish": + self.nonlinearity = nn.Mish() + elif non_linearity == "silu": + self.nonlinearity = nn.SiLU() + else: + self.nonlinearity = None + + self.upsample = None + if add_upsample: + self.upsample = Downsample1D(out_channels, use_conv=True) + + self.downsample = None + if add_downsample: + self.downsample = Downsample1D(out_channels, use_conv=True) + + if self.upsample and self.downsample: + raise ValueError("Block cannot downsample and upsample") + + def forward(self, hidden_states, temb): + hidden_states = self.resnets[0](hidden_states, temb) + for resnet in self.resnets[1:]: + hidden_states = resnet(hidden_states, temb) + + if self.upsample: + hidden_states = self.upsample(hidden_states) + if self.downsample: + self.downsample = self.downsample(hidden_states) + + return hidden_states + + +class OutConv1DBlock(nn.Module): + def __init__(self, num_groups_out, out_channels, embed_dim, act_fn): + super().__init__() + self.final_conv1d_1 = nn.Conv1d(embed_dim, embed_dim, 5, padding=2) + self.final_conv1d_gn = nn.GroupNorm(num_groups_out, embed_dim) + if act_fn == "silu": + self.final_conv1d_act = nn.SiLU() + if act_fn == "mish": + self.final_conv1d_act = nn.Mish() + self.final_conv1d_2 = nn.Conv1d(embed_dim, out_channels, 1) + + def forward(self, hidden_states, temb=None): + hidden_states = self.final_conv1d_1(hidden_states) + hidden_states = rearrange_dims(hidden_states) + hidden_states = self.final_conv1d_gn(hidden_states) + hidden_states = rearrange_dims(hidden_states) + hidden_states = self.final_conv1d_act(hidden_states) + hidden_states = self.final_conv1d_2(hidden_states) + return hidden_states + + +class OutValueFunctionBlock(nn.Module): + def __init__(self, fc_dim, embed_dim): + super().__init__() + self.final_block = nn.ModuleList( + [ + nn.Linear(fc_dim + embed_dim, fc_dim // 2), + nn.Mish(), + nn.Linear(fc_dim // 2, 1), + ] + ) + + def forward(self, hidden_states, temb): + hidden_states = hidden_states.view(hidden_states.shape[0], -1) + hidden_states = torch.cat((hidden_states, temb), dim=-1) + for layer in self.final_block: + hidden_states = layer(hidden_states) + + return hidden_states + + +_kernels = { + "linear": [1 / 8, 3 / 8, 3 / 8, 1 / 8], + "cubic": [-0.01171875, -0.03515625, 0.11328125, 0.43359375, 0.43359375, 0.11328125, -0.03515625, -0.01171875], + "lanczos3": [ + 0.003689131001010537, + 0.015056144446134567, + -0.03399861603975296, + -0.066637322306633, + 0.13550527393817902, + 0.44638532400131226, + 0.44638532400131226, + 0.13550527393817902, + -0.066637322306633, + -0.03399861603975296, + 0.015056144446134567, + 0.003689131001010537, + ], +} + + +class Downsample1d(nn.Module): + def __init__(self, kernel="linear", pad_mode="reflect"): + super().__init__() + self.pad_mode = pad_mode + kernel_1d = torch.tensor(_kernels[kernel]) + self.pad = kernel_1d.shape[0] // 2 - 1 + self.register_buffer("kernel", kernel_1d) + + def forward(self, hidden_states): + hidden_states = F.pad(hidden_states, (self.pad,) * 2, self.pad_mode) + weight = hidden_states.new_zeros([hidden_states.shape[1], hidden_states.shape[1], self.kernel.shape[0]]) + indices = torch.arange(hidden_states.shape[1], device=hidden_states.device) + weight[indices, indices] = self.kernel.to(weight) + return F.conv1d(hidden_states, weight, stride=2) + + +class Upsample1d(nn.Module): + def __init__(self, kernel="linear", pad_mode="reflect"): + super().__init__() + self.pad_mode = pad_mode + kernel_1d = torch.tensor(_kernels[kernel]) * 2 + self.pad = kernel_1d.shape[0] // 2 - 1 + self.register_buffer("kernel", kernel_1d) + + def forward(self, hidden_states, temb=None): + hidden_states = F.pad(hidden_states, ((self.pad + 1) // 2,) * 2, self.pad_mode) + weight = hidden_states.new_zeros([hidden_states.shape[1], hidden_states.shape[1], self.kernel.shape[0]]) + indices = torch.arange(hidden_states.shape[1], device=hidden_states.device) + weight[indices, indices] = self.kernel.to(weight) + return F.conv_transpose1d(hidden_states, weight, stride=2, padding=self.pad * 2 + 1) + + +class SelfAttention1d(nn.Module): + def __init__(self, in_channels, n_head=1, dropout_rate=0.0): + super().__init__() + self.channels = in_channels + self.group_norm = nn.GroupNorm(1, num_channels=in_channels) + self.num_heads = n_head + + self.query = nn.Linear(self.channels, self.channels) + self.key = nn.Linear(self.channels, self.channels) + self.value = nn.Linear(self.channels, self.channels) + + self.proj_attn = nn.Linear(self.channels, self.channels, 1) + + self.dropout = nn.Dropout(dropout_rate, inplace=True) + + def transpose_for_scores(self, projection: torch.Tensor) -> torch.Tensor: + new_projection_shape = projection.size()[:-1] + (self.num_heads, -1) + # move heads to 2nd position (B, T, H * D) -> (B, T, H, D) -> (B, H, T, D) + new_projection = projection.view(new_projection_shape).permute(0, 2, 1, 3) + return new_projection + + def forward(self, hidden_states): + residual = hidden_states + batch, channel_dim, seq = hidden_states.shape + + hidden_states = self.group_norm(hidden_states) + hidden_states = hidden_states.transpose(1, 2) + + query_proj = self.query(hidden_states) + key_proj = self.key(hidden_states) + value_proj = self.value(hidden_states) + + query_states = self.transpose_for_scores(query_proj) + key_states = self.transpose_for_scores(key_proj) + value_states = self.transpose_for_scores(value_proj) + + scale = 1 / math.sqrt(math.sqrt(key_states.shape[-1])) + + attention_scores = torch.matmul(query_states * scale, key_states.transpose(-1, -2) * scale) + attention_probs = torch.softmax(attention_scores, dim=-1) + + # compute attention output + hidden_states = torch.matmul(attention_probs, value_states) + + hidden_states = hidden_states.permute(0, 2, 1, 3).contiguous() + new_hidden_states_shape = hidden_states.size()[:-2] + (self.channels,) + hidden_states = hidden_states.view(new_hidden_states_shape) + + # compute next hidden_states + hidden_states = self.proj_attn(hidden_states) + hidden_states = hidden_states.transpose(1, 2) + hidden_states = self.dropout(hidden_states) + + output = hidden_states + residual + + return output + + +class ResConvBlock(nn.Module): + def __init__(self, in_channels, mid_channels, out_channels, is_last=False): + super().__init__() + self.is_last = is_last + self.has_conv_skip = in_channels != out_channels + + if self.has_conv_skip: + self.conv_skip = nn.Conv1d(in_channels, out_channels, 1, bias=False) + + self.conv_1 = nn.Conv1d(in_channels, mid_channels, 5, padding=2) + self.group_norm_1 = nn.GroupNorm(1, mid_channels) + self.gelu_1 = nn.GELU() + self.conv_2 = nn.Conv1d(mid_channels, out_channels, 5, padding=2) + + if not self.is_last: + self.group_norm_2 = nn.GroupNorm(1, out_channels) + self.gelu_2 = nn.GELU() + + def forward(self, hidden_states): + residual = self.conv_skip(hidden_states) if self.has_conv_skip else hidden_states + + hidden_states = self.conv_1(hidden_states) + hidden_states = self.group_norm_1(hidden_states) + hidden_states = self.gelu_1(hidden_states) + hidden_states = self.conv_2(hidden_states) + + if not self.is_last: + hidden_states = self.group_norm_2(hidden_states) + hidden_states = self.gelu_2(hidden_states) + + output = hidden_states + residual + return output + + +class UNetMidBlock1D(nn.Module): + def __init__(self, mid_channels, in_channels, out_channels=None): + super().__init__() + + out_channels = in_channels if out_channels is None else out_channels + + # there is always at least one resnet + self.down = Downsample1d("cubic") + resnets = [ + ResConvBlock(in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + attentions = [ + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(out_channels, out_channels // 32), + ] + self.up = Upsample1d(kernel="cubic") + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, temb=None): + hidden_states = self.down(hidden_states) + for attn, resnet in zip(self.attentions, self.resnets): + hidden_states = resnet(hidden_states) + hidden_states = attn(hidden_states) + + hidden_states = self.up(hidden_states) + + return hidden_states + + +class AttnDownBlock1D(nn.Module): + def __init__(self, out_channels, in_channels, mid_channels=None): + super().__init__() + mid_channels = out_channels if mid_channels is None else mid_channels + + self.down = Downsample1d("cubic") + resnets = [ + ResConvBlock(in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + attentions = [ + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(out_channels, out_channels // 32), + ] + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, temb=None): + hidden_states = self.down(hidden_states) + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states) + hidden_states = attn(hidden_states) + + return hidden_states, (hidden_states,) + + +class DownBlock1D(nn.Module): + def __init__(self, out_channels, in_channels, mid_channels=None): + super().__init__() + mid_channels = out_channels if mid_channels is None else mid_channels + + self.down = Downsample1d("cubic") + resnets = [ + ResConvBlock(in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, temb=None): + hidden_states = self.down(hidden_states) + + for resnet in self.resnets: + hidden_states = resnet(hidden_states) + + return hidden_states, (hidden_states,) + + +class DownBlock1DNoSkip(nn.Module): + def __init__(self, out_channels, in_channels, mid_channels=None): + super().__init__() + mid_channels = out_channels if mid_channels is None else mid_channels + + resnets = [ + ResConvBlock(in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, temb=None): + hidden_states = torch.cat([hidden_states, temb], dim=1) + for resnet in self.resnets: + hidden_states = resnet(hidden_states) + + return hidden_states, (hidden_states,) + + +class AttnUpBlock1D(nn.Module): + def __init__(self, in_channels, out_channels, mid_channels=None): + super().__init__() + mid_channels = out_channels if mid_channels is None else mid_channels + + resnets = [ + ResConvBlock(2 * in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + attentions = [ + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(mid_channels, mid_channels // 32), + SelfAttention1d(out_channels, out_channels // 32), + ] + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + self.up = Upsample1d(kernel="cubic") + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None): + res_hidden_states = res_hidden_states_tuple[-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states) + hidden_states = attn(hidden_states) + + hidden_states = self.up(hidden_states) + + return hidden_states + + +class UpBlock1D(nn.Module): + def __init__(self, in_channels, out_channels, mid_channels=None): + super().__init__() + mid_channels = in_channels if mid_channels is None else mid_channels + + resnets = [ + ResConvBlock(2 * in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels), + ] + + self.resnets = nn.ModuleList(resnets) + self.up = Upsample1d(kernel="cubic") + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None): + res_hidden_states = res_hidden_states_tuple[-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + for resnet in self.resnets: + hidden_states = resnet(hidden_states) + + hidden_states = self.up(hidden_states) + + return hidden_states + + +class UpBlock1DNoSkip(nn.Module): + def __init__(self, in_channels, out_channels, mid_channels=None): + super().__init__() + mid_channels = in_channels if mid_channels is None else mid_channels + + resnets = [ + ResConvBlock(2 * in_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, mid_channels), + ResConvBlock(mid_channels, mid_channels, out_channels, is_last=True), + ] + + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None): + res_hidden_states = res_hidden_states_tuple[-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + for resnet in self.resnets: + hidden_states = resnet(hidden_states) + + return hidden_states + + +def get_down_block(down_block_type, num_layers, in_channels, out_channels, temb_channels, add_downsample): + if down_block_type == "DownResnetBlock1D": + return DownResnetBlock1D( + in_channels=in_channels, + num_layers=num_layers, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + ) + elif down_block_type == "DownBlock1D": + return DownBlock1D(out_channels=out_channels, in_channels=in_channels) + elif down_block_type == "AttnDownBlock1D": + return AttnDownBlock1D(out_channels=out_channels, in_channels=in_channels) + elif down_block_type == "DownBlock1DNoSkip": + return DownBlock1DNoSkip(out_channels=out_channels, in_channels=in_channels) + raise ValueError(f"{down_block_type} does not exist.") + + +def get_up_block(up_block_type, num_layers, in_channels, out_channels, temb_channels, add_upsample): + if up_block_type == "UpResnetBlock1D": + return UpResnetBlock1D( + in_channels=in_channels, + num_layers=num_layers, + out_channels=out_channels, + temb_channels=temb_channels, + add_upsample=add_upsample, + ) + elif up_block_type == "UpBlock1D": + return UpBlock1D(in_channels=in_channels, out_channels=out_channels) + elif up_block_type == "AttnUpBlock1D": + return AttnUpBlock1D(in_channels=in_channels, out_channels=out_channels) + elif up_block_type == "UpBlock1DNoSkip": + return UpBlock1DNoSkip(in_channels=in_channels, out_channels=out_channels) + raise ValueError(f"{up_block_type} does not exist.") + + +def get_mid_block(mid_block_type, num_layers, in_channels, mid_channels, out_channels, embed_dim, add_downsample): + if mid_block_type == "MidResTemporalBlock1D": + return MidResTemporalBlock1D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + embed_dim=embed_dim, + add_downsample=add_downsample, + ) + elif mid_block_type == "ValueFunctionMidBlock1D": + return ValueFunctionMidBlock1D(in_channels=in_channels, out_channels=out_channels, embed_dim=embed_dim) + elif mid_block_type == "UNetMidBlock1D": + return UNetMidBlock1D(in_channels=in_channels, mid_channels=mid_channels, out_channels=out_channels) + raise ValueError(f"{mid_block_type} does not exist.") + + +def get_out_block(*, out_block_type, num_groups_out, embed_dim, out_channels, act_fn, fc_dim): + if out_block_type == "OutConv1DBlock": + return OutConv1DBlock(num_groups_out, out_channels, embed_dim, act_fn) + elif out_block_type == "ValueFunction": + return OutValueFunctionBlock(fc_dim, embed_dim) + return None diff --git a/diffusers/src/diffusers/models/unet_2d.py b/diffusers/src/diffusers/models/unet_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..6b67990dac0652c854265b3ccea203617fa5d5ef --- /dev/null +++ b/diffusers/src/diffusers/models/unet_2d.py @@ -0,0 +1,314 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import torch +import torch.nn as nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .embeddings import GaussianFourierProjection, TimestepEmbedding, Timesteps +from .modeling_utils import ModelMixin +from .unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block + + +@dataclass +class UNet2DOutput(BaseOutput): + """ + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Hidden states output. Output of last layer of model. + """ + + sample: torch.FloatTensor + + +class UNet2DModel(ModelMixin, ConfigMixin): + r""" + UNet2DModel is a 2D UNet model that takes in a noisy sample and a timestep and returns sample shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the model (such as downloading or saving, etc.) + + Parameters: + sample_size (`int` or `Tuple[int, int]`, *optional*, defaults to `None`): + Height and width of input/output sample. + in_channels (`int`, *optional*, defaults to 3): Number of channels in the input image. + out_channels (`int`, *optional*, defaults to 3): Number of channels in the output. + center_input_sample (`bool`, *optional*, defaults to `False`): Whether to center the input sample. + time_embedding_type (`str`, *optional*, defaults to `"positional"`): Type of time embedding to use. + freq_shift (`int`, *optional*, defaults to 0): Frequency shift for fourier time embedding. + flip_sin_to_cos (`bool`, *optional*, defaults to : + obj:`True`): Whether to flip sin to cos for fourier time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("DownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D")`): Tuple of downsample block + types. + mid_block_type (`str`, *optional*, defaults to `"UNetMidBlock2D"`): + The mid block type. Choose from `UNetMidBlock2D` or `UnCLIPUNetMidBlock2D`. + up_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("AttnUpBlock2D", "AttnUpBlock2D", "AttnUpBlock2D", "UpBlock2D")`): Tuple of upsample block types. + block_out_channels (`Tuple[int]`, *optional*, defaults to : + obj:`(224, 448, 672, 896)`): Tuple of block output channels. + layers_per_block (`int`, *optional*, defaults to `2`): The number of layers per block. + mid_block_scale_factor (`float`, *optional*, defaults to `1`): The scale factor for the mid block. + downsample_padding (`int`, *optional*, defaults to `1`): The padding for the downsample convolution. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + attention_head_dim (`int`, *optional*, defaults to `8`): The attention head dimension. + norm_num_groups (`int`, *optional*, defaults to `32`): The number of groups for the normalization. + norm_eps (`float`, *optional*, defaults to `1e-5`): The epsilon for the normalization. + resnet_time_scale_shift (`str`, *optional*, defaults to `"default"`): Time scale shift config + for resnet blocks, see [`~models.resnet.ResnetBlock2D`]. Choose from `default` or `scale_shift`. + class_embed_type (`str`, *optional*, defaults to None): The type of class embedding to use which is ultimately + summed with the time embeddings. Choose from `None`, `"timestep"`, or `"identity"`. + num_class_embeds (`int`, *optional*, defaults to None): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + """ + + @register_to_config + def __init__( + self, + sample_size: Optional[Union[int, Tuple[int, int]]] = None, + in_channels: int = 3, + out_channels: int = 3, + center_input_sample: bool = False, + time_embedding_type: str = "positional", + freq_shift: int = 0, + flip_sin_to_cos: bool = True, + down_block_types: Tuple[str] = ("DownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D", "AttnDownBlock2D"), + up_block_types: Tuple[str] = ("AttnUpBlock2D", "AttnUpBlock2D", "AttnUpBlock2D", "UpBlock2D"), + block_out_channels: Tuple[int] = (224, 448, 672, 896), + layers_per_block: int = 2, + mid_block_scale_factor: float = 1, + downsample_padding: int = 1, + act_fn: str = "silu", + attention_head_dim: Optional[int] = 8, + norm_num_groups: int = 32, + norm_eps: float = 1e-5, + resnet_time_scale_shift: str = "default", + add_attention: bool = True, + class_embed_type: Optional[str] = None, + num_class_embeds: Optional[int] = None, + ): + super().__init__() + + self.sample_size = sample_size + time_embed_dim = block_out_channels[0] * 4 + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + # input + self.conv_in = nn.Conv2d(in_channels, block_out_channels[0], kernel_size=3, padding=(1, 1)) + + # time + if time_embedding_type == "fourier": + self.time_proj = GaussianFourierProjection(embedding_size=block_out_channels[0], scale=16) + timestep_input_dim = 2 * block_out_channels[0] + elif time_embedding_type == "positional": + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + + self.time_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + else: + self.class_embedding = None + + self.down_blocks = nn.ModuleList([]) + self.mid_block = None + self.up_blocks = nn.ModuleList([]) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attn_num_head_channels=attention_head_dim, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + attn_num_head_channels=attention_head_dim, + resnet_groups=norm_num_groups, + add_attention=add_attention, + ) + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + is_final_block = i == len(block_out_channels) - 1 + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=time_embed_dim, + add_upsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attn_num_head_channels=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + num_groups_out = norm_num_groups if norm_num_groups is not None else min(block_out_channels[0] // 4, 32) + self.conv_norm_out = nn.GroupNorm(num_channels=block_out_channels[0], num_groups=num_groups_out, eps=norm_eps) + self.conv_act = nn.SiLU() + self.conv_out = nn.Conv2d(block_out_channels[0], out_channels, kernel_size=3, padding=1) + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + class_labels: Optional[torch.Tensor] = None, + return_dict: bool = True, + ) -> Union[UNet2DOutput, Tuple]: + r""" + Args: + sample (`torch.FloatTensor`): (batch, channel, height, width) noisy inputs tensor + timestep (`torch.FloatTensor` or `float` or `int): (batch) timesteps + class_labels (`torch.FloatTensor`, *optional*, defaults to `None`): + Optional class labels for conditioning. Their embeddings will be summed with the timestep embeddings. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unet_2d.UNet2DOutput`] instead of a plain tuple. + + Returns: + [`~models.unet_2d.UNet2DOutput`] or `tuple`: [`~models.unet_2d.UNet2DOutput`] if `return_dict` is True, + otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + """ + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + timesteps = torch.tensor([timesteps], dtype=torch.long, device=sample.device) + elif torch.is_tensor(timesteps) and len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps * torch.ones(sample.shape[0], dtype=timesteps.dtype, device=timesteps.device) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=self.dtype) + emb = self.time_embedding(t_emb) + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when doing class conditioning") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + # 2. pre-process + skip_sample = sample + sample = self.conv_in(sample) + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "skip_conv"): + sample, res_samples, skip_sample = downsample_block( + hidden_states=sample, temb=emb, skip_sample=skip_sample + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + + down_block_res_samples += res_samples + + # 4. mid + sample = self.mid_block(sample, emb) + + # 5. up + skip_sample = None + for upsample_block in self.up_blocks: + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + if hasattr(upsample_block, "skip_conv"): + sample, skip_sample = upsample_block(sample, res_samples, emb, skip_sample) + else: + sample = upsample_block(sample, res_samples, emb) + + # 6. post-process + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if skip_sample is not None: + sample += skip_sample + + if self.config.time_embedding_type == "fourier": + timesteps = timesteps.reshape((sample.shape[0], *([1] * len(sample.shape[1:])))) + sample = sample / timesteps + + if not return_dict: + return (sample,) + + return UNet2DOutput(sample=sample) diff --git a/diffusers/src/diffusers/models/unet_2d_blocks.py b/diffusers/src/diffusers/models/unet_2d_blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..0b6a767d7a7a84201ca22df86262e16146b2cc13 --- /dev/null +++ b/diffusers/src/diffusers/models/unet_2d_blocks.py @@ -0,0 +1,2749 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from typing import Optional + +import numpy as np +import torch +from torch import nn + +from .attention import AdaGroupNorm, AttentionBlock +from .cross_attention import CrossAttention, CrossAttnAddedKVProcessor +from .dual_transformer_2d import DualTransformer2DModel +from .resnet import Downsample2D, FirDownsample2D, FirUpsample2D, KDownsample2D, KUpsample2D, ResnetBlock2D, Upsample2D +from .transformer_2d import Transformer2DModel + + +def get_down_block( + down_block_type, + num_layers, + in_channels, + out_channels, + temb_channels, + add_downsample, + resnet_eps, + resnet_act_fn, + attn_num_head_channels, + resnet_groups=None, + cross_attention_dim=None, + downsample_padding=None, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + resnet_time_scale_shift="default", +): + down_block_type = down_block_type[7:] if down_block_type.startswith("UNetRes") else down_block_type + if down_block_type == "DownBlock2D": + return DownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "ResnetDownsampleBlock2D": + return ResnetDownsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnDownBlock2D": + return AttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "CrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnDownBlock2D") + return CrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "SimpleCrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnDownBlock2D") + return SimpleCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "SkipDownBlock2D": + return SkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnSkipDownBlock2D": + return AttnSkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + downsample_padding=downsample_padding, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "DownEncoderBlock2D": + return DownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnDownEncoderBlock2D": + return AttnDownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "KDownBlock2D": + return KDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif down_block_type == "KCrossAttnDownBlock2D": + return KCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + add_self_attention=True if not add_downsample else False, + ) + raise ValueError(f"{down_block_type} does not exist.") + + +def get_up_block( + up_block_type, + num_layers, + in_channels, + out_channels, + prev_output_channel, + temb_channels, + add_upsample, + resnet_eps, + resnet_act_fn, + attn_num_head_channels, + resnet_groups=None, + cross_attention_dim=None, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + resnet_time_scale_shift="default", +): + up_block_type = up_block_type[7:] if up_block_type.startswith("UNetRes") else up_block_type + if up_block_type == "UpBlock2D": + return UpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "ResnetUpsampleBlock2D": + return ResnetUpsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "CrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnUpBlock2D") + return CrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "SimpleCrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnUpBlock2D") + return SimpleCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "AttnUpBlock2D": + return AttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "SkipUpBlock2D": + return SkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "AttnSkipUpBlock2D": + return AttnSkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "UpDecoderBlock2D": + return UpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "AttnUpDecoderBlock2D": + return AttnUpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attn_num_head_channels=attn_num_head_channels, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "KUpBlock2D": + return KUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif up_block_type == "KCrossAttnUpBlock2D": + return KCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + ) + + raise ValueError(f"{up_block_type} does not exist.") + + +class UNetMidBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + add_attention: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + ): + super().__init__() + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + self.add_attention = add_attention + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for _ in range(num_layers): + if self.add_attention: + attentions.append( + AttentionBlock( + in_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + ) + ) + else: + attentions.append(None) + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states, temb=None): + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + if attn is not None: + hidden_states = attn(hidden_states) + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class UNetMidBlock2DCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + cross_attention_dim=1280, + dual_cross_attention=False, + use_linear_projection=False, + upcast_attention=False, + ): + super().__init__() + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for _ in range(num_layers): + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + in_channels // attn_num_head_channels, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + in_channels // attn_num_head_channels, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class UNetMidBlock2DSimpleCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + cross_attention_dim=1280, + ): + super().__init__() + + self.has_cross_attention = True + + self.attn_num_head_channels = attn_num_head_channels + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + self.num_heads = in_channels // self.attn_num_head_channels + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for _ in range(num_layers): + attentions.append( + CrossAttention( + query_dim=in_channels, + cross_attention_dim=in_channels, + heads=self.num_heads, + dim_head=attn_num_head_channels, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + processor=CrossAttnAddedKVProcessor(), + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + # resnet + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class AttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + downsample_padding=1, + add_downsample=True, + ): + super().__init__() + resnets = [] + attentions = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states, temb=None): + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states) + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class CrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + downsample_padding=1, + add_downsample=True, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + # TODO(Patrick, William) - attention mask is not used + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + cross_attention_kwargs, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class DownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_downsample=True, + downsample_padding=1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, temb=None): + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class DownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_downsample=True, + downsample_padding=1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states): + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=None) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states + + +class AttnDownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + add_downsample=True, + downsample_padding=1, + ): + super().__init__() + resnets = [] + attentions = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states): + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=None) + hidden_states = attn(hidden_states) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states + + +class AttnSkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=np.sqrt(2.0), + downsample_padding=1, + add_downsample=True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + self.attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward(self, hidden_states, temb=None, skip_sample=None): + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class SkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor=np.sqrt(2.0), + add_downsample=True, + downsample_padding=1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward(self, hidden_states, temb=None, skip_sample=None): + output_states = () + + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class ResnetDownsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_downsample=True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, temb=None): + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class SimpleCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + add_downsample=True, + ): + super().__init__() + + self.has_cross_attention = True + + resnets = [] + attentions = [] + + self.attn_num_head_channels = attn_num_head_channels + self.num_heads = out_channels // self.attn_num_head_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + CrossAttention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=attn_num_head_channels, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + processor=CrossAttnAddedKVProcessor(), + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + output_states = () + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + for resnet, attn in zip(self.resnets, self.attentions): + # resnet + hidden_states = resnet(hidden_states, temb) + + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class KDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + add_downsample=False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + # YiYi's comments- might be able to use FirDownsample2D, look into details later + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, temb=None): + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class KCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + cross_attention_dim: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_group_size: int = 32, + add_downsample=True, + attn_num_head_channels: int = 64, + add_self_attention: bool = False, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + out_channels, + out_channels // attn_num_head_channels, + attn_num_head_channels, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm=True, + group_size=resnet_group_size, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_downsample: + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + attention_mask, + cross_attention_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + + if self.downsamplers is None: + output_states += (None,) + else: + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class AttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + attentions = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None): + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class CrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + add_upsample=True, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states, + res_hidden_states_tuple, + temb=None, + encoder_hidden_states=None, + cross_attention_kwargs=None, + upsample_size=None, + attention_mask=None, + ): + # TODO(Patrick, William) - attention mask is not used + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + cross_attention_kwargs, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + + return hidden_states + + +class UpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, upsample_size=None): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + + return hidden_states + + +class UpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + def forward(self, hidden_states): + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=None) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class AttnUpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + attentions = [] + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + def forward(self, hidden_states): + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=None) + hidden_states = attn(hidden_states) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class AttnSkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=np.sqrt(2.0), + upsample_padding=1, + add_upsample=True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(resnet_in_channels + res_skip_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions.append( + AttentionBlock( + out_channels, + num_head_channels=attn_num_head_channels, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, skip_sample=None): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + + hidden_states = self.attentions[0](hidden_states) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb) + + return hidden_states, skip_sample + + +class SkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor=np.sqrt(2.0), + add_upsample=True, + upsample_padding=1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min((resnet_in_channels + res_skip_channels) // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, skip_sample=None): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb) + + return hidden_states, skip_sample + + +class ResnetUpsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, upsample_size=None): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb) + + return hidden_states + + +class SimpleCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + self.num_heads = out_channels // self.attn_num_head_channels + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + CrossAttention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=attn_num_head_channels, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + processor=CrossAttnAddedKVProcessor(), + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states, + res_hidden_states_tuple, + temb=None, + encoder_hidden_states=None, + upsample_size=None, + attention_mask=None, + cross_attention_kwargs=None, + ): + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + for resnet, attn in zip(self.resnets, self.attentions): + # resnet + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb) + + return hidden_states + + +class KUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 5, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: Optional[int] = 32, + add_upsample=True, + ): + super().__init__() + resnets = [] + k_in_channels = 2 * out_channels + k_out_channels = in_channels + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=k_out_channels if (i == num_layers - 1) else out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, upsample_size=None): + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class KCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + attn_num_head_channels=1, # attention dim_head + cross_attention_dim: int = 768, + add_upsample: bool = True, + upcast_attention: bool = False, + ): + super().__init__() + resnets = [] + attentions = [] + + is_first_block = in_channels == out_channels == temb_channels + is_middle_block = in_channels != out_channels + add_self_attention = True if is_first_block else False + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + # in_channels, and out_channels for the block (k-unet) + k_in_channels = out_channels if is_first_block else 2 * out_channels + k_out_channels = in_channels + + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + if is_middle_block and (i == num_layers - 1): + conv_2d_out_channels = k_out_channels + else: + conv_2d_out_channels = None + + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + conv_2d_out_channels=conv_2d_out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + k_out_channels if (i == num_layers - 1) else out_channels, + k_out_channels // attn_num_head_channels + if (i == num_layers - 1) + else out_channels // attn_num_head_channels, + attn_num_head_channels, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm=True, + upcast_attention=upcast_attention, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states, + res_hidden_states_tuple, + temb=None, + encoder_hidden_states=None, + cross_attention_kwargs=None, + upsample_size=None, + attention_mask=None, + ): + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + attention_mask, + cross_attention_kwargs, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +# can potentially later be renamed to `No-feed-forward` attention +class KAttentionBlock(nn.Module): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm (: + obj: `int`, *optional*): The number of diffusion steps used during training. See `Transformer2DModel`. + attention_bias (: + obj: `bool`, *optional*, defaults to `False`): Configure if the attentions should contain a bias parameter. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout: float = 0.0, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + upcast_attention: bool = False, + temb_channels: int = 768, # for ada_group_norm + add_self_attention: bool = False, + cross_attention_norm: bool = False, + group_size: int = 32, + ): + super().__init__() + self.add_self_attention = add_self_attention + + # 1. Self-Attn + if add_self_attention: + self.norm1 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn1 = CrossAttention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=None, + cross_attention_norm=None, + ) + + # 2. Cross-Attn + self.norm2 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn2 = CrossAttention( + query_dim=dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + cross_attention_norm=cross_attention_norm, + ) + + def _to_3d(self, hidden_states, height, weight): + return hidden_states.permute(0, 2, 3, 1).reshape(hidden_states.shape[0], height * weight, -1) + + def _to_4d(self, hidden_states, height, weight): + return hidden_states.permute(0, 2, 1).reshape(hidden_states.shape[0], -1, height, weight) + + def forward( + self, + hidden_states, + encoder_hidden_states=None, + emb=None, + attention_mask=None, + cross_attention_kwargs=None, + ): + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + + # 1. Self-Attention + if self.add_self_attention: + norm_hidden_states = self.norm1(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=None, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + # 2. Cross-Attention/None + norm_hidden_states = self.norm2(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + return hidden_states diff --git a/diffusers/src/diffusers/models/unet_2d_blocks_flax.py b/diffusers/src/diffusers/models/unet_2d_blocks_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..96e76cb06a59a31beebf4449786b72a7c838a298 --- /dev/null +++ b/diffusers/src/diffusers/models/unet_2d_blocks_flax.py @@ -0,0 +1,365 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import flax.linen as nn +import jax.numpy as jnp + +from .attention_flax import FlaxTransformer2DModel +from .resnet_flax import FlaxDownsample2D, FlaxResnetBlock2D, FlaxUpsample2D + + +class FlaxCrossAttnDownBlock2D(nn.Module): + r""" + Cross Attention 2D Downsizing block - original architecture from Unet transformers: + https://arxiv.org/abs/2103.06104 + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of attention blocks layers + attn_num_head_channels (:obj:`int`, *optional*, defaults to 1): + Number of attention heads of each spatial transformer block + add_downsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add downsampling layer before each final output + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + dropout: float = 0.0 + num_layers: int = 1 + attn_num_head_channels: int = 1 + add_downsample: bool = True + use_linear_projection: bool = False + only_cross_attention: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + attentions = [] + + for i in range(self.num_layers): + in_channels = self.in_channels if i == 0 else self.out_channels + + res_block = FlaxResnetBlock2D( + in_channels=in_channels, + out_channels=self.out_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + resnets.append(res_block) + + attn_block = FlaxTransformer2DModel( + in_channels=self.out_channels, + n_heads=self.attn_num_head_channels, + d_head=self.out_channels // self.attn_num_head_channels, + depth=1, + use_linear_projection=self.use_linear_projection, + only_cross_attention=self.only_cross_attention, + dtype=self.dtype, + ) + attentions.append(attn_block) + + self.resnets = resnets + self.attentions = attentions + + if self.add_downsample: + self.downsamplers_0 = FlaxDownsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, temb, encoder_hidden_states, deterministic=True): + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb, deterministic=deterministic) + hidden_states = attn(hidden_states, encoder_hidden_states, deterministic=deterministic) + output_states += (hidden_states,) + + if self.add_downsample: + hidden_states = self.downsamplers_0(hidden_states) + output_states += (hidden_states,) + + return hidden_states, output_states + + +class FlaxDownBlock2D(nn.Module): + r""" + Flax 2D downsizing block + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of attention blocks layers + add_downsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add downsampling layer before each final output + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + dropout: float = 0.0 + num_layers: int = 1 + add_downsample: bool = True + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + + for i in range(self.num_layers): + in_channels = self.in_channels if i == 0 else self.out_channels + + res_block = FlaxResnetBlock2D( + in_channels=in_channels, + out_channels=self.out_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + resnets.append(res_block) + self.resnets = resnets + + if self.add_downsample: + self.downsamplers_0 = FlaxDownsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, temb, deterministic=True): + output_states = () + + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb, deterministic=deterministic) + output_states += (hidden_states,) + + if self.add_downsample: + hidden_states = self.downsamplers_0(hidden_states) + output_states += (hidden_states,) + + return hidden_states, output_states + + +class FlaxCrossAttnUpBlock2D(nn.Module): + r""" + Cross Attention 2D Upsampling block - original architecture from Unet transformers: + https://arxiv.org/abs/2103.06104 + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of attention blocks layers + attn_num_head_channels (:obj:`int`, *optional*, defaults to 1): + Number of attention heads of each spatial transformer block + add_upsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add upsampling layer before each final output + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + prev_output_channel: int + dropout: float = 0.0 + num_layers: int = 1 + attn_num_head_channels: int = 1 + add_upsample: bool = True + use_linear_projection: bool = False + only_cross_attention: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + attentions = [] + + for i in range(self.num_layers): + res_skip_channels = self.in_channels if (i == self.num_layers - 1) else self.out_channels + resnet_in_channels = self.prev_output_channel if i == 0 else self.out_channels + + res_block = FlaxResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=self.out_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + resnets.append(res_block) + + attn_block = FlaxTransformer2DModel( + in_channels=self.out_channels, + n_heads=self.attn_num_head_channels, + d_head=self.out_channels // self.attn_num_head_channels, + depth=1, + use_linear_projection=self.use_linear_projection, + only_cross_attention=self.only_cross_attention, + dtype=self.dtype, + ) + attentions.append(attn_block) + + self.resnets = resnets + self.attentions = attentions + + if self.add_upsample: + self.upsamplers_0 = FlaxUpsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, res_hidden_states_tuple, temb, encoder_hidden_states, deterministic=True): + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = jnp.concatenate((hidden_states, res_hidden_states), axis=-1) + + hidden_states = resnet(hidden_states, temb, deterministic=deterministic) + hidden_states = attn(hidden_states, encoder_hidden_states, deterministic=deterministic) + + if self.add_upsample: + hidden_states = self.upsamplers_0(hidden_states) + + return hidden_states + + +class FlaxUpBlock2D(nn.Module): + r""" + Flax 2D upsampling block + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + prev_output_channel (:obj:`int`): + Output channels from the previous block + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of attention blocks layers + add_downsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add downsampling layer before each final output + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + prev_output_channel: int + dropout: float = 0.0 + num_layers: int = 1 + add_upsample: bool = True + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + + for i in range(self.num_layers): + res_skip_channels = self.in_channels if (i == self.num_layers - 1) else self.out_channels + resnet_in_channels = self.prev_output_channel if i == 0 else self.out_channels + + res_block = FlaxResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=self.out_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + resnets.append(res_block) + + self.resnets = resnets + + if self.add_upsample: + self.upsamplers_0 = FlaxUpsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, res_hidden_states_tuple, temb, deterministic=True): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = jnp.concatenate((hidden_states, res_hidden_states), axis=-1) + + hidden_states = resnet(hidden_states, temb, deterministic=deterministic) + + if self.add_upsample: + hidden_states = self.upsamplers_0(hidden_states) + + return hidden_states + + +class FlaxUNetMidBlock2DCrossAttn(nn.Module): + r""" + Cross Attention 2D Mid-level block - original architecture from Unet transformers: https://arxiv.org/abs/2103.06104 + + Parameters: + in_channels (:obj:`int`): + Input channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of attention blocks layers + attn_num_head_channels (:obj:`int`, *optional*, defaults to 1): + Number of attention heads of each spatial transformer block + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + dropout: float = 0.0 + num_layers: int = 1 + attn_num_head_channels: int = 1 + use_linear_projection: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + # there is always at least one resnet + resnets = [ + FlaxResnetBlock2D( + in_channels=self.in_channels, + out_channels=self.in_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + ] + + attentions = [] + + for _ in range(self.num_layers): + attn_block = FlaxTransformer2DModel( + in_channels=self.in_channels, + n_heads=self.attn_num_head_channels, + d_head=self.in_channels // self.attn_num_head_channels, + depth=1, + use_linear_projection=self.use_linear_projection, + dtype=self.dtype, + ) + attentions.append(attn_block) + + res_block = FlaxResnetBlock2D( + in_channels=self.in_channels, + out_channels=self.in_channels, + dropout_prob=self.dropout, + dtype=self.dtype, + ) + resnets.append(res_block) + + self.resnets = resnets + self.attentions = attentions + + def __call__(self, hidden_states, temb, encoder_hidden_states, deterministic=True): + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + hidden_states = attn(hidden_states, encoder_hidden_states, deterministic=deterministic) + hidden_states = resnet(hidden_states, temb, deterministic=deterministic) + + return hidden_states diff --git a/diffusers/src/diffusers/models/unet_2d_condition.py b/diffusers/src/diffusers/models/unet_2d_condition.py new file mode 100644 index 0000000000000000000000000000000000000000..daffef765077ff3e559fac50f0d342adf0782c14 --- /dev/null +++ b/diffusers/src/diffusers/models/unet_2d_condition.py @@ -0,0 +1,621 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.utils.checkpoint + +from ..configuration_utils import ConfigMixin, register_to_config +from ..loaders import UNet2DConditionLoadersMixin +from ..utils import BaseOutput, logging +from .cross_attention import AttnProcessor +from .embeddings import GaussianFourierProjection, TimestepEmbedding, Timesteps +from .modeling_utils import ModelMixin +from .unet_2d_blocks import ( + CrossAttnDownBlock2D, + CrossAttnUpBlock2D, + DownBlock2D, + UNetMidBlock2DCrossAttn, + UNetMidBlock2DSimpleCrossAttn, + UpBlock2D, + get_down_block, + get_up_block, +) + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +class UNet2DConditionOutput(BaseOutput): + """ + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Hidden states conditioned on `encoder_hidden_states` input. Output of last layer of model. + """ + + sample: torch.FloatTensor + + +class UNet2DConditionModel(ModelMixin, ConfigMixin, UNet2DConditionLoadersMixin): + r""" + UNet2DConditionModel is a conditional 2D UNet model that takes in a noisy sample, conditional state, and a timestep + and returns sample shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the models (such as downloading or saving, etc.) + + Parameters: + sample_size (`int` or `Tuple[int, int]`, *optional*, defaults to `None`): + Height and width of input/output sample. + in_channels (`int`, *optional*, defaults to 4): The number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 4): The number of channels in the output. + center_input_sample (`bool`, *optional*, defaults to `False`): Whether to center the input sample. + flip_sin_to_cos (`bool`, *optional*, defaults to `False`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, *optional*, defaults to 0): The frequency shift to apply to the time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to `("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D")`): + The tuple of downsample blocks to use. + mid_block_type (`str`, *optional*, defaults to `"UNetMidBlock2DCrossAttn"`): + The mid block type. Choose from `UNetMidBlock2DCrossAttn` or `UNetMidBlock2DSimpleCrossAttn`, will skip the + mid block layer if `None`. + up_block_types (`Tuple[str]`, *optional*, defaults to `("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D",)`): + The tuple of upsample blocks to use. + only_cross_attention(`bool` or `Tuple[bool]`, *optional*, default to `False`): + Whether to include self-attention in the basic transformer blocks, see + [`~models.attention.BasicTransformerBlock`]. + block_out_channels (`Tuple[int]`, *optional*, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, *optional*, defaults to 2): The number of layers per block. + downsample_padding (`int`, *optional*, defaults to 1): The padding to use for the downsampling convolution. + mid_block_scale_factor (`float`, *optional*, defaults to 1.0): The scale factor to use for the mid block. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + norm_num_groups (`int`, *optional*, defaults to 32): The number of groups to use for the normalization. + If `None`, it will skip the normalization and activation layers in post-processing + norm_eps (`float`, *optional*, defaults to 1e-5): The epsilon to use for the normalization. + cross_attention_dim (`int`, *optional*, defaults to 1280): The dimension of the cross attention features. + attention_head_dim (`int`, *optional*, defaults to 8): The dimension of the attention heads. + resnet_time_scale_shift (`str`, *optional*, defaults to `"default"`): Time scale shift config + for resnet blocks, see [`~models.resnet.ResnetBlock2D`]. Choose from `default` or `scale_shift`. + class_embed_type (`str`, *optional*, defaults to None): The type of class embedding to use which is ultimately + summed with the time embeddings. Choose from `None`, `"timestep"`, or `"identity"`. + num_class_embeds (`int`, *optional*, defaults to None): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + time_embedding_type (`str`, *optional*, default to `positional`): + The type of position embedding to use for timesteps. Choose from `positional` or `fourier`. + timestep_post_act (`str, *optional*, default to `None`): + The second activation function to use in timestep embedding. Choose from `silu`, `mish` and `gelu`. + time_cond_proj_dim (`int`, *optional*, default to `None`): + The dimension of `cond_proj` layer in timestep embedding. + conv_in_kernel (`int`, *optional*, default to `3`): The kernel size of `conv_in` layer. + conv_out_kernel (`int`, *optional*, default to `3`): the Kernel size of `conv_out` layer. + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + sample_size: Optional[int] = None, + in_channels: int = 4, + out_channels: int = 4, + center_input_sample: bool = False, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + mid_block_type: Optional[str] = "UNetMidBlock2DCrossAttn", + up_block_types: Tuple[str] = ("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D"), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int] = (320, 640, 1280, 1280), + layers_per_block: int = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: int = 1280, + attention_head_dim: Union[int, Tuple[int]] = 8, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + time_embedding_type: str = "positional", # fourier, positional + timestep_post_act: Optional[str] = None, + time_cond_proj_dim: Optional[int] = None, + conv_in_kernel: int = 3, + conv_out_kernel: int = 3, + ): + super().__init__() + + self.sample_size = sample_size + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(attention_head_dim, int) and len(attention_head_dim) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `attention_head_dim` as `down_block_types`. `attention_head_dim`: {attention_head_dim}. `down_block_types`: {down_block_types}." + ) + + # input + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=conv_in_kernel, padding=conv_in_padding + ) + + # time + if time_embedding_type == "fourier": + time_embed_dim = block_out_channels[0] * 2 + if time_embed_dim % 2 != 0: + raise ValueError(f"`time_embed_dim` should be divisible by 2, but is {time_embed_dim}.") + self.time_proj = GaussianFourierProjection( + time_embed_dim // 2, set_W_to_weight=False, log=False, flip_sin_to_cos=flip_sin_to_cos + ) + timestep_input_dim = time_embed_dim + elif time_embedding_type == "positional": + time_embed_dim = block_out_channels[0] * 4 + + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + else: + raise ValueError( + f"{time_embedding_type} does not exist. Pleaes make sure to use one of `fourier` or `positional`." + ) + + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + post_act_fn=timestep_post_act, + cond_proj_dim=time_cond_proj_dim, + ) + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + else: + self.class_embedding = None + + self.down_blocks = nn.ModuleList([]) + self.up_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[i], + downsample_padding=downsample_padding, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + # mid + if mid_block_type == "UNetMidBlock2DCrossAttn": + self.mid_block = UNetMidBlock2DCrossAttn( + in_channels=block_out_channels[-1], + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[-1], + resnet_groups=norm_num_groups, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + elif mid_block_type == "UNetMidBlock2DSimpleCrossAttn": + self.mid_block = UNetMidBlock2DSimpleCrossAttn( + in_channels=block_out_channels[-1], + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[-1], + resnet_groups=norm_num_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif mid_block_type is None: + self.mid_block = None + else: + raise ValueError(f"unknown mid_block_type : {mid_block_type}") + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_attention_head_dim = list(reversed(attention_head_dim)) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=reversed_attention_head_dim[i], + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + if norm_num_groups is not None: + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + self.conv_act = nn.SiLU() + else: + self.conv_norm_out = None + self.conv_act = None + + conv_out_padding = (conv_out_kernel - 1) // 2 + self.conv_out = nn.Conv2d( + block_out_channels[0], out_channels, kernel_size=conv_out_kernel, padding=conv_out_padding + ) + + @property + def attn_processors(self) -> Dict[str, AttnProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttnProcessor]): + if hasattr(module, "set_processor"): + processors[f"{name}.processor"] = module.processor + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + def set_attn_processor(self, processor: Union[AttnProcessor, Dict[str, AttnProcessor]]): + r""" + Parameters: + `processor (`dict` of `AttnProcessor` or `AttnProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + of **all** `CrossAttention` layers. + In case `processor` is a dict, the key needs to define the path to the corresponding cross attention processor. This is strongly recommended when setting trainablae attention processors.: + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + def set_attention_slice(self, slice_size): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + `"max"`, maxium amount of memory will be saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_slicable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_slicable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_slicable_dims(module) + + num_slicable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_slicable_layers * [1] + + slice_size = num_slicable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value=False): + if isinstance(module, (CrossAttnDownBlock2D, DownBlock2D, CrossAttnUpBlock2D, UpBlock2D)): + module.gradient_checkpointing = value + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + return_dict: bool = True, + ) -> Union[UNet2DConditionOutput, Tuple]: + r""" + Args: + sample (`torch.FloatTensor`): (batch, channel, height, width) noisy inputs tensor + timestep (`torch.FloatTensor` or `float` or `int`): (batch) timesteps + encoder_hidden_states (`torch.FloatTensor`): (batch, sequence_length, feature_dim) encoder hidden states + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain tuple. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttnProcessor` as defined under + `self.processor` in + [diffusers.cross_attention](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py). + + Returns: + [`~models.unet_2d_condition.UNet2DConditionOutput`] or `tuple`: + [`~models.unet_2d_condition.UNet2DConditionOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layears). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2**self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + if any(s % default_overall_up_factor != 0 for s in sample.shape[-2:]): + logger.info("Forward upsample size to force interpolation output size.") + forward_upsample_size = True + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=self.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + # 2. pre-process + sample = self.conv_in(sample) + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + + down_block_res_samples += res_samples + + # 4. mid + if self.mid_block is not None: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + ) + else: + sample = upsample_block( + hidden_states=sample, temb=emb, res_hidden_states_tuple=res_samples, upsample_size=upsample_size + ) + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if not return_dict: + return (sample,) + + return UNet2DConditionOutput(sample=sample) diff --git a/diffusers/src/diffusers/models/unet_2d_condition_flax.py b/diffusers/src/diffusers/models/unet_2d_condition_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..8d8308c5bfb938f67083b5a56bcd0d1704dc320b --- /dev/null +++ b/diffusers/src/diffusers/models/unet_2d_condition_flax.py @@ -0,0 +1,321 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from typing import Tuple, Union + +import flax +import flax.linen as nn +import jax +import jax.numpy as jnp +from flax.core.frozen_dict import FrozenDict + +from ..configuration_utils import ConfigMixin, flax_register_to_config +from ..utils import BaseOutput +from .embeddings_flax import FlaxTimestepEmbedding, FlaxTimesteps +from .modeling_flax_utils import FlaxModelMixin +from .unet_2d_blocks_flax import ( + FlaxCrossAttnDownBlock2D, + FlaxCrossAttnUpBlock2D, + FlaxDownBlock2D, + FlaxUNetMidBlock2DCrossAttn, + FlaxUpBlock2D, +) + + +@flax.struct.dataclass +class FlaxUNet2DConditionOutput(BaseOutput): + """ + Args: + sample (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)`): + Hidden states conditioned on `encoder_hidden_states` input. Output of last layer of model. + """ + + sample: jnp.ndarray + + +@flax_register_to_config +class FlaxUNet2DConditionModel(nn.Module, FlaxModelMixin, ConfigMixin): + r""" + FlaxUNet2DConditionModel is a conditional 2D UNet model that takes in a noisy sample, conditional state, and a + timestep and returns sample shaped output. + + This model inherits from [`FlaxModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the models (such as downloading or saving, etc.) + + Also, this model is a Flax Linen [flax.linen.Module](https://flax.readthedocs.io/en/latest/flax.linen.html#module) + subclass. Use it as a regular Flax linen Module and refer to the Flax documentation for all matter related to + general usage and behavior. + + Finally, this model supports inherent JAX features such as: + - [Just-In-Time (JIT) compilation](https://jax.readthedocs.io/en/latest/jax.html#just-in-time-compilation-jit) + - [Automatic Differentiation](https://jax.readthedocs.io/en/latest/jax.html#automatic-differentiation) + - [Vectorization](https://jax.readthedocs.io/en/latest/jax.html#vectorization-vmap) + - [Parallelization](https://jax.readthedocs.io/en/latest/jax.html#parallelization-pmap) + + Parameters: + sample_size (`int`, *optional*): + The size of the input sample. + in_channels (`int`, *optional*, defaults to 4): + The number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 4): + The number of channels in the output. + down_block_types (`Tuple[str]`, *optional*, defaults to `("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D")`): + The tuple of downsample blocks to use. The corresponding class names will be: "FlaxCrossAttnDownBlock2D", + "FlaxCrossAttnDownBlock2D", "FlaxCrossAttnDownBlock2D", "FlaxDownBlock2D" + up_block_types (`Tuple[str]`, *optional*, defaults to `("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D",)`): + The tuple of upsample blocks to use. The corresponding class names will be: "FlaxUpBlock2D", + "FlaxCrossAttnUpBlock2D", "FlaxCrossAttnUpBlock2D", "FlaxCrossAttnUpBlock2D" + block_out_channels (`Tuple[int]`, *optional*, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, *optional*, defaults to 2): + The number of layers per block. + attention_head_dim (`int` or `Tuple[int]`, *optional*, defaults to 8): + The dimension of the attention heads. + cross_attention_dim (`int`, *optional*, defaults to 768): + The dimension of the cross attention features. + dropout (`float`, *optional*, defaults to 0): + Dropout probability for down, up and bottleneck blocks. + flip_sin_to_cos (`bool`, *optional*, defaults to `True`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, *optional*, defaults to 0): The frequency shift to apply to the time embedding. + + """ + + sample_size: int = 32 + in_channels: int = 4 + out_channels: int = 4 + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ) + up_block_types: Tuple[str] = ("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D") + only_cross_attention: Union[bool, Tuple[bool]] = False + block_out_channels: Tuple[int] = (320, 640, 1280, 1280) + layers_per_block: int = 2 + attention_head_dim: Union[int, Tuple[int]] = 8 + cross_attention_dim: int = 1280 + dropout: float = 0.0 + use_linear_projection: bool = False + dtype: jnp.dtype = jnp.float32 + flip_sin_to_cos: bool = True + freq_shift: int = 0 + + def init_weights(self, rng: jax.random.KeyArray) -> FrozenDict: + # init input tensors + sample_shape = (1, self.in_channels, self.sample_size, self.sample_size) + sample = jnp.zeros(sample_shape, dtype=jnp.float32) + timesteps = jnp.ones((1,), dtype=jnp.int32) + encoder_hidden_states = jnp.zeros((1, 1, self.cross_attention_dim), dtype=jnp.float32) + + params_rng, dropout_rng = jax.random.split(rng) + rngs = {"params": params_rng, "dropout": dropout_rng} + + return self.init(rngs, sample, timesteps, encoder_hidden_states)["params"] + + def setup(self): + block_out_channels = self.block_out_channels + time_embed_dim = block_out_channels[0] * 4 + + # input + self.conv_in = nn.Conv( + block_out_channels[0], + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + # time + self.time_proj = FlaxTimesteps( + block_out_channels[0], flip_sin_to_cos=self.flip_sin_to_cos, freq_shift=self.config.freq_shift + ) + self.time_embedding = FlaxTimestepEmbedding(time_embed_dim, dtype=self.dtype) + + only_cross_attention = self.only_cross_attention + if isinstance(only_cross_attention, bool): + only_cross_attention = (only_cross_attention,) * len(self.down_block_types) + + attention_head_dim = self.attention_head_dim + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(self.down_block_types) + + # down + down_blocks = [] + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(self.down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + if down_block_type == "CrossAttnDownBlock2D": + down_block = FlaxCrossAttnDownBlock2D( + in_channels=input_channel, + out_channels=output_channel, + dropout=self.dropout, + num_layers=self.layers_per_block, + attn_num_head_channels=attention_head_dim[i], + add_downsample=not is_final_block, + use_linear_projection=self.use_linear_projection, + only_cross_attention=only_cross_attention[i], + dtype=self.dtype, + ) + else: + down_block = FlaxDownBlock2D( + in_channels=input_channel, + out_channels=output_channel, + dropout=self.dropout, + num_layers=self.layers_per_block, + add_downsample=not is_final_block, + dtype=self.dtype, + ) + + down_blocks.append(down_block) + self.down_blocks = down_blocks + + # mid + self.mid_block = FlaxUNetMidBlock2DCrossAttn( + in_channels=block_out_channels[-1], + dropout=self.dropout, + attn_num_head_channels=attention_head_dim[-1], + use_linear_projection=self.use_linear_projection, + dtype=self.dtype, + ) + + # up + up_blocks = [] + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_attention_head_dim = list(reversed(attention_head_dim)) + only_cross_attention = list(reversed(only_cross_attention)) + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(self.up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + is_final_block = i == len(block_out_channels) - 1 + + if up_block_type == "CrossAttnUpBlock2D": + up_block = FlaxCrossAttnUpBlock2D( + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + num_layers=self.layers_per_block + 1, + attn_num_head_channels=reversed_attention_head_dim[i], + add_upsample=not is_final_block, + dropout=self.dropout, + use_linear_projection=self.use_linear_projection, + only_cross_attention=only_cross_attention[i], + dtype=self.dtype, + ) + else: + up_block = FlaxUpBlock2D( + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + num_layers=self.layers_per_block + 1, + add_upsample=not is_final_block, + dropout=self.dropout, + dtype=self.dtype, + ) + + up_blocks.append(up_block) + prev_output_channel = output_channel + self.up_blocks = up_blocks + + # out + self.conv_norm_out = nn.GroupNorm(num_groups=32, epsilon=1e-5) + self.conv_out = nn.Conv( + self.out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + def __call__( + self, + sample, + timesteps, + encoder_hidden_states, + return_dict: bool = True, + train: bool = False, + ) -> Union[FlaxUNet2DConditionOutput, Tuple]: + r""" + Args: + sample (`jnp.ndarray`): (batch, channel, height, width) noisy inputs tensor + timestep (`jnp.ndarray` or `float` or `int`): timesteps + encoder_hidden_states (`jnp.ndarray`): (batch_size, sequence_length, hidden_size) encoder hidden states + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition_flax.FlaxUNet2DConditionOutput`] instead of a + plain tuple. + train (`bool`, *optional*, defaults to `False`): + Use deterministic functions and disable dropout when not training. + + Returns: + [`~models.unet_2d_condition_flax.FlaxUNet2DConditionOutput`] or `tuple`: + [`~models.unet_2d_condition_flax.FlaxUNet2DConditionOutput`] if `return_dict` is True, otherwise a `tuple`. + When returning a tuple, the first element is the sample tensor. + """ + # 1. time + if not isinstance(timesteps, jnp.ndarray): + timesteps = jnp.array([timesteps], dtype=jnp.int32) + elif isinstance(timesteps, jnp.ndarray) and len(timesteps.shape) == 0: + timesteps = timesteps.astype(dtype=jnp.float32) + timesteps = jnp.expand_dims(timesteps, 0) + + t_emb = self.time_proj(timesteps) + t_emb = self.time_embedding(t_emb) + + # 2. pre-process + sample = jnp.transpose(sample, (0, 2, 3, 1)) + sample = self.conv_in(sample) + + # 3. down + down_block_res_samples = (sample,) + for down_block in self.down_blocks: + if isinstance(down_block, FlaxCrossAttnDownBlock2D): + sample, res_samples = down_block(sample, t_emb, encoder_hidden_states, deterministic=not train) + else: + sample, res_samples = down_block(sample, t_emb, deterministic=not train) + down_block_res_samples += res_samples + + # 4. mid + sample = self.mid_block(sample, t_emb, encoder_hidden_states, deterministic=not train) + + # 5. up + for up_block in self.up_blocks: + res_samples = down_block_res_samples[-(self.layers_per_block + 1) :] + down_block_res_samples = down_block_res_samples[: -(self.layers_per_block + 1)] + if isinstance(up_block, FlaxCrossAttnUpBlock2D): + sample = up_block( + sample, + temb=t_emb, + encoder_hidden_states=encoder_hidden_states, + res_hidden_states_tuple=res_samples, + deterministic=not train, + ) + else: + sample = up_block(sample, temb=t_emb, res_hidden_states_tuple=res_samples, deterministic=not train) + + # 6. post-process + sample = self.conv_norm_out(sample) + sample = nn.silu(sample) + sample = self.conv_out(sample) + sample = jnp.transpose(sample, (0, 3, 1, 2)) + + if not return_dict: + return (sample,) + + return FlaxUNet2DConditionOutput(sample=sample) diff --git a/diffusers/src/diffusers/models/vae.py b/diffusers/src/diffusers/models/vae.py new file mode 100644 index 0000000000000000000000000000000000000000..4893f7344f26cba225b60514a903e465f3e8f82a --- /dev/null +++ b/diffusers/src/diffusers/models/vae.py @@ -0,0 +1,356 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Optional + +import numpy as np +import torch +import torch.nn as nn + +from ..utils import BaseOutput, randn_tensor +from .unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block + + +@dataclass +class DecoderOutput(BaseOutput): + """ + Output of decoding method. + + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Decoded output sample of the model. Output of the last layer of the model. + """ + + sample: torch.FloatTensor + + +class Encoder(nn.Module): + def __init__( + self, + in_channels=3, + out_channels=3, + down_block_types=("DownEncoderBlock2D",), + block_out_channels=(64,), + layers_per_block=2, + norm_num_groups=32, + act_fn="silu", + double_z=True, + ): + super().__init__() + self.layers_per_block = layers_per_block + + self.conv_in = torch.nn.Conv2d(in_channels, block_out_channels[0], kernel_size=3, stride=1, padding=1) + + self.mid_block = None + self.down_blocks = nn.ModuleList([]) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=self.layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + add_downsample=not is_final_block, + resnet_eps=1e-6, + downsample_padding=0, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attn_num_head_channels=None, + temb_channels=None, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + resnet_eps=1e-6, + resnet_act_fn=act_fn, + output_scale_factor=1, + resnet_time_scale_shift="default", + attn_num_head_channels=None, + resnet_groups=norm_num_groups, + temb_channels=None, + ) + + # out + self.conv_norm_out = nn.GroupNorm(num_channels=block_out_channels[-1], num_groups=norm_num_groups, eps=1e-6) + self.conv_act = nn.SiLU() + + conv_out_channels = 2 * out_channels if double_z else out_channels + self.conv_out = nn.Conv2d(block_out_channels[-1], conv_out_channels, 3, padding=1) + + def forward(self, x): + sample = x + sample = self.conv_in(sample) + + # down + for down_block in self.down_blocks: + sample = down_block(sample) + + # middle + sample = self.mid_block(sample) + + # post-process + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + return sample + + +class Decoder(nn.Module): + def __init__( + self, + in_channels=3, + out_channels=3, + up_block_types=("UpDecoderBlock2D",), + block_out_channels=(64,), + layers_per_block=2, + norm_num_groups=32, + act_fn="silu", + ): + super().__init__() + self.layers_per_block = layers_per_block + + self.conv_in = nn.Conv2d(in_channels, block_out_channels[-1], kernel_size=3, stride=1, padding=1) + + self.mid_block = None + self.up_blocks = nn.ModuleList([]) + + # mid + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + resnet_eps=1e-6, + resnet_act_fn=act_fn, + output_scale_factor=1, + resnet_time_scale_shift="default", + attn_num_head_channels=None, + resnet_groups=norm_num_groups, + temb_channels=None, + ) + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + + is_final_block = i == len(block_out_channels) - 1 + + up_block = get_up_block( + up_block_type, + num_layers=self.layers_per_block + 1, + in_channels=prev_output_channel, + out_channels=output_channel, + prev_output_channel=None, + add_upsample=not is_final_block, + resnet_eps=1e-6, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attn_num_head_channels=None, + temb_channels=None, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + self.conv_norm_out = nn.GroupNorm(num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=1e-6) + self.conv_act = nn.SiLU() + self.conv_out = nn.Conv2d(block_out_channels[0], out_channels, 3, padding=1) + + def forward(self, z): + sample = z + sample = self.conv_in(sample) + + # middle + sample = self.mid_block(sample) + + # up + for up_block in self.up_blocks: + sample = up_block(sample) + + # post-process + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + return sample + + +class VectorQuantizer(nn.Module): + """ + Improved version over VectorQuantizer, can be used as a drop-in replacement. Mostly avoids costly matrix + multiplications and allows for post-hoc remapping of indices. + """ + + # NOTE: due to a bug the beta term was applied to the wrong term. for + # backwards compatibility we use the buggy version by default, but you can + # specify legacy=False to fix it. + def __init__( + self, n_e, vq_embed_dim, beta, remap=None, unknown_index="random", sane_index_shape=False, legacy=True + ): + super().__init__() + self.n_e = n_e + self.vq_embed_dim = vq_embed_dim + self.beta = beta + self.legacy = legacy + + self.embedding = nn.Embedding(self.n_e, self.vq_embed_dim) + self.embedding.weight.data.uniform_(-1.0 / self.n_e, 1.0 / self.n_e) + + self.remap = remap + if self.remap is not None: + self.register_buffer("used", torch.tensor(np.load(self.remap))) + self.re_embed = self.used.shape[0] + self.unknown_index = unknown_index # "random" or "extra" or integer + if self.unknown_index == "extra": + self.unknown_index = self.re_embed + self.re_embed = self.re_embed + 1 + print( + f"Remapping {self.n_e} indices to {self.re_embed} indices. " + f"Using {self.unknown_index} for unknown indices." + ) + else: + self.re_embed = n_e + + self.sane_index_shape = sane_index_shape + + def remap_to_used(self, inds): + ishape = inds.shape + assert len(ishape) > 1 + inds = inds.reshape(ishape[0], -1) + used = self.used.to(inds) + match = (inds[:, :, None] == used[None, None, ...]).long() + new = match.argmax(-1) + unknown = match.sum(2) < 1 + if self.unknown_index == "random": + new[unknown] = torch.randint(0, self.re_embed, size=new[unknown].shape).to(device=new.device) + else: + new[unknown] = self.unknown_index + return new.reshape(ishape) + + def unmap_to_all(self, inds): + ishape = inds.shape + assert len(ishape) > 1 + inds = inds.reshape(ishape[0], -1) + used = self.used.to(inds) + if self.re_embed > self.used.shape[0]: # extra token + inds[inds >= self.used.shape[0]] = 0 # simply set to zero + back = torch.gather(used[None, :][inds.shape[0] * [0], :], 1, inds) + return back.reshape(ishape) + + def forward(self, z): + # reshape z -> (batch, height, width, channel) and flatten + z = z.permute(0, 2, 3, 1).contiguous() + z_flattened = z.view(-1, self.vq_embed_dim) + + # distances from z to embeddings e_j (z - e)^2 = z^2 + e^2 - 2 e * z + min_encoding_indices = torch.argmin(torch.cdist(z_flattened, self.embedding.weight), dim=1) + + z_q = self.embedding(min_encoding_indices).view(z.shape) + perplexity = None + min_encodings = None + + # compute loss for embedding + if not self.legacy: + loss = self.beta * torch.mean((z_q.detach() - z) ** 2) + torch.mean((z_q - z.detach()) ** 2) + else: + loss = torch.mean((z_q.detach() - z) ** 2) + self.beta * torch.mean((z_q - z.detach()) ** 2) + + # preserve gradients + z_q = z + (z_q - z).detach() + + # reshape back to match original input shape + z_q = z_q.permute(0, 3, 1, 2).contiguous() + + if self.remap is not None: + min_encoding_indices = min_encoding_indices.reshape(z.shape[0], -1) # add batch axis + min_encoding_indices = self.remap_to_used(min_encoding_indices) + min_encoding_indices = min_encoding_indices.reshape(-1, 1) # flatten + + if self.sane_index_shape: + min_encoding_indices = min_encoding_indices.reshape(z_q.shape[0], z_q.shape[2], z_q.shape[3]) + + return z_q, loss, (perplexity, min_encodings, min_encoding_indices) + + def get_codebook_entry(self, indices, shape): + # shape specifying (batch, height, width, channel) + if self.remap is not None: + indices = indices.reshape(shape[0], -1) # add batch axis + indices = self.unmap_to_all(indices) + indices = indices.reshape(-1) # flatten again + + # get quantized latent vectors + z_q = self.embedding(indices) + + if shape is not None: + z_q = z_q.view(shape) + # reshape back to match original input shape + z_q = z_q.permute(0, 3, 1, 2).contiguous() + + return z_q + + +class DiagonalGaussianDistribution(object): + def __init__(self, parameters, deterministic=False): + self.parameters = parameters + self.mean, self.logvar = torch.chunk(parameters, 2, dim=1) + self.logvar = torch.clamp(self.logvar, -30.0, 20.0) + self.deterministic = deterministic + self.std = torch.exp(0.5 * self.logvar) + self.var = torch.exp(self.logvar) + if self.deterministic: + self.var = self.std = torch.zeros_like( + self.mean, device=self.parameters.device, dtype=self.parameters.dtype + ) + + def sample(self, generator: Optional[torch.Generator] = None) -> torch.FloatTensor: + # make sure sample is on the same device as the parameters and has same dtype + sample = randn_tensor( + self.mean.shape, generator=generator, device=self.parameters.device, dtype=self.parameters.dtype + ) + x = self.mean + self.std * sample + return x + + def kl(self, other=None): + if self.deterministic: + return torch.Tensor([0.0]) + else: + if other is None: + return 0.5 * torch.sum(torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar, dim=[1, 2, 3]) + else: + return 0.5 * torch.sum( + torch.pow(self.mean - other.mean, 2) / other.var + + self.var / other.var + - 1.0 + - self.logvar + + other.logvar, + dim=[1, 2, 3], + ) + + def nll(self, sample, dims=[1, 2, 3]): + if self.deterministic: + return torch.Tensor([0.0]) + logtwopi = np.log(2.0 * np.pi) + return 0.5 * torch.sum(logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var, dim=dims) + + def mode(self): + return self.mean diff --git a/diffusers/src/diffusers/models/vae_flax.py b/diffusers/src/diffusers/models/vae_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..70cbc3b8920ac650e3e5ca614671a6e5fca0ef40 --- /dev/null +++ b/diffusers/src/diffusers/models/vae_flax.py @@ -0,0 +1,866 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +# JAX implementation of VQGAN from taming-transformers https://github.com/CompVis/taming-transformers + +import math +from functools import partial +from typing import Tuple + +import flax +import flax.linen as nn +import jax +import jax.numpy as jnp +from flax.core.frozen_dict import FrozenDict + +from ..configuration_utils import ConfigMixin, flax_register_to_config +from ..utils import BaseOutput +from .modeling_flax_utils import FlaxModelMixin + + +@flax.struct.dataclass +class FlaxDecoderOutput(BaseOutput): + """ + Output of decoding method. + + Args: + sample (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)`): + Decoded output sample of the model. Output of the last layer of the model. + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + + sample: jnp.ndarray + + +@flax.struct.dataclass +class FlaxAutoencoderKLOutput(BaseOutput): + """ + Output of AutoencoderKL encoding method. + + Args: + latent_dist (`FlaxDiagonalGaussianDistribution`): + Encoded outputs of `Encoder` represented as the mean and logvar of `FlaxDiagonalGaussianDistribution`. + `FlaxDiagonalGaussianDistribution` allows for sampling latents from the distribution. + """ + + latent_dist: "FlaxDiagonalGaussianDistribution" + + +class FlaxUpsample2D(nn.Module): + """ + Flax implementation of 2D Upsample layer + + Args: + in_channels (`int`): + Input channels + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + + in_channels: int + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.conv = nn.Conv( + self.in_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + def __call__(self, hidden_states): + batch, height, width, channels = hidden_states.shape + hidden_states = jax.image.resize( + hidden_states, + shape=(batch, height * 2, width * 2, channels), + method="nearest", + ) + hidden_states = self.conv(hidden_states) + return hidden_states + + +class FlaxDownsample2D(nn.Module): + """ + Flax implementation of 2D Downsample layer + + Args: + in_channels (`int`): + Input channels + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + + in_channels: int + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.conv = nn.Conv( + self.in_channels, + kernel_size=(3, 3), + strides=(2, 2), + padding="VALID", + dtype=self.dtype, + ) + + def __call__(self, hidden_states): + pad = ((0, 0), (0, 1), (0, 1), (0, 0)) # pad height and width dim + hidden_states = jnp.pad(hidden_states, pad_width=pad) + hidden_states = self.conv(hidden_states) + return hidden_states + + +class FlaxResnetBlock2D(nn.Module): + """ + Flax implementation of 2D Resnet Block. + + Args: + in_channels (`int`): + Input channels + out_channels (`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + groups (:obj:`int`, *optional*, defaults to `32`): + The number of groups to use for group norm. + use_nin_shortcut (:obj:`bool`, *optional*, defaults to `None`): + Whether to use `nin_shortcut`. This activates a new layer inside ResNet block + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + + in_channels: int + out_channels: int = None + dropout: float = 0.0 + groups: int = 32 + use_nin_shortcut: bool = None + dtype: jnp.dtype = jnp.float32 + + def setup(self): + out_channels = self.in_channels if self.out_channels is None else self.out_channels + + self.norm1 = nn.GroupNorm(num_groups=self.groups, epsilon=1e-6) + self.conv1 = nn.Conv( + out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + self.norm2 = nn.GroupNorm(num_groups=self.groups, epsilon=1e-6) + self.dropout_layer = nn.Dropout(self.dropout) + self.conv2 = nn.Conv( + out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + use_nin_shortcut = self.in_channels != out_channels if self.use_nin_shortcut is None else self.use_nin_shortcut + + self.conv_shortcut = None + if use_nin_shortcut: + self.conv_shortcut = nn.Conv( + out_channels, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + + def __call__(self, hidden_states, deterministic=True): + residual = hidden_states + hidden_states = self.norm1(hidden_states) + hidden_states = nn.swish(hidden_states) + hidden_states = self.conv1(hidden_states) + + hidden_states = self.norm2(hidden_states) + hidden_states = nn.swish(hidden_states) + hidden_states = self.dropout_layer(hidden_states, deterministic) + hidden_states = self.conv2(hidden_states) + + if self.conv_shortcut is not None: + residual = self.conv_shortcut(residual) + + return hidden_states + residual + + +class FlaxAttentionBlock(nn.Module): + r""" + Flax Convolutional based multi-head attention block for diffusion-based VAE. + + Parameters: + channels (:obj:`int`): + Input channels + num_head_channels (:obj:`int`, *optional*, defaults to `None`): + Number of attention heads + num_groups (:obj:`int`, *optional*, defaults to `32`): + The number of groups to use for group norm + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + + """ + channels: int + num_head_channels: int = None + num_groups: int = 32 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.num_heads = self.channels // self.num_head_channels if self.num_head_channels is not None else 1 + + dense = partial(nn.Dense, self.channels, dtype=self.dtype) + + self.group_norm = nn.GroupNorm(num_groups=self.num_groups, epsilon=1e-6) + self.query, self.key, self.value = dense(), dense(), dense() + self.proj_attn = dense() + + def transpose_for_scores(self, projection): + new_projection_shape = projection.shape[:-1] + (self.num_heads, -1) + # move heads to 2nd position (B, T, H * D) -> (B, T, H, D) + new_projection = projection.reshape(new_projection_shape) + # (B, T, H, D) -> (B, H, T, D) + new_projection = jnp.transpose(new_projection, (0, 2, 1, 3)) + return new_projection + + def __call__(self, hidden_states): + residual = hidden_states + batch, height, width, channels = hidden_states.shape + + hidden_states = self.group_norm(hidden_states) + + hidden_states = hidden_states.reshape((batch, height * width, channels)) + + query = self.query(hidden_states) + key = self.key(hidden_states) + value = self.value(hidden_states) + + # transpose + query = self.transpose_for_scores(query) + key = self.transpose_for_scores(key) + value = self.transpose_for_scores(value) + + # compute attentions + scale = 1 / math.sqrt(math.sqrt(self.channels / self.num_heads)) + attn_weights = jnp.einsum("...qc,...kc->...qk", query * scale, key * scale) + attn_weights = nn.softmax(attn_weights, axis=-1) + + # attend to values + hidden_states = jnp.einsum("...kc,...qk->...qc", value, attn_weights) + + hidden_states = jnp.transpose(hidden_states, (0, 2, 1, 3)) + new_hidden_states_shape = hidden_states.shape[:-2] + (self.channels,) + hidden_states = hidden_states.reshape(new_hidden_states_shape) + + hidden_states = self.proj_attn(hidden_states) + hidden_states = hidden_states.reshape((batch, height, width, channels)) + hidden_states = hidden_states + residual + return hidden_states + + +class FlaxDownEncoderBlock2D(nn.Module): + r""" + Flax Resnet blocks-based Encoder block for diffusion-based VAE. + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of Resnet layer block + resnet_groups (:obj:`int`, *optional*, defaults to `32`): + The number of groups to use for the Resnet block group norm + add_downsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add downsample layer + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + dropout: float = 0.0 + num_layers: int = 1 + resnet_groups: int = 32 + add_downsample: bool = True + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + for i in range(self.num_layers): + in_channels = self.in_channels if i == 0 else self.out_channels + + res_block = FlaxResnetBlock2D( + in_channels=in_channels, + out_channels=self.out_channels, + dropout=self.dropout, + groups=self.resnet_groups, + dtype=self.dtype, + ) + resnets.append(res_block) + self.resnets = resnets + + if self.add_downsample: + self.downsamplers_0 = FlaxDownsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, deterministic=True): + for resnet in self.resnets: + hidden_states = resnet(hidden_states, deterministic=deterministic) + + if self.add_downsample: + hidden_states = self.downsamplers_0(hidden_states) + + return hidden_states + + +class FlaxUpDecoderBlock2D(nn.Module): + r""" + Flax Resnet blocks-based Decoder block for diffusion-based VAE. + + Parameters: + in_channels (:obj:`int`): + Input channels + out_channels (:obj:`int`): + Output channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of Resnet layer block + resnet_groups (:obj:`int`, *optional*, defaults to `32`): + The number of groups to use for the Resnet block group norm + add_upsample (:obj:`bool`, *optional*, defaults to `True`): + Whether to add upsample layer + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + out_channels: int + dropout: float = 0.0 + num_layers: int = 1 + resnet_groups: int = 32 + add_upsample: bool = True + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnets = [] + for i in range(self.num_layers): + in_channels = self.in_channels if i == 0 else self.out_channels + res_block = FlaxResnetBlock2D( + in_channels=in_channels, + out_channels=self.out_channels, + dropout=self.dropout, + groups=self.resnet_groups, + dtype=self.dtype, + ) + resnets.append(res_block) + + self.resnets = resnets + + if self.add_upsample: + self.upsamplers_0 = FlaxUpsample2D(self.out_channels, dtype=self.dtype) + + def __call__(self, hidden_states, deterministic=True): + for resnet in self.resnets: + hidden_states = resnet(hidden_states, deterministic=deterministic) + + if self.add_upsample: + hidden_states = self.upsamplers_0(hidden_states) + + return hidden_states + + +class FlaxUNetMidBlock2D(nn.Module): + r""" + Flax Unet Mid-Block module. + + Parameters: + in_channels (:obj:`int`): + Input channels + dropout (:obj:`float`, *optional*, defaults to 0.0): + Dropout rate + num_layers (:obj:`int`, *optional*, defaults to 1): + Number of Resnet layer block + resnet_groups (:obj:`int`, *optional*, defaults to `32`): + The number of groups to use for the Resnet and Attention block group norm + attn_num_head_channels (:obj:`int`, *optional*, defaults to `1`): + Number of attention heads for each attention block + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int + dropout: float = 0.0 + num_layers: int = 1 + resnet_groups: int = 32 + attn_num_head_channels: int = 1 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + resnet_groups = self.resnet_groups if self.resnet_groups is not None else min(self.in_channels // 4, 32) + + # there is always at least one resnet + resnets = [ + FlaxResnetBlock2D( + in_channels=self.in_channels, + out_channels=self.in_channels, + dropout=self.dropout, + groups=resnet_groups, + dtype=self.dtype, + ) + ] + + attentions = [] + + for _ in range(self.num_layers): + attn_block = FlaxAttentionBlock( + channels=self.in_channels, + num_head_channels=self.attn_num_head_channels, + num_groups=resnet_groups, + dtype=self.dtype, + ) + attentions.append(attn_block) + + res_block = FlaxResnetBlock2D( + in_channels=self.in_channels, + out_channels=self.in_channels, + dropout=self.dropout, + groups=resnet_groups, + dtype=self.dtype, + ) + resnets.append(res_block) + + self.resnets = resnets + self.attentions = attentions + + def __call__(self, hidden_states, deterministic=True): + hidden_states = self.resnets[0](hidden_states, deterministic=deterministic) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + hidden_states = attn(hidden_states) + hidden_states = resnet(hidden_states, deterministic=deterministic) + + return hidden_states + + +class FlaxEncoder(nn.Module): + r""" + Flax Implementation of VAE Encoder. + + This model is a Flax Linen [flax.linen.Module](https://flax.readthedocs.io/en/latest/flax.linen.html#module) + subclass. Use it as a regular Flax linen Module and refer to the Flax documentation for all matter related to + general usage and behavior. + + Finally, this model supports inherent JAX features such as: + - [Just-In-Time (JIT) compilation](https://jax.readthedocs.io/en/latest/jax.html#just-in-time-compilation-jit) + - [Automatic Differentiation](https://jax.readthedocs.io/en/latest/jax.html#automatic-differentiation) + - [Vectorization](https://jax.readthedocs.io/en/latest/jax.html#vectorization-vmap) + - [Parallelization](https://jax.readthedocs.io/en/latest/jax.html#parallelization-pmap) + + Parameters: + in_channels (:obj:`int`, *optional*, defaults to 3): + Input channels + out_channels (:obj:`int`, *optional*, defaults to 3): + Output channels + down_block_types (:obj:`Tuple[str]`, *optional*, defaults to `(DownEncoderBlock2D)`): + DownEncoder block type + block_out_channels (:obj:`Tuple[str]`, *optional*, defaults to `(64,)`): + Tuple containing the number of output channels for each block + layers_per_block (:obj:`int`, *optional*, defaults to `2`): + Number of Resnet layer for each block + norm_num_groups (:obj:`int`, *optional*, defaults to `32`): + norm num group + act_fn (:obj:`str`, *optional*, defaults to `silu`): + Activation function + double_z (:obj:`bool`, *optional*, defaults to `False`): + Whether to double the last output channels + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + Parameters `dtype` + """ + in_channels: int = 3 + out_channels: int = 3 + down_block_types: Tuple[str] = ("DownEncoderBlock2D",) + block_out_channels: Tuple[int] = (64,) + layers_per_block: int = 2 + norm_num_groups: int = 32 + act_fn: str = "silu" + double_z: bool = False + dtype: jnp.dtype = jnp.float32 + + def setup(self): + block_out_channels = self.block_out_channels + # in + self.conv_in = nn.Conv( + block_out_channels[0], + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + # downsampling + down_blocks = [] + output_channel = block_out_channels[0] + for i, _ in enumerate(self.down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = FlaxDownEncoderBlock2D( + in_channels=input_channel, + out_channels=output_channel, + num_layers=self.layers_per_block, + resnet_groups=self.norm_num_groups, + add_downsample=not is_final_block, + dtype=self.dtype, + ) + down_blocks.append(down_block) + self.down_blocks = down_blocks + + # middle + self.mid_block = FlaxUNetMidBlock2D( + in_channels=block_out_channels[-1], + resnet_groups=self.norm_num_groups, + attn_num_head_channels=None, + dtype=self.dtype, + ) + + # end + conv_out_channels = 2 * self.out_channels if self.double_z else self.out_channels + self.conv_norm_out = nn.GroupNorm(num_groups=self.norm_num_groups, epsilon=1e-6) + self.conv_out = nn.Conv( + conv_out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + def __call__(self, sample, deterministic: bool = True): + # in + sample = self.conv_in(sample) + + # downsampling + for block in self.down_blocks: + sample = block(sample, deterministic=deterministic) + + # middle + sample = self.mid_block(sample, deterministic=deterministic) + + # end + sample = self.conv_norm_out(sample) + sample = nn.swish(sample) + sample = self.conv_out(sample) + + return sample + + +class FlaxDecoder(nn.Module): + r""" + Flax Implementation of VAE Decoder. + + This model is a Flax Linen [flax.linen.Module](https://flax.readthedocs.io/en/latest/flax.linen.html#module) + subclass. Use it as a regular Flax linen Module and refer to the Flax documentation for all matter related to + general usage and behavior. + + Finally, this model supports inherent JAX features such as: + - [Just-In-Time (JIT) compilation](https://jax.readthedocs.io/en/latest/jax.html#just-in-time-compilation-jit) + - [Automatic Differentiation](https://jax.readthedocs.io/en/latest/jax.html#automatic-differentiation) + - [Vectorization](https://jax.readthedocs.io/en/latest/jax.html#vectorization-vmap) + - [Parallelization](https://jax.readthedocs.io/en/latest/jax.html#parallelization-pmap) + + Parameters: + in_channels (:obj:`int`, *optional*, defaults to 3): + Input channels + out_channels (:obj:`int`, *optional*, defaults to 3): + Output channels + up_block_types (:obj:`Tuple[str]`, *optional*, defaults to `(UpDecoderBlock2D)`): + UpDecoder block type + block_out_channels (:obj:`Tuple[str]`, *optional*, defaults to `(64,)`): + Tuple containing the number of output channels for each block + layers_per_block (:obj:`int`, *optional*, defaults to `2`): + Number of Resnet layer for each block + norm_num_groups (:obj:`int`, *optional*, defaults to `32`): + norm num group + act_fn (:obj:`str`, *optional*, defaults to `silu`): + Activation function + double_z (:obj:`bool`, *optional*, defaults to `False`): + Whether to double the last output channels + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + parameters `dtype` + """ + in_channels: int = 3 + out_channels: int = 3 + up_block_types: Tuple[str] = ("UpDecoderBlock2D",) + block_out_channels: int = (64,) + layers_per_block: int = 2 + norm_num_groups: int = 32 + act_fn: str = "silu" + dtype: jnp.dtype = jnp.float32 + + def setup(self): + block_out_channels = self.block_out_channels + + # z to block_in + self.conv_in = nn.Conv( + block_out_channels[-1], + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + # middle + self.mid_block = FlaxUNetMidBlock2D( + in_channels=block_out_channels[-1], + resnet_groups=self.norm_num_groups, + attn_num_head_channels=None, + dtype=self.dtype, + ) + + # upsampling + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + up_blocks = [] + for i, _ in enumerate(self.up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + + is_final_block = i == len(block_out_channels) - 1 + + up_block = FlaxUpDecoderBlock2D( + in_channels=prev_output_channel, + out_channels=output_channel, + num_layers=self.layers_per_block + 1, + resnet_groups=self.norm_num_groups, + add_upsample=not is_final_block, + dtype=self.dtype, + ) + up_blocks.append(up_block) + prev_output_channel = output_channel + + self.up_blocks = up_blocks + + # end + self.conv_norm_out = nn.GroupNorm(num_groups=self.norm_num_groups, epsilon=1e-6) + self.conv_out = nn.Conv( + self.out_channels, + kernel_size=(3, 3), + strides=(1, 1), + padding=((1, 1), (1, 1)), + dtype=self.dtype, + ) + + def __call__(self, sample, deterministic: bool = True): + # z to block_in + sample = self.conv_in(sample) + + # middle + sample = self.mid_block(sample, deterministic=deterministic) + + # upsampling + for block in self.up_blocks: + sample = block(sample, deterministic=deterministic) + + sample = self.conv_norm_out(sample) + sample = nn.swish(sample) + sample = self.conv_out(sample) + + return sample + + +class FlaxDiagonalGaussianDistribution(object): + def __init__(self, parameters, deterministic=False): + # Last axis to account for channels-last + self.mean, self.logvar = jnp.split(parameters, 2, axis=-1) + self.logvar = jnp.clip(self.logvar, -30.0, 20.0) + self.deterministic = deterministic + self.std = jnp.exp(0.5 * self.logvar) + self.var = jnp.exp(self.logvar) + if self.deterministic: + self.var = self.std = jnp.zeros_like(self.mean) + + def sample(self, key): + return self.mean + self.std * jax.random.normal(key, self.mean.shape) + + def kl(self, other=None): + if self.deterministic: + return jnp.array([0.0]) + + if other is None: + return 0.5 * jnp.sum(self.mean**2 + self.var - 1.0 - self.logvar, axis=[1, 2, 3]) + + return 0.5 * jnp.sum( + jnp.square(self.mean - other.mean) / other.var + self.var / other.var - 1.0 - self.logvar + other.logvar, + axis=[1, 2, 3], + ) + + def nll(self, sample, axis=[1, 2, 3]): + if self.deterministic: + return jnp.array([0.0]) + + logtwopi = jnp.log(2.0 * jnp.pi) + return 0.5 * jnp.sum(logtwopi + self.logvar + jnp.square(sample - self.mean) / self.var, axis=axis) + + def mode(self): + return self.mean + + +@flax_register_to_config +class FlaxAutoencoderKL(nn.Module, FlaxModelMixin, ConfigMixin): + r""" + Flax Implementation of Variational Autoencoder (VAE) model with KL loss from the paper Auto-Encoding Variational + Bayes by Diederik P. Kingma and Max Welling. + + This model is a Flax Linen [flax.linen.Module](https://flax.readthedocs.io/en/latest/flax.linen.html#module) + subclass. Use it as a regular Flax linen Module and refer to the Flax documentation for all matter related to + general usage and behavior. + + Finally, this model supports inherent JAX features such as: + - [Just-In-Time (JIT) compilation](https://jax.readthedocs.io/en/latest/jax.html#just-in-time-compilation-jit) + - [Automatic Differentiation](https://jax.readthedocs.io/en/latest/jax.html#automatic-differentiation) + - [Vectorization](https://jax.readthedocs.io/en/latest/jax.html#vectorization-vmap) + - [Parallelization](https://jax.readthedocs.io/en/latest/jax.html#parallelization-pmap) + + Parameters: + in_channels (:obj:`int`, *optional*, defaults to 3): + Input channels + out_channels (:obj:`int`, *optional*, defaults to 3): + Output channels + down_block_types (:obj:`Tuple[str]`, *optional*, defaults to `(DownEncoderBlock2D)`): + DownEncoder block type + up_block_types (:obj:`Tuple[str]`, *optional*, defaults to `(UpDecoderBlock2D)`): + UpDecoder block type + block_out_channels (:obj:`Tuple[str]`, *optional*, defaults to `(64,)`): + Tuple containing the number of output channels for each block + layers_per_block (:obj:`int`, *optional*, defaults to `2`): + Number of Resnet layer for each block + act_fn (:obj:`str`, *optional*, defaults to `silu`): + Activation function + latent_channels (:obj:`int`, *optional*, defaults to `4`): + Latent space channels + norm_num_groups (:obj:`int`, *optional*, defaults to `32`): + Norm num group + sample_size (:obj:`int`, *optional*, defaults to 32): + Sample input size + scaling_factor (`float`, *optional*, defaults to 0.18215): + The component-wise standard deviation of the trained latent space computed using the first batch of the + training set. This is used to scale the latent space to have unit variance when training the diffusion + model. The latents are scaled with the formula `z = z * scaling_factor` before being passed to the + diffusion model. When decoding, the latents are scaled back to the original scale with the formula: `z = 1 + / scaling_factor * z`. For more details, refer to sections 4.3.2 and D.1 of the [High-Resolution Image + Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) paper. + dtype (:obj:`jnp.dtype`, *optional*, defaults to jnp.float32): + parameters `dtype` + """ + in_channels: int = 3 + out_channels: int = 3 + down_block_types: Tuple[str] = ("DownEncoderBlock2D",) + up_block_types: Tuple[str] = ("UpDecoderBlock2D",) + block_out_channels: Tuple[int] = (64,) + layers_per_block: int = 1 + act_fn: str = "silu" + latent_channels: int = 4 + norm_num_groups: int = 32 + sample_size: int = 32 + scaling_factor: float = 0.18215 + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.encoder = FlaxEncoder( + in_channels=self.config.in_channels, + out_channels=self.config.latent_channels, + down_block_types=self.config.down_block_types, + block_out_channels=self.config.block_out_channels, + layers_per_block=self.config.layers_per_block, + act_fn=self.config.act_fn, + norm_num_groups=self.config.norm_num_groups, + double_z=True, + dtype=self.dtype, + ) + self.decoder = FlaxDecoder( + in_channels=self.config.latent_channels, + out_channels=self.config.out_channels, + up_block_types=self.config.up_block_types, + block_out_channels=self.config.block_out_channels, + layers_per_block=self.config.layers_per_block, + norm_num_groups=self.config.norm_num_groups, + act_fn=self.config.act_fn, + dtype=self.dtype, + ) + self.quant_conv = nn.Conv( + 2 * self.config.latent_channels, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + self.post_quant_conv = nn.Conv( + self.config.latent_channels, + kernel_size=(1, 1), + strides=(1, 1), + padding="VALID", + dtype=self.dtype, + ) + + def init_weights(self, rng: jax.random.KeyArray) -> FrozenDict: + # init input tensors + sample_shape = (1, self.in_channels, self.sample_size, self.sample_size) + sample = jnp.zeros(sample_shape, dtype=jnp.float32) + + params_rng, dropout_rng, gaussian_rng = jax.random.split(rng, 3) + rngs = {"params": params_rng, "dropout": dropout_rng, "gaussian": gaussian_rng} + + return self.init(rngs, sample)["params"] + + def encode(self, sample, deterministic: bool = True, return_dict: bool = True): + sample = jnp.transpose(sample, (0, 2, 3, 1)) + + hidden_states = self.encoder(sample, deterministic=deterministic) + moments = self.quant_conv(hidden_states) + posterior = FlaxDiagonalGaussianDistribution(moments) + + if not return_dict: + return (posterior,) + + return FlaxAutoencoderKLOutput(latent_dist=posterior) + + def decode(self, latents, deterministic: bool = True, return_dict: bool = True): + if latents.shape[-1] != self.config.latent_channels: + latents = jnp.transpose(latents, (0, 2, 3, 1)) + + hidden_states = self.post_quant_conv(latents) + hidden_states = self.decoder(hidden_states, deterministic=deterministic) + + hidden_states = jnp.transpose(hidden_states, (0, 3, 1, 2)) + + if not return_dict: + return (hidden_states,) + + return FlaxDecoderOutput(sample=hidden_states) + + def __call__(self, sample, sample_posterior=False, deterministic: bool = True, return_dict: bool = True): + posterior = self.encode(sample, deterministic=deterministic, return_dict=return_dict) + if sample_posterior: + rng = self.make_rng("gaussian") + hidden_states = posterior.latent_dist.sample(rng) + else: + hidden_states = posterior.latent_dist.mode() + + sample = self.decode(hidden_states, return_dict=return_dict).sample + + if not return_dict: + return (sample,) + + return FlaxDecoderOutput(sample=sample) diff --git a/diffusers/src/diffusers/models/vq_model.py b/diffusers/src/diffusers/models/vq_model.py new file mode 100644 index 0000000000000000000000000000000000000000..0341798fc4c42a43d296661dff08e17790100559 --- /dev/null +++ b/diffusers/src/diffusers/models/vq_model.py @@ -0,0 +1,156 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import torch +import torch.nn as nn + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .modeling_utils import ModelMixin +from .vae import Decoder, DecoderOutput, Encoder, VectorQuantizer + + +@dataclass +class VQEncoderOutput(BaseOutput): + """ + Output of VQModel encoding method. + + Args: + latents (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Encoded output sample of the model. Output of the last layer of the model. + """ + + latents: torch.FloatTensor + + +class VQModel(ModelMixin, ConfigMixin): + r"""VQ-VAE model from the paper Neural Discrete Representation Learning by Aaron van den Oord, Oriol Vinyals and Koray + Kavukcuoglu. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the model (such as downloading or saving, etc.) + + Parameters: + in_channels (int, *optional*, defaults to 3): Number of channels in the input image. + out_channels (int, *optional*, defaults to 3): Number of channels in the output. + down_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("DownEncoderBlock2D",)`): Tuple of downsample block types. + up_block_types (`Tuple[str]`, *optional*, defaults to : + obj:`("UpDecoderBlock2D",)`): Tuple of upsample block types. + block_out_channels (`Tuple[int]`, *optional*, defaults to : + obj:`(64,)`): Tuple of block output channels. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + latent_channels (`int`, *optional*, defaults to `3`): Number of channels in the latent space. + sample_size (`int`, *optional*, defaults to `32`): TODO + num_vq_embeddings (`int`, *optional*, defaults to `256`): Number of codebook vectors in the VQ-VAE. + vq_embed_dim (`int`, *optional*): Hidden dim of codebook vectors in the VQ-VAE. + scaling_factor (`float`, *optional*, defaults to `0.18215`): + The component-wise standard deviation of the trained latent space computed using the first batch of the + training set. This is used to scale the latent space to have unit variance when training the diffusion + model. The latents are scaled with the formula `z = z * scaling_factor` before being passed to the + diffusion model. When decoding, the latents are scaled back to the original scale with the formula: `z = 1 + / scaling_factor * z`. For more details, refer to sections 4.3.2 and D.1 of the [High-Resolution Image + Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752) paper. + """ + + @register_to_config + def __init__( + self, + in_channels: int = 3, + out_channels: int = 3, + down_block_types: Tuple[str] = ("DownEncoderBlock2D",), + up_block_types: Tuple[str] = ("UpDecoderBlock2D",), + block_out_channels: Tuple[int] = (64,), + layers_per_block: int = 1, + act_fn: str = "silu", + latent_channels: int = 3, + sample_size: int = 32, + num_vq_embeddings: int = 256, + norm_num_groups: int = 32, + vq_embed_dim: Optional[int] = None, + scaling_factor: float = 0.18215, + ): + super().__init__() + + # pass init params to Encoder + self.encoder = Encoder( + in_channels=in_channels, + out_channels=latent_channels, + down_block_types=down_block_types, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + act_fn=act_fn, + norm_num_groups=norm_num_groups, + double_z=False, + ) + + vq_embed_dim = vq_embed_dim if vq_embed_dim is not None else latent_channels + + self.quant_conv = nn.Conv2d(latent_channels, vq_embed_dim, 1) + self.quantize = VectorQuantizer(num_vq_embeddings, vq_embed_dim, beta=0.25, remap=None, sane_index_shape=False) + self.post_quant_conv = nn.Conv2d(vq_embed_dim, latent_channels, 1) + + # pass init params to Decoder + self.decoder = Decoder( + in_channels=latent_channels, + out_channels=out_channels, + up_block_types=up_block_types, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + act_fn=act_fn, + norm_num_groups=norm_num_groups, + ) + + def encode(self, x: torch.FloatTensor, return_dict: bool = True) -> VQEncoderOutput: + h = self.encoder(x) + h = self.quant_conv(h) + + if not return_dict: + return (h,) + + return VQEncoderOutput(latents=h) + + def decode( + self, h: torch.FloatTensor, force_not_quantize: bool = False, return_dict: bool = True + ) -> Union[DecoderOutput, torch.FloatTensor]: + # also go through quantization layer + if not force_not_quantize: + quant, emb_loss, info = self.quantize(h) + else: + quant = h + quant = self.post_quant_conv(quant) + dec = self.decoder(quant) + + if not return_dict: + return (dec,) + + return DecoderOutput(sample=dec) + + def forward(self, sample: torch.FloatTensor, return_dict: bool = True) -> Union[DecoderOutput, torch.FloatTensor]: + r""" + Args: + sample (`torch.FloatTensor`): Input sample. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`DecoderOutput`] instead of a plain tuple. + """ + x = sample + h = self.encode(x).latents + dec = self.decode(h).sample + + if not return_dict: + return (dec,) + + return DecoderOutput(sample=dec) diff --git a/diffusers/src/diffusers/optimization.py b/diffusers/src/diffusers/optimization.py new file mode 100644 index 0000000000000000000000000000000000000000..a5eedc68038d827e1cee9fd3d9173d2594ac5d95 --- /dev/null +++ b/diffusers/src/diffusers/optimization.py @@ -0,0 +1,293 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +"""PyTorch optimization for diffusion models.""" + +import math +from enum import Enum +from typing import Optional, Union + +from torch.optim import Optimizer +from torch.optim.lr_scheduler import LambdaLR + +from .utils import logging + + +logger = logging.get_logger(__name__) + + +class SchedulerType(Enum): + LINEAR = "linear" + COSINE = "cosine" + COSINE_WITH_RESTARTS = "cosine_with_restarts" + POLYNOMIAL = "polynomial" + CONSTANT = "constant" + CONSTANT_WITH_WARMUP = "constant_with_warmup" + + +def get_constant_schedule(optimizer: Optimizer, last_epoch: int = -1): + """ + Create a schedule with a constant learning rate, using the learning rate set in optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + return LambdaLR(optimizer, lambda _: 1, last_epoch=last_epoch) + + +def get_constant_schedule_with_warmup(optimizer: Optimizer, num_warmup_steps: int, last_epoch: int = -1): + """ + Create a schedule with a constant learning rate preceded by a warmup period during which the learning rate + increases linearly between 0 and the initial lr set in the optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + num_warmup_steps (`int`): + The number of steps for the warmup phase. + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step: int): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1.0, num_warmup_steps)) + return 1.0 + + return LambdaLR(optimizer, lr_lambda, last_epoch=last_epoch) + + +def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, last_epoch=-1): + """ + Create a schedule with a learning rate that decreases linearly from the initial lr set in the optimizer to 0, after + a warmup period during which it increases linearly from 0 to the initial lr set in the optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + num_warmup_steps (`int`): + The number of steps for the warmup phase. + num_training_steps (`int`): + The total number of training steps. + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step: int): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + return max( + 0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps)) + ) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def get_cosine_schedule_with_warmup( + optimizer: Optimizer, num_warmup_steps: int, num_training_steps: int, num_cycles: float = 0.5, last_epoch: int = -1 +): + """ + Create a schedule with a learning rate that decreases following the values of the cosine function between the + initial lr set in the optimizer to 0, after a warmup period during which it increases linearly between 0 and the + initial lr set in the optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + num_warmup_steps (`int`): + The number of steps for the warmup phase. + num_training_steps (`int`): + The total number of training steps. + num_periods (`float`, *optional*, defaults to 0.5): + The number of periods of the cosine function in a schedule (the default is to just decrease from the max + value to 0 following a half-cosine). + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps)) + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def get_cosine_with_hard_restarts_schedule_with_warmup( + optimizer: Optimizer, num_warmup_steps: int, num_training_steps: int, num_cycles: int = 1, last_epoch: int = -1 +): + """ + Create a schedule with a learning rate that decreases following the values of the cosine function between the + initial lr set in the optimizer to 0, with several hard restarts, after a warmup period during which it increases + linearly between 0 and the initial lr set in the optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + num_warmup_steps (`int`): + The number of steps for the warmup phase. + num_training_steps (`int`): + The total number of training steps. + num_cycles (`int`, *optional*, defaults to 1): + The number of hard restarts to use. + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + """ + + def lr_lambda(current_step): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps)) + if progress >= 1.0: + return 0.0 + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * ((float(num_cycles) * progress) % 1.0)))) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def get_polynomial_decay_schedule_with_warmup( + optimizer, num_warmup_steps, num_training_steps, lr_end=1e-7, power=1.0, last_epoch=-1 +): + """ + Create a schedule with a learning rate that decreases as a polynomial decay from the initial lr set in the + optimizer to end lr defined by *lr_end*, after a warmup period during which it increases linearly from 0 to the + initial lr set in the optimizer. + + Args: + optimizer ([`~torch.optim.Optimizer`]): + The optimizer for which to schedule the learning rate. + num_warmup_steps (`int`): + The number of steps for the warmup phase. + num_training_steps (`int`): + The total number of training steps. + lr_end (`float`, *optional*, defaults to 1e-7): + The end LR. + power (`float`, *optional*, defaults to 1.0): + Power factor. + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + + Note: *power* defaults to 1.0 as in the fairseq implementation, which in turn is based on the original BERT + implementation at + https://github.com/google-research/bert/blob/f39e881b169b9d53bea03d2d341b31707a6c052b/optimization.py#L37 + + Return: + `torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. + + """ + + lr_init = optimizer.defaults["lr"] + if not (lr_init > lr_end): + raise ValueError(f"lr_end ({lr_end}) must be be smaller than initial lr ({lr_init})") + + def lr_lambda(current_step: int): + if current_step < num_warmup_steps: + return float(current_step) / float(max(1, num_warmup_steps)) + elif current_step > num_training_steps: + return lr_end / lr_init # as LambdaLR multiplies by lr_init + else: + lr_range = lr_init - lr_end + decay_steps = num_training_steps - num_warmup_steps + pct_remaining = 1 - (current_step - num_warmup_steps) / decay_steps + decay = lr_range * pct_remaining**power + lr_end + return decay / lr_init # as LambdaLR multiplies by lr_init + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +TYPE_TO_SCHEDULER_FUNCTION = { + SchedulerType.LINEAR: get_linear_schedule_with_warmup, + SchedulerType.COSINE: get_cosine_schedule_with_warmup, + SchedulerType.COSINE_WITH_RESTARTS: get_cosine_with_hard_restarts_schedule_with_warmup, + SchedulerType.POLYNOMIAL: get_polynomial_decay_schedule_with_warmup, + SchedulerType.CONSTANT: get_constant_schedule, + SchedulerType.CONSTANT_WITH_WARMUP: get_constant_schedule_with_warmup, +} + + +def get_scheduler( + name: Union[str, SchedulerType], + optimizer: Optimizer, + num_warmup_steps: Optional[int] = None, + num_training_steps: Optional[int] = None, + num_cycles: int = 1, + power: float = 1.0, +): + """ + Unified API to get any scheduler from its name. + + Args: + name (`str` or `SchedulerType`): + The name of the scheduler to use. + optimizer (`torch.optim.Optimizer`): + The optimizer that will be used during training. + num_warmup_steps (`int`, *optional*): + The number of warmup steps to do. This is not required by all schedulers (hence the argument being + optional), the function will raise an error if it's unset and the scheduler type requires it. + num_training_steps (`int``, *optional*): + The number of training steps to do. This is not required by all schedulers (hence the argument being + optional), the function will raise an error if it's unset and the scheduler type requires it. + num_cycles (`int`, *optional*): + The number of hard restarts used in `COSINE_WITH_RESTARTS` scheduler. + power (`float`, *optional*, defaults to 1.0): + Power factor. See `POLYNOMIAL` scheduler + last_epoch (`int`, *optional*, defaults to -1): + The index of the last epoch when resuming training. + """ + name = SchedulerType(name) + schedule_func = TYPE_TO_SCHEDULER_FUNCTION[name] + if name == SchedulerType.CONSTANT: + return schedule_func(optimizer) + + # All other schedulers require `num_warmup_steps` + if num_warmup_steps is None: + raise ValueError(f"{name} requires `num_warmup_steps`, please provide that argument.") + + if name == SchedulerType.CONSTANT_WITH_WARMUP: + return schedule_func(optimizer, num_warmup_steps=num_warmup_steps) + + # All other schedulers require `num_training_steps` + if num_training_steps is None: + raise ValueError(f"{name} requires `num_training_steps`, please provide that argument.") + + if name == SchedulerType.COSINE_WITH_RESTARTS: + return schedule_func( + optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps, num_cycles=num_cycles + ) + + if name == SchedulerType.POLYNOMIAL: + return schedule_func( + optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps, power=power + ) + + return schedule_func(optimizer, num_warmup_steps=num_warmup_steps, num_training_steps=num_training_steps) diff --git a/diffusers/src/diffusers/pipeline_utils.py b/diffusers/src/diffusers/pipeline_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e8df9e93eb6e7cd94c3590046e22af9269a4de92 --- /dev/null +++ b/diffusers/src/diffusers/pipeline_utils.py @@ -0,0 +1,19 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +# NOTE: This file is deprecated and will be removed in a future version. +# It only exists so that temporarely `from diffusers.pipelines import DiffusionPipeline` works + +from .pipelines import DiffusionPipeline, ImagePipelineOutput # noqa: F401 diff --git a/diffusers/src/diffusers/pipelines/README.md b/diffusers/src/diffusers/pipelines/README.md new file mode 100644 index 0000000000000000000000000000000000000000..07f5601ee9178e41cced43d13108b9e129015a9f --- /dev/null +++ b/diffusers/src/diffusers/pipelines/README.md @@ -0,0 +1,171 @@ +# 🧨 Diffusers Pipelines + +Pipelines provide a simple way to run state-of-the-art diffusion models in inference. +Most diffusion systems consist of multiple independently-trained models and highly adaptable scheduler +components - all of which are needed to have a functioning end-to-end diffusion system. + +As an example, [Stable Diffusion](https://huggingface.co/blog/stable_diffusion) has three independently trained models: +- [Autoencoder](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/models/vae.py#L392) +- [Conditional Unet](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/models/unet_2d_condition.py#L12) +- [CLIP text encoder](https://huggingface.co/docs/transformers/v4.21.2/en/model_doc/clip#transformers.CLIPTextModel) +- a scheduler component, [scheduler](https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_pndm.py), +- a [CLIPFeatureExtractor](https://huggingface.co/docs/transformers/v4.21.2/en/model_doc/clip#transformers.CLIPFeatureExtractor), +- as well as a [safety checker](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/safety_checker.py). +All of these components are necessary to run stable diffusion in inference even though they were trained +or created independently from each other. + +To that end, we strive to offer all open-sourced, state-of-the-art diffusion system under a unified API. +More specifically, we strive to provide pipelines that +- 1. can load the officially published weights and yield 1-to-1 the same outputs as the original implementation according to the corresponding paper (*e.g.* [LDMTextToImagePipeline](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/latent_diffusion), uses the officially released weights of [High-Resolution Image Synthesis with Latent Diffusion Models](https://arxiv.org/abs/2112.10752)), +- 2. have a simple user interface to run the model in inference (see the [Pipelines API](#pipelines-api) section), +- 3. are easy to understand with code that is self-explanatory and can be read along-side the official paper (see [Pipelines summary](#pipelines-summary)), +- 4. can easily be contributed by the community (see the [Contribution](#contribution) section). + +**Note** that pipelines do not (and should not) offer any training functionality. +If you are looking for *official* training examples, please have a look at [examples](https://github.com/huggingface/diffusers/tree/main/examples). + + +## Pipelines Summary + +The following table summarizes all officially supported pipelines, their corresponding paper, and if +available a colab notebook to directly try them out. + +| Pipeline | Source | Tasks | Colab +|-------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|:---:|:---:| +| [dance diffusion](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/dance_diffusion) | [**Dance Diffusion**](https://github.com/Harmonai-org/sample-generator) | *Unconditional Audio Generation* | +| [ddpm](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/ddpm) | [**Denoising Diffusion Probabilistic Models**](https://arxiv.org/abs/2006.11239) | *Unconditional Image Generation* | +| [ddim](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/ddim) | [**Denoising Diffusion Implicit Models**](https://arxiv.org/abs/2010.02502) | *Unconditional Image Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [latent_diffusion](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752) | *Text-to-Image Generation* | +| [latent_diffusion_uncond](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/latent_diffusion_uncond) | [**High-Resolution Image Synthesis with Latent Diffusion Models**](https://arxiv.org/abs/2112.10752) | *Unconditional Image Generation* | +| [pndm](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pndm) | [**Pseudo Numerical Methods for Diffusion Models on Manifolds**](https://arxiv.org/abs/2202.09778) | *Unconditional Image Generation* | +| [score_sde_ve](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/score_sde_ve) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | *Unconditional Image Generation* | +| [score_sde_vp](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/score_sde_vp) | [**Score-Based Generative Modeling through Stochastic Differential Equations**](https://openreview.net/forum?id=PxTIG12RRHS) | *Unconditional Image Generation* | +| [stable_diffusion](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | *Text-to-Image Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/stable_diffusion.ipynb) +| [stable_diffusion](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | *Image-to-Image Text-Guided Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) +| [stable_diffusion](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion) | [**Stable Diffusion**](https://stability.ai/blog/stable-diffusion-public-release) | *Text-Guided Image Inpainting* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) +| [stochastic_karras_ve](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stochastic_karras_ve) | [**Elucidating the Design Space of Diffusion-Based Generative Models**](https://arxiv.org/abs/2206.00364) | *Unconditional Image Generation* | + +**Note**: Pipelines are simple examples of how to play around with the diffusion systems as described in the corresponding papers. +However, most of them can be adapted to use different scheduler components or even different model components. Some pipeline examples are shown in the [Examples](#examples) below. + +## Pipelines API + +Diffusion models often consist of multiple independently-trained models or other previously existing components. + + +Each model has been trained independently on a different task and the scheduler can easily be swapped out and replaced with a different one. +During inference, we however want to be able to easily load all components and use them in inference - even if one component, *e.g.* CLIP's text encoder, originates from a different library, such as [Transformers](https://github.com/huggingface/transformers). To that end, all pipelines provide the following functionality: + +- [`from_pretrained` method](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/pipeline_utils.py#L139) that accepts a Hugging Face Hub repository id, *e.g.* [runwayml/stable-diffusion-v1-5](https://huggingface.co/runwayml/stable-diffusion-v1-5) or a path to a local directory, *e.g.* +"./stable-diffusion". To correctly retrieve which models and components should be loaded, one has to provide a `model_index.json` file, *e.g.* [runwayml/stable-diffusion-v1-5/model_index.json](https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/model_index.json), which defines all components that should be +loaded into the pipelines. More specifically, for each model/component one needs to define the format `: ["", ""]`. `` is the attribute name given to the loaded instance of `` which can be found in the library or pipeline folder called `""`. +- [`save_pretrained`](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/pipeline_utils.py#L90) that accepts a local path, *e.g.* `./stable-diffusion` under which all models/components of the pipeline will be saved. For each component/model a folder is created inside the local path that is named after the given attribute name, *e.g.* `./stable_diffusion/unet`. +In addition, a `model_index.json` file is created at the root of the local path, *e.g.* `./stable_diffusion/model_index.json` so that the complete pipeline can again be instantiated +from the local path. +- [`to`](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/pipeline_utils.py#L118) which accepts a `string` or `torch.device` to move all models that are of type `torch.nn.Module` to the passed device. The behavior is fully analogous to [PyTorch's `to` method](https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.to). +- [`__call__`] method to use the pipeline in inference. `__call__` defines inference logic of the pipeline and should ideally encompass all aspects of it, from pre-processing to forwarding tensors to the different models and schedulers, as well as post-processing. The API of the `__call__` method can strongly vary from pipeline to pipeline. *E.g.* a text-to-image pipeline, such as [`StableDiffusionPipeline`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) should accept among other things the text prompt to generate the image. A pure image generation pipeline, such as [DDPMPipeline](https://github.com/huggingface/diffusers/tree/main/src/diffusers/pipelines/ddpm) on the other hand can be run without providing any inputs. To better understand what inputs can be adapted for +each pipeline, one should look directly into the respective pipeline. + +**Note**: All pipelines have PyTorch's autograd disabled by decorating the `__call__` method with a [`torch.no_grad`](https://pytorch.org/docs/stable/generated/torch.no_grad.html) decorator because pipelines should +not be used for training. If you want to store the gradients during the forward pass, we recommend writing your own pipeline, see also our [community-examples](https://github.com/huggingface/diffusers/tree/main/examples/community) + +## Contribution + +We are more than happy about any contribution to the officially supported pipelines 🤗. We aspire +all of our pipelines to be **self-contained**, **easy-to-tweak**, **beginner-friendly** and for **one-purpose-only**. + +- **Self-contained**: A pipeline shall be as self-contained as possible. More specifically, this means that all functionality should be either directly defined in the pipeline file itself, should be inherited from (and only from) the [`DiffusionPipeline` class](https://github.com/huggingface/diffusers/blob/5cbed8e0d157f65d3ddc2420dfd09f2df630e978/src/diffusers/pipeline_utils.py#L56) or be directly attached to the model and scheduler components of the pipeline. +- **Easy-to-use**: Pipelines should be extremely easy to use - one should be able to load the pipeline and +use it for its designated task, *e.g.* text-to-image generation, in just a couple of lines of code. Most +logic including pre-processing, an unrolled diffusion loop, and post-processing should all happen inside the `__call__` method. +- **Easy-to-tweak**: Certain pipelines will not be able to handle all use cases and tasks that you might like them to. If you want to use a certain pipeline for a specific use case that is not yet supported, you might have to copy the pipeline file and tweak the code to your needs. We try to make the pipeline code as readable as possible so that each part –from pre-processing to diffusing to post-processing– can easily be adapted. If you would like the community to benefit from your customized pipeline, we would love to see a contribution to our [community-examples](https://github.com/huggingface/diffusers/tree/main/examples/community). If you feel that an important pipeline should be part of the official pipelines but isn't, a contribution to the [official pipelines](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines) would be even better. +- **One-purpose-only**: Pipelines should be used for one task and one task only. Even if two tasks are very similar from a modeling point of view, *e.g.* image2image translation and in-painting, pipelines shall be used for one task only to keep them *easy-to-tweak* and *readable*. + +## Examples + +### Text-to-Image generation with Stable Diffusion + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline, LMSDiscreteScheduler + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).images[0] + +image.save("astronaut_rides_horse.png") +``` + +### Image-to-Image text-guided generation with Stable Diffusion + +The `StableDiffusionImg2ImgPipeline` lets you pass a text prompt and an initial image to condition the generation of new images. + +```python +import requests +from PIL import Image +from io import BytesIO + +from diffusers import StableDiffusionImg2ImgPipeline + +# load the pipeline +device = "cuda" +pipe = StableDiffusionImg2ImgPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + torch_dtype=torch.float16, +).to(device) + +# let's download an initial image +url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((768, 512)) + +prompt = "A fantasy landscape, trending on artstation" + +images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + +images[0].save("fantasy_landscape.png") +``` +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) + +### Tweak prompts reusing seeds and latents + +You can generate your own latents to reproduce results, or tweak your prompt on a specific result you liked. [This notebook](https://github.com/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb) shows how to do it step by step. You can also run it in Google Colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/pcuenca/diffusers-examples/blob/main/notebooks/stable-diffusion-seeds.ipynb). + + +### In-painting using Stable Diffusion + +The `StableDiffusionInpaintPipeline` lets you edit specific parts of an image by providing a mask and text prompt. + +```python +import PIL +import requests +import torch +from io import BytesIO + +from diffusers import StableDiffusionInpaintPipeline + +def download_image(url): + response = requests.get(url) + return PIL.Image.open(BytesIO(response.content)).convert("RGB") + +img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" +mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + +init_image = download_image(img_url).resize((512, 512)) +mask_image = download_image(mask_url).resize((512, 512)) + +pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + torch_dtype=torch.float16, +) +pipe = pipe.to("cuda") + +prompt = "Face of a yellow cat, high resolution, sitting on a park bench" +image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0] +``` + +You can also run this example on colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) diff --git a/diffusers/src/diffusers/pipelines/__init__.py b/diffusers/src/diffusers/pipelines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dfb2fd83cb71a090439ebec482ed3a872383f309 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/__init__.py @@ -0,0 +1,118 @@ +from ..utils import ( + OptionalDependencyNotAvailable, + is_flax_available, + is_k_diffusion_available, + is_librosa_available, + is_onnx_available, + is_torch_available, + is_transformers_available, +) + + +try: + if not is_torch_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_pt_objects import * # noqa F403 +else: + from .dance_diffusion import DanceDiffusionPipeline + from .ddim import DDIMPipeline + from .ddpm import DDPMPipeline + from .dit import DiTPipeline + from .latent_diffusion import LDMSuperResolutionPipeline + from .latent_diffusion_uncond import LDMPipeline + from .pipeline_utils import AudioPipelineOutput, DiffusionPipeline, ImagePipelineOutput + from .pndm import PNDMPipeline + from .repaint import RePaintPipeline + from .score_sde_ve import ScoreSdeVePipeline + from .stochastic_karras_ve import KarrasVePipeline + +try: + if not (is_torch_available() and is_librosa_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_torch_and_librosa_objects import * # noqa F403 +else: + from .audio_diffusion import AudioDiffusionPipeline, Mel + +try: + if not (is_torch_available() and is_transformers_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_torch_and_transformers_objects import * # noqa F403 +else: + from .alt_diffusion import AltDiffusionImg2ImgPipeline, AltDiffusionPipeline + from .latent_diffusion import LDMTextToImagePipeline + from .paint_by_example import PaintByExamplePipeline + from .stable_diffusion import ( + CycleDiffusionPipeline, + StableDiffusionDepth2ImgPipeline, + StableDiffusionImageVariationPipeline, + StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipeline, + StableDiffusionInpaintPipelineLegacy, + StableDiffusionInstructPix2PixPipeline, + StableDiffusionLatentUpscalePipeline, + StableDiffusionPipeline, + StableDiffusionUpscalePipeline, + ) + from .stable_diffusion_safe import StableDiffusionPipelineSafe + from .unclip import UnCLIPImageVariationPipeline, UnCLIPPipeline + from .versatile_diffusion import ( + VersatileDiffusionDualGuidedPipeline, + VersatileDiffusionImageVariationPipeline, + VersatileDiffusionPipeline, + VersatileDiffusionTextToImagePipeline, + ) + from .vq_diffusion import VQDiffusionPipeline + +try: + if not is_onnx_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_onnx_objects import * # noqa F403 +else: + from .onnx_utils import OnnxRuntimeModel + +try: + if not (is_torch_available() and is_transformers_available() and is_onnx_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_torch_and_transformers_and_onnx_objects import * # noqa F403 +else: + from .stable_diffusion import ( + OnnxStableDiffusionImg2ImgPipeline, + OnnxStableDiffusionInpaintPipeline, + OnnxStableDiffusionInpaintPipelineLegacy, + OnnxStableDiffusionPipeline, + StableDiffusionOnnxPipeline, + ) + +try: + if not (is_torch_available() and is_transformers_available() and is_k_diffusion_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_torch_and_transformers_and_k_diffusion_objects import * # noqa F403 +else: + from .stable_diffusion import StableDiffusionKDiffusionPipeline + +try: + if not is_flax_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_flax_objects import * # noqa F403 +else: + from .pipeline_flax_utils import FlaxDiffusionPipeline + + +try: + if not (is_flax_available() and is_transformers_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_flax_and_transformers_objects import * # noqa F403 +else: + from .stable_diffusion import ( + FlaxStableDiffusionImg2ImgPipeline, + FlaxStableDiffusionInpaintPipeline, + FlaxStableDiffusionPipeline, + ) diff --git a/diffusers/src/diffusers/pipelines/alt_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/alt_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dab2d8db1045ef27ff5d2234951c1488f547401b --- /dev/null +++ b/diffusers/src/diffusers/pipelines/alt_diffusion/__init__.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +import numpy as np +import PIL +from PIL import Image + +from ...utils import BaseOutput, is_torch_available, is_transformers_available + + +@dataclass +# Copied from diffusers.pipelines.stable_diffusion.__init__.StableDiffusionPipelineOutput with Stable->Alt +class AltDiffusionPipelineOutput(BaseOutput): + """ + Output class for Alt Diffusion pipelines. + + Args: + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, + num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. + nsfw_content_detected (`List[bool]`) + List of flags denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, or `None` if safety checking could not be performed. + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + nsfw_content_detected: Optional[List[bool]] + + +if is_transformers_available() and is_torch_available(): + from .modeling_roberta_series import RobertaSeriesModelWithTransformation + from .pipeline_alt_diffusion import AltDiffusionPipeline + from .pipeline_alt_diffusion_img2img import AltDiffusionImg2ImgPipeline diff --git a/diffusers/src/diffusers/pipelines/alt_diffusion/modeling_roberta_series.py b/diffusers/src/diffusers/pipelines/alt_diffusion/modeling_roberta_series.py new file mode 100644 index 0000000000000000000000000000000000000000..637d6dd18698f3c6f1787c5e4d4514e4fc254908 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/alt_diffusion/modeling_roberta_series.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass +from typing import Optional, Tuple + +import torch +from torch import nn +from transformers import RobertaPreTrainedModel, XLMRobertaConfig, XLMRobertaModel +from transformers.utils import ModelOutput + + +@dataclass +class TransformationModelOutput(ModelOutput): + """ + Base class for text model's outputs that also contains a pooling of the last hidden states. + + Args: + text_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim)` *optional* returned when model is initialized with `with_projection=True`): + The text embeddings obtained by applying the projection layer to the pooler_output. + last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`): + Sequence of hidden-states at the output of the last layer of the model. + hidden_states (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed or when `config.output_hidden_states=True`): + Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, + + one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`. + + Hidden-states of the model at the output of each layer plus the optional initial embedding outputs. + attentions (`tuple(torch.FloatTensor)`, *optional*, returned when `output_attentions=True` is passed or when `config.output_attentions=True`): + Tuple of `torch.FloatTensor` (one for each layer) of shape `(batch_size, num_heads, sequence_length, + sequence_length)`. + + Attentions weights after the attention softmax, used to compute the weighted average in the self-attention + heads. + """ + + projection_state: Optional[torch.FloatTensor] = None + last_hidden_state: torch.FloatTensor = None + hidden_states: Optional[Tuple[torch.FloatTensor]] = None + attentions: Optional[Tuple[torch.FloatTensor]] = None + + +class RobertaSeriesConfig(XLMRobertaConfig): + def __init__( + self, + pad_token_id=1, + bos_token_id=0, + eos_token_id=2, + project_dim=512, + pooler_fn="cls", + learn_encoder=False, + use_attention_mask=True, + **kwargs, + ): + super().__init__(pad_token_id=pad_token_id, bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs) + self.project_dim = project_dim + self.pooler_fn = pooler_fn + self.learn_encoder = learn_encoder + self.use_attention_mask = use_attention_mask + + +class RobertaSeriesModelWithTransformation(RobertaPreTrainedModel): + _keys_to_ignore_on_load_unexpected = [r"pooler"] + _keys_to_ignore_on_load_missing = [r"position_ids", r"predictions.decoder.bias"] + base_model_prefix = "roberta" + config_class = RobertaSeriesConfig + + def __init__(self, config): + super().__init__(config) + self.roberta = XLMRobertaModel(config) + self.transformation = nn.Linear(config.hidden_size, config.project_dim) + self.post_init() + + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + token_type_ids: Optional[torch.Tensor] = None, + position_ids: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + inputs_embeds: Optional[torch.Tensor] = None, + encoder_hidden_states: Optional[torch.Tensor] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + ): + r""" """ + + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + outputs = self.base_model( + input_ids=input_ids, + attention_mask=attention_mask, + token_type_ids=token_type_ids, + position_ids=position_ids, + head_mask=head_mask, + inputs_embeds=inputs_embeds, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + projection_state = self.transformation(outputs.last_hidden_state) + + return TransformationModelOutput( + projection_state=projection_state, + last_hidden_state=outputs.last_hidden_state, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) diff --git a/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion.py b/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..5a2b040a3a7d63a9e8ff38628cd90e191e1028e9 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion.py @@ -0,0 +1,657 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Any, Callable, Dict, List, Optional, Union + +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, XLMRobertaTokenizer + +from diffusers.utils import is_accelerate_available + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import deprecate, logging, randn_tensor, replace_example_docstring +from ..pipeline_utils import DiffusionPipeline +from ..stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from . import AltDiffusionPipelineOutput, RobertaSeriesModelWithTransformation + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +EXAMPLE_DOC_STRING = """ + Examples: + ```py + >>> import torch + >>> from diffusers import AltDiffusionPipeline + + >>> pipe = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion-m9", torch_dtype=torch.float16) + >>> pipe = pipe.to("cuda") + + >>> # "dark elf princess, highly detailed, d & d, fantasy, highly detailed, digital painting, trending on artstation, concept art, sharp focus, illustration, art by artgerm and greg rutkowski and fuji choko and viktoria gavrilenko and hoang lap" + >>> prompt = "黑暗精灵公主,非常详细,幻想,非常详细,数字绘画,概念艺术,敏锐的焦点,插图" + >>> image = pipe(prompt).images[0] + ``` +""" + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline with Stable->Alt, CLIPTextModel->RobertaSeriesModelWithTransformation, CLIPTokenizer->XLMRobertaTokenizer, AltDiffusionSafetyChecker->StableDiffusionSafetyChecker +class AltDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Alt Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`RobertaSeriesModelWithTransformation`]): + Frozen text-encoder. Alt Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.RobertaSeriesModelWithTransformation), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`XLMRobertaTokenizer`): + Tokenizer of class + [XLMRobertaTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.XLMRobertaTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: RobertaSeriesModelWithTransformation, + tokenizer: XLMRobertaTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Alt Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. + + When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several + steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously invoked, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + @replace_example_docstring(EXAMPLE_DOC_STRING) + def __call__( + self, + prompt: Union[str, List[str]] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttnProcessor` as defined under + `self.processor` in + [diffusers.cross_attention](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py). + + Examples: + + Returns: + [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, height, width, callback_steps, negative_prompt, prompt_embeds, negative_prompt_embeds + ) + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + if output_type == "latent": + image = latents + has_nsfw_concept = None + elif output_type == "pil": + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 10. Convert to PIL + image = self.numpy_to_pil(image) + else: + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + if not return_dict: + return (image, has_nsfw_concept) + + return AltDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion_img2img.py b/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..ac485f0c9ca0886c08a6eec50f99944fb7a9d034 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion_img2img.py @@ -0,0 +1,690 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, XLMRobertaTokenizer + +from diffusers.utils import is_accelerate_available + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import PIL_INTERPOLATION, deprecate, logging, randn_tensor, replace_example_docstring +from ..pipeline_utils import DiffusionPipeline +from ..stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from . import AltDiffusionPipelineOutput, RobertaSeriesModelWithTransformation + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +EXAMPLE_DOC_STRING = """ + Examples: + ```py + >>> import requests + >>> import torch + >>> from PIL import Image + >>> from io import BytesIO + + >>> from diffusers import AltDiffusionImg2ImgPipeline + + >>> device = "cuda" + >>> model_id_or_path = "BAAI/AltDiffusion-m9" + >>> pipe = AltDiffusionImg2ImgPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) + >>> pipe = pipe.to(device) + + >>> url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + + >>> response = requests.get(url) + >>> init_image = Image.open(BytesIO(response.content)).convert("RGB") + >>> init_image = init_image.resize((768, 512)) + + >>> # "A fantasy landscape, trending on artstation" + >>> prompt = "幻想风景, artstation" + + >>> images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + >>> images[0].save("幻想风景.png") + ``` +""" + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline with Stable->Alt, CLIPTextModel->RobertaSeriesModelWithTransformation, CLIPTokenizer->XLMRobertaTokenizer, AltDiffusionSafetyChecker->StableDiffusionSafetyChecker +class AltDiffusionImg2ImgPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image to image generation using Alt Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`RobertaSeriesModelWithTransformation`]): + Frozen text-encoder. Alt Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.RobertaSeriesModelWithTransformation), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`XLMRobertaTokenizer`): + Tokenizer of class + [XLMRobertaTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.XLMRobertaTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: RobertaSeriesModelWithTransformation, + tokenizer: XLMRobertaTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Alt Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, prompt, strength, callback_steps, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None + ): + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def get_timesteps(self, num_inference_steps, strength, device): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + timesteps = self.scheduler.timesteps[t_start:] + + return timesteps, num_inference_steps - t_start + + def prepare_latents(self, image, timestep, batch_size, num_images_per_prompt, dtype, device, generator=None): + if not isinstance(image, (torch.Tensor, PIL.Image.Image, list)): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or list but is {type(image)}" + ) + + image = image.to(device=device, dtype=dtype) + + batch_size = batch_size * num_images_per_prompt + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if isinstance(generator, list): + init_latents = [ + self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size) + ] + init_latents = torch.cat(init_latents, dim=0) + else: + init_latents = self.vae.encode(image).latent_dist.sample(generator) + + init_latents = self.vae.config.scaling_factor * init_latents + + if batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] == 0: + # expand init_latents for batch_size + deprecation_message = ( + f"You have passed {batch_size} text prompts (`prompt`), but only {init_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = batch_size // init_latents.shape[0] + init_latents = torch.cat([init_latents] * additional_image_per_prompt, dim=0) + elif batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts." + ) + else: + init_latents = torch.cat([init_latents], dim=0) + + shape = init_latents.shape + noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + + # get latents + init_latents = self.scheduler.add_noise(init_latents, noise, timestep) + latents = init_latents + + return latents + + @torch.no_grad() + @replace_example_docstring(EXAMPLE_DOC_STRING) + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Examples: + + Returns: + [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.AltDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, strength, callback_steps, negative_prompt, prompt_embeds, negative_prompt_embeds) + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Preprocess image + image = preprocess(image) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + latents = self.prepare_latents( + image, latent_timestep, batch_size, num_images_per_prompt, prompt_embeds.dtype, device, generator + ) + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return AltDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/audio_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/audio_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..58554c45ea52b9897293217652db36fdace7549f --- /dev/null +++ b/diffusers/src/diffusers/pipelines/audio_diffusion/__init__.py @@ -0,0 +1,2 @@ +from .mel import Mel +from .pipeline_audio_diffusion import AudioDiffusionPipeline diff --git a/diffusers/src/diffusers/pipelines/audio_diffusion/mel.py b/diffusers/src/diffusers/pipelines/audio_diffusion/mel.py new file mode 100644 index 0000000000000000000000000000000000000000..ccb296098aca5e718aaf25ef899b947e2d4a8b6c --- /dev/null +++ b/diffusers/src/diffusers/pipelines/audio_diffusion/mel.py @@ -0,0 +1,160 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +import numpy as np # noqa: E402 + +from ...configuration_utils import ConfigMixin, register_to_config +from ...schedulers.scheduling_utils import SchedulerMixin + + +try: + import librosa # noqa: E402 + + _librosa_can_be_imported = True + _import_error = "" +except Exception as e: + _librosa_can_be_imported = False + _import_error = ( + f"Cannot import librosa because {e}. Make sure to correctly install librosa to be able to install it." + ) + + +from PIL import Image # noqa: E402 + + +class Mel(ConfigMixin, SchedulerMixin): + """ + Parameters: + x_res (`int`): x resolution of spectrogram (time) + y_res (`int`): y resolution of spectrogram (frequency bins) + sample_rate (`int`): sample rate of audio + n_fft (`int`): number of Fast Fourier Transforms + hop_length (`int`): hop length (a higher number is recommended for lower than 256 y_res) + top_db (`int`): loudest in decibels + n_iter (`int`): number of iterations for Griffin Linn mel inversion + """ + + config_name = "mel_config.json" + + @register_to_config + def __init__( + self, + x_res: int = 256, + y_res: int = 256, + sample_rate: int = 22050, + n_fft: int = 2048, + hop_length: int = 512, + top_db: int = 80, + n_iter: int = 32, + ): + self.hop_length = hop_length + self.sr = sample_rate + self.n_fft = n_fft + self.top_db = top_db + self.n_iter = n_iter + self.set_resolution(x_res, y_res) + self.audio = None + + if not _librosa_can_be_imported: + raise ValueError(_import_error) + + def set_resolution(self, x_res: int, y_res: int): + """Set resolution. + + Args: + x_res (`int`): x resolution of spectrogram (time) + y_res (`int`): y resolution of spectrogram (frequency bins) + """ + self.x_res = x_res + self.y_res = y_res + self.n_mels = self.y_res + self.slice_size = self.x_res * self.hop_length - 1 + + def load_audio(self, audio_file: str = None, raw_audio: np.ndarray = None): + """Load audio. + + Args: + audio_file (`str`): must be a file on disk due to Librosa limitation or + raw_audio (`np.ndarray`): audio as numpy array + """ + if audio_file is not None: + self.audio, _ = librosa.load(audio_file, mono=True, sr=self.sr) + else: + self.audio = raw_audio + + # Pad with silence if necessary. + if len(self.audio) < self.x_res * self.hop_length: + self.audio = np.concatenate([self.audio, np.zeros((self.x_res * self.hop_length - len(self.audio),))]) + + def get_number_of_slices(self) -> int: + """Get number of slices in audio. + + Returns: + `int`: number of spectograms audio can be sliced into + """ + return len(self.audio) // self.slice_size + + def get_audio_slice(self, slice: int = 0) -> np.ndarray: + """Get slice of audio. + + Args: + slice (`int`): slice number of audio (out of get_number_of_slices()) + + Returns: + `np.ndarray`: audio as numpy array + """ + return self.audio[self.slice_size * slice : self.slice_size * (slice + 1)] + + def get_sample_rate(self) -> int: + """Get sample rate: + + Returns: + `int`: sample rate of audio + """ + return self.sr + + def audio_slice_to_image(self, slice: int) -> Image.Image: + """Convert slice of audio to spectrogram. + + Args: + slice (`int`): slice number of audio to convert (out of get_number_of_slices()) + + Returns: + `PIL Image`: grayscale image of x_res x y_res + """ + S = librosa.feature.melspectrogram( + y=self.get_audio_slice(slice), sr=self.sr, n_fft=self.n_fft, hop_length=self.hop_length, n_mels=self.n_mels + ) + log_S = librosa.power_to_db(S, ref=np.max, top_db=self.top_db) + bytedata = (((log_S + self.top_db) * 255 / self.top_db).clip(0, 255) + 0.5).astype(np.uint8) + image = Image.fromarray(bytedata) + return image + + def image_to_audio(self, image: Image.Image) -> np.ndarray: + """Converts spectrogram to audio. + + Args: + image (`PIL Image`): x_res x y_res grayscale image + + Returns: + audio (`np.ndarray`): raw audio + """ + bytedata = np.frombuffer(image.tobytes(), dtype="uint8").reshape((image.height, image.width)) + log_S = bytedata.astype("float") * self.top_db / 255 - self.top_db + S = librosa.db_to_power(log_S) + audio = librosa.feature.inverse.mel_to_audio( + S, sr=self.sr, n_fft=self.n_fft, hop_length=self.hop_length, n_iter=self.n_iter + ) + return audio diff --git a/diffusers/src/diffusers/pipelines/audio_diffusion/pipeline_audio_diffusion.py b/diffusers/src/diffusers/pipelines/audio_diffusion/pipeline_audio_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..f8a7c29f62bfb8321a534ad6dd3e57ef36583518 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/audio_diffusion/pipeline_audio_diffusion.py @@ -0,0 +1,266 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from math import acos, sin +from typing import List, Tuple, Union + +import numpy as np +import torch +from PIL import Image + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import DDIMScheduler, DDPMScheduler +from ...utils import randn_tensor +from ..pipeline_utils import AudioPipelineOutput, BaseOutput, DiffusionPipeline, ImagePipelineOutput +from .mel import Mel + + +class AudioDiffusionPipeline(DiffusionPipeline): + """ + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqae ([`AutoencoderKL`]): Variational AutoEncoder for Latent Audio Diffusion or None + unet ([`UNet2DConditionModel`]): UNET model + mel ([`Mel`]): transform audio <-> spectrogram + scheduler ([`DDIMScheduler` or `DDPMScheduler`]): de-noising scheduler + """ + + _optional_components = ["vqvae"] + + def __init__( + self, + vqvae: AutoencoderKL, + unet: UNet2DConditionModel, + mel: Mel, + scheduler: Union[DDIMScheduler, DDPMScheduler], + ): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler, mel=mel, vqvae=vqvae) + + def get_input_dims(self) -> Tuple: + """Returns dimension of input image + + Returns: + `Tuple`: (height, width) + """ + input_module = self.vqvae if self.vqvae is not None else self.unet + # For backwards compatibility + sample_size = ( + (input_module.sample_size, input_module.sample_size) + if type(input_module.sample_size) == int + else input_module.sample_size + ) + return sample_size + + def get_default_steps(self) -> int: + """Returns default number of steps recommended for inference + + Returns: + `int`: number of steps + """ + return 50 if isinstance(self.scheduler, DDIMScheduler) else 1000 + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + audio_file: str = None, + raw_audio: np.ndarray = None, + slice: int = 0, + start_step: int = 0, + steps: int = None, + generator: torch.Generator = None, + mask_start_secs: float = 0, + mask_end_secs: float = 0, + step_generator: torch.Generator = None, + eta: float = 0, + noise: torch.Tensor = None, + encoding: torch.Tensor = None, + return_dict=True, + ) -> Union[ + Union[AudioPipelineOutput, ImagePipelineOutput], + Tuple[List[Image.Image], Tuple[int, List[np.ndarray]]], + ]: + """Generate random mel spectrogram from audio input and convert to audio. + + Args: + batch_size (`int`): number of samples to generate + audio_file (`str`): must be a file on disk due to Librosa limitation or + raw_audio (`np.ndarray`): audio as numpy array + slice (`int`): slice number of audio to convert + start_step (int): step to start from + steps (`int`): number of de-noising steps (defaults to 50 for DDIM, 1000 for DDPM) + generator (`torch.Generator`): random number generator or None + mask_start_secs (`float`): number of seconds of audio to mask (not generate) at start + mask_end_secs (`float`): number of seconds of audio to mask (not generate) at end + step_generator (`torch.Generator`): random number generator used to de-noise or None + eta (`float`): parameter between 0 and 1 used with DDIM scheduler + noise (`torch.Tensor`): noise tensor of shape (batch_size, 1, height, width) or None + encoding (`torch.Tensor`): for UNet2DConditionModel shape (batch_size, seq_length, cross_attention_dim) + return_dict (`bool`): if True return AudioPipelineOutput, ImagePipelineOutput else Tuple + + Returns: + `List[PIL Image]`: mel spectrograms (`float`, `List[np.ndarray]`): sample rate and raw audios + """ + + steps = steps or self.get_default_steps() + self.scheduler.set_timesteps(steps) + step_generator = step_generator or generator + # For backwards compatibility + if type(self.unet.sample_size) == int: + self.unet.sample_size = (self.unet.sample_size, self.unet.sample_size) + input_dims = self.get_input_dims() + self.mel.set_resolution(x_res=input_dims[1], y_res=input_dims[0]) + if noise is None: + noise = randn_tensor( + ( + batch_size, + self.unet.in_channels, + self.unet.sample_size[0], + self.unet.sample_size[1], + ), + generator=generator, + device=self.device, + ) + images = noise + mask = None + + if audio_file is not None or raw_audio is not None: + self.mel.load_audio(audio_file, raw_audio) + input_image = self.mel.audio_slice_to_image(slice) + input_image = np.frombuffer(input_image.tobytes(), dtype="uint8").reshape( + (input_image.height, input_image.width) + ) + input_image = (input_image / 255) * 2 - 1 + input_images = torch.tensor(input_image[np.newaxis, :, :], dtype=torch.float).to(self.device) + + if self.vqvae is not None: + input_images = self.vqvae.encode(torch.unsqueeze(input_images, 0)).latent_dist.sample( + generator=generator + )[0] + input_images = self.vqvae.config.scaling_factor * input_images + + if start_step > 0: + images[0, 0] = self.scheduler.add_noise(input_images, noise, self.scheduler.timesteps[start_step - 1]) + + pixels_per_second = ( + self.unet.sample_size[1] * self.mel.get_sample_rate() / self.mel.x_res / self.mel.hop_length + ) + mask_start = int(mask_start_secs * pixels_per_second) + mask_end = int(mask_end_secs * pixels_per_second) + mask = self.scheduler.add_noise(input_images, noise, torch.tensor(self.scheduler.timesteps[start_step:])) + + for step, t in enumerate(self.progress_bar(self.scheduler.timesteps[start_step:])): + if isinstance(self.unet, UNet2DConditionModel): + model_output = self.unet(images, t, encoding)["sample"] + else: + model_output = self.unet(images, t)["sample"] + + if isinstance(self.scheduler, DDIMScheduler): + images = self.scheduler.step( + model_output=model_output, + timestep=t, + sample=images, + eta=eta, + generator=step_generator, + )["prev_sample"] + else: + images = self.scheduler.step( + model_output=model_output, + timestep=t, + sample=images, + generator=step_generator, + )["prev_sample"] + + if mask is not None: + if mask_start > 0: + images[:, :, :, :mask_start] = mask[:, step, :, :mask_start] + if mask_end > 0: + images[:, :, :, -mask_end:] = mask[:, step, :, -mask_end:] + + if self.vqvae is not None: + # 0.18215 was scaling factor used in training to ensure unit variance + images = 1 / self.vqvae.config.scaling_factor * images + images = self.vqvae.decode(images)["sample"] + + images = (images / 2 + 0.5).clamp(0, 1) + images = images.cpu().permute(0, 2, 3, 1).numpy() + images = (images * 255).round().astype("uint8") + images = list( + map(lambda _: Image.fromarray(_[:, :, 0]), images) + if images.shape[3] == 1 + else map(lambda _: Image.fromarray(_, mode="RGB").convert("L"), images) + ) + + audios = list(map(lambda _: self.mel.image_to_audio(_), images)) + if not return_dict: + return images, (self.mel.get_sample_rate(), audios) + + return BaseOutput(**AudioPipelineOutput(np.array(audios)[:, np.newaxis, :]), **ImagePipelineOutput(images)) + + @torch.no_grad() + def encode(self, images: List[Image.Image], steps: int = 50) -> np.ndarray: + """Reverse step process: recover noisy image from generated image. + + Args: + images (`List[PIL Image]`): list of images to encode + steps (`int`): number of encoding steps to perform (defaults to 50) + + Returns: + `np.ndarray`: noise tensor of shape (batch_size, 1, height, width) + """ + + # Only works with DDIM as this method is deterministic + assert isinstance(self.scheduler, DDIMScheduler) + self.scheduler.set_timesteps(steps) + sample = np.array( + [np.frombuffer(image.tobytes(), dtype="uint8").reshape((1, image.height, image.width)) for image in images] + ) + sample = (sample / 255) * 2 - 1 + sample = torch.Tensor(sample).to(self.device) + + for t in self.progress_bar(torch.flip(self.scheduler.timesteps, (0,))): + prev_timestep = t - self.scheduler.num_train_timesteps // self.scheduler.num_inference_steps + alpha_prod_t = self.scheduler.alphas_cumprod[t] + alpha_prod_t_prev = ( + self.scheduler.alphas_cumprod[prev_timestep] + if prev_timestep >= 0 + else self.scheduler.final_alpha_cumprod + ) + beta_prod_t = 1 - alpha_prod_t + model_output = self.unet(sample, t)["sample"] + pred_sample_direction = (1 - alpha_prod_t_prev) ** (0.5) * model_output + sample = (sample - pred_sample_direction) * alpha_prod_t_prev ** (-0.5) + sample = sample * alpha_prod_t ** (0.5) + beta_prod_t ** (0.5) * model_output + + return sample + + @staticmethod + def slerp(x0: torch.Tensor, x1: torch.Tensor, alpha: float) -> torch.Tensor: + """Spherical Linear intERPolation + + Args: + x0 (`torch.Tensor`): first tensor to interpolate between + x1 (`torch.Tensor`): seconds tensor to interpolate between + alpha (`float`): interpolation between 0 and 1 + + Returns: + `torch.Tensor`: interpolated tensor + """ + + theta = acos(torch.dot(torch.flatten(x0), torch.flatten(x1)) / torch.norm(x0) / torch.norm(x1)) + return sin((1 - alpha) * theta) * x0 / sin(theta) + sin(alpha * theta) * x1 / sin(theta) diff --git a/diffusers/src/diffusers/pipelines/dance_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/dance_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..55d7f8ff9807083a10c844f7003cf0696d8258a3 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/dance_diffusion/__init__.py @@ -0,0 +1 @@ +from .pipeline_dance_diffusion import DanceDiffusionPipeline diff --git a/diffusers/src/diffusers/pipelines/dance_diffusion/pipeline_dance_diffusion.py b/diffusers/src/diffusers/pipelines/dance_diffusion/pipeline_dance_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..437e9a606e3dba235b84f803c84f73b763ca2a92 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/dance_diffusion/pipeline_dance_diffusion.py @@ -0,0 +1,123 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import List, Optional, Tuple, Union + +import torch + +from ...utils import logging, randn_tensor +from ..pipeline_utils import AudioPipelineOutput, DiffusionPipeline + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class DanceDiffusionPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet ([`UNet1DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image. Can be one of + [`IPNDMScheduler`]. + """ + + def __init__(self, unet, scheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + num_inference_steps: int = 100, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + audio_length_in_s: Optional[float] = None, + return_dict: bool = True, + ) -> Union[AudioPipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of audio samples to generate. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality audio sample at + the expense of slower inference. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + audio_length_in_s (`float`, *optional*, defaults to `self.unet.config.sample_size/self.unet.config.sample_rate`): + The length of the generated audio sample in seconds. Note that the output of the pipeline, *i.e.* + `sample_size`, will be `audio_length_in_s` * `self.unet.sample_rate`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.AudioPipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.AudioPipelineOutput`] or `tuple`: [`~pipelines.utils.AudioPipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + if audio_length_in_s is None: + audio_length_in_s = self.unet.config.sample_size / self.unet.config.sample_rate + + sample_size = audio_length_in_s * self.unet.sample_rate + + down_scale_factor = 2 ** len(self.unet.up_blocks) + if sample_size < 3 * down_scale_factor: + raise ValueError( + f"{audio_length_in_s} is too small. Make sure it's bigger or equal to" + f" {3 * down_scale_factor / self.unet.sample_rate}." + ) + + original_sample_size = int(sample_size) + if sample_size % down_scale_factor != 0: + sample_size = ((audio_length_in_s * self.unet.sample_rate) // down_scale_factor + 1) * down_scale_factor + logger.info( + f"{audio_length_in_s} is increased to {sample_size / self.unet.sample_rate} so that it can be handled" + f" by the model. It will be cut to {original_sample_size / self.unet.sample_rate} after the denoising" + " process." + ) + sample_size = int(sample_size) + + dtype = next(iter(self.unet.parameters())).dtype + shape = (batch_size, self.unet.in_channels, sample_size) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + audio = randn_tensor(shape, generator=generator, device=self.device, dtype=dtype) + + # set step values + self.scheduler.set_timesteps(num_inference_steps, device=audio.device) + self.scheduler.timesteps = self.scheduler.timesteps.to(dtype) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(audio, t).sample + + # 2. compute previous image: x_t -> t_t-1 + audio = self.scheduler.step(model_output, t, audio).prev_sample + + audio = audio.clamp(-1, 1).float().cpu().numpy() + + audio = audio[:, :, :original_sample_size] + + if not return_dict: + return (audio,) + + return AudioPipelineOutput(audios=audio) diff --git a/diffusers/src/diffusers/pipelines/ddim/__init__.py b/diffusers/src/diffusers/pipelines/ddim/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..85e8118e75e7e4352f8efb12552ba9fff4bf491c --- /dev/null +++ b/diffusers/src/diffusers/pipelines/ddim/__init__.py @@ -0,0 +1 @@ +from .pipeline_ddim import DDIMPipeline diff --git a/diffusers/src/diffusers/pipelines/ddim/pipeline_ddim.py b/diffusers/src/diffusers/pipelines/ddim/pipeline_ddim.py new file mode 100644 index 0000000000000000000000000000000000000000..afc67a959de05bcd4de5480c9305f987c3149626 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/ddim/pipeline_ddim.py @@ -0,0 +1,117 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import torch + +from ...schedulers import DDIMScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class DDIMPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image. Can be one of + [`DDPMScheduler`], or [`DDIMScheduler`]. + """ + + def __init__(self, unet, scheduler): + super().__init__() + + # make sure scheduler can always be converted to DDIM + scheduler = DDIMScheduler.from_config(scheduler.config) + + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + eta: float = 0.0, + num_inference_steps: int = 50, + use_clipped_model_output: Optional[bool] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + eta (`float`, *optional*, defaults to 0.0): + The eta parameter which controls the scale of the variance (0 is DDIM and 1 is one type of DDPM). + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + use_clipped_model_output (`bool`, *optional*, defaults to `None`): + if `True` or `False`, see documentation for `DDIMScheduler.step`. If `None`, nothing is passed + downstream to the scheduler. So use `None` for schedulers which don't support this argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + # Sample gaussian noise to begin loop + if isinstance(self.unet.sample_size, int): + image_shape = (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size) + else: + image_shape = (batch_size, self.unet.in_channels, *self.unet.sample_size) + + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + image = randn_tensor(image_shape, generator=generator, device=self.device, dtype=self.unet.dtype) + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(image, t).sample + + # 2. predict previous mean of image x_t-1 and add variance depending on eta + # eta corresponds to η in paper and should be between [0, 1] + # do x_t -> x_t-1 + image = self.scheduler.step( + model_output, t, image, eta=eta, use_clipped_model_output=use_clipped_model_output, generator=generator + ).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/ddpm/__init__.py b/diffusers/src/diffusers/pipelines/ddpm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bb228ee012e80493b617b314c867ecadba7ca1ce --- /dev/null +++ b/diffusers/src/diffusers/pipelines/ddpm/__init__.py @@ -0,0 +1 @@ +from .pipeline_ddpm import DDPMPipeline diff --git a/diffusers/src/diffusers/pipelines/ddpm/pipeline_ddpm.py b/diffusers/src/diffusers/pipelines/ddpm/pipeline_ddpm.py new file mode 100644 index 0000000000000000000000000000000000000000..3525c15645c46c7b49a55fa3bf189aab89d8a6c4 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/ddpm/pipeline_ddpm.py @@ -0,0 +1,100 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import List, Optional, Tuple, Union + +import torch + +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class DDPMPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image. Can be one of + [`DDPMScheduler`], or [`DDIMScheduler`]. + """ + + def __init__(self, unet, scheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + num_inference_steps: int = 1000, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + num_inference_steps (`int`, *optional*, defaults to 1000): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + # Sample gaussian noise to begin loop + if isinstance(self.unet.sample_size, int): + image_shape = (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size) + else: + image_shape = (batch_size, self.unet.in_channels, *self.unet.sample_size) + + if self.device.type == "mps": + # randn does not work reproducibly on mps + image = randn_tensor(image_shape, generator=generator) + image = image.to(self.device) + else: + image = randn_tensor(image_shape, generator=generator, device=self.device) + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(image, t).sample + + # 2. compute previous image: x_t -> x_t-1 + image = self.scheduler.step(model_output, t, image, generator=generator).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/dit/__init__.py b/diffusers/src/diffusers/pipelines/dit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4ef0729cb4905d5e177ba15533375fce50084406 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/dit/__init__.py @@ -0,0 +1 @@ +from .pipeline_dit import DiTPipeline diff --git a/diffusers/src/diffusers/pipelines/dit/pipeline_dit.py b/diffusers/src/diffusers/pipelines/dit/pipeline_dit.py new file mode 100644 index 0000000000000000000000000000000000000000..9ab9a543d5b9d10376e71892a23d9387105a7ce7 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/dit/pipeline_dit.py @@ -0,0 +1,199 @@ +# Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) +# William Peebles and Saining Xie +# +# Copyright (c) 2021 OpenAI +# MIT License +# +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import Dict, List, Optional, Tuple, Union + +import torch + +from ...models import AutoencoderKL, Transformer2DModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class DiTPipeline(DiffusionPipeline): + r""" + This pipeline inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + transformer ([`Transformer2DModel`]): + Class conditioned Transformer in Diffusion model to denoise the encoded image latents. + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + scheduler ([`DDIMScheduler`]): + A scheduler to be used in combination with `dit` to denoise the encoded image latents. + """ + + def __init__( + self, + transformer: Transformer2DModel, + vae: AutoencoderKL, + scheduler: KarrasDiffusionSchedulers, + id2label: Optional[Dict[int, str]] = None, + ): + super().__init__() + self.register_modules(transformer=transformer, vae=vae, scheduler=scheduler) + + # create a imagenet -> id dictionary for easier use + self.labels = {} + if id2label is not None: + for key, value in id2label.items(): + for label in value.split(","): + self.labels[label.lstrip().rstrip()] = int(key) + self.labels = dict(sorted(self.labels.items())) + + def get_label_ids(self, label: Union[str, List[str]]) -> List[int]: + r""" + + Map label strings, *e.g.* from ImageNet, to corresponding class ids. + + Parameters: + label (`str` or `dict` of `str`): label strings to be mapped to class ids. + + Returns: + `list` of `int`: Class ids to be processed by pipeline. + """ + + if not isinstance(label, list): + label = list(label) + + for l in label: + if l not in self.labels: + raise ValueError( + f"{l} does not exist. Please make sure to select one of the following labels: \n {self.labels}." + ) + + return [self.labels[l] for l in label] + + @torch.no_grad() + def __call__( + self, + class_labels: List[int], + guidance_scale: float = 4.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + num_inference_steps: int = 50, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Function invoked when calling the pipeline for generation. + + Args: + class_labels (List[int]): + List of imagenet class labels for the images to be generated. + guidance_scale (`float`, *optional*, defaults to 4.0): + Scale of the guidance signal. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + num_inference_steps (`int`, *optional*, defaults to 250): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`ImagePipelineOutput`] instead of a plain tuple. + """ + + batch_size = len(class_labels) + latent_size = self.transformer.config.sample_size + latent_channels = self.transformer.config.in_channels + + latents = randn_tensor( + shape=(batch_size, latent_channels, latent_size, latent_size), + generator=generator, + device=self.device, + dtype=self.transformer.dtype, + ) + latent_model_input = torch.cat([latents] * 2) if guidance_scale > 1 else latents + + class_labels = torch.tensor(class_labels, device=self.device).reshape(-1) + class_null = torch.tensor([1000] * batch_size, device=self.device) + class_labels_input = torch.cat([class_labels, class_null], 0) if guidance_scale > 1 else class_labels + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + if guidance_scale > 1: + half = latent_model_input[: len(latent_model_input) // 2] + latent_model_input = torch.cat([half, half], dim=0) + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + timesteps = t + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = latent_model_input.device.type == "mps" + if isinstance(timesteps, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=latent_model_input.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(latent_model_input.device) + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(latent_model_input.shape[0]) + # predict noise model_output + noise_pred = self.transformer( + latent_model_input, timestep=timesteps, class_labels=class_labels_input + ).sample + + # perform guidance + if guidance_scale > 1: + eps, rest = noise_pred[:, :latent_channels], noise_pred[:, latent_channels:] + cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0) + + half_eps = uncond_eps + guidance_scale * (cond_eps - uncond_eps) + eps = torch.cat([half_eps, half_eps], dim=0) + + noise_pred = torch.cat([eps, rest], dim=1) + + # learned sigma + if self.transformer.config.out_channels // 2 == latent_channels: + model_output, _ = torch.split(noise_pred, latent_channels, dim=1) + else: + model_output = noise_pred + + # compute previous image: x_t -> x_t-1 + latent_model_input = self.scheduler.step(model_output, t, latent_model_input).prev_sample + + if guidance_scale > 1: + latents, _ = latent_model_input.chunk(2, dim=0) + else: + latents = latent_model_input + + latents = 1 / self.vae.config.scaling_factor * latents + samples = self.vae.decode(latents).sample + + samples = (samples / 2 + 0.5).clamp(0, 1) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + samples = samples.cpu().permute(0, 2, 3, 1).float().numpy() + + if output_type == "pil": + samples = self.numpy_to_pil(samples) + + if not return_dict: + return (samples,) + + return ImagePipelineOutput(images=samples) diff --git a/diffusers/src/diffusers/pipelines/latent_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/latent_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0cce9a89bcbeaac8468d75e9d16c9d3731f738c7 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/latent_diffusion/__init__.py @@ -0,0 +1,6 @@ +from ...utils import is_transformers_available +from .pipeline_latent_diffusion_superresolution import LDMSuperResolutionPipeline + + +if is_transformers_available(): + from .pipeline_latent_diffusion import LDMBertModel, LDMTextToImagePipeline diff --git a/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py b/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..d27244e3bacccf573aad5b3f2c3865d034baea46 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion.py @@ -0,0 +1,724 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import List, Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.utils.checkpoint +from transformers import PretrainedConfig, PreTrainedModel, PreTrainedTokenizer +from transformers.activations import ACT2FN +from transformers.modeling_outputs import BaseModelOutput +from transformers.utils import logging + +from ...models import AutoencoderKL, UNet2DConditionModel, UNet2DModel, VQModel +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class LDMTextToImagePipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) Model to encode and decode images to and from latent representations. + bert ([`LDMBertModel`]): + Text-encoder model based on [BERT](https://huggingface.co/docs/transformers/model_doc/bert) architecture. + tokenizer (`transformers.BertTokenizer`): + Tokenizer of class + [BertTokenizer](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + + def __init__( + self, + vqvae: Union[VQModel, AutoencoderKL], + bert: PreTrainedModel, + tokenizer: PreTrainedTokenizer, + unet: Union[UNet2DModel, UNet2DConditionModel], + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + ): + super().__init__() + self.register_modules(vqvae=vqvae, bert=bert, tokenizer=tokenizer, unet=unet, scheduler=scheduler) + self.vae_scale_factor = 2 ** (len(self.vqvae.config.block_out_channels) - 1) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 1.0, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[Tuple, ImagePipelineOutput]: + r""" + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 1.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt` at + the, usually at the expense of lower image quality. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # get unconditional embeddings for classifier free guidance + if guidance_scale != 1.0: + uncond_input = self.tokenizer( + [""] * batch_size, padding="max_length", max_length=77, truncation=True, return_tensors="pt" + ) + negative_prompt_embeds = self.bert(uncond_input.input_ids.to(self.device))[0] + + # get prompt text embeddings + text_input = self.tokenizer(prompt, padding="max_length", max_length=77, truncation=True, return_tensors="pt") + prompt_embeds = self.bert(text_input.input_ids.to(self.device))[0] + + # get the initial random noise unless the user supplied it + latents_shape = (batch_size, self.unet.in_channels, height // 8, width // 8) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(latents_shape, generator=generator, device=self.device, dtype=prompt_embeds.dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + latents = latents.to(self.device) + + self.scheduler.set_timesteps(num_inference_steps) + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + + extra_kwargs = {} + if accepts_eta: + extra_kwargs["eta"] = eta + + for t in self.progress_bar(self.scheduler.timesteps): + if guidance_scale == 1.0: + # guidance_scale of 1 means no guidance + latents_input = latents + context = prompt_embeds + else: + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + latents_input = torch.cat([latents] * 2) + context = torch.cat([negative_prompt_embeds, prompt_embeds]) + + # predict the noise residual + noise_pred = self.unet(latents_input, t, encoder_hidden_states=context).sample + # perform guidance + if guidance_scale != 1.0: + noise_pred_uncond, noise_prediction_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_prediction_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_kwargs).prev_sample + + # scale and decode the image latents with vae + latents = 1 / self.vqvae.config.scaling_factor * latents + image = self.vqvae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) + + +################################################################################ +# Code for the text transformer model +################################################################################ +""" PyTorch LDMBERT model.""" + + +logger = logging.get_logger(__name__) + +LDMBERT_PRETRAINED_MODEL_ARCHIVE_LIST = [ + "ldm-bert", + # See all LDMBert models at https://huggingface.co/models?filter=ldmbert +] + + +LDMBERT_PRETRAINED_CONFIG_ARCHIVE_MAP = { + "ldm-bert": "https://huggingface.co/valhalla/ldm-bert/blob/main/config.json", +} + + +""" LDMBERT model configuration""" + + +class LDMBertConfig(PretrainedConfig): + model_type = "ldmbert" + keys_to_ignore_at_inference = ["past_key_values"] + attribute_map = {"num_attention_heads": "encoder_attention_heads", "hidden_size": "d_model"} + + def __init__( + self, + vocab_size=30522, + max_position_embeddings=77, + encoder_layers=32, + encoder_ffn_dim=5120, + encoder_attention_heads=8, + head_dim=64, + encoder_layerdrop=0.0, + activation_function="gelu", + d_model=1280, + dropout=0.1, + attention_dropout=0.0, + activation_dropout=0.0, + init_std=0.02, + classifier_dropout=0.0, + scale_embedding=False, + use_cache=True, + pad_token_id=0, + **kwargs, + ): + self.vocab_size = vocab_size + self.max_position_embeddings = max_position_embeddings + self.d_model = d_model + self.encoder_ffn_dim = encoder_ffn_dim + self.encoder_layers = encoder_layers + self.encoder_attention_heads = encoder_attention_heads + self.head_dim = head_dim + self.dropout = dropout + self.attention_dropout = attention_dropout + self.activation_dropout = activation_dropout + self.activation_function = activation_function + self.init_std = init_std + self.encoder_layerdrop = encoder_layerdrop + self.classifier_dropout = classifier_dropout + self.use_cache = use_cache + self.num_hidden_layers = encoder_layers + self.scale_embedding = scale_embedding # scale factor will be sqrt(d_model) if True + + super().__init__(pad_token_id=pad_token_id, **kwargs) + + +def _expand_mask(mask: torch.Tensor, dtype: torch.dtype, tgt_len: Optional[int] = None): + """ + Expands attention_mask from `[bsz, seq_len]` to `[bsz, 1, tgt_seq_len, src_seq_len]`. + """ + bsz, src_len = mask.size() + tgt_len = tgt_len if tgt_len is not None else src_len + + expanded_mask = mask[:, None, None, :].expand(bsz, 1, tgt_len, src_len).to(dtype) + + inverted_mask = 1.0 - expanded_mask + + return inverted_mask.masked_fill(inverted_mask.to(torch.bool), torch.finfo(dtype).min) + + +# Copied from transformers.models.bart.modeling_bart.BartAttention with Bart->LDMBert +class LDMBertAttention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__( + self, + embed_dim: int, + num_heads: int, + head_dim: int, + dropout: float = 0.0, + is_decoder: bool = False, + bias: bool = False, + ): + super().__init__() + self.embed_dim = embed_dim + self.num_heads = num_heads + self.dropout = dropout + self.head_dim = head_dim + self.inner_dim = head_dim * num_heads + + self.scaling = self.head_dim**-0.5 + self.is_decoder = is_decoder + + self.k_proj = nn.Linear(embed_dim, self.inner_dim, bias=bias) + self.v_proj = nn.Linear(embed_dim, self.inner_dim, bias=bias) + self.q_proj = nn.Linear(embed_dim, self.inner_dim, bias=bias) + self.out_proj = nn.Linear(self.inner_dim, embed_dim) + + def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int): + return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2).contiguous() + + def forward( + self, + hidden_states: torch.Tensor, + key_value_states: Optional[torch.Tensor] = None, + past_key_value: Optional[Tuple[torch.Tensor]] = None, + attention_mask: Optional[torch.Tensor] = None, + layer_head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + """Input shape: Batch x Time x Channel""" + + # if key_value_states are provided this layer is used as a cross-attention layer + # for the decoder + is_cross_attention = key_value_states is not None + + bsz, tgt_len, _ = hidden_states.size() + + # get query proj + query_states = self.q_proj(hidden_states) * self.scaling + # get key, value proj + if is_cross_attention and past_key_value is not None: + # reuse k,v, cross_attentions + key_states = past_key_value[0] + value_states = past_key_value[1] + elif is_cross_attention: + # cross_attentions + key_states = self._shape(self.k_proj(key_value_states), -1, bsz) + value_states = self._shape(self.v_proj(key_value_states), -1, bsz) + elif past_key_value is not None: + # reuse k, v, self_attention + key_states = self._shape(self.k_proj(hidden_states), -1, bsz) + value_states = self._shape(self.v_proj(hidden_states), -1, bsz) + key_states = torch.cat([past_key_value[0], key_states], dim=2) + value_states = torch.cat([past_key_value[1], value_states], dim=2) + else: + # self_attention + key_states = self._shape(self.k_proj(hidden_states), -1, bsz) + value_states = self._shape(self.v_proj(hidden_states), -1, bsz) + + if self.is_decoder: + # if cross_attention save Tuple(torch.Tensor, torch.Tensor) of all cross attention key/value_states. + # Further calls to cross_attention layer can then reuse all cross-attention + # key/value_states (first "if" case) + # if uni-directional self-attention (decoder) save Tuple(torch.Tensor, torch.Tensor) of + # all previous decoder key/value_states. Further calls to uni-directional self-attention + # can concat previous decoder key/value_states to current projected key/value_states (third "elif" case) + # if encoder bi-directional self-attention `past_key_value` is always `None` + past_key_value = (key_states, value_states) + + proj_shape = (bsz * self.num_heads, -1, self.head_dim) + query_states = self._shape(query_states, tgt_len, bsz).view(*proj_shape) + key_states = key_states.view(*proj_shape) + value_states = value_states.view(*proj_shape) + + src_len = key_states.size(1) + attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) + + if attn_weights.size() != (bsz * self.num_heads, tgt_len, src_len): + raise ValueError( + f"Attention weights should be of size {(bsz * self.num_heads, tgt_len, src_len)}, but is" + f" {attn_weights.size()}" + ) + + if attention_mask is not None: + if attention_mask.size() != (bsz, 1, tgt_len, src_len): + raise ValueError( + f"Attention mask should be of size {(bsz, 1, tgt_len, src_len)}, but is {attention_mask.size()}" + ) + attn_weights = attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + attention_mask + attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len) + + attn_weights = nn.functional.softmax(attn_weights, dim=-1) + + if layer_head_mask is not None: + if layer_head_mask.size() != (self.num_heads,): + raise ValueError( + f"Head mask for a single layer should be of size {(self.num_heads,)}, but is" + f" {layer_head_mask.size()}" + ) + attn_weights = layer_head_mask.view(1, -1, 1, 1) * attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + attn_weights = attn_weights.view(bsz * self.num_heads, tgt_len, src_len) + + if output_attentions: + # this operation is a bit awkward, but it's required to + # make sure that attn_weights keeps its gradient. + # In order to do so, attn_weights have to be reshaped + # twice and have to be reused in the following + attn_weights_reshaped = attn_weights.view(bsz, self.num_heads, tgt_len, src_len) + attn_weights = attn_weights_reshaped.view(bsz * self.num_heads, tgt_len, src_len) + else: + attn_weights_reshaped = None + + attn_probs = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training) + + attn_output = torch.bmm(attn_probs, value_states) + + if attn_output.size() != (bsz * self.num_heads, tgt_len, self.head_dim): + raise ValueError( + f"`attn_output` should be of size {(bsz, self.num_heads, tgt_len, self.head_dim)}, but is" + f" {attn_output.size()}" + ) + + attn_output = attn_output.view(bsz, self.num_heads, tgt_len, self.head_dim) + attn_output = attn_output.transpose(1, 2) + + # Use the `embed_dim` from the config (stored in the class) rather than `hidden_state` because `attn_output` can be + # partitioned across GPUs when using tensor-parallelism. + attn_output = attn_output.reshape(bsz, tgt_len, self.inner_dim) + + attn_output = self.out_proj(attn_output) + + return attn_output, attn_weights_reshaped, past_key_value + + +class LDMBertEncoderLayer(nn.Module): + def __init__(self, config: LDMBertConfig): + super().__init__() + self.embed_dim = config.d_model + self.self_attn = LDMBertAttention( + embed_dim=self.embed_dim, + num_heads=config.encoder_attention_heads, + head_dim=config.head_dim, + dropout=config.attention_dropout, + ) + self.self_attn_layer_norm = nn.LayerNorm(self.embed_dim) + self.dropout = config.dropout + self.activation_fn = ACT2FN[config.activation_function] + self.activation_dropout = config.activation_dropout + self.fc1 = nn.Linear(self.embed_dim, config.encoder_ffn_dim) + self.fc2 = nn.Linear(config.encoder_ffn_dim, self.embed_dim) + self.final_layer_norm = nn.LayerNorm(self.embed_dim) + + def forward( + self, + hidden_states: torch.FloatTensor, + attention_mask: torch.FloatTensor, + layer_head_mask: torch.FloatTensor, + output_attentions: Optional[bool] = False, + ) -> Tuple[torch.FloatTensor, Optional[torch.FloatTensor]]: + """ + Args: + hidden_states (`torch.FloatTensor`): input to the layer of shape `(seq_len, batch, embed_dim)` + attention_mask (`torch.FloatTensor`): attention mask of size + `(batch, 1, tgt_len, src_len)` where padding elements are indicated by very large negative values. + layer_head_mask (`torch.FloatTensor`): mask for attention heads in a given layer of size + `(encoder_attention_heads,)`. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + """ + residual = hidden_states + hidden_states = self.self_attn_layer_norm(hidden_states) + hidden_states, attn_weights, _ = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + layer_head_mask=layer_head_mask, + output_attentions=output_attentions, + ) + hidden_states = nn.functional.dropout(hidden_states, p=self.dropout, training=self.training) + hidden_states = residual + hidden_states + + residual = hidden_states + hidden_states = self.final_layer_norm(hidden_states) + hidden_states = self.activation_fn(self.fc1(hidden_states)) + hidden_states = nn.functional.dropout(hidden_states, p=self.activation_dropout, training=self.training) + hidden_states = self.fc2(hidden_states) + hidden_states = nn.functional.dropout(hidden_states, p=self.dropout, training=self.training) + hidden_states = residual + hidden_states + + if hidden_states.dtype == torch.float16 and ( + torch.isinf(hidden_states).any() or torch.isnan(hidden_states).any() + ): + clamp_value = torch.finfo(hidden_states.dtype).max - 1000 + hidden_states = torch.clamp(hidden_states, min=-clamp_value, max=clamp_value) + + outputs = (hidden_states,) + + if output_attentions: + outputs += (attn_weights,) + + return outputs + + +# Copied from transformers.models.bart.modeling_bart.BartPretrainedModel with Bart->LDMBert +class LDMBertPreTrainedModel(PreTrainedModel): + config_class = LDMBertConfig + base_model_prefix = "model" + _supports_gradient_checkpointing = True + _keys_to_ignore_on_load_unexpected = [r"encoder\.version", r"decoder\.version"] + + def _init_weights(self, module): + std = self.config.init_std + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.Embedding): + module.weight.data.normal_(mean=0.0, std=std) + if module.padding_idx is not None: + module.weight.data[module.padding_idx].zero_() + + def _set_gradient_checkpointing(self, module, value=False): + if isinstance(module, (LDMBertEncoder,)): + module.gradient_checkpointing = value + + @property + def dummy_inputs(self): + pad_token = self.config.pad_token_id + input_ids = torch.tensor([[0, 6, 10, 4, 2], [0, 8, 12, 2, pad_token]], device=self.device) + dummy_inputs = { + "attention_mask": input_ids.ne(pad_token), + "input_ids": input_ids, + } + return dummy_inputs + + +class LDMBertEncoder(LDMBertPreTrainedModel): + """ + Transformer encoder consisting of *config.encoder_layers* self attention layers. Each layer is a + [`LDMBertEncoderLayer`]. + + Args: + config: LDMBertConfig + embed_tokens (nn.Embedding): output embedding + """ + + def __init__(self, config: LDMBertConfig): + super().__init__(config) + + self.dropout = config.dropout + + embed_dim = config.d_model + self.padding_idx = config.pad_token_id + self.max_source_positions = config.max_position_embeddings + + self.embed_tokens = nn.Embedding(config.vocab_size, embed_dim) + self.embed_positions = nn.Embedding(config.max_position_embeddings, embed_dim) + self.layers = nn.ModuleList([LDMBertEncoderLayer(config) for _ in range(config.encoder_layers)]) + self.layer_norm = nn.LayerNorm(embed_dim) + + self.gradient_checkpointing = False + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.embed_tokens + + def set_input_embeddings(self, value): + self.embed_tokens = value + + def forward( + self, + input_ids: torch.LongTensor = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + head_mask: Optional[torch.Tensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> Union[Tuple, BaseModelOutput]: + r""" + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. Padding will be ignored by default should you + provide it. + + Indices can be obtained using [`BartTokenizer`]. See [`PreTrainedTokenizer.encode`] and + [`PreTrainedTokenizer.__call__`] for details. + + [What are input IDs?](../glossary#input-ids) + attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`: + + - 1 for tokens that are **not masked**, + - 0 for tokens that are **masked**. + + [What are attention masks?](../glossary#attention-mask) + head_mask (`torch.Tensor` of shape `(encoder_layers, encoder_attention_heads)`, *optional*): + Mask to nullify selected heads of the attention modules. Mask values selected in `[0, 1]`: + + - 1 indicates the head is **not masked**, + - 0 indicates the head is **masked**. + + inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`, *optional*): + Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation. + This is useful if you want more control over how to convert `input_ids` indices into associated vectors + than the model's internal embedding lookup matrix. + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under + returned tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors + for more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.BaseModelOutput`] instead of a plain tuple. + """ + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + # retrieve input_ids and inputs_embeds + if input_ids is not None and inputs_embeds is not None: + raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time") + elif input_ids is not None: + input_shape = input_ids.size() + input_ids = input_ids.view(-1, input_shape[-1]) + elif inputs_embeds is not None: + input_shape = inputs_embeds.size()[:-1] + else: + raise ValueError("You have to specify either input_ids or inputs_embeds") + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) + + seq_len = input_shape[1] + if position_ids is None: + position_ids = torch.arange(seq_len, dtype=torch.long, device=inputs_embeds.device).expand((1, -1)) + embed_pos = self.embed_positions(position_ids) + + hidden_states = inputs_embeds + embed_pos + hidden_states = nn.functional.dropout(hidden_states, p=self.dropout, training=self.training) + + # expand attention_mask + if attention_mask is not None: + # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len] + attention_mask = _expand_mask(attention_mask, inputs_embeds.dtype) + + encoder_states = () if output_hidden_states else None + all_attentions = () if output_attentions else None + + # check if head_mask has a correct number of layers specified if desired + if head_mask is not None: + if head_mask.size()[0] != (len(self.layers)): + raise ValueError( + f"The head_mask should be specified for {len(self.layers)} layers, but it is for" + f" {head_mask.size()[0]}." + ) + + for idx, encoder_layer in enumerate(self.layers): + if output_hidden_states: + encoder_states = encoder_states + (hidden_states,) + if self.gradient_checkpointing and self.training: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs, output_attentions) + + return custom_forward + + layer_outputs = torch.utils.checkpoint.checkpoint( + create_custom_forward(encoder_layer), + hidden_states, + attention_mask, + (head_mask[idx] if head_mask is not None else None), + ) + else: + layer_outputs = encoder_layer( + hidden_states, + attention_mask, + layer_head_mask=(head_mask[idx] if head_mask is not None else None), + output_attentions=output_attentions, + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_attentions = all_attentions + (layer_outputs[1],) + + hidden_states = self.layer_norm(hidden_states) + + if output_hidden_states: + encoder_states = encoder_states + (hidden_states,) + + if not return_dict: + return tuple(v for v in [hidden_states, encoder_states, all_attentions] if v is not None) + return BaseModelOutput( + last_hidden_state=hidden_states, hidden_states=encoder_states, attentions=all_attentions + ) + + +class LDMBertModel(LDMBertPreTrainedModel): + _no_split_modules = [] + + def __init__(self, config: LDMBertConfig): + super().__init__(config) + self.model = LDMBertEncoder(config) + self.to_logits = nn.Linear(config.hidden_size, config.vocab_size) + + def forward( + self, + input_ids=None, + attention_mask=None, + position_ids=None, + head_mask=None, + inputs_embeds=None, + output_attentions=None, + output_hidden_states=None, + return_dict=None, + ): + outputs = self.model( + input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + head_mask=head_mask, + inputs_embeds=inputs_embeds, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + return outputs diff --git a/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion_superresolution.py b/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion_superresolution.py new file mode 100644 index 0000000000000000000000000000000000000000..4a6e56d47bd83154a01e27c00422aa027728880c --- /dev/null +++ b/diffusers/src/diffusers/pipelines/latent_diffusion/pipeline_latent_diffusion_superresolution.py @@ -0,0 +1,164 @@ +import inspect +from typing import List, Optional, Tuple, Union + +import numpy as np +import PIL +import torch +import torch.utils.checkpoint + +from ...models import UNet2DModel, VQModel +from ...schedulers import ( + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, +) +from ...utils import PIL_INTERPOLATION, deprecate, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +def preprocess(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image) + return 2.0 * image - 1.0 + + +class LDMSuperResolutionPipeline(DiffusionPipeline): + r""" + A pipeline for image super-resolution using Latent + + This class inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) VAE Model to encode and decode images to and from latent representations. + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latens. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], [`EulerDiscreteScheduler`], + [`EulerAncestralDiscreteScheduler`], [`DPMSolverMultistepScheduler`], or [`PNDMScheduler`]. + """ + + def __init__( + self, + vqvae: VQModel, + unet: UNet2DModel, + scheduler: Union[ + DDIMScheduler, + PNDMScheduler, + LMSDiscreteScheduler, + EulerDiscreteScheduler, + EulerAncestralDiscreteScheduler, + DPMSolverMultistepScheduler, + ], + ): + super().__init__() + self.register_modules(vqvae=vqvae, unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + image: Union[torch.Tensor, PIL.Image.Image] = None, + batch_size: Optional[int] = 1, + num_inference_steps: Optional[int] = 100, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[Tuple, ImagePipelineOutput]: + r""" + Args: + image (`torch.Tensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + batch_size (`int`, *optional*, defaults to 1): + Number of images to generate. + num_inference_steps (`int`, *optional*, defaults to 100): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + if isinstance(image, PIL.Image.Image): + batch_size = 1 + elif isinstance(image, torch.Tensor): + batch_size = image.shape[0] + else: + raise ValueError(f"`image` has to be of type `PIL.Image.Image` or `torch.Tensor` but is {type(image)}") + + if isinstance(image, PIL.Image.Image): + image = preprocess(image) + + height, width = image.shape[-2:] + + # in_channels should be 6: 3 for latents, 3 for low resolution image + latents_shape = (batch_size, self.unet.in_channels // 2, height, width) + latents_dtype = next(self.unet.parameters()).dtype + + latents = randn_tensor(latents_shape, generator=generator, device=self.device, dtype=latents_dtype) + + image = image.to(device=self.device, dtype=latents_dtype) + + # set timesteps and move to the correct device + self.scheduler.set_timesteps(num_inference_steps, device=self.device) + timesteps_tensor = self.scheduler.timesteps + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature. + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_kwargs = {} + if accepts_eta: + extra_kwargs["eta"] = eta + + for t in self.progress_bar(timesteps_tensor): + # concat latents and low resolution image in the channel dimension. + latents_input = torch.cat([latents, image], dim=1) + latents_input = self.scheduler.scale_model_input(latents_input, t) + # predict the noise residual + noise_pred = self.unet(latents_input, t).sample + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_kwargs).prev_sample + + # decode the image latents with the VQVAE + image = self.vqvae.decode(latents).sample + image = torch.clamp(image, -1.0, 1.0) + image = image / 2 + 0.5 + image = image.cpu().permute(0, 2, 3, 1).numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/__init__.py b/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1b9fc5270a62bbb18d1393263101d4b9f73b7511 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/__init__.py @@ -0,0 +1 @@ +from .pipeline_latent_diffusion_uncond import LDMPipeline diff --git a/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/pipeline_latent_diffusion_uncond.py b/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/pipeline_latent_diffusion_uncond.py new file mode 100644 index 0000000000000000000000000000000000000000..dfd351d336c0139a23fd787937ac6d819ade6c00 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/latent_diffusion_uncond/pipeline_latent_diffusion_uncond.py @@ -0,0 +1,111 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import List, Optional, Tuple, Union + +import torch + +from ...models import UNet2DModel, VQModel +from ...schedulers import DDIMScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class LDMPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) Model to encode and decode images to and from latent representations. + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + [`DDIMScheduler`] is to be used in combination with `unet` to denoise the encoded image latents. + """ + + def __init__(self, vqvae: VQModel, unet: UNet2DModel, scheduler: DDIMScheduler): + super().__init__() + self.register_modules(vqvae=vqvae, unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + eta: float = 0.0, + num_inference_steps: int = 50, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[Tuple, ImagePipelineOutput]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + Number of images to generate. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + latents = randn_tensor( + (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + generator=generator, + ) + latents = latents.to(self.device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + + self.scheduler.set_timesteps(num_inference_steps) + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + + extra_kwargs = {} + if accepts_eta: + extra_kwargs["eta"] = eta + + for t in self.progress_bar(self.scheduler.timesteps): + latent_model_input = self.scheduler.scale_model_input(latents, t) + # predict the noise residual + noise_prediction = self.unet(latent_model_input, t).sample + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_prediction, t, latents, **extra_kwargs).prev_sample + + # decode the image latents with the VAE + image = self.vqvae.decode(latents).sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/onnx_utils.py b/diffusers/src/diffusers/pipelines/onnx_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f414524f0a2993f61ac5939cf36e35827892de0d --- /dev/null +++ b/diffusers/src/diffusers/pipelines/onnx_utils.py @@ -0,0 +1,212 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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. + + +import os +import shutil +from pathlib import Path +from typing import Optional, Union + +import numpy as np +from huggingface_hub import hf_hub_download + +from ..utils import ONNX_EXTERNAL_WEIGHTS_NAME, ONNX_WEIGHTS_NAME, is_onnx_available, logging + + +if is_onnx_available(): + import onnxruntime as ort + + +logger = logging.get_logger(__name__) + +ORT_TO_NP_TYPE = { + "tensor(bool)": np.bool_, + "tensor(int8)": np.int8, + "tensor(uint8)": np.uint8, + "tensor(int16)": np.int16, + "tensor(uint16)": np.uint16, + "tensor(int32)": np.int32, + "tensor(uint32)": np.uint32, + "tensor(int64)": np.int64, + "tensor(uint64)": np.uint64, + "tensor(float16)": np.float16, + "tensor(float)": np.float32, + "tensor(double)": np.float64, +} + + +class OnnxRuntimeModel: + def __init__(self, model=None, **kwargs): + logger.info("`diffusers.OnnxRuntimeModel` is experimental and might change in the future.") + self.model = model + self.model_save_dir = kwargs.get("model_save_dir", None) + self.latest_model_name = kwargs.get("latest_model_name", ONNX_WEIGHTS_NAME) + + def __call__(self, **kwargs): + inputs = {k: np.array(v) for k, v in kwargs.items()} + return self.model.run(None, inputs) + + @staticmethod + def load_model(path: Union[str, Path], provider=None, sess_options=None): + """ + Loads an ONNX Inference session with an ExecutionProvider. Default provider is `CPUExecutionProvider` + + Arguments: + path (`str` or `Path`): + Directory from which to load + provider(`str`, *optional*): + Onnxruntime execution provider to use for loading the model, defaults to `CPUExecutionProvider` + """ + if provider is None: + logger.info("No onnxruntime provider specified, using CPUExecutionProvider") + provider = "CPUExecutionProvider" + + return ort.InferenceSession(path, providers=[provider], sess_options=sess_options) + + def _save_pretrained(self, save_directory: Union[str, Path], file_name: Optional[str] = None, **kwargs): + """ + Save a model and its configuration file to a directory, so that it can be re-loaded using the + [`~optimum.onnxruntime.modeling_ort.ORTModel.from_pretrained`] class method. It will always save the + latest_model_name. + + Arguments: + save_directory (`str` or `Path`): + Directory where to save the model file. + file_name(`str`, *optional*): + Overwrites the default model file name from `"model.onnx"` to `file_name`. This allows you to save the + model with a different name. + """ + model_file_name = file_name if file_name is not None else ONNX_WEIGHTS_NAME + + src_path = self.model_save_dir.joinpath(self.latest_model_name) + dst_path = Path(save_directory).joinpath(model_file_name) + try: + shutil.copyfile(src_path, dst_path) + except shutil.SameFileError: + pass + + # copy external weights (for models >2GB) + src_path = self.model_save_dir.joinpath(ONNX_EXTERNAL_WEIGHTS_NAME) + if src_path.exists(): + dst_path = Path(save_directory).joinpath(ONNX_EXTERNAL_WEIGHTS_NAME) + try: + shutil.copyfile(src_path, dst_path) + except shutil.SameFileError: + pass + + def save_pretrained( + self, + save_directory: Union[str, os.PathLike], + **kwargs, + ): + """ + Save a model to a directory, so that it can be re-loaded using the [`~OnnxModel.from_pretrained`] class + method.: + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + """ + if os.path.isfile(save_directory): + logger.error(f"Provided path ({save_directory}) should be a directory, not a file") + return + + os.makedirs(save_directory, exist_ok=True) + + # saving model weights/files + self._save_pretrained(save_directory, **kwargs) + + @classmethod + def _from_pretrained( + cls, + model_id: Union[str, Path], + use_auth_token: Optional[Union[bool, str, None]] = None, + revision: Optional[Union[str, None]] = None, + force_download: bool = False, + cache_dir: Optional[str] = None, + file_name: Optional[str] = None, + provider: Optional[str] = None, + sess_options: Optional["ort.SessionOptions"] = None, + **kwargs, + ): + """ + Load a model from a directory or the HF Hub. + + Arguments: + model_id (`str` or `Path`): + Directory from which to load + use_auth_token (`str` or `bool`): + Is needed to load models from a private or gated repository + revision (`str`): + Revision is the specific model version to use. It can be a branch name, a tag name, or a commit id + cache_dir (`Union[str, Path]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + file_name(`str`): + Overwrites the default model file name from `"model.onnx"` to `file_name`. This allows you to load + different model files from the same repository or directory. + provider(`str`): + The ONNX runtime provider, e.g. `CPUExecutionProvider` or `CUDAExecutionProvider`. + kwargs (`Dict`, *optional*): + kwargs will be passed to the model during initialization + """ + model_file_name = file_name if file_name is not None else ONNX_WEIGHTS_NAME + # load model from local directory + if os.path.isdir(model_id): + model = OnnxRuntimeModel.load_model( + os.path.join(model_id, model_file_name), provider=provider, sess_options=sess_options + ) + kwargs["model_save_dir"] = Path(model_id) + # load model from hub + else: + # download model + model_cache_path = hf_hub_download( + repo_id=model_id, + filename=model_file_name, + use_auth_token=use_auth_token, + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + ) + kwargs["model_save_dir"] = Path(model_cache_path).parent + kwargs["latest_model_name"] = Path(model_cache_path).name + model = OnnxRuntimeModel.load_model(model_cache_path, provider=provider, sess_options=sess_options) + return cls(model=model, **kwargs) + + @classmethod + def from_pretrained( + cls, + model_id: Union[str, Path], + force_download: bool = True, + use_auth_token: Optional[str] = None, + cache_dir: Optional[str] = None, + **model_kwargs, + ): + revision = None + if len(str(model_id).split("@")) == 2: + model_id, revision = model_id.split("@") + + return cls._from_pretrained( + model_id=model_id, + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + use_auth_token=use_auth_token, + **model_kwargs, + ) diff --git a/diffusers/src/diffusers/pipelines/paint_by_example/__init__.py b/diffusers/src/diffusers/pipelines/paint_by_example/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f0fc8cb71e3f4e1e8baf16c7143658ca64934306 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/paint_by_example/__init__.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +import numpy as np +import PIL +from PIL import Image + +from ...utils import is_torch_available, is_transformers_available + + +if is_transformers_available() and is_torch_available(): + from .image_encoder import PaintByExampleImageEncoder + from .pipeline_paint_by_example import PaintByExamplePipeline diff --git a/diffusers/src/diffusers/pipelines/paint_by_example/image_encoder.py b/diffusers/src/diffusers/pipelines/paint_by_example/image_encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..df577e1678b5a5bec18b35bd9df48b0ef757288f --- /dev/null +++ b/diffusers/src/diffusers/pipelines/paint_by_example/image_encoder.py @@ -0,0 +1,67 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import torch +from torch import nn +from transformers import CLIPPreTrainedModel, CLIPVisionModel + +from ...models.attention import BasicTransformerBlock +from ...utils import logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class PaintByExampleImageEncoder(CLIPPreTrainedModel): + def __init__(self, config, proj_size=768): + super().__init__(config) + self.proj_size = proj_size + + self.model = CLIPVisionModel(config) + self.mapper = PaintByExampleMapper(config) + self.final_layer_norm = nn.LayerNorm(config.hidden_size) + self.proj_out = nn.Linear(config.hidden_size, self.proj_size) + + # uncondition for scaling + self.uncond_vector = nn.Parameter(torch.randn((1, 1, self.proj_size))) + + def forward(self, pixel_values, return_uncond_vector=False): + clip_output = self.model(pixel_values=pixel_values) + latent_states = clip_output.pooler_output + latent_states = self.mapper(latent_states[:, None]) + latent_states = self.final_layer_norm(latent_states) + latent_states = self.proj_out(latent_states) + if return_uncond_vector: + return latent_states, self.uncond_vector + + return latent_states + + +class PaintByExampleMapper(nn.Module): + def __init__(self, config): + super().__init__() + num_layers = (config.num_hidden_layers + 1) // 5 + hid_size = config.hidden_size + num_heads = 1 + self.blocks = nn.ModuleList( + [ + BasicTransformerBlock(hid_size, num_heads, hid_size, activation_fn="gelu", attention_bias=True) + for _ in range(num_layers) + ] + ) + + def forward(self, hidden_states): + for block in self.blocks: + hidden_states = block(hidden_states) + + return hidden_states diff --git a/diffusers/src/diffusers/pipelines/paint_by_example/pipeline_paint_by_example.py b/diffusers/src/diffusers/pipelines/paint_by_example/pipeline_paint_by_example.py new file mode 100644 index 0000000000000000000000000000000000000000..bc6d90d4d3a6d444c6ade0118fefd4de2b406053 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/paint_by_example/pipeline_paint_by_example.py @@ -0,0 +1,578 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor + +from diffusers.utils import is_accelerate_available + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from ..stable_diffusion import StableDiffusionPipelineOutput +from ..stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from .image_encoder import PaintByExampleImageEncoder + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def prepare_mask_and_masked_image(image, mask): + """ + Prepares a pair (image, mask) to be consumed by the Paint by Example pipeline. This means that those inputs will be + converted to ``torch.Tensor`` with shapes ``batch x channels x height x width`` where ``channels`` is ``3`` for the + ``image`` and ``1`` for the ``mask``. + + The ``image`` will be converted to ``torch.float32`` and normalized to be in ``[-1, 1]``. The ``mask`` will be + binarized (``mask > 0.5``) and cast to ``torch.float32`` too. + + Args: + image (Union[np.array, PIL.Image, torch.Tensor]): The image to inpaint. + It can be a ``PIL.Image``, or a ``height x width x 3`` ``np.array`` or a ``channels x height x width`` + ``torch.Tensor`` or a ``batch x channels x height x width`` ``torch.Tensor``. + mask (_type_): The mask to apply to the image, i.e. regions to inpaint. + It can be a ``PIL.Image``, or a ``height x width`` ``np.array`` or a ``1 x height x width`` + ``torch.Tensor`` or a ``batch x 1 x height x width`` ``torch.Tensor``. + + + Raises: + ValueError: ``torch.Tensor`` images should be in the ``[-1, 1]`` range. ValueError: ``torch.Tensor`` mask + should be in the ``[0, 1]`` range. ValueError: ``mask`` and ``image`` should have the same spatial dimensions. + TypeError: ``mask`` is a ``torch.Tensor`` but ``image`` is not + (ot the other way around). + + Returns: + tuple[torch.Tensor]: The pair (mask, masked_image) as ``torch.Tensor`` with 4 + dimensions: ``batch x channels x height x width``. + """ + if isinstance(image, torch.Tensor): + if not isinstance(mask, torch.Tensor): + raise TypeError(f"`image` is a torch.Tensor but `mask` (type: {type(mask)} is not") + + # Batch single image + if image.ndim == 3: + assert image.shape[0] == 3, "Image outside a batch should be of shape (3, H, W)" + image = image.unsqueeze(0) + + # Batch and add channel dim for single mask + if mask.ndim == 2: + mask = mask.unsqueeze(0).unsqueeze(0) + + # Batch single mask or add channel dim + if mask.ndim == 3: + # Batched mask + if mask.shape[0] == image.shape[0]: + mask = mask.unsqueeze(1) + else: + mask = mask.unsqueeze(0) + + assert image.ndim == 4 and mask.ndim == 4, "Image and Mask must have 4 dimensions" + assert image.shape[-2:] == mask.shape[-2:], "Image and Mask must have the same spatial dimensions" + assert image.shape[0] == mask.shape[0], "Image and Mask must have the same batch size" + assert mask.shape[1] == 1, "Mask image must have a single channel" + + # Check image is in [-1, 1] + if image.min() < -1 or image.max() > 1: + raise ValueError("Image should be in [-1, 1] range") + + # Check mask is in [0, 1] + if mask.min() < 0 or mask.max() > 1: + raise ValueError("Mask should be in [0, 1] range") + + # paint-by-example inverses the mask + mask = 1 - mask + + # Binarize mask + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + + # Image as float32 + image = image.to(dtype=torch.float32) + elif isinstance(mask, torch.Tensor): + raise TypeError(f"`mask` is a torch.Tensor but `image` (type: {type(image)} is not") + else: + if isinstance(image, PIL.Image.Image): + image = [image] + + image = np.concatenate([np.array(i.convert("RGB"))[None, :] for i in image], axis=0) + image = image.transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + + # preprocess mask + if isinstance(mask, PIL.Image.Image): + mask = [mask] + + mask = np.concatenate([np.array(m.convert("L"))[None, None, :] for m in mask], axis=0) + mask = mask.astype(np.float32) / 255.0 + + # paint-by-example inverses the mask + mask = 1 - mask + + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + + masked_image = image * mask + + return mask, masked_image + + +class PaintByExamplePipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + # TODO: feature_extractor is required to encode initial images (if they are in PIL format), + # we should give a descriptive message if the pipeline doesn't have one. + _optional_components = ["safety_checker"] + + def __init__( + self, + vae: AutoencoderKL, + image_encoder: PaintByExampleImageEncoder, + unet: UNet2DConditionModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = False, + ): + super().__init__() + + self.register_modules( + vae=vae, + image_encoder=image_encoder, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.vae, self.image_encoder]: + cpu_offload(cpu_offloaded_model, execution_device=device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_image_variation.StableDiffusionImageVariationPipeline.check_inputs + def check_inputs(self, image, height, width, callback_steps): + if ( + not isinstance(image, torch.Tensor) + and not isinstance(image, PIL.Image.Image) + and not isinstance(image, list) + ): + raise ValueError( + "`image` has to be of type `torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]` but is" + f" {type(image)}" + ) + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_inpaint.StableDiffusionInpaintPipeline.prepare_mask_latents + def prepare_mask_latents( + self, mask, masked_image, batch_size, height, width, dtype, device, generator, do_classifier_free_guidance + ): + # resize the mask to latents shape as we concatenate the mask to the latents + # we do that before converting to dtype to avoid breaking in case we're using cpu_offload + # and half precision + mask = torch.nn.functional.interpolate( + mask, size=(height // self.vae_scale_factor, width // self.vae_scale_factor) + ) + mask = mask.to(device=device, dtype=dtype) + + masked_image = masked_image.to(device=device, dtype=dtype) + + # encode the mask image into latents space so we can concatenate it to the latents + if isinstance(generator, list): + masked_image_latents = [ + self.vae.encode(masked_image[i : i + 1]).latent_dist.sample(generator=generator[i]) + for i in range(batch_size) + ] + masked_image_latents = torch.cat(masked_image_latents, dim=0) + else: + masked_image_latents = self.vae.encode(masked_image).latent_dist.sample(generator=generator) + masked_image_latents = self.vae.config.scaling_factor * masked_image_latents + + # duplicate mask and masked_image_latents for each generation per prompt, using mps friendly method + if mask.shape[0] < batch_size: + if not batch_size % mask.shape[0] == 0: + raise ValueError( + "The passed mask and the required batch size don't match. Masks are supposed to be duplicated to" + f" a total batch size of {batch_size}, but {mask.shape[0]} masks were passed. Make sure the number" + " of masks that you pass is divisible by the total requested batch size." + ) + mask = mask.repeat(batch_size // mask.shape[0], 1, 1, 1) + if masked_image_latents.shape[0] < batch_size: + if not batch_size % masked_image_latents.shape[0] == 0: + raise ValueError( + "The passed images and the required batch size don't match. Images are supposed to be duplicated" + f" to a total batch size of {batch_size}, but {masked_image_latents.shape[0]} images were passed." + " Make sure the number of images that you pass is divisible by the total requested batch size." + ) + masked_image_latents = masked_image_latents.repeat(batch_size // masked_image_latents.shape[0], 1, 1, 1) + + mask = torch.cat([mask] * 2) if do_classifier_free_guidance else mask + masked_image_latents = ( + torch.cat([masked_image_latents] * 2) if do_classifier_free_guidance else masked_image_latents + ) + + # aligning device to prevent device errors when concating it with the latent model input + masked_image_latents = masked_image_latents.to(device=device, dtype=dtype) + return mask, masked_image_latents + + def _encode_image(self, image, device, num_images_per_prompt, do_classifier_free_guidance): + dtype = next(self.image_encoder.parameters()).dtype + + if not isinstance(image, torch.Tensor): + image = self.feature_extractor(images=image, return_tensors="pt").pixel_values + + image = image.to(device=device, dtype=dtype) + image_embeddings, negative_prompt_embeds = self.image_encoder(image, return_uncond_vector=True) + + # duplicate image embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = image_embeddings.shape + image_embeddings = image_embeddings.repeat(1, num_images_per_prompt, 1) + image_embeddings = image_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + if do_classifier_free_guidance: + negative_prompt_embeds = negative_prompt_embeds.repeat(1, image_embeddings.shape[0], 1) + negative_prompt_embeds = negative_prompt_embeds.view(bs_embed * num_images_per_prompt, 1, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + image_embeddings = torch.cat([negative_prompt_embeds, image_embeddings]) + + return image_embeddings + + @torch.no_grad() + def __call__( + self, + example_image: Union[torch.FloatTensor, PIL.Image.Image], + image: Union[torch.FloatTensor, PIL.Image.Image], + mask_image: Union[torch.FloatTensor, PIL.Image.Image], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 5.0, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + example_image (`torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]`): + The exemplar image to guide the image generation. + image (`torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]`): + `Image`, or tensor representing an image batch which will be inpainted, *i.e.* parts of the image will + be masked out with `mask_image` and repainted according to `prompt`. + mask_image (`torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + repainted, while black pixels will be preserved. If `mask_image` is a PIL image, it will be converted + to a single channel (luminance) before use. If it's a tensor, it should contain one color channel (L) + instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 1. Define call parameters + if isinstance(image, PIL.Image.Image): + batch_size = 1 + elif isinstance(image, list): + batch_size = len(image) + else: + batch_size = image.shape[0] + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 2. Preprocess mask and image + mask, masked_image = prepare_mask_and_masked_image(image, mask_image) + height, width = masked_image.shape[-2:] + + # 3. Check inputs + self.check_inputs(example_image, height, width, callback_steps) + + # 4. Encode input image + image_embeddings = self._encode_image( + example_image, device, num_images_per_prompt, do_classifier_free_guidance + ) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 6. Prepare latent variables + num_channels_latents = self.vae.config.latent_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + image_embeddings.dtype, + device, + generator, + latents, + ) + + # 7. Prepare mask latent variables + mask, masked_image_latents = self.prepare_mask_latents( + mask, + masked_image, + batch_size * num_images_per_prompt, + height, + width, + image_embeddings.dtype, + device, + generator, + do_classifier_free_guidance, + ) + + # 8. Check that sizes of mask, masked image and latents match + num_channels_mask = mask.shape[1] + num_channels_masked_image = masked_image_latents.shape[1] + if num_channels_latents + num_channels_mask + num_channels_masked_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_mask`: {num_channels_mask} + `num_channels_masked_image`: {num_channels_masked_image}" + f" = {num_channels_latents+num_channels_masked_image+num_channels_mask}. Please verify the config of" + " `pipeline.unet` or your `mask_image` or `image` input." + ) + + # 9. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 10. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + + # concat latents, mask, masked_image_latents in the channel dimension + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + latent_model_input = torch.cat([latent_model_input, masked_image_latents, mask], dim=1) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=image_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 11. Post-processing + image = self.decode_latents(latents) + + # 12. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, image_embeddings.dtype) + + # 13. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/pipeline_flax_utils.py b/diffusers/src/diffusers/pipelines/pipeline_flax_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7a22248bf168f79d02c9b81bc36cb349d1fd33f9 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/pipeline_flax_utils.py @@ -0,0 +1,549 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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. + +import importlib +import inspect +import os +from typing import Any, Dict, List, Optional, Union + +import flax +import numpy as np +import PIL +from flax.core.frozen_dict import FrozenDict +from huggingface_hub import snapshot_download +from PIL import Image +from tqdm.auto import tqdm + +from ..configuration_utils import ConfigMixin +from ..models.modeling_flax_utils import FLAX_WEIGHTS_NAME, FlaxModelMixin +from ..schedulers.scheduling_utils_flax import SCHEDULER_CONFIG_NAME, FlaxSchedulerMixin +from ..utils import CONFIG_NAME, DIFFUSERS_CACHE, BaseOutput, http_user_agent, is_transformers_available, logging + + +if is_transformers_available(): + from transformers import FlaxPreTrainedModel + +INDEX_FILE = "diffusion_flax_model.bin" + + +logger = logging.get_logger(__name__) + + +LOADABLE_CLASSES = { + "diffusers": { + "FlaxModelMixin": ["save_pretrained", "from_pretrained"], + "FlaxSchedulerMixin": ["save_pretrained", "from_pretrained"], + "FlaxDiffusionPipeline": ["save_pretrained", "from_pretrained"], + }, + "transformers": { + "PreTrainedTokenizer": ["save_pretrained", "from_pretrained"], + "PreTrainedTokenizerFast": ["save_pretrained", "from_pretrained"], + "FlaxPreTrainedModel": ["save_pretrained", "from_pretrained"], + "FeatureExtractionMixin": ["save_pretrained", "from_pretrained"], + "ProcessorMixin": ["save_pretrained", "from_pretrained"], + "ImageProcessingMixin": ["save_pretrained", "from_pretrained"], + }, +} + +ALL_IMPORTABLE_CLASSES = {} +for library in LOADABLE_CLASSES: + ALL_IMPORTABLE_CLASSES.update(LOADABLE_CLASSES[library]) + + +def import_flax_or_no_model(module, class_name): + try: + # 1. First make sure that if a Flax object is present, import this one + class_obj = getattr(module, "Flax" + class_name) + except AttributeError: + # 2. If this doesn't work, it's not a model and we don't append "Flax" + class_obj = getattr(module, class_name) + except AttributeError: + raise ValueError(f"Neither Flax{class_name} nor {class_name} exist in {module}") + + return class_obj + + +@flax.struct.dataclass +class FlaxImagePipelineOutput(BaseOutput): + """ + Output class for image pipelines. + + Args: + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, + num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + + +class FlaxDiffusionPipeline(ConfigMixin): + r""" + Base class for all models. + + [`FlaxDiffusionPipeline`] takes care of storing all components (models, schedulers, processors) for diffusion + pipelines and handles methods for loading, downloading and saving models as well as a few methods common to all + pipelines to: + + - enabling/disabling the progress bar for the denoising iteration + + Class attributes: + + - **config_name** ([`str`]) -- name of the config file that will store the class and module names of all + components of the diffusion pipeline. + """ + config_name = "model_index.json" + + def register_modules(self, **kwargs): + # import it here to avoid circular import + from diffusers import pipelines + + for name, module in kwargs.items(): + if module is None: + register_dict = {name: (None, None)} + else: + # retrieve library + library = module.__module__.split(".")[0] + + # check if the module is a pipeline module + pipeline_dir = module.__module__.split(".")[-2] + path = module.__module__.split(".") + is_pipeline_module = pipeline_dir in path and hasattr(pipelines, pipeline_dir) + + # if library is not in LOADABLE_CLASSES, then it is a custom module. + # Or if it's a pipeline module, then the module is inside the pipeline + # folder so we set the library to module name. + if library not in LOADABLE_CLASSES or is_pipeline_module: + library = pipeline_dir + + # retrieve class_name + class_name = module.__class__.__name__ + + register_dict = {name: (library, class_name)} + + # save model index config + self.register_to_config(**register_dict) + + # set models + setattr(self, name, module) + + def save_pretrained(self, save_directory: Union[str, os.PathLike], params: Union[Dict, FrozenDict]): + # TODO: handle inference_state + """ + Save all variables of the pipeline that can be saved and loaded as well as the pipelines configuration file to + a directory. A pipeline variable can be saved and loaded if its class implements both a save and loading + method. The pipeline can easily be re-loaded using the `[`~FlaxDiffusionPipeline.from_pretrained`]` class + method. + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + """ + self.save_config(save_directory) + + model_index_dict = dict(self.config) + model_index_dict.pop("_class_name") + model_index_dict.pop("_diffusers_version") + model_index_dict.pop("_module", None) + + for pipeline_component_name in model_index_dict.keys(): + sub_model = getattr(self, pipeline_component_name) + if sub_model is None: + # edge case for saving a pipeline with safety_checker=None + continue + + model_cls = sub_model.__class__ + + save_method_name = None + # search for the model's base class in LOADABLE_CLASSES + for library_name, library_classes in LOADABLE_CLASSES.items(): + library = importlib.import_module(library_name) + for base_class, save_load_methods in library_classes.items(): + class_candidate = getattr(library, base_class, None) + if class_candidate is not None and issubclass(model_cls, class_candidate): + # if we found a suitable base class in LOADABLE_CLASSES then grab its save method + save_method_name = save_load_methods[0] + break + if save_method_name is not None: + break + + save_method = getattr(sub_model, save_method_name) + expects_params = "params" in set(inspect.signature(save_method).parameters.keys()) + + if expects_params: + save_method( + os.path.join(save_directory, pipeline_component_name), params=params[pipeline_component_name] + ) + else: + save_method(os.path.join(save_directory, pipeline_component_name)) + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.PathLike]], **kwargs): + r""" + Instantiate a Flax diffusion pipeline from pre-trained pipeline weights. + + The pipeline is set in evaluation mode by default using `model.eval()` (Dropout modules are deactivated). + + The warning *Weights from XXX not initialized from pretrained model* means that the weights of XXX do not come + pretrained with the rest of the model. It is up to you to train those weights with a downstream fine-tuning + task. + + The warning *Weights from XXX not used in YYY* means that the layer XXX is not used by YYY, therefore those + weights are discarded. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *repo id* of a pretrained pipeline hosted inside a model repo on + https://huggingface.co/ Valid repo ids have to be located under a user or organization name, like + `CompVis/ldm-text2im-large-256`. + - A path to a *directory* containing pipeline weights saved using + [`~FlaxDiffusionPipeline.save_pretrained`], e.g., `./my_pipeline_directory/`. + dtype (`str` or `jnp.dtype`, *optional*): + Override the default `jnp.dtype` and load the model under this dtype. If `"auto"` is passed the dtype + will be automatically derived from the model's weights. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `huggingface-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + mirror (`str`, *optional*): + Mirror source to accelerate downloads in China. If you are from China and have an accessibility + problem, you can set this option to resolve it. Note that we do not guarantee the timeliness or safety. + Please refer to the mirror site for more information. specify the folder name here. + + kwargs (remaining dictionary of keyword arguments, *optional*): + Can be used to overwrite load - and saveable variables - *i.e.* the pipeline components - of the + specific pipeline class. The overwritten components are then directly passed to the pipelines + `__init__` method. See example below for more information. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models), *e.g.* `"runwayml/stable-diffusion-v1-5"` + + + + + + Activate the special ["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use + this method in a firewalled environment. + + + + Examples: + + ```py + >>> from diffusers import FlaxDiffusionPipeline + + >>> # Download pipeline from huggingface.co and cache. + >>> # Requires to be logged in to Hugging Face hub, + >>> # see more in [the documentation](https://huggingface.co/docs/hub/security-tokens) + >>> pipeline, params = FlaxDiffusionPipeline.from_pretrained( + ... "runwayml/stable-diffusion-v1-5", + ... revision="bf16", + ... dtype=jnp.bfloat16, + ... ) + + >>> # Download pipeline, but use a different scheduler + >>> from diffusers import FlaxDPMSolverMultistepScheduler + + >>> model_id = "runwayml/stable-diffusion-v1-5" + >>> sched, sched_state = FlaxDPMSolverMultistepScheduler.from_pretrained( + ... model_id, + ... subfolder="scheduler", + ... ) + + >>> dpm_pipe, dpm_params = FlaxStableDiffusionPipeline.from_pretrained( + ... model_id, revision="bf16", dtype=jnp.bfloat16, scheduler=dpmpp + ... ) + >>> dpm_params["scheduler"] = dpmpp_state + ``` + """ + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + resume_download = kwargs.pop("resume_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", False) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + from_pt = kwargs.pop("from_pt", False) + dtype = kwargs.pop("dtype", None) + + # 1. Download the checkpoints and configs + # use snapshot download here to get it working from from_pretrained + if not os.path.isdir(pretrained_model_name_or_path): + config_dict = cls.load_config( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + ) + # make sure we only download sub-folders and `diffusers` filenames + folder_names = [k for k in config_dict.keys() if not k.startswith("_")] + allow_patterns = [os.path.join(k, "*") for k in folder_names] + allow_patterns += [FLAX_WEIGHTS_NAME, SCHEDULER_CONFIG_NAME, CONFIG_NAME, cls.config_name] + + # make sure we don't download PyTorch weights, unless when using from_pt + ignore_patterns = "*.bin" if not from_pt else [] + + if cls != FlaxDiffusionPipeline: + requested_pipeline_class = cls.__name__ + else: + requested_pipeline_class = config_dict.get("_class_name", cls.__name__) + requested_pipeline_class = ( + requested_pipeline_class + if requested_pipeline_class.startswith("Flax") + else "Flax" + requested_pipeline_class + ) + + user_agent = {"pipeline_class": requested_pipeline_class} + user_agent = http_user_agent(user_agent) + + # download all allow_patterns + cached_folder = snapshot_download( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + allow_patterns=allow_patterns, + ignore_patterns=ignore_patterns, + user_agent=user_agent, + ) + else: + cached_folder = pretrained_model_name_or_path + + config_dict = cls.load_config(cached_folder) + + # 2. Load the pipeline class, if using custom module then load it from the hub + # if we load from explicit class, let's use it + if cls != FlaxDiffusionPipeline: + pipeline_class = cls + else: + diffusers_module = importlib.import_module(cls.__module__.split(".")[0]) + class_name = ( + config_dict["_class_name"] + if config_dict["_class_name"].startswith("Flax") + else "Flax" + config_dict["_class_name"] + ) + pipeline_class = getattr(diffusers_module, class_name) + + # some modules can be passed directly to the init + # in this case they are already instantiated in `kwargs` + # extract them here + expected_modules = set(inspect.signature(pipeline_class.__init__).parameters.keys()) + passed_class_obj = {k: kwargs.pop(k) for k in expected_modules if k in kwargs} + + init_dict, _, _ = pipeline_class.extract_init_dict(config_dict, **kwargs) + + init_kwargs = {} + + # inference_params + params = {} + + # import it here to avoid circular import + from diffusers import pipelines + + # 3. Load each module in the pipeline + for name, (library_name, class_name) in init_dict.items(): + if class_name is None: + # edge case for when the pipeline was saved with safety_checker=None + init_kwargs[name] = None + continue + + is_pipeline_module = hasattr(pipelines, library_name) + loaded_sub_model = None + sub_model_should_be_defined = True + + # if the model is in a pipeline module, then we load it from the pipeline + if name in passed_class_obj: + # 1. check that passed_class_obj has correct parent class + if not is_pipeline_module: + library = importlib.import_module(library_name) + class_obj = getattr(library, class_name) + importable_classes = LOADABLE_CLASSES[library_name] + class_candidates = {c: getattr(library, c, None) for c in importable_classes.keys()} + + expected_class_obj = None + for class_name, class_candidate in class_candidates.items(): + if class_candidate is not None and issubclass(class_obj, class_candidate): + expected_class_obj = class_candidate + + if not issubclass(passed_class_obj[name].__class__, expected_class_obj): + raise ValueError( + f"{passed_class_obj[name]} is of type: {type(passed_class_obj[name])}, but should be" + f" {expected_class_obj}" + ) + elif passed_class_obj[name] is None: + logger.warning( + f"You have passed `None` for {name} to disable its functionality in {pipeline_class}. Note" + f" that this might lead to problems when using {pipeline_class} and is not recommended." + ) + sub_model_should_be_defined = False + else: + logger.warning( + f"You have passed a non-standard module {passed_class_obj[name]}. We cannot verify whether it" + " has the correct type" + ) + + # set passed class object + loaded_sub_model = passed_class_obj[name] + elif is_pipeline_module: + pipeline_module = getattr(pipelines, library_name) + class_obj = import_flax_or_no_model(pipeline_module, class_name) + + importable_classes = ALL_IMPORTABLE_CLASSES + class_candidates = {c: class_obj for c in importable_classes.keys()} + else: + # else we just import it from the library. + library = importlib.import_module(library_name) + class_obj = import_flax_or_no_model(library, class_name) + + importable_classes = LOADABLE_CLASSES[library_name] + class_candidates = {c: getattr(library, c, None) for c in importable_classes.keys()} + + if loaded_sub_model is None and sub_model_should_be_defined: + load_method_name = None + for class_name, class_candidate in class_candidates.items(): + if class_candidate is not None and issubclass(class_obj, class_candidate): + load_method_name = importable_classes[class_name][1] + + load_method = getattr(class_obj, load_method_name) + + # check if the module is in a subdirectory + if os.path.isdir(os.path.join(cached_folder, name)): + loadable_folder = os.path.join(cached_folder, name) + else: + loaded_sub_model = cached_folder + + if issubclass(class_obj, FlaxModelMixin): + loaded_sub_model, loaded_params = load_method(loadable_folder, from_pt=from_pt, dtype=dtype) + params[name] = loaded_params + elif is_transformers_available() and issubclass(class_obj, FlaxPreTrainedModel): + if from_pt: + # TODO(Suraj): Fix this in Transformers. We should be able to use `_do_init=False` here + loaded_sub_model = load_method(loadable_folder, from_pt=from_pt) + loaded_params = loaded_sub_model.params + del loaded_sub_model._params + else: + loaded_sub_model, loaded_params = load_method(loadable_folder, _do_init=False) + params[name] = loaded_params + elif issubclass(class_obj, FlaxSchedulerMixin): + loaded_sub_model, scheduler_state = load_method(loadable_folder) + params[name] = scheduler_state + else: + loaded_sub_model = load_method(loadable_folder) + + init_kwargs[name] = loaded_sub_model # UNet(...), # DiffusionSchedule(...) + + model = pipeline_class(**init_kwargs, dtype=dtype) + return model, params + + @staticmethod + def _get_signature_keys(obj): + parameters = inspect.signature(obj.__init__).parameters + required_parameters = {k: v for k, v in parameters.items() if v.default == inspect._empty} + optional_parameters = set({k for k, v in parameters.items() if v.default != inspect._empty}) + expected_modules = set(required_parameters.keys()) - set(["self"]) + return expected_modules, optional_parameters + + @property + def components(self) -> Dict[str, Any]: + r""" + + The `self.components` property can be useful to run different pipelines with the same weights and + configurations to not have to re-allocate memory. + + Examples: + + ```py + >>> from diffusers import ( + ... FlaxStableDiffusionPipeline, + ... FlaxStableDiffusionImg2ImgPipeline, + ... ) + + >>> text2img = FlaxStableDiffusionPipeline.from_pretrained( + ... "runwayml/stable-diffusion-v1-5", revision="bf16", dtype=jnp.bfloat16 + ... ) + >>> img2img = FlaxStableDiffusionImg2ImgPipeline(**text2img.components) + ``` + + Returns: + A dictionary containing all the modules needed to initialize the pipeline. + """ + expected_modules, optional_parameters = self._get_signature_keys(self) + components = { + k: getattr(self, k) for k in self.config.keys() if not k.startswith("_") and k not in optional_parameters + } + + if set(components.keys()) != expected_modules: + raise ValueError( + f"{self} has been incorrectly initialized or {self.__class__} is incorrectly implemented. Expected" + f" {expected_modules} to be defined, but {components} are defined." + ) + + return components + + @staticmethod + def numpy_to_pil(images): + """ + Convert a numpy image or a batch of images to a PIL image. + """ + if images.ndim == 3: + images = images[None, ...] + images = (images * 255).round().astype("uint8") + if images.shape[-1] == 1: + # special case for grayscale (single channel) images + pil_images = [Image.fromarray(image.squeeze(), mode="L") for image in images] + else: + pil_images = [Image.fromarray(image) for image in images] + + return pil_images + + # TODO: make it compatible with jax.lax + def progress_bar(self, iterable): + if not hasattr(self, "_progress_bar_config"): + self._progress_bar_config = {} + elif not isinstance(self._progress_bar_config, dict): + raise ValueError( + f"`self._progress_bar_config` should be of type `dict`, but is {type(self._progress_bar_config)}." + ) + + return tqdm(iterable, **self._progress_bar_config) + + def set_progress_bar_config(self, **kwargs): + self._progress_bar_config = kwargs diff --git a/diffusers/src/diffusers/pipelines/pipeline_utils.py b/diffusers/src/diffusers/pipelines/pipeline_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b6cf92abfcdf03394a84b5769b33e7584021db46 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/pipeline_utils.py @@ -0,0 +1,934 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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. + +import importlib +import inspect +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union + +import numpy as np +import PIL +import torch +from huggingface_hub import model_info, snapshot_download +from packaging import version +from PIL import Image +from tqdm.auto import tqdm + +import diffusers + +from ..configuration_utils import ConfigMixin +from ..models.modeling_utils import _LOW_CPU_MEM_USAGE_DEFAULT +from ..schedulers.scheduling_utils import SCHEDULER_CONFIG_NAME +from ..utils import ( + CONFIG_NAME, + DIFFUSERS_CACHE, + FLAX_WEIGHTS_NAME, + HF_HUB_OFFLINE, + ONNX_WEIGHTS_NAME, + WEIGHTS_NAME, + BaseOutput, + deprecate, + get_class_from_dynamic_module, + http_user_agent, + is_accelerate_available, + is_safetensors_available, + is_torch_version, + is_transformers_available, + logging, +) + + +if is_transformers_available(): + import transformers + from transformers import PreTrainedModel + + +INDEX_FILE = "diffusion_pytorch_model.bin" +CUSTOM_PIPELINE_FILE_NAME = "pipeline.py" +DUMMY_MODULES_FOLDER = "diffusers.utils" +TRANSFORMERS_DUMMY_MODULES_FOLDER = "transformers.utils" + + +logger = logging.get_logger(__name__) + + +LOADABLE_CLASSES = { + "diffusers": { + "ModelMixin": ["save_pretrained", "from_pretrained"], + "SchedulerMixin": ["save_pretrained", "from_pretrained"], + "DiffusionPipeline": ["save_pretrained", "from_pretrained"], + "OnnxRuntimeModel": ["save_pretrained", "from_pretrained"], + }, + "transformers": { + "PreTrainedTokenizer": ["save_pretrained", "from_pretrained"], + "PreTrainedTokenizerFast": ["save_pretrained", "from_pretrained"], + "PreTrainedModel": ["save_pretrained", "from_pretrained"], + "FeatureExtractionMixin": ["save_pretrained", "from_pretrained"], + "ProcessorMixin": ["save_pretrained", "from_pretrained"], + "ImageProcessingMixin": ["save_pretrained", "from_pretrained"], + }, + "onnxruntime.training": { + "ORTModule": ["save_pretrained", "from_pretrained"], + }, +} + +ALL_IMPORTABLE_CLASSES = {} +for library in LOADABLE_CLASSES: + ALL_IMPORTABLE_CLASSES.update(LOADABLE_CLASSES[library]) + + +@dataclass +class ImagePipelineOutput(BaseOutput): + """ + Output class for image pipelines. + + Args: + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, + num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + + +@dataclass +class AudioPipelineOutput(BaseOutput): + """ + Output class for audio pipelines. + + Args: + audios (`np.ndarray`) + List of denoised samples of shape `(batch_size, num_channels, sample_rate)`. Numpy array present the + denoised audio samples of the diffusion pipeline. + """ + + audios: np.ndarray + + +def is_safetensors_compatible(info) -> bool: + filenames = set(sibling.rfilename for sibling in info.siblings) + pt_filenames = set(filename for filename in filenames if filename.endswith(".bin")) + is_safetensors_compatible = any(file.endswith(".safetensors") for file in filenames) + for pt_filename in pt_filenames: + prefix, raw = os.path.split(pt_filename) + if raw == "pytorch_model.bin": + # transformers specific + sf_filename = os.path.join(prefix, "model.safetensors") + else: + sf_filename = pt_filename[: -len(".bin")] + ".safetensors" + if is_safetensors_compatible and sf_filename not in filenames: + logger.warning(f"{sf_filename} not found") + is_safetensors_compatible = False + return is_safetensors_compatible + + +class DiffusionPipeline(ConfigMixin): + r""" + Base class for all models. + + [`DiffusionPipeline`] takes care of storing all components (models, schedulers, processors) for diffusion pipelines + and handles methods for loading, downloading and saving models as well as a few methods common to all pipelines to: + + - move all PyTorch modules to the device of your choice + - enabling/disabling the progress bar for the denoising iteration + + Class attributes: + + - **config_name** (`str`) -- name of the config file that will store the class and module names of all + components of the diffusion pipeline. + - **_optional_components** (List[`str`]) -- list of all components that are optional so they don't have to be + passed for the pipeline to function (should be overridden by subclasses). + """ + config_name = "model_index.json" + _optional_components = [] + + def register_modules(self, **kwargs): + # import it here to avoid circular import + from diffusers import pipelines + + for name, module in kwargs.items(): + # retrieve library + if module is None: + register_dict = {name: (None, None)} + else: + library = module.__module__.split(".")[0] + + # check if the module is a pipeline module + pipeline_dir = module.__module__.split(".")[-2] if len(module.__module__.split(".")) > 2 else None + path = module.__module__.split(".") + is_pipeline_module = pipeline_dir in path and hasattr(pipelines, pipeline_dir) + + # if library is not in LOADABLE_CLASSES, then it is a custom module. + # Or if it's a pipeline module, then the module is inside the pipeline + # folder so we set the library to module name. + if library not in LOADABLE_CLASSES or is_pipeline_module: + library = pipeline_dir + + # retrieve class_name + class_name = module.__class__.__name__ + + register_dict = {name: (library, class_name)} + + # save model index config + self.register_to_config(**register_dict) + + # set models + setattr(self, name, module) + + def save_pretrained( + self, + save_directory: Union[str, os.PathLike], + safe_serialization: bool = False, + ): + """ + Save all variables of the pipeline that can be saved and loaded as well as the pipelines configuration file to + a directory. A pipeline variable can be saved and loaded if its class implements both a save and loading + method. The pipeline can easily be re-loaded using the `[`~DiffusionPipeline.from_pretrained`]` class method. + + Arguments: + save_directory (`str` or `os.PathLike`): + Directory to which to save. Will be created if it doesn't exist. + safe_serialization (`bool`, *optional*, defaults to `False`): + Whether to save the model using `safetensors` or the traditional PyTorch way (that uses `pickle`). + """ + self.save_config(save_directory) + + model_index_dict = dict(self.config) + model_index_dict.pop("_class_name") + model_index_dict.pop("_diffusers_version") + model_index_dict.pop("_module", None) + + expected_modules, optional_kwargs = self._get_signature_keys(self) + + def is_saveable_module(name, value): + if name not in expected_modules: + return False + if name in self._optional_components and value[0] is None: + return False + return True + + model_index_dict = {k: v for k, v in model_index_dict.items() if is_saveable_module(k, v)} + + for pipeline_component_name in model_index_dict.keys(): + sub_model = getattr(self, pipeline_component_name) + model_cls = sub_model.__class__ + + save_method_name = None + # search for the model's base class in LOADABLE_CLASSES + for library_name, library_classes in LOADABLE_CLASSES.items(): + library = importlib.import_module(library_name) + for base_class, save_load_methods in library_classes.items(): + class_candidate = getattr(library, base_class, None) + if class_candidate is not None and issubclass(model_cls, class_candidate): + # if we found a suitable base class in LOADABLE_CLASSES then grab its save method + save_method_name = save_load_methods[0] + break + if save_method_name is not None: + break + + save_method = getattr(sub_model, save_method_name) + + # Call the save method with the argument safe_serialization only if it's supported + save_method_signature = inspect.signature(save_method) + save_method_accept_safe = "safe_serialization" in save_method_signature.parameters + if save_method_accept_safe: + save_method( + os.path.join(save_directory, pipeline_component_name), safe_serialization=safe_serialization + ) + else: + save_method(os.path.join(save_directory, pipeline_component_name)) + + def to(self, torch_device: Optional[Union[str, torch.device]] = None): + if torch_device is None: + return self + + module_names, _, _ = self.extract_init_dict(dict(self.config)) + for name in module_names.keys(): + module = getattr(self, name) + if isinstance(module, torch.nn.Module): + if module.dtype == torch.float16 and str(torch_device) in ["cpu"]: + logger.warning( + "Pipelines loaded with `torch_dtype=torch.float16` cannot run with `cpu` device. It" + " is not recommended to move them to `cpu` as running them will fail. Please make" + " sure to use an accelerator to run the pipeline in inference, due to the lack of" + " support for`float16` operations on this device in PyTorch. Please, remove the" + " `torch_dtype=torch.float16` argument, or use another device for inference." + ) + module.to(torch_device) + return self + + @property + def device(self) -> torch.device: + r""" + Returns: + `torch.device`: The torch device on which the pipeline is located. + """ + module_names, _, _ = self.extract_init_dict(dict(self.config)) + for name in module_names.keys(): + module = getattr(self, name) + if isinstance(module, torch.nn.Module): + return module.device + return torch.device("cpu") + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path: Optional[Union[str, os.PathLike]], **kwargs): + r""" + Instantiate a PyTorch diffusion pipeline from pre-trained pipeline weights. + + The pipeline is set in evaluation mode by default using `model.eval()` (Dropout modules are deactivated). + + The warning *Weights from XXX not initialized from pretrained model* means that the weights of XXX do not come + pretrained with the rest of the model. It is up to you to train those weights with a downstream fine-tuning + task. + + The warning *Weights from XXX not used in YYY* means that the layer XXX is not used by YYY, therefore those + weights are discarded. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *repo id* of a pretrained pipeline hosted inside a model repo on + https://huggingface.co/ Valid repo ids have to be located under a user or organization name, like + `CompVis/ldm-text2im-large-256`. + - A path to a *directory* containing pipeline weights saved using + [`~DiffusionPipeline.save_pretrained`], e.g., `./my_pipeline_directory/`. + torch_dtype (`str` or `torch.dtype`, *optional*): + Override the default `torch.dtype` and load the model under this dtype. If `"auto"` is passed the dtype + will be automatically derived from the model's weights. + custom_pipeline (`str`, *optional*): + + + + This is an experimental feature and is likely to change in the future. + + + + Can be either: + + - A string, the *repo id* of a custom pipeline hosted inside a model repo on + https://huggingface.co/. Valid repo ids have to be located under a user or organization name, + like `hf-internal-testing/diffusers-dummy-pipeline`. + + + + It is required that the model repo has a file, called `pipeline.py` that defines the custom + pipeline. + + + + - A string, the *file name* of a community pipeline hosted on GitHub under + https://github.com/huggingface/diffusers/tree/main/examples/community. Valid file names have to + match exactly the file name without `.py` located under the above link, *e.g.* + `clip_guided_stable_diffusion`. + + + + Community pipelines are always loaded from the current `main` branch of GitHub. + + + + - A path to a *directory* containing a custom pipeline, e.g., `./my_pipeline_directory/`. + + + + It is required that the directory has a file, called `pipeline.py` that defines the custom + pipeline. + + + + For more information on how to load and create custom pipelines, please have a look at [Loading and + Adding Custom + Pipelines](https://huggingface.co/docs/diffusers/using-diffusers/custom_pipeline_overview) + + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `huggingface-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + custom_revision (`str`, *optional*, defaults to `"main"` when loading from the Hub and to local version of `diffusers` when loading from GitHub): + The specific model version to use. It can be a branch name, a tag name, or a commit id similar to + `revision` when loading a custom pipeline from the Hub. It can be a diffusers version when loading a + custom pipeline from GitHub. + mirror (`str`, *optional*): + Mirror source to accelerate downloads in China. If you are from China and have an accessibility + problem, you can set this option to resolve it. Note that we do not guarantee the timeliness or safety. + Please refer to the mirror site for more information. specify the folder name here. + device_map (`str` or `Dict[str, Union[int, str, torch.device]]`, *optional*): + A map that specifies where each submodule should go. It doesn't need to be refined to each + parameter/buffer name, once a given module name is inside, every submodule of it will be sent to the + same device. + + To have Accelerate compute the most optimized `device_map` automatically, set `device_map="auto"`. For + more information about each option see [designing a device + map](https://hf.co/docs/accelerate/main/en/usage_guides/big_modeling#designing-a-device-map). + low_cpu_mem_usage (`bool`, *optional*, defaults to `True` if torch version >= 1.9.0 else `False`): + Speed up model loading by not initializing the weights and only loading the pre-trained weights. This + also tries to not use more than 1x model size in CPU memory (including peak memory) while loading the + model. This is only supported when torch version >= 1.9.0. If you are using an older version of torch, + setting this argument to `True` will raise an error. + return_cached_folder (`bool`, *optional*, defaults to `False`): + If set to `True`, path to downloaded cached folder will be returned in addition to loaded pipeline. + kwargs (remaining dictionary of keyword arguments, *optional*): + Can be used to overwrite load - and saveable variables - *i.e.* the pipeline components - of the + specific pipeline class. The overwritten components are then directly passed to the pipelines + `__init__` method. See example below for more information. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models), *e.g.* `"runwayml/stable-diffusion-v1-5"` + + + + + + Activate the special ["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use + this method in a firewalled environment. + + + + Examples: + + ```py + >>> from diffusers import DiffusionPipeline + + >>> # Download pipeline from huggingface.co and cache. + >>> pipeline = DiffusionPipeline.from_pretrained("CompVis/ldm-text2im-large-256") + + >>> # Download pipeline that requires an authorization token + >>> # For more information on access tokens, please refer to this section + >>> # of the documentation](https://huggingface.co/docs/hub/security-tokens) + >>> pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + + >>> # Use a different scheduler + >>> from diffusers import LMSDiscreteScheduler + + >>> scheduler = LMSDiscreteScheduler.from_config(pipeline.scheduler.config) + >>> pipeline.scheduler = scheduler + ``` + """ + cache_dir = kwargs.pop("cache_dir", DIFFUSERS_CACHE) + resume_download = kwargs.pop("resume_download", False) + force_download = kwargs.pop("force_download", False) + proxies = kwargs.pop("proxies", None) + local_files_only = kwargs.pop("local_files_only", HF_HUB_OFFLINE) + use_auth_token = kwargs.pop("use_auth_token", None) + revision = kwargs.pop("revision", None) + from_flax = kwargs.pop("from_flax", False) + torch_dtype = kwargs.pop("torch_dtype", None) + custom_pipeline = kwargs.pop("custom_pipeline", None) + custom_revision = kwargs.pop("custom_revision", None) + provider = kwargs.pop("provider", None) + sess_options = kwargs.pop("sess_options", None) + device_map = kwargs.pop("device_map", None) + low_cpu_mem_usage = kwargs.pop("low_cpu_mem_usage", _LOW_CPU_MEM_USAGE_DEFAULT) + return_cached_folder = kwargs.pop("return_cached_folder", False) + + # 1. Download the checkpoints and configs + # use snapshot download here to get it working from from_pretrained + if not os.path.isdir(pretrained_model_name_or_path): + config_dict = cls.load_config( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + force_download=force_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + ) + # make sure we only download sub-folders and `diffusers` filenames + folder_names = [k for k in config_dict.keys() if not k.startswith("_")] + allow_patterns = [os.path.join(k, "*") for k in folder_names] + allow_patterns += [ + WEIGHTS_NAME, + SCHEDULER_CONFIG_NAME, + CONFIG_NAME, + ONNX_WEIGHTS_NAME, + cls.config_name, + ] + + # make sure we don't download flax weights + ignore_patterns = ["*.msgpack"] + + if from_flax: + ignore_patterns = ["*.bin", "*.safetensors"] + allow_patterns += [ + FLAX_WEIGHTS_NAME, + ] + + if custom_pipeline is not None: + allow_patterns += [CUSTOM_PIPELINE_FILE_NAME] + + if cls != DiffusionPipeline: + requested_pipeline_class = cls.__name__ + else: + requested_pipeline_class = config_dict.get("_class_name", cls.__name__) + user_agent = {"pipeline_class": requested_pipeline_class} + if custom_pipeline is not None and not custom_pipeline.endswith(".py"): + user_agent["custom_pipeline"] = custom_pipeline + + user_agent = http_user_agent(user_agent) + + if is_safetensors_available() and not local_files_only: + info = model_info( + pretrained_model_name_or_path, + use_auth_token=use_auth_token, + revision=revision, + ) + if is_safetensors_compatible(info): + ignore_patterns.append("*.bin") + else: + # as a safety mechanism we also don't download safetensors if + # not all safetensors files are there + ignore_patterns.append("*.safetensors") + else: + ignore_patterns.append("*.safetensors") + + # download all allow_patterns + cached_folder = snapshot_download( + pretrained_model_name_or_path, + cache_dir=cache_dir, + resume_download=resume_download, + proxies=proxies, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + revision=revision, + allow_patterns=allow_patterns, + ignore_patterns=ignore_patterns, + user_agent=user_agent, + ) + else: + cached_folder = pretrained_model_name_or_path + config_dict = cls.load_config(cached_folder) + + # 2. Load the pipeline class, if using custom module then load it from the hub + # if we load from explicit class, let's use it + if custom_pipeline is not None: + if custom_pipeline.endswith(".py"): + path = Path(custom_pipeline) + # decompose into folder & file + file_name = path.name + custom_pipeline = path.parent.absolute() + else: + file_name = CUSTOM_PIPELINE_FILE_NAME + + pipeline_class = get_class_from_dynamic_module( + custom_pipeline, module_file=file_name, cache_dir=cache_dir, revision=custom_revision + ) + elif cls != DiffusionPipeline: + pipeline_class = cls + else: + diffusers_module = importlib.import_module(cls.__module__.split(".")[0]) + pipeline_class = getattr(diffusers_module, config_dict["_class_name"]) + + # To be removed in 1.0.0 + if pipeline_class.__name__ == "StableDiffusionInpaintPipeline" and version.parse( + version.parse(config_dict["_diffusers_version"]).base_version + ) <= version.parse("0.5.1"): + from diffusers import StableDiffusionInpaintPipeline, StableDiffusionInpaintPipelineLegacy + + pipeline_class = StableDiffusionInpaintPipelineLegacy + + deprecation_message = ( + "You are using a legacy checkpoint for inpainting with Stable Diffusion, therefore we are loading the" + f" {StableDiffusionInpaintPipelineLegacy} class instead of {StableDiffusionInpaintPipeline}. For" + " better inpainting results, we strongly suggest using Stable Diffusion's official inpainting" + " checkpoint: https://huggingface.co/runwayml/stable-diffusion-inpainting instead or adapting your" + f" checkpoint {pretrained_model_name_or_path} to the format of" + " https://huggingface.co/runwayml/stable-diffusion-inpainting. Note that we do not actively maintain" + " the {StableDiffusionInpaintPipelineLegacy} class and will likely remove it in version 1.0.0." + ) + deprecate("StableDiffusionInpaintPipelineLegacy", "1.0.0", deprecation_message, standard_warn=False) + + # some modules can be passed directly to the init + # in this case they are already instantiated in `kwargs` + # extract them here + expected_modules, optional_kwargs = cls._get_signature_keys(pipeline_class) + passed_class_obj = {k: kwargs.pop(k) for k in expected_modules if k in kwargs} + passed_pipe_kwargs = {k: kwargs.pop(k) for k in optional_kwargs if k in kwargs} + + init_dict, unused_kwargs, _ = pipeline_class.extract_init_dict(config_dict, **kwargs) + + # define init kwargs + init_kwargs = {k: init_dict.pop(k) for k in optional_kwargs if k in init_dict} + init_kwargs = {**init_kwargs, **passed_pipe_kwargs} + + # remove `null` components + def load_module(name, value): + if value[0] is None: + return False + if name in passed_class_obj and passed_class_obj[name] is None: + return False + return True + + init_dict = {k: v for k, v in init_dict.items() if load_module(k, v)} + + # Special case: safety_checker must be loaded separately when using `from_flax` + if from_flax and "safety_checker" in init_dict and "safety_checker" not in passed_class_obj: + raise NotImplementedError( + "The safety checker cannot be automatically loaded when loading weights `from_flax`." + " Please, pass `safety_checker=None` to `from_pretrained`, and load the safety checker" + " separately if you need it." + ) + + if len(unused_kwargs) > 0: + logger.warning( + f"Keyword arguments {unused_kwargs} are not expected by {pipeline_class.__name__} and will be ignored." + ) + + if low_cpu_mem_usage and not is_accelerate_available(): + low_cpu_mem_usage = False + logger.warning( + "Cannot initialize model with low cpu memory usage because `accelerate` was not found in the" + " environment. Defaulting to `low_cpu_mem_usage=False`. It is strongly recommended to install" + " `accelerate` for faster and less memory-intense model loading. You can do so with: \n```\npip" + " install accelerate\n```\n." + ) + + if device_map is not None and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Loading and dispatching requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `device_map=None`." + ) + + if low_cpu_mem_usage is True and not is_torch_version(">=", "1.9.0"): + raise NotImplementedError( + "Low memory initialization requires torch >= 1.9.0. Please either update your PyTorch version or set" + " `low_cpu_mem_usage=False`." + ) + + if low_cpu_mem_usage is False and device_map is not None: + raise ValueError( + f"You cannot set `low_cpu_mem_usage` to False while using device_map={device_map} for loading and" + " dispatching. Please make sure to set `low_cpu_mem_usage=True`." + ) + + # import it here to avoid circular import + from diffusers import pipelines + + # 3. Load each module in the pipeline + for name, (library_name, class_name) in init_dict.items(): + # 3.1 - now that JAX/Flax is an official framework of the library, we might load from Flax names + if class_name.startswith("Flax"): + class_name = class_name[4:] + + is_pipeline_module = hasattr(pipelines, library_name) + loaded_sub_model = None + + # if the model is in a pipeline module, then we load it from the pipeline + if name in passed_class_obj: + # 1. check that passed_class_obj has correct parent class + if not is_pipeline_module: + library = importlib.import_module(library_name) + class_obj = getattr(library, class_name) + importable_classes = LOADABLE_CLASSES[library_name] + class_candidates = {c: getattr(library, c, None) for c in importable_classes.keys()} + + expected_class_obj = None + for class_name, class_candidate in class_candidates.items(): + if class_candidate is not None and issubclass(class_obj, class_candidate): + expected_class_obj = class_candidate + + if not issubclass(passed_class_obj[name].__class__, expected_class_obj): + raise ValueError( + f"{passed_class_obj[name]} is of type: {type(passed_class_obj[name])}, but should be" + f" {expected_class_obj}" + ) + else: + logger.warning( + f"You have passed a non-standard module {passed_class_obj[name]}. We cannot verify whether it" + " has the correct type" + ) + + # set passed class object + loaded_sub_model = passed_class_obj[name] + elif is_pipeline_module: + pipeline_module = getattr(pipelines, library_name) + class_obj = getattr(pipeline_module, class_name) + importable_classes = ALL_IMPORTABLE_CLASSES + class_candidates = {c: class_obj for c in importable_classes.keys()} + else: + # else we just import it from the library. + library = importlib.import_module(library_name) + + class_obj = getattr(library, class_name) + importable_classes = LOADABLE_CLASSES[library_name] + class_candidates = {c: getattr(library, c, None) for c in importable_classes.keys()} + + if loaded_sub_model is None: + load_method_name = None + for class_name, class_candidate in class_candidates.items(): + if class_candidate is not None and issubclass(class_obj, class_candidate): + load_method_name = importable_classes[class_name][1] + + if load_method_name is None: + none_module = class_obj.__module__ + is_dummy_path = none_module.startswith(DUMMY_MODULES_FOLDER) or none_module.startswith( + TRANSFORMERS_DUMMY_MODULES_FOLDER + ) + if is_dummy_path and "dummy" in none_module: + # call class_obj for nice error message of missing requirements + class_obj() + + raise ValueError( + f"The component {class_obj} of {pipeline_class} cannot be loaded as it does not seem to have" + f" any of the loading methods defined in {ALL_IMPORTABLE_CLASSES}." + ) + + load_method = getattr(class_obj, load_method_name) + loading_kwargs = {} + + if issubclass(class_obj, torch.nn.Module): + loading_kwargs["torch_dtype"] = torch_dtype + if issubclass(class_obj, diffusers.OnnxRuntimeModel): + loading_kwargs["provider"] = provider + loading_kwargs["sess_options"] = sess_options + + is_diffusers_model = issubclass(class_obj, diffusers.ModelMixin) + is_transformers_model = ( + is_transformers_available() + and issubclass(class_obj, PreTrainedModel) + and version.parse(version.parse(transformers.__version__).base_version) >= version.parse("4.20.0") + ) + + # When loading a transformers model, if the device_map is None, the weights will be initialized as opposed to diffusers. + # To make default loading faster we set the `low_cpu_mem_usage=low_cpu_mem_usage` flag which is `True` by default. + # This makes sure that the weights won't be initialized which significantly speeds up loading. + if is_diffusers_model or is_transformers_model: + loading_kwargs["device_map"] = device_map + if from_flax: + loading_kwargs["from_flax"] = True + + # if `from_flax` and model is transformer model, can currently not load with `low_cpu_mem_usage` + if not (from_flax and is_transformers_model): + loading_kwargs["low_cpu_mem_usage"] = low_cpu_mem_usage + else: + loading_kwargs["low_cpu_mem_usage"] = False + + # check if the module is in a subdirectory + if os.path.isdir(os.path.join(cached_folder, name)): + loaded_sub_model = load_method(os.path.join(cached_folder, name), **loading_kwargs) + else: + # else load from the root directory + loaded_sub_model = load_method(cached_folder, **loading_kwargs) + + init_kwargs[name] = loaded_sub_model # UNet(...), # DiffusionSchedule(...) + + # 4. Potentially add passed objects if expected + missing_modules = set(expected_modules) - set(init_kwargs.keys()) + passed_modules = list(passed_class_obj.keys()) + optional_modules = pipeline_class._optional_components + if len(missing_modules) > 0 and missing_modules <= set(passed_modules + optional_modules): + for module in missing_modules: + init_kwargs[module] = passed_class_obj.get(module, None) + elif len(missing_modules) > 0: + passed_modules = set(list(init_kwargs.keys()) + list(passed_class_obj.keys())) - optional_kwargs + raise ValueError( + f"Pipeline {pipeline_class} expected {expected_modules}, but only {passed_modules} were passed." + ) + + # 5. Instantiate the pipeline + model = pipeline_class(**init_kwargs) + + if return_cached_folder: + return model, cached_folder + return model + + @staticmethod + def _get_signature_keys(obj): + parameters = inspect.signature(obj.__init__).parameters + required_parameters = {k: v for k, v in parameters.items() if v.default == inspect._empty} + optional_parameters = set({k for k, v in parameters.items() if v.default != inspect._empty}) + expected_modules = set(required_parameters.keys()) - set(["self"]) + return expected_modules, optional_parameters + + @property + def components(self) -> Dict[str, Any]: + r""" + + The `self.components` property can be useful to run different pipelines with the same weights and + configurations to not have to re-allocate memory. + + Examples: + + ```py + >>> from diffusers import ( + ... StableDiffusionPipeline, + ... StableDiffusionImg2ImgPipeline, + ... StableDiffusionInpaintPipeline, + ... ) + + >>> text2img = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + >>> img2img = StableDiffusionImg2ImgPipeline(**text2img.components) + >>> inpaint = StableDiffusionInpaintPipeline(**text2img.components) + ``` + + Returns: + A dictionary containing all the modules needed to initialize the pipeline. + """ + expected_modules, optional_parameters = self._get_signature_keys(self) + components = { + k: getattr(self, k) for k in self.config.keys() if not k.startswith("_") and k not in optional_parameters + } + + if set(components.keys()) != expected_modules: + raise ValueError( + f"{self} has been incorrectly initialized or {self.__class__} is incorrectly implemented. Expected" + f" {expected_modules} to be defined, but {components} are defined." + ) + + return components + + @staticmethod + def numpy_to_pil(images): + """ + Convert a numpy image or a batch of images to a PIL image. + """ + if images.ndim == 3: + images = images[None, ...] + images = (images * 255).round().astype("uint8") + if images.shape[-1] == 1: + # special case for grayscale (single channel) images + pil_images = [Image.fromarray(image.squeeze(), mode="L") for image in images] + else: + pil_images = [Image.fromarray(image) for image in images] + + return pil_images + + def progress_bar(self, iterable=None, total=None): + if not hasattr(self, "_progress_bar_config"): + self._progress_bar_config = {} + elif not isinstance(self._progress_bar_config, dict): + raise ValueError( + f"`self._progress_bar_config` should be of type `dict`, but is {type(self._progress_bar_config)}." + ) + + if iterable is not None: + return tqdm(iterable, **self._progress_bar_config) + elif total is not None: + return tqdm(total=total, **self._progress_bar_config) + else: + raise ValueError("Either `total` or `iterable` has to be defined.") + + def set_progress_bar_config(self, **kwargs): + self._progress_bar_config = kwargs + + def enable_xformers_memory_efficient_attention(self, attention_op: Optional[Callable] = None): + r""" + Enable memory efficient attention as implemented in xformers. + + When this option is enabled, you should observe lower GPU memory usage and a potential speed up at inference + time. Speed up at training time is not guaranteed. + + Warning: When Memory Efficient Attention and Sliced attention are both enabled, the Memory Efficient Attention + is used. + + Parameters: + attention_op (`Callable`, *optional*): + Override the default `None` operator for use as `op` argument to the + [`memory_efficient_attention()`](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.memory_efficient_attention) + function of xFormers. + + Examples: + + ```py + >>> import torch + >>> from diffusers import DiffusionPipeline + >>> from xformers.ops import MemoryEfficientAttentionFlashAttentionOp + + >>> pipe = DiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1", torch_dtype=torch.float16) + >>> pipe = pipe.to("cuda") + >>> pipe.enable_xformers_memory_efficient_attention(attention_op=MemoryEfficientAttentionFlashAttentionOp) + >>> # Workaround for not accepting attention shape using VAE for Flash Attention + >>> pipe.vae.enable_xformers_memory_efficient_attention(attention_op=None) + ``` + """ + self.set_use_memory_efficient_attention_xformers(True, attention_op) + + def disable_xformers_memory_efficient_attention(self): + r""" + Disable memory efficient attention as implemented in xformers. + """ + self.set_use_memory_efficient_attention_xformers(False) + + def set_use_memory_efficient_attention_xformers( + self, valid: bool, attention_op: Optional[Callable] = None + ) -> None: + # Recursively walk through all the children. + # Any children which exposes the set_use_memory_efficient_attention_xformers method + # gets the message + def fn_recursive_set_mem_eff(module: torch.nn.Module): + if hasattr(module, "set_use_memory_efficient_attention_xformers"): + module.set_use_memory_efficient_attention_xformers(valid, attention_op) + + for child in module.children(): + fn_recursive_set_mem_eff(child) + + module_names, _, _ = self.extract_init_dict(dict(self.config)) + for module_name in module_names: + module = getattr(self, module_name) + if isinstance(module, torch.nn.Module): + fn_recursive_set_mem_eff(module) + + def enable_attention_slicing(self, slice_size: Optional[Union[str, int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + `"max"`, maxium amount of memory will be saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + self.set_attention_slice(slice_size) + + def disable_attention_slicing(self): + r""" + Disable sliced attention computation. If `enable_attention_slicing` was previously invoked, this method will go + back to computing attention in one step. + """ + # set slice_size = `None` to disable `attention slicing` + self.enable_attention_slicing(None) + + def set_attention_slice(self, slice_size: Optional[int]): + module_names, _, _ = self.extract_init_dict(dict(self.config)) + for module_name in module_names: + module = getattr(self, module_name) + if isinstance(module, torch.nn.Module) and hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size) diff --git a/diffusers/src/diffusers/pipelines/pndm/__init__.py b/diffusers/src/diffusers/pipelines/pndm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..488eb4f5f2b29c071fdc044ef282bc2838148c1e --- /dev/null +++ b/diffusers/src/diffusers/pipelines/pndm/__init__.py @@ -0,0 +1 @@ +from .pipeline_pndm import PNDMPipeline diff --git a/diffusers/src/diffusers/pipelines/pndm/pipeline_pndm.py b/diffusers/src/diffusers/pipelines/pndm/pipeline_pndm.py new file mode 100644 index 0000000000000000000000000000000000000000..d39995097a06c404e0aada25ee3998901b4cc1c6 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/pndm/pipeline_pndm.py @@ -0,0 +1,99 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import List, Optional, Tuple, Union + +import torch + +from ...models import UNet2DModel +from ...schedulers import PNDMScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class PNDMPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet (`UNet2DModel`): U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + The `PNDMScheduler` to be used in combination with `unet` to denoise the encoded image. + """ + + unet: UNet2DModel + scheduler: PNDMScheduler + + def __init__(self, unet: UNet2DModel, scheduler: PNDMScheduler): + super().__init__() + + scheduler = PNDMScheduler.from_config(scheduler.config) + + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + num_inference_steps: int = 50, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, `optional`, defaults to 1): The number of images to generate. + num_inference_steps (`int`, `optional`, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + generator (`torch.Generator`, `optional`): A [torch + generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + output_type (`str`, `optional`, defaults to `"pil"`): The output format of the generate image. Choose + between [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, `optional`, defaults to `True`): Whether or not to return a + [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + # For more information on the sampling method you can take a look at Algorithm 2 of + # the official paper: https://arxiv.org/pdf/2202.09778.pdf + + # Sample gaussian noise to begin loop + image = randn_tensor( + (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + generator=generator, + device=self.device, + ) + + self.scheduler.set_timesteps(num_inference_steps) + for t in self.progress_bar(self.scheduler.timesteps): + model_output = self.unet(image, t).sample + + image = self.scheduler.step(model_output, t, image).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/repaint/__init__.py b/diffusers/src/diffusers/pipelines/repaint/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..16bc86d1cedf6243fb92f7ba331b5a6188133298 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/repaint/__init__.py @@ -0,0 +1 @@ +from .pipeline_repaint import RePaintPipeline diff --git a/diffusers/src/diffusers/pipelines/repaint/pipeline_repaint.py b/diffusers/src/diffusers/pipelines/repaint/pipeline_repaint.py new file mode 100644 index 0000000000000000000000000000000000000000..5cd77241f51d09e822da27d56e9843baa1991354 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/repaint/pipeline_repaint.py @@ -0,0 +1,174 @@ +# Copyright 2022 ETH Zurich Computer Vision Lab and The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import List, Optional, Tuple, Union + +import numpy as np +import PIL +import torch + +from ...models import UNet2DModel +from ...schedulers import RePaintScheduler +from ...utils import PIL_INTERPOLATION, deprecate, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess +def _preprocess_image(image: Union[List, PIL.Image.Image, torch.Tensor]): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +def _preprocess_mask(mask: Union[List, PIL.Image.Image, torch.Tensor]): + if isinstance(mask, torch.Tensor): + return mask + elif isinstance(mask, PIL.Image.Image): + mask = [mask] + + if isinstance(mask[0], PIL.Image.Image): + w, h = mask[0].size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = [np.array(m.convert("L").resize((w, h), resample=PIL_INTERPOLATION["nearest"]))[None, :] for m in mask] + mask = np.concatenate(mask, axis=0) + mask = mask.astype(np.float32) / 255.0 + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + elif isinstance(mask[0], torch.Tensor): + mask = torch.cat(mask, dim=0) + return mask + + +class RePaintPipeline(DiffusionPipeline): + unet: UNet2DModel + scheduler: RePaintScheduler + + def __init__(self, unet, scheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + image: Union[torch.Tensor, PIL.Image.Image], + mask_image: Union[torch.Tensor, PIL.Image.Image], + num_inference_steps: int = 250, + eta: float = 0.0, + jump_length: int = 10, + jump_n_sample: int = 10, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + image (`torch.FloatTensor` or `PIL.Image.Image`): + The original image to inpaint on. + mask_image (`torch.FloatTensor` or `PIL.Image.Image`): + The mask_image where 0.0 values define which part of the original image to inpaint (change). + num_inference_steps (`int`, *optional*, defaults to 1000): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + eta (`float`): + The weight of noise for added noise in a diffusion step. Its value is between 0.0 and 1.0 - 0.0 is DDIM + and 1.0 is DDPM scheduler respectively. + jump_length (`int`, *optional*, defaults to 10): + The number of steps taken forward in time before going backward in time for a single jump ("j" in + RePaint paper). Take a look at Figure 9 and 10 in https://arxiv.org/pdf/2201.09865.pdf. + jump_n_sample (`int`, *optional*, defaults to 10): + The number of times we will make forward time jump for a given chosen time sample. Take a look at + Figure 9 and 10 in https://arxiv.org/pdf/2201.09865.pdf. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + message = "Please use `image` instead of `original_image`." + original_image = deprecate("original_image", "0.15.0", message, take_from=kwargs) + original_image = original_image or image + + original_image = _preprocess_image(original_image) + original_image = original_image.to(device=self.device, dtype=self.unet.dtype) + mask_image = _preprocess_mask(mask_image) + mask_image = mask_image.to(device=self.device, dtype=self.unet.dtype) + + batch_size = original_image.shape[0] + + # sample gaussian noise to begin the loop + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + image_shape = original_image.shape + image = randn_tensor(image_shape, generator=generator, device=self.device, dtype=self.unet.dtype) + + # set step values + self.scheduler.set_timesteps(num_inference_steps, jump_length, jump_n_sample, self.device) + self.scheduler.eta = eta + + t_last = self.scheduler.timesteps[0] + 1 + generator = generator[0] if isinstance(generator, list) else generator + for i, t in enumerate(self.progress_bar(self.scheduler.timesteps)): + if t < t_last: + # predict the noise residual + model_output = self.unet(image, t).sample + # compute previous image: x_t -> x_t-1 + image = self.scheduler.step(model_output, t, image, original_image, mask_image, generator).prev_sample + + else: + # compute the reverse: x_t-1 -> x_t + image = self.scheduler.undo_step(image, t_last, generator) + t_last = t + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/score_sde_ve/__init__.py b/diffusers/src/diffusers/pipelines/score_sde_ve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c7c2a85c067b707c155e78a3c8b84562999134e7 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/score_sde_ve/__init__.py @@ -0,0 +1 @@ +from .pipeline_score_sde_ve import ScoreSdeVePipeline diff --git a/diffusers/src/diffusers/pipelines/score_sde_ve/pipeline_score_sde_ve.py b/diffusers/src/diffusers/pipelines/score_sde_ve/pipeline_score_sde_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..741c2947f4d45691ba5265ae15ab9380f28ad33b --- /dev/null +++ b/diffusers/src/diffusers/pipelines/score_sde_ve/pipeline_score_sde_ve.py @@ -0,0 +1,101 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import torch + +from ...models import UNet2DModel +from ...schedulers import ScoreSdeVeScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class ScoreSdeVePipeline(DiffusionPipeline): + r""" + Parameters: + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. scheduler ([`SchedulerMixin`]): + The [`ScoreSdeVeScheduler`] scheduler to be used in combination with `unet` to denoise the encoded image. + """ + unet: UNet2DModel + scheduler: ScoreSdeVeScheduler + + def __init__(self, unet: UNet2DModel, scheduler: DiffusionPipeline): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + num_inference_steps: int = 2000, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + img_size = self.unet.config.sample_size + shape = (batch_size, 3, img_size, img_size) + + model = self.unet + + sample = randn_tensor(shape, generator=generator) * self.scheduler.init_noise_sigma + sample = sample.to(self.device) + + self.scheduler.set_timesteps(num_inference_steps) + self.scheduler.set_sigmas(num_inference_steps) + + for i, t in enumerate(self.progress_bar(self.scheduler.timesteps)): + sigma_t = self.scheduler.sigmas[i] * torch.ones(shape[0], device=self.device) + + # correction step + for _ in range(self.scheduler.config.correct_steps): + model_output = self.unet(sample, sigma_t).sample + sample = self.scheduler.step_correct(model_output, sample, generator=generator).prev_sample + + # prediction step + model_output = model(sample, sigma_t).sample + output = self.scheduler.step_pred(model_output, t, sample, generator=generator) + + sample, sample_mean = output.prev_sample, output.prev_sample_mean + + sample = sample_mean.clamp(0, 1) + sample = sample.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + sample = self.numpy_to_pil(sample) + + if not return_dict: + return (sample,) + + return ImagePipelineOutput(images=sample) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/README.md b/diffusers/src/diffusers/pipelines/stable_diffusion/README.md new file mode 100644 index 0000000000000000000000000000000000000000..be4c5d942b2e313ebfac5acc22764de8bae48bf5 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/README.md @@ -0,0 +1,176 @@ +# Stable Diffusion + +## Overview + +Stable Diffusion was proposed in [Stable Diffusion Announcement](https://stability.ai/blog/stable-diffusion-announcement) by Patrick Esser and Robin Rombach and the Stability AI team. + +The summary of the model is the following: + +*Stable Diffusion is a text-to-image model that will empower billions of people to create stunning art within seconds. It is a breakthrough in speed and quality meaning that it can run on consumer GPUs. You can see some of the amazing output that has been created by this model without pre or post-processing on this page. The model itself builds upon the work of the team at CompVis and Runway in their widely used latent diffusion model combined with insights from the conditional diffusion models by our lead generative AI developer Katherine Crowson, Dall-E 2 by Open AI, Imagen by Google Brain and many others. We are delighted that AI media generation is a cooperative field and hope it can continue this way to bring the gift of creativity to all.* + +## Tips: + +- Stable Diffusion has the same architecture as [Latent Diffusion](https://arxiv.org/abs/2112.10752) but uses a frozen CLIP Text Encoder instead of training the text encoder jointly with the diffusion model. +- An in-detail explanation of the Stable Diffusion model can be found under [Stable Diffusion with 🧨 Diffusers](https://huggingface.co/blog/stable_diffusion). +- If you don't want to rely on the Hugging Face Hub and having to pass a authentication token, you can +download the weights with `git lfs install; git clone https://huggingface.co/runwayml/stable-diffusion-v1-5` and instead pass the local path to the cloned folder to `from_pretrained` as shown below. +- Stable Diffusion can work with a variety of different samplers as is shown below. + +## Available Pipelines: + +| Pipeline | Tasks | Colab +|---|---|:---:| +| [pipeline_stable_diffusion.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) | *Text-to-Image Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/training_example.ipynb) +| [pipeline_stable_diffusion_img2img](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py) | *Image-to-Image Text-Guided Generation* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/image_2_image_using_diffusers.ipynb) +| [pipeline_stable_diffusion_inpaint](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) | *Text-Guided Image Inpainting* | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/notebooks/blob/main/diffusers/in_painting_with_stable_diffusion_using_diffusers.ipynb) + +## Examples: + +### Using Stable Diffusion without being logged into the Hub. + +If you want to download the model weights using a single Python line, you need to be logged in via `huggingface-cli login`. + +```python +from diffusers import DiffusionPipeline + +pipeline = DiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +``` + +This however can make it difficult to build applications on top of `diffusers` as you will always have to pass the token around. A potential way to solve this issue is by downloading the weights to a local path `"./stable-diffusion-v1-5"`: + +``` +git lfs install +git clone https://huggingface.co/runwayml/stable-diffusion-v1-5 +``` + +and simply passing the local path to `from_pretrained`: + +```python +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("./stable-diffusion-v1-5") +``` + +### Text-to-Image with default PLMS scheduler + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline + +pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") +pipe = pipe.to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).sample[0] + +image.save("astronaut_rides_horse.png") +``` + +### Text-to-Image with DDIM scheduler + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline, DDIMScheduler + +scheduler = DDIMScheduler.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="scheduler") + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + scheduler=scheduler, +).to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).sample[0] + +image.save("astronaut_rides_horse.png") +``` + +### Text-to-Image with K-LMS scheduler + +```python +# make sure you're logged in with `huggingface-cli login` +from diffusers import StableDiffusionPipeline, LMSDiscreteScheduler + +lms = LMSDiscreteScheduler.from_pretrained("CompVis/stable-diffusion-v1-4", subfolder="scheduler") + +pipe = StableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + scheduler=lms, +).to("cuda") + +prompt = "a photo of an astronaut riding a horse on mars" +image = pipe(prompt).sample[0] + +image.save("astronaut_rides_horse.png") +``` + +### CycleDiffusion using Stable Diffusion and DDIM scheduler + +```python +import requests +import torch +from PIL import Image +from io import BytesIO + +from diffusers import CycleDiffusionPipeline, DDIMScheduler + + +# load the scheduler. CycleDiffusion only supports stochastic schedulers. + +# load the pipeline +# make sure you're logged in with `huggingface-cli login` +model_id_or_path = "CompVis/stable-diffusion-v1-4" +scheduler = DDIMScheduler.from_pretrained(model_id_or_path, subfolder="scheduler") +pipe = CycleDiffusionPipeline.from_pretrained(model_id_or_path, scheduler=scheduler).to("cuda") + +# let's download an initial image +url = "https://raw.githubusercontent.com/ChenWu98/cycle-diffusion/main/data/dalle2/An%20astronaut%20riding%20a%20horse.png" +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((512, 512)) +init_image.save("horse.png") + +# let's specify a prompt +source_prompt = "An astronaut riding a horse" +prompt = "An astronaut riding an elephant" + +# call the pipeline +image = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.8, + guidance_scale=2, + source_guidance_scale=1, +).images[0] + +image.save("horse_to_elephant.png") + +# let's try another example +# See more samples at the original repo: https://github.com/ChenWu98/cycle-diffusion +url = "https://raw.githubusercontent.com/ChenWu98/cycle-diffusion/main/data/dalle2/A%20black%20colored%20car.png" +response = requests.get(url) +init_image = Image.open(BytesIO(response.content)).convert("RGB") +init_image = init_image.resize((512, 512)) +init_image.save("black.png") + +source_prompt = "A black colored car" +prompt = "A blue colored car" + +# call the pipeline +torch.manual_seed(0) +image = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.85, + guidance_scale=3, + source_guidance_scale=1, +).images[0] + +image.save("black_to_blue.png") +``` diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/stable_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bf07127cde5b978446add7d2ab9e6bc1cf396c85 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/__init__.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass +from typing import List, Optional, Union + +import numpy as np +import PIL +from PIL import Image + +from ...utils import ( + BaseOutput, + OptionalDependencyNotAvailable, + is_flax_available, + is_k_diffusion_available, + is_k_diffusion_version, + is_onnx_available, + is_torch_available, + is_transformers_available, + is_transformers_version, +) + + +@dataclass +class StableDiffusionPipelineOutput(BaseOutput): + """ + Output class for Stable Diffusion pipelines. + + Args: + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, + num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. + nsfw_content_detected (`List[bool]`) + List of flags denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, or `None` if safety checking could not be performed. + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + nsfw_content_detected: Optional[List[bool]] + + +if is_transformers_available() and is_torch_available(): + from .pipeline_cycle_diffusion import CycleDiffusionPipeline + from .pipeline_stable_diffusion import StableDiffusionPipeline + from .pipeline_stable_diffusion_img2img import StableDiffusionImg2ImgPipeline + from .pipeline_stable_diffusion_inpaint import StableDiffusionInpaintPipeline + from .pipeline_stable_diffusion_inpaint_legacy import StableDiffusionInpaintPipelineLegacy + from .pipeline_stable_diffusion_instruct_pix2pix import StableDiffusionInstructPix2PixPipeline + from .pipeline_stable_diffusion_latent_upscale import StableDiffusionLatentUpscalePipeline + from .pipeline_stable_diffusion_upscale import StableDiffusionUpscalePipeline + from .safety_checker import StableDiffusionSafetyChecker + +try: + if not (is_transformers_available() and is_torch_available() and is_transformers_version(">=", "4.25.0")): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ...utils.dummy_torch_and_transformers_objects import StableDiffusionImageVariationPipeline +else: + from .pipeline_stable_diffusion_image_variation import StableDiffusionImageVariationPipeline + + +try: + if not (is_transformers_available() and is_torch_available() and is_transformers_version(">=", "4.26.0")): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ...utils.dummy_torch_and_transformers_objects import StableDiffusionDepth2ImgPipeline +else: + from .pipeline_stable_diffusion_depth2img import StableDiffusionDepth2ImgPipeline + + +try: + if not (is_torch_available() and is_transformers_available() and is_k_diffusion_version(">=", "0.0.12")): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ...utils.dummy_torch_and_transformers_and_k_diffusion_objects import * # noqa F403 +else: + from .pipeline_stable_diffusion_k_diffusion import StableDiffusionKDiffusionPipeline + +if is_transformers_available() and is_onnx_available(): + from .pipeline_onnx_stable_diffusion import OnnxStableDiffusionPipeline, StableDiffusionOnnxPipeline + from .pipeline_onnx_stable_diffusion_img2img import OnnxStableDiffusionImg2ImgPipeline + from .pipeline_onnx_stable_diffusion_inpaint import OnnxStableDiffusionInpaintPipeline + from .pipeline_onnx_stable_diffusion_inpaint_legacy import OnnxStableDiffusionInpaintPipelineLegacy + +if is_transformers_available() and is_flax_available(): + import flax + + @flax.struct.dataclass + class FlaxStableDiffusionPipelineOutput(BaseOutput): + """ + Output class for Stable Diffusion pipelines. + + Args: + images (`np.ndarray`) + Array of shape `(batch_size, height, width, num_channels)` with images from the diffusion pipeline. + nsfw_content_detected (`List[bool]`) + List of flags denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content. + """ + + images: np.ndarray + nsfw_content_detected: List[bool] + + from ...schedulers.scheduling_pndm_flax import PNDMSchedulerState + from .pipeline_flax_stable_diffusion import FlaxStableDiffusionPipeline + from .pipeline_flax_stable_diffusion_img2img import FlaxStableDiffusionImg2ImgPipeline + from .pipeline_flax_stable_diffusion_inpaint import FlaxStableDiffusionInpaintPipeline + from .safety_checker_flax import FlaxStableDiffusionSafetyChecker diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/convert_from_ckpt.py b/diffusers/src/diffusers/pipelines/stable_diffusion/convert_from_ckpt.py new file mode 100644 index 0000000000000000000000000000000000000000..a460ecfb77c8e262b91d357faca87b5be6e4c7dd --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/convert_from_ckpt.py @@ -0,0 +1,1021 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +""" Conversion script for the Stable Diffusion checkpoints.""" + +import os +import re +import tempfile +from typing import Optional + +import requests +import torch +from transformers import AutoFeatureExtractor, BertTokenizerFast, CLIPTextModel, CLIPTokenizer, CLIPVisionConfig + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + HeunDiscreteScheduler, + LDMTextToImagePipeline, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.pipelines.latent_diffusion.pipeline_latent_diffusion import LDMBertConfig, LDMBertModel +from diffusers.pipelines.paint_by_example import PaintByExampleImageEncoder, PaintByExamplePipeline +from diffusers.pipelines.stable_diffusion import StableDiffusionSafetyChecker + +from ...utils import is_omegaconf_available, is_safetensors_available, logging +from ...utils.import_utils import BACKENDS_MAPPING + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item.replace("in_layers.0", "norm1") + new_item = new_item.replace("in_layers.2", "conv1") + + new_item = new_item.replace("out_layers.0", "norm2") + new_item = new_item.replace("out_layers.3", "conv2") + + new_item = new_item.replace("emb_layers.1", "time_emb_proj") + new_item = new_item.replace("skip_connection", "conv_shortcut") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("nin_shortcut", "conv_shortcut") + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + # new_item = new_item.replace('norm.weight', 'group_norm.weight') + # new_item = new_item.replace('norm.bias', 'group_norm.bias') + + # new_item = new_item.replace('proj_out.weight', 'proj_attn.weight') + # new_item = new_item.replace('proj_out.bias', 'proj_attn.bias') + + # new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("norm.weight", "group_norm.weight") + new_item = new_item.replace("norm.bias", "group_norm.bias") + + new_item = new_item.replace("q.weight", "query.weight") + new_item = new_item.replace("q.bias", "query.bias") + + new_item = new_item.replace("k.weight", "key.weight") + new_item = new_item.replace("k.bias", "key.bias") + + new_item = new_item.replace("v.weight", "value.weight") + new_item = new_item.replace("v.bias", "value.bias") + + new_item = new_item.replace("proj_out.weight", "proj_attn.weight") + new_item = new_item.replace("proj_out.bias", "proj_attn.bias") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + """ + This does the final conversion step: take locally converted weights and apply a global renaming to them. It splits + attention layers, and takes into account additional replacements that may arise. + + Assigns the weights to the new checkpoint. + """ + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + # Splits the attention layers into three variables. + if attention_paths_to_split is not None: + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape) + checkpoint[path_map["key"]] = key.reshape(target_shape) + checkpoint[path_map["value"]] = value.reshape(target_shape) + + for path in paths: + new_path = path["new"] + + # These have already been assigned + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + # Global renaming happens here + new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") + new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") + new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + # proj_attn.weight has to be converted from conv 1D to linear + if "proj_attn.weight" in new_path: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] + else: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def conv_attn_to_linear(checkpoint): + keys = list(checkpoint.keys()) + attn_keys = ["query.weight", "key.weight", "value.weight"] + for key in keys: + if ".".join(key.split(".")[-2:]) in attn_keys: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0, 0] + elif "proj_attn.weight" in key: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0] + + +def create_unet_diffusers_config(original_config, image_size: int): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + unet_params = original_config.model.params.unet_config.params + vae_params = original_config.model.params.first_stage_config.params.ddconfig + + block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] + + down_block_types = [] + resolution = 1 + for i in range(len(block_out_channels)): + block_type = "CrossAttnDownBlock2D" if resolution in unet_params.attention_resolutions else "DownBlock2D" + down_block_types.append(block_type) + if i != len(block_out_channels) - 1: + resolution *= 2 + + up_block_types = [] + for i in range(len(block_out_channels)): + block_type = "CrossAttnUpBlock2D" if resolution in unet_params.attention_resolutions else "UpBlock2D" + up_block_types.append(block_type) + resolution //= 2 + + vae_scale_factor = 2 ** (len(vae_params.ch_mult) - 1) + + head_dim = unet_params.num_heads if "num_heads" in unet_params else None + use_linear_projection = ( + unet_params.use_linear_in_transformer if "use_linear_in_transformer" in unet_params else False + ) + if use_linear_projection: + # stable diffusion 2-base-512 and 2-768 + if head_dim is None: + head_dim = [5, 10, 20, 20] + + config = dict( + sample_size=image_size // vae_scale_factor, + in_channels=unet_params.in_channels, + out_channels=unet_params.out_channels, + down_block_types=tuple(down_block_types), + up_block_types=tuple(up_block_types), + block_out_channels=tuple(block_out_channels), + layers_per_block=unet_params.num_res_blocks, + cross_attention_dim=unet_params.context_dim, + attention_head_dim=head_dim, + use_linear_projection=use_linear_projection, + ) + + return config + + +def create_vae_diffusers_config(original_config, image_size: int): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + vae_params = original_config.model.params.first_stage_config.params.ddconfig + _ = original_config.model.params.first_stage_config.params.embed_dim + + block_out_channels = [vae_params.ch * mult for mult in vae_params.ch_mult] + down_block_types = ["DownEncoderBlock2D"] * len(block_out_channels) + up_block_types = ["UpDecoderBlock2D"] * len(block_out_channels) + + config = dict( + sample_size=image_size, + in_channels=vae_params.in_channels, + out_channels=vae_params.out_ch, + down_block_types=tuple(down_block_types), + up_block_types=tuple(up_block_types), + block_out_channels=tuple(block_out_channels), + latent_channels=vae_params.z_channels, + layers_per_block=vae_params.num_res_blocks, + ) + return config + + +def create_diffusers_schedular(original_config): + schedular = DDIMScheduler( + num_train_timesteps=original_config.model.params.timesteps, + beta_start=original_config.model.params.linear_start, + beta_end=original_config.model.params.linear_end, + beta_schedule="scaled_linear", + ) + return schedular + + +def create_ldm_bert_config(original_config): + bert_params = original_config.model.parms.cond_stage_config.params + config = LDMBertConfig( + d_model=bert_params.n_embed, + encoder_layers=bert_params.n_layer, + encoder_ffn_dim=bert_params.n_embed * 4, + ) + return config + + +def convert_ldm_unet_checkpoint(checkpoint, config, path=None, extract_ema=False): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + + # extract state_dict for UNet + unet_state_dict = {} + keys = list(checkpoint.keys()) + + unet_key = "model.diffusion_model." + # at least a 100 parameters have to start with `model_ema` in order for the checkpoint to be EMA + if sum(k.startswith("model_ema") for k in keys) > 100 and extract_ema: + print(f"Checkpoint {path} has both EMA and non-EMA weights.") + print( + "In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA" + " weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag." + ) + for key in keys: + if key.startswith("model.diffusion_model"): + flat_ema_key = "model_ema." + "".join(key.split(".")[1:]) + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(flat_ema_key) + else: + if sum(k.startswith("model_ema") for k in keys) > 100: + print( + "In this conversion only the non-EMA weights are extracted. If you want to instead extract the EMA" + " weights (usually better for inference), please make sure to add the `--extract_ema` flag." + ) + + for key in keys: + if key.startswith(unet_key): + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(key) + + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = unet_state_dict["time_embed.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = unet_state_dict["time_embed.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = unet_state_dict["time_embed.2.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = unet_state_dict["time_embed.2.bias"] + + new_checkpoint["conv_in.weight"] = unet_state_dict["input_blocks.0.0.weight"] + new_checkpoint["conv_in.bias"] = unet_state_dict["input_blocks.0.0.bias"] + + new_checkpoint["conv_norm_out.weight"] = unet_state_dict["out.0.weight"] + new_checkpoint["conv_norm_out.bias"] = unet_state_dict["out.0.bias"] + new_checkpoint["conv_out.weight"] = unet_state_dict["out.2.weight"] + new_checkpoint["conv_out.bias"] = unet_state_dict["out.2.bias"] + + # Retrieves the keys for the input blocks only + num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "input_blocks" in layer}) + input_blocks = { + layer_id: [key for key in unet_state_dict if f"input_blocks.{layer_id}" in key] + for layer_id in range(num_input_blocks) + } + + # Retrieves the keys for the middle blocks only + num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "middle_block" in layer}) + middle_blocks = { + layer_id: [key for key in unet_state_dict if f"middle_block.{layer_id}" in key] + for layer_id in range(num_middle_blocks) + } + + # Retrieves the keys for the output blocks only + num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "output_blocks" in layer}) + output_blocks = { + layer_id: [key for key in unet_state_dict if f"output_blocks.{layer_id}" in key] + for layer_id in range(num_output_blocks) + } + + for i in range(1, num_input_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + layer_in_block_id = (i - 1) % (config["layers_per_block"] + 1) + + resnets = [ + key for key in input_blocks[i] if f"input_blocks.{i}.0" in key and f"input_blocks.{i}.0.op" not in key + ] + attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] + + if f"input_blocks.{i}.0.op.weight" in unet_state_dict: + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.weight" + ) + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.bias" + ) + + paths = renew_resnet_paths(resnets) + meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = {"old": f"input_blocks.{i}.1", "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + resnet_0 = middle_blocks[0] + attentions = middle_blocks[1] + resnet_1 = middle_blocks[2] + + resnet_0_paths = renew_resnet_paths(resnet_0) + assign_to_checkpoint(resnet_0_paths, new_checkpoint, unet_state_dict, config=config) + + resnet_1_paths = renew_resnet_paths(resnet_1) + assign_to_checkpoint(resnet_1_paths, new_checkpoint, unet_state_dict, config=config) + + attentions_paths = renew_attention_paths(attentions) + meta_path = {"old": "middle_block.1", "new": "mid_block.attentions.0"} + assign_to_checkpoint( + attentions_paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + for i in range(num_output_blocks): + block_id = i // (config["layers_per_block"] + 1) + layer_in_block_id = i % (config["layers_per_block"] + 1) + output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] + output_block_list = {} + + for layer in output_block_layers: + layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) + if layer_id in output_block_list: + output_block_list[layer_id].append(layer_name) + else: + output_block_list[layer_id] = [layer_name] + + if len(output_block_list) > 1: + resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] + attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] + + resnet_0_paths = renew_resnet_paths(resnets) + paths = renew_resnet_paths(resnets) + + meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + output_block_list = {k: sorted(v) for k, v in output_block_list.items()} + if ["conv.bias", "conv.weight"] in output_block_list.values(): + index = list(output_block_list.values()).index(["conv.bias", "conv.weight"]) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.bias" + ] + + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"output_blocks.{i}.1", + "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", + } + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + else: + resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) + for path in resnet_0_paths: + old_path = ".".join(["output_blocks", str(i), path["old"]]) + new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) + + new_checkpoint[new_path] = unet_state_dict[old_path] + + return new_checkpoint + + +def convert_ldm_vae_checkpoint(checkpoint, config): + # extract state dict for VAE + vae_state_dict = {} + vae_key = "first_stage_model." + keys = list(checkpoint.keys()) + for key in keys: + if key.startswith(vae_key): + vae_state_dict[key.replace(vae_key, "")] = checkpoint.get(key) + + new_checkpoint = {} + + new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] + new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] + new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] + + new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] + new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] + + # Retrieves the keys for the encoder down blocks only + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) + down_blocks = { + layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + # Retrieves the keys for the decoder up blocks only + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) + up_blocks = { + layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) + } + + for i in range(num_down_blocks): + resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] + + if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.weight" + ) + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.bias" + ) + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + resnets = [ + key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key + ] + + if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.bias" + ] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + return new_checkpoint + + +def convert_ldm_bert_checkpoint(checkpoint, config): + def _copy_attn_layer(hf_attn_layer, pt_attn_layer): + hf_attn_layer.q_proj.weight.data = pt_attn_layer.to_q.weight + hf_attn_layer.k_proj.weight.data = pt_attn_layer.to_k.weight + hf_attn_layer.v_proj.weight.data = pt_attn_layer.to_v.weight + + hf_attn_layer.out_proj.weight = pt_attn_layer.to_out.weight + hf_attn_layer.out_proj.bias = pt_attn_layer.to_out.bias + + def _copy_linear(hf_linear, pt_linear): + hf_linear.weight = pt_linear.weight + hf_linear.bias = pt_linear.bias + + def _copy_layer(hf_layer, pt_layer): + # copy layer norms + _copy_linear(hf_layer.self_attn_layer_norm, pt_layer[0][0]) + _copy_linear(hf_layer.final_layer_norm, pt_layer[1][0]) + + # copy attn + _copy_attn_layer(hf_layer.self_attn, pt_layer[0][1]) + + # copy MLP + pt_mlp = pt_layer[1][1] + _copy_linear(hf_layer.fc1, pt_mlp.net[0][0]) + _copy_linear(hf_layer.fc2, pt_mlp.net[2]) + + def _copy_layers(hf_layers, pt_layers): + for i, hf_layer in enumerate(hf_layers): + if i != 0: + i += i + pt_layer = pt_layers[i : i + 2] + _copy_layer(hf_layer, pt_layer) + + hf_model = LDMBertModel(config).eval() + + # copy embeds + hf_model.model.embed_tokens.weight = checkpoint.transformer.token_emb.weight + hf_model.model.embed_positions.weight.data = checkpoint.transformer.pos_emb.emb.weight + + # copy layer norm + _copy_linear(hf_model.model.layer_norm, checkpoint.transformer.norm) + + # copy hidden layers + _copy_layers(hf_model.model.layers, checkpoint.transformer.attn_layers.layers) + + _copy_linear(hf_model.to_logits, checkpoint.transformer.to_logits) + + return hf_model + + +def convert_ldm_clip_checkpoint(checkpoint): + text_model = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14") + + keys = list(checkpoint.keys()) + + text_model_dict = {} + + for key in keys: + if key.startswith("cond_stage_model.transformer"): + text_model_dict[key[len("cond_stage_model.transformer.") :]] = checkpoint[key] + + text_model.load_state_dict(text_model_dict) + + return text_model + + +textenc_conversion_lst = [ + ("cond_stage_model.model.positional_embedding", "text_model.embeddings.position_embedding.weight"), + ("cond_stage_model.model.token_embedding.weight", "text_model.embeddings.token_embedding.weight"), + ("cond_stage_model.model.ln_final.weight", "text_model.final_layer_norm.weight"), + ("cond_stage_model.model.ln_final.bias", "text_model.final_layer_norm.bias"), +] +textenc_conversion_map = {x[0]: x[1] for x in textenc_conversion_lst} + +textenc_transformer_conversion_lst = [ + # (stable-diffusion, HF Diffusers) + ("resblocks.", "text_model.encoder.layers."), + ("ln_1", "layer_norm1"), + ("ln_2", "layer_norm2"), + (".c_fc.", ".fc1."), + (".c_proj.", ".fc2."), + (".attn", ".self_attn"), + ("ln_final.", "transformer.text_model.final_layer_norm."), + ("token_embedding.weight", "transformer.text_model.embeddings.token_embedding.weight"), + ("positional_embedding", "transformer.text_model.embeddings.position_embedding.weight"), +] +protected = {re.escape(x[0]): x[1] for x in textenc_transformer_conversion_lst} +textenc_pattern = re.compile("|".join(protected.keys())) + + +def convert_paint_by_example_checkpoint(checkpoint): + config = CLIPVisionConfig.from_pretrained("openai/clip-vit-large-patch14") + model = PaintByExampleImageEncoder(config) + + keys = list(checkpoint.keys()) + + text_model_dict = {} + + for key in keys: + if key.startswith("cond_stage_model.transformer"): + text_model_dict[key[len("cond_stage_model.transformer.") :]] = checkpoint[key] + + # load clip vision + model.model.load_state_dict(text_model_dict) + + # load mapper + keys_mapper = { + k[len("cond_stage_model.mapper.res") :]: v + for k, v in checkpoint.items() + if k.startswith("cond_stage_model.mapper") + } + + MAPPING = { + "attn.c_qkv": ["attn1.to_q", "attn1.to_k", "attn1.to_v"], + "attn.c_proj": ["attn1.to_out.0"], + "ln_1": ["norm1"], + "ln_2": ["norm3"], + "mlp.c_fc": ["ff.net.0.proj"], + "mlp.c_proj": ["ff.net.2"], + } + + mapped_weights = {} + for key, value in keys_mapper.items(): + prefix = key[: len("blocks.i")] + suffix = key.split(prefix)[-1].split(".")[-1] + name = key.split(prefix)[-1].split(suffix)[0][1:-1] + mapped_names = MAPPING[name] + + num_splits = len(mapped_names) + for i, mapped_name in enumerate(mapped_names): + new_name = ".".join([prefix, mapped_name, suffix]) + shape = value.shape[0] // num_splits + mapped_weights[new_name] = value[i * shape : (i + 1) * shape] + + model.mapper.load_state_dict(mapped_weights) + + # load final layer norm + model.final_layer_norm.load_state_dict( + { + "bias": checkpoint["cond_stage_model.final_ln.bias"], + "weight": checkpoint["cond_stage_model.final_ln.weight"], + } + ) + + # load final proj + model.proj_out.load_state_dict( + { + "bias": checkpoint["proj_out.bias"], + "weight": checkpoint["proj_out.weight"], + } + ) + + # load uncond vector + model.uncond_vector.data = torch.nn.Parameter(checkpoint["learnable_vector"]) + return model + + +def convert_open_clip_checkpoint(checkpoint): + text_model = CLIPTextModel.from_pretrained("stabilityai/stable-diffusion-2", subfolder="text_encoder") + + keys = list(checkpoint.keys()) + + text_model_dict = {} + + d_model = int(checkpoint["cond_stage_model.model.text_projection"].shape[0]) + + text_model_dict["text_model.embeddings.position_ids"] = text_model.text_model.embeddings.get_buffer("position_ids") + + for key in keys: + if "resblocks.23" in key: # Diffusers drops the final layer and only uses the penultimate layer + continue + if key in textenc_conversion_map: + text_model_dict[textenc_conversion_map[key]] = checkpoint[key] + if key.startswith("cond_stage_model.model.transformer."): + new_key = key[len("cond_stage_model.model.transformer.") :] + if new_key.endswith(".in_proj_weight"): + new_key = new_key[: -len(".in_proj_weight")] + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + text_model_dict[new_key + ".q_proj.weight"] = checkpoint[key][:d_model, :] + text_model_dict[new_key + ".k_proj.weight"] = checkpoint[key][d_model : d_model * 2, :] + text_model_dict[new_key + ".v_proj.weight"] = checkpoint[key][d_model * 2 :, :] + elif new_key.endswith(".in_proj_bias"): + new_key = new_key[: -len(".in_proj_bias")] + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + text_model_dict[new_key + ".q_proj.bias"] = checkpoint[key][:d_model] + text_model_dict[new_key + ".k_proj.bias"] = checkpoint[key][d_model : d_model * 2] + text_model_dict[new_key + ".v_proj.bias"] = checkpoint[key][d_model * 2 :] + else: + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + + text_model_dict[new_key] = checkpoint[key] + + text_model.load_state_dict(text_model_dict) + + return text_model + + +def load_pipeline_from_original_stable_diffusion_ckpt( + checkpoint_path: str, + original_config_file: str = None, + image_size: int = 512, + prediction_type: str = None, + model_type: str = None, + extract_ema: bool = False, + scheduler_type: str = "pndm", + num_in_channels: Optional[int] = None, + upcast_attention: Optional[bool] = None, + device: str = None, + from_safetensors: bool = False, +) -> StableDiffusionPipeline: + """ + Load a Stable Diffusion pipeline object from a CompVis-style `.ckpt`/`.safetensors` file and (ideally) a `.yaml` + config file. + + Although many of the arguments can be automatically inferred, some of these rely on brittle checks against the + global step count, which will likely fail for models that have undergone further fine-tuning. Therefore, it is + recommended that you override the default values and/or supply an `original_config_file` wherever possible. + + Args: + checkpoint_path (`str`): Path to `.ckpt` file. + original_config_file (`str`): + Path to `.yaml` config file corresponding to the original architecture. If `None`, will be automatically + inferred by looking for a key that only exists in SD2.0 models. + image_size (`int`, *optional*, defaults to 512): + The image size that the model was trained on. Use 512 for Stable Diffusion v1.X and Stable Diffusion v2 + Base. Use 768 for Stable Diffusion v2. + prediction_type (`str`, *optional*): + The prediction type that the model was trained on. Use `'epsilon'` for Stable Diffusion v1.X and Stable + Diffusion v2 Base. Use `'v_prediction'` for Stable Diffusion v2. + num_in_channels (`int`, *optional*, defaults to None): + The number of input channels. If `None`, it will be automatically inferred. + scheduler_type (`str`, *optional*, defaults to 'pndm'): + Type of scheduler to use. Should be one of `["pndm", "lms", "heun", "euler", "euler-ancestral", "dpm", + "ddim"]`. + model_type (`str`, *optional*, defaults to `None`): + The pipeline type. `None` to automatically infer, or one of `["FrozenOpenCLIPEmbedder", + "FrozenCLIPEmbedder", "PaintByExample"]`. + extract_ema (`bool`, *optional*, defaults to `False`): Only relevant for + checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights or not. Defaults to + `False`. Pass `True` to extract the EMA weights. EMA weights usually yield higher quality images for + inference. Non-EMA weights are usually better to continue fine-tuning. + upcast_attention (`bool`, *optional*, defaults to `None`): + Whether the attention computation should always be upcasted. This is necessary when running stable + diffusion 2.1. + device (`str`, *optional*, defaults to `None`): + The device to use. Pass `None` to determine automatically. :param from_safetensors: If `checkpoint_path` is + in `safetensors` format, load checkpoint with safetensors instead of PyTorch. :return: A + StableDiffusionPipeline object representing the passed-in `.ckpt`/`.safetensors` file. + """ + if prediction_type == "v-prediction": + prediction_type = "v_prediction" + + if not is_omegaconf_available(): + raise ValueError(BACKENDS_MAPPING["omegaconf"][1]) + + from omegaconf import OmegaConf + + if from_safetensors: + if not is_safetensors_available(): + raise ValueError(BACKENDS_MAPPING["safetensors"][1]) + + from safetensors import safe_open + + checkpoint = {} + with safe_open(checkpoint_path, framework="pt", device="cpu") as f: + for key in f.keys(): + checkpoint[key] = f.get_tensor(key) + else: + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + checkpoint = torch.load(checkpoint_path, map_location=device) + else: + checkpoint = torch.load(checkpoint_path, map_location=device) + + # Sometimes models don't have the global_step item + if "global_step" in checkpoint: + global_step = checkpoint["global_step"] + else: + print("global_step key not found in model") + global_step = None + + if "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + with tempfile.TemporaryDirectory() as tmpdir: + if original_config_file is None: + key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" + + original_config_file = os.path.join(tmpdir, "inference.yaml") + if key_name in checkpoint and checkpoint[key_name].shape[-1] == 1024: + if not os.path.isfile("v2-inference-v.yaml"): + # model_type = "v2" + r = requests.get( + " https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/configs/stable-diffusion/v2-inference-v.yaml" + ) + open(original_config_file, "wb").write(r.content) + + if global_step == 110000: + # v2.1 needs to upcast attention + upcast_attention = True + else: + if not os.path.isfile("v1-inference.yaml"): + # model_type = "v1" + r = requests.get( + " https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml" + ) + open(original_config_file, "wb").write(r.content) + + original_config = OmegaConf.load(original_config_file) + + if num_in_channels is not None: + original_config["model"]["params"]["unet_config"]["params"]["in_channels"] = num_in_channels + + if ( + "parameterization" in original_config["model"]["params"] + and original_config["model"]["params"]["parameterization"] == "v" + ): + if prediction_type is None: + # NOTE: For stable diffusion 2 base it is recommended to pass `prediction_type=="epsilon"` + # as it relies on a brittle global step parameter here + prediction_type = "epsilon" if global_step == 875000 else "v_prediction" + if image_size is None: + # NOTE: For stable diffusion 2 base one has to pass `image_size==512` + # as it relies on a brittle global step parameter here + image_size = 512 if global_step == 875000 else 768 + else: + if prediction_type is None: + prediction_type = "epsilon" + if image_size is None: + image_size = 512 + + num_train_timesteps = original_config.model.params.timesteps + beta_start = original_config.model.params.linear_start + beta_end = original_config.model.params.linear_end + + scheduler = DDIMScheduler( + beta_end=beta_end, + beta_schedule="scaled_linear", + beta_start=beta_start, + num_train_timesteps=num_train_timesteps, + steps_offset=1, + clip_sample=False, + set_alpha_to_one=False, + prediction_type=prediction_type, + ) + # make sure scheduler works correctly with DDIM + scheduler.register_to_config(clip_sample=False) + + if scheduler_type == "pndm": + config = dict(scheduler.config) + config["skip_prk_steps"] = True + scheduler = PNDMScheduler.from_config(config) + elif scheduler_type == "lms": + scheduler = LMSDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "heun": + scheduler = HeunDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "euler": + scheduler = EulerDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "euler-ancestral": + scheduler = EulerAncestralDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "dpm": + scheduler = DPMSolverMultistepScheduler.from_config(scheduler.config) + elif scheduler_type == "ddim": + scheduler = scheduler + else: + raise ValueError(f"Scheduler of type {scheduler_type} doesn't exist!") + + # Convert the UNet2DConditionModel model. + unet_config = create_unet_diffusers_config(original_config, image_size=image_size) + unet_config["upcast_attention"] = upcast_attention + unet = UNet2DConditionModel(**unet_config) + + converted_unet_checkpoint = convert_ldm_unet_checkpoint( + checkpoint, unet_config, path=checkpoint_path, extract_ema=extract_ema + ) + + unet.load_state_dict(converted_unet_checkpoint) + + # Convert the VAE model. + vae_config = create_vae_diffusers_config(original_config, image_size=image_size) + converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) + + vae = AutoencoderKL(**vae_config) + vae.load_state_dict(converted_vae_checkpoint) + + # Convert the text model. + if model_type is None: + model_type = original_config.model.params.cond_stage_config.target.split(".")[-1] + logger.debug(f"no `model_type` given, `model_type` inferred as: {model_type}") + + if model_type == "FrozenOpenCLIPEmbedder": + text_model = convert_open_clip_checkpoint(checkpoint) + tokenizer = CLIPTokenizer.from_pretrained("stabilityai/stable-diffusion-2", subfolder="tokenizer") + pipe = StableDiffusionPipeline( + vae=vae, + text_encoder=text_model, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + elif model_type == "PaintByExample": + vision_model = convert_paint_by_example_checkpoint(checkpoint) + tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") + feature_extractor = AutoFeatureExtractor.from_pretrained("CompVis/stable-diffusion-safety-checker") + pipe = PaintByExamplePipeline( + vae=vae, + image_encoder=vision_model, + unet=unet, + scheduler=scheduler, + safety_checker=None, + feature_extractor=feature_extractor, + ) + elif model_type == "FrozenCLIPEmbedder": + text_model = convert_ldm_clip_checkpoint(checkpoint) + tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") + safety_checker = StableDiffusionSafetyChecker.from_pretrained("CompVis/stable-diffusion-safety-checker") + feature_extractor = AutoFeatureExtractor.from_pretrained("CompVis/stable-diffusion-safety-checker") + pipe = StableDiffusionPipeline( + vae=vae, + text_encoder=text_model, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + else: + text_config = create_ldm_bert_config(original_config) + text_model = convert_ldm_bert_checkpoint(checkpoint, text_config) + tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased") + pipe = LDMTextToImagePipeline(vqvae=vae, bert=text_model, tokenizer=tokenizer, unet=unet, scheduler=scheduler) + + return pipe diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_cycle_diffusion.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_cycle_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..703bfc537341131a5d9b267dafe13598aa5899ca --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_cycle_diffusion.py @@ -0,0 +1,746 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from diffusers.utils import is_accelerate_available + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import DDIMScheduler +from ...utils import PIL_INTERPOLATION, deprecate, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +def posterior_sample(scheduler, latents, timestep, clean_latents, generator, eta): + # 1. get previous step value (=t-1) + prev_timestep = timestep - scheduler.config.num_train_timesteps // scheduler.num_inference_steps + + if prev_timestep <= 0: + return clean_latents + + # 2. compute alphas, betas + alpha_prod_t = scheduler.alphas_cumprod[timestep] + alpha_prod_t_prev = ( + scheduler.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else scheduler.final_alpha_cumprod + ) + + variance = scheduler._get_variance(timestep, prev_timestep) + std_dev_t = eta * variance ** (0.5) + + # direction pointing to x_t + e_t = (latents - alpha_prod_t ** (0.5) * clean_latents) / (1 - alpha_prod_t) ** (0.5) + dir_xt = (1.0 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * e_t + noise = std_dev_t * randn_tensor( + clean_latents.shape, dtype=clean_latents.dtype, device=clean_latents.device, generator=generator + ) + prev_latents = alpha_prod_t_prev ** (0.5) * clean_latents + dir_xt + noise + + return prev_latents + + +def compute_noise(scheduler, prev_latents, latents, timestep, noise_pred, eta): + # 1. get previous step value (=t-1) + prev_timestep = timestep - scheduler.config.num_train_timesteps // scheduler.num_inference_steps + + # 2. compute alphas, betas + alpha_prod_t = scheduler.alphas_cumprod[timestep] + alpha_prod_t_prev = ( + scheduler.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else scheduler.final_alpha_cumprod + ) + + beta_prod_t = 1 - alpha_prod_t + + # 3. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_original_sample = (latents - beta_prod_t ** (0.5) * noise_pred) / alpha_prod_t ** (0.5) + + # 4. Clip "predicted x_0" + if scheduler.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -1, 1) + + # 5. compute variance: "sigma_t(η)" -> see formula (16) + # σ_t = sqrt((1 − α_t−1)/(1 − α_t)) * sqrt(1 − α_t/α_t−1) + variance = scheduler._get_variance(timestep, prev_timestep) + std_dev_t = eta * variance ** (0.5) + + # 6. compute "direction pointing to x_t" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * noise_pred + + noise = (prev_latents - (alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction)) / ( + variance ** (0.5) * eta + ) + return noise + + +class CycleDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image to image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/CompVis/stable-diffusion-v1-4) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: DDIMScheduler, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.check_inputs + def check_inputs( + self, prompt, strength, callback_steps, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None + ): + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.get_timesteps + def get_timesteps(self, num_inference_steps, strength, device): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + timesteps = self.scheduler.timesteps[t_start:] + + return timesteps, num_inference_steps - t_start + + def prepare_latents(self, image, timestep, batch_size, num_images_per_prompt, dtype, device, generator=None): + image = image.to(device=device, dtype=dtype) + + batch_size = image.shape[0] + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if isinstance(generator, list): + init_latents = [ + self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size) + ] + init_latents = torch.cat(init_latents, dim=0) + else: + init_latents = self.vae.encode(image).latent_dist.sample(generator) + + init_latents = self.vae.config.scaling_factor * init_latents + + if batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] == 0: + # expand init_latents for batch_size + deprecation_message = ( + f"You have passed {batch_size} text prompts (`prompt`), but only {init_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = batch_size // init_latents.shape[0] + init_latents = torch.cat([init_latents] * additional_image_per_prompt * num_images_per_prompt, dim=0) + elif batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts." + ) + else: + init_latents = torch.cat([init_latents] * num_images_per_prompt, dim=0) + + # add noise to latents using the timestep + shape = init_latents.shape + noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + + # get latents + clean_latents = init_latents + init_latents = self.scheduler.add_noise(init_latents, noise, timestep) + latents = init_latents + + return latents, clean_latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + source_prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + source_guidance_scale: Optional[float] = 1, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + source_guidance_scale (`float`, *optional*, defaults to 1): + Guidance scale for the source prompt. This is useful to control the amount of influence the source + prompt for encoding. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.1): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 1. Check inputs + self.check_inputs(prompt, strength, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + prompt_embeds=prompt_embeds, + ) + source_prompt_embeds = self._encode_prompt( + source_prompt, device, num_images_per_prompt, do_classifier_free_guidance, None + ) + + # 4. Preprocess image + image = preprocess(image) + + # 5. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + latents, clean_latents = self.prepare_latents( + image, latent_timestep, batch_size, num_images_per_prompt, prompt_embeds.dtype, device, generator + ) + source_latents = latents + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + generator = extra_step_kwargs.pop("generator", None) + + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) + source_latent_model_input = torch.cat([source_latents] * 2) + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + source_latent_model_input = self.scheduler.scale_model_input(source_latent_model_input, t) + + # predict the noise residual + concat_latent_model_input = torch.stack( + [ + source_latent_model_input[0], + latent_model_input[0], + source_latent_model_input[1], + latent_model_input[1], + ], + dim=0, + ) + concat_prompt_embeds = torch.stack( + [ + source_prompt_embeds[0], + prompt_embeds[0], + source_prompt_embeds[1], + prompt_embeds[1], + ], + dim=0, + ) + concat_noise_pred = self.unet( + concat_latent_model_input, t, encoder_hidden_states=concat_prompt_embeds + ).sample + + # perform guidance + ( + source_noise_pred_uncond, + noise_pred_uncond, + source_noise_pred_text, + noise_pred_text, + ) = concat_noise_pred.chunk(4, dim=0) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + source_noise_pred = source_noise_pred_uncond + source_guidance_scale * ( + source_noise_pred_text - source_noise_pred_uncond + ) + + # Sample source_latents from the posterior distribution. + prev_source_latents = posterior_sample( + self.scheduler, source_latents, t, clean_latents, generator=generator, **extra_step_kwargs + ) + # Compute noise. + noise = compute_noise( + self.scheduler, prev_source_latents, source_latents, t, source_noise_pred, **extra_step_kwargs + ) + source_latents = prev_source_latents + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step( + noise_pred, t, latents, variance_noise=noise, **extra_step_kwargs + ).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..0aed9966a97e7f08c8f4e81794aaa14ea5aa46a4 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion.py @@ -0,0 +1,431 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import warnings +from functools import partial +from typing import Dict, List, Optional, Union + +import jax +import jax.numpy as jnp +import numpy as np +from flax.core.frozen_dict import FrozenDict +from flax.jax_utils import unreplicate +from flax.training.common_utils import shard +from packaging import version +from PIL import Image +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel + +from ...models import FlaxAutoencoderKL, FlaxUNet2DConditionModel +from ...schedulers import ( + FlaxDDIMScheduler, + FlaxDPMSolverMultistepScheduler, + FlaxLMSDiscreteScheduler, + FlaxPNDMScheduler, +) +from ...utils import deprecate, logging +from ..pipeline_flax_utils import FlaxDiffusionPipeline +from . import FlaxStableDiffusionPipelineOutput +from .safety_checker_flax import FlaxStableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +# Set to True to use python for loop instead of jax.fori_loop for easier debugging +DEBUG = False + + +class FlaxStableDiffusionPipeline(FlaxDiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`FlaxDiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`FlaxAutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`FlaxCLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.FlaxCLIPTextModel), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`FlaxUNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`FlaxDDIMScheduler`], [`FlaxLMSDiscreteScheduler`], [`FlaxPNDMScheduler`], or + [`FlaxDPMSolverMultistepScheduler`]. + safety_checker ([`FlaxStableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: FlaxAutoencoderKL, + text_encoder: FlaxCLIPTextModel, + tokenizer: CLIPTokenizer, + unet: FlaxUNet2DConditionModel, + scheduler: Union[ + FlaxDDIMScheduler, FlaxPNDMScheduler, FlaxLMSDiscreteScheduler, FlaxDPMSolverMultistepScheduler + ], + safety_checker: FlaxStableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + dtype: jnp.dtype = jnp.float32, + ): + super().__init__() + self.dtype = dtype + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + def prepare_inputs(self, prompt: Union[str, List[str]]): + if not isinstance(prompt, (str, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + return text_input.input_ids + + def _get_has_nsfw_concepts(self, features, params): + has_nsfw_concepts = self.safety_checker(features, params) + return has_nsfw_concepts + + def _run_safety_checker(self, images, safety_model_params, jit=False): + # safety_model_params should already be replicated when jit is True + pil_images = [Image.fromarray(image) for image in images] + features = self.feature_extractor(pil_images, return_tensors="np").pixel_values + + if jit: + features = shard(features) + has_nsfw_concepts = _p_get_has_nsfw_concepts(self, features, safety_model_params) + has_nsfw_concepts = unshard(has_nsfw_concepts) + safety_model_params = unreplicate(safety_model_params) + else: + has_nsfw_concepts = self._get_has_nsfw_concepts(features, safety_model_params) + + images_was_copied = False + for idx, has_nsfw_concept in enumerate(has_nsfw_concepts): + if has_nsfw_concept: + if not images_was_copied: + images_was_copied = True + images = images.copy() + + images[idx] = np.zeros(images[idx].shape, dtype=np.uint8) # black image + + if any(has_nsfw_concepts): + warnings.warn( + "Potential NSFW content was detected in one or more images. A black image will be returned" + " instead. Try again with a different prompt and/or seed." + ) + + return images, has_nsfw_concepts + + def _generate( + self, + prompt_ids: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + num_inference_steps: int, + height: int, + width: int, + guidance_scale: float, + latents: Optional[jnp.array] = None, + neg_prompt_ids: Optional[jnp.array] = None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # get prompt text embeddings + prompt_embeds = self.text_encoder(prompt_ids, params=params["text_encoder"])[0] + + # TODO: currently it is assumed `do_classifier_free_guidance = guidance_scale > 1.0` + # implement this conditional `do_classifier_free_guidance = guidance_scale > 1.0` + batch_size = prompt_ids.shape[0] + + max_length = prompt_ids.shape[-1] + + if neg_prompt_ids is None: + uncond_input = self.tokenizer( + [""] * batch_size, padding="max_length", max_length=max_length, return_tensors="np" + ).input_ids + else: + uncond_input = neg_prompt_ids + negative_prompt_embeds = self.text_encoder(uncond_input, params=params["text_encoder"])[0] + context = jnp.concatenate([negative_prompt_embeds, prompt_embeds]) + + latents_shape = ( + batch_size, + self.unet.in_channels, + height // self.vae_scale_factor, + width // self.vae_scale_factor, + ) + if latents is None: + latents = jax.random.normal(prng_seed, shape=latents_shape, dtype=jnp.float32) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + + def loop_body(step, args): + latents, scheduler_state = args + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + latents_input = jnp.concatenate([latents] * 2) + + t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step] + timestep = jnp.broadcast_to(t, latents_input.shape[0]) + + latents_input = self.scheduler.scale_model_input(scheduler_state, latents_input, t) + + # predict the noise residual + noise_pred = self.unet.apply( + {"params": params["unet"]}, + jnp.array(latents_input), + jnp.array(timestep, dtype=jnp.int32), + encoder_hidden_states=context, + ).sample + # perform guidance + noise_pred_uncond, noise_prediction_text = jnp.split(noise_pred, 2, axis=0) + noise_pred = noise_pred_uncond + guidance_scale * (noise_prediction_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents, scheduler_state = self.scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple() + return latents, scheduler_state + + scheduler_state = self.scheduler.set_timesteps( + params["scheduler"], num_inference_steps=num_inference_steps, shape=latents.shape + ) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * params["scheduler"].init_noise_sigma + + if DEBUG: + # run with python for loop + for i in range(num_inference_steps): + latents, scheduler_state = loop_body(i, (latents, scheduler_state)) + else: + latents, _ = jax.lax.fori_loop(0, num_inference_steps, loop_body, (latents, scheduler_state)) + + # scale and decode the image latents with vae + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.apply({"params": params["vae"]}, latents, method=self.vae.decode).sample + + image = (image / 2 + 0.5).clip(0, 1).transpose(0, 2, 3, 1) + return image + + def __call__( + self, + prompt_ids: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + num_inference_steps: int = 50, + height: Optional[int] = None, + width: Optional[int] = None, + guidance_scale: Union[float, jnp.array] = 7.5, + latents: jnp.array = None, + neg_prompt_ids: jnp.array = None, + return_dict: bool = True, + jit: bool = False, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + latents (`jnp.array`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. tensor will ge generated + by sampling using the supplied random `generator`. + jit (`bool`, defaults to `False`): + Whether to run `pmap` versions of the generation and safety scoring functions. NOTE: This argument + exists because `__call__` is not yet end-to-end pmap-able. It will be removed in a future release. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] instead of + a plain tuple. + + Returns: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a + `tuple. When returning a tuple, the first element is a list with the generated images, and the second + element is a list of `bool`s denoting whether the corresponding generated image likely represents + "not-safe-for-work" (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + if isinstance(guidance_scale, float): + # Convert to a tensor so each device gets a copy. Follow the prompt_ids for + # shape information, as they may be sharded (when `jit` is `True`), or not. + guidance_scale = jnp.array([guidance_scale] * prompt_ids.shape[0]) + if len(prompt_ids.shape) > 2: + # Assume sharded + guidance_scale = guidance_scale[:, None] + + if jit: + images = _p_generate( + self, + prompt_ids, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + else: + images = self._generate( + prompt_ids, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + + if self.safety_checker is not None: + safety_params = params["safety_checker"] + images_uint8_casted = (images * 255).round().astype("uint8") + num_devices, batch_size = images.shape[:2] + + images_uint8_casted = np.asarray(images_uint8_casted).reshape(num_devices * batch_size, height, width, 3) + images_uint8_casted, has_nsfw_concept = self._run_safety_checker(images_uint8_casted, safety_params, jit) + images = np.asarray(images) + + # block images + if any(has_nsfw_concept): + for i, is_nsfw in enumerate(has_nsfw_concept): + if is_nsfw: + images[i] = np.asarray(images_uint8_casted[i]) + + images = images.reshape(num_devices, batch_size, height, width, 3) + else: + images = np.asarray(images) + has_nsfw_concept = False + + if not return_dict: + return (images, has_nsfw_concept) + + return FlaxStableDiffusionPipelineOutput(images=images, nsfw_content_detected=has_nsfw_concept) + + +# Static argnums are pipe, num_inference_steps, height, width. A change would trigger recompilation. +# Non-static args are (sharded) input tensors mapped over their first dimension (hence, `0`). +@partial( + jax.pmap, + in_axes=(None, 0, 0, 0, None, None, None, 0, 0, 0), + static_broadcasted_argnums=(0, 4, 5, 6), +) +def _p_generate( + pipe, + prompt_ids, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, +): + return pipe._generate( + prompt_ids, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + + +@partial(jax.pmap, static_broadcasted_argnums=(0,)) +def _p_get_has_nsfw_concepts(pipe, features, params): + return pipe._get_has_nsfw_concepts(features, params) + + +def unshard(x: jnp.ndarray): + # einops.rearrange(x, 'd b ... -> (d b) ...') + num_devices, batch_size = x.shape[:2] + rest = x.shape[2:] + return x.reshape(num_devices * batch_size, *rest) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_img2img.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..4144cb5110676af94c5188679a4af29f2aaa1e21 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_img2img.py @@ -0,0 +1,466 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import warnings +from functools import partial +from typing import Dict, List, Optional, Union + +import jax +import jax.numpy as jnp +import numpy as np +from flax.core.frozen_dict import FrozenDict +from flax.jax_utils import unreplicate +from flax.training.common_utils import shard +from PIL import Image +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel + +from ...models import FlaxAutoencoderKL, FlaxUNet2DConditionModel +from ...schedulers import ( + FlaxDDIMScheduler, + FlaxDPMSolverMultistepScheduler, + FlaxLMSDiscreteScheduler, + FlaxPNDMScheduler, +) +from ...utils import PIL_INTERPOLATION, logging +from ..pipeline_flax_utils import FlaxDiffusionPipeline +from . import FlaxStableDiffusionPipelineOutput +from .safety_checker_flax import FlaxStableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +# Set to True to use python for loop instead of jax.fori_loop for easier debugging +DEBUG = False + + +class FlaxStableDiffusionImg2ImgPipeline(FlaxDiffusionPipeline): + r""" + Pipeline for image-to-image generation using Stable Diffusion. + + This model inherits from [`FlaxDiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`FlaxAutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`FlaxCLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.FlaxCLIPTextModel), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`FlaxUNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`FlaxDDIMScheduler`], [`FlaxLMSDiscreteScheduler`], [`FlaxPNDMScheduler`], or + [`FlaxDPMSolverMultistepScheduler`]. + safety_checker ([`FlaxStableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: FlaxAutoencoderKL, + text_encoder: FlaxCLIPTextModel, + tokenizer: CLIPTokenizer, + unet: FlaxUNet2DConditionModel, + scheduler: Union[ + FlaxDDIMScheduler, FlaxPNDMScheduler, FlaxLMSDiscreteScheduler, FlaxDPMSolverMultistepScheduler + ], + safety_checker: FlaxStableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + dtype: jnp.dtype = jnp.float32, + ): + super().__init__() + self.dtype = dtype + + if safety_checker is None: + logger.warn( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + def prepare_inputs(self, prompt: Union[str, List[str]], image: Union[Image.Image, List[Image.Image]]): + if not isinstance(prompt, (str, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if not isinstance(image, (Image.Image, list)): + raise ValueError(f"image has to be of type `PIL.Image.Image` or list but is {type(image)}") + + if isinstance(image, Image.Image): + image = [image] + + processed_images = jnp.concatenate([preprocess(img, jnp.float32) for img in image]) + + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + return text_input.input_ids, processed_images + + def _get_has_nsfw_concepts(self, features, params): + has_nsfw_concepts = self.safety_checker(features, params) + return has_nsfw_concepts + + def _run_safety_checker(self, images, safety_model_params, jit=False): + # safety_model_params should already be replicated when jit is True + pil_images = [Image.fromarray(image) for image in images] + features = self.feature_extractor(pil_images, return_tensors="np").pixel_values + + if jit: + features = shard(features) + has_nsfw_concepts = _p_get_has_nsfw_concepts(self, features, safety_model_params) + has_nsfw_concepts = unshard(has_nsfw_concepts) + safety_model_params = unreplicate(safety_model_params) + else: + has_nsfw_concepts = self._get_has_nsfw_concepts(features, safety_model_params) + + images_was_copied = False + for idx, has_nsfw_concept in enumerate(has_nsfw_concepts): + if has_nsfw_concept: + if not images_was_copied: + images_was_copied = True + images = images.copy() + + images[idx] = np.zeros(images[idx].shape, dtype=np.uint8) # black image + + if any(has_nsfw_concepts): + warnings.warn( + "Potential NSFW content was detected in one or more images. A black image will be returned" + " instead. Try again with a different prompt and/or seed." + ) + + return images, has_nsfw_concepts + + def get_timestep_start(self, num_inference_steps, strength): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + + return t_start + + def _generate( + self, + prompt_ids: jnp.array, + image: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + start_timestep: int, + num_inference_steps: int, + height: int, + width: int, + guidance_scale: float, + noise: Optional[jnp.array] = None, + neg_prompt_ids: Optional[jnp.array] = None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # get prompt text embeddings + prompt_embeds = self.text_encoder(prompt_ids, params=params["text_encoder"])[0] + + # TODO: currently it is assumed `do_classifier_free_guidance = guidance_scale > 1.0` + # implement this conditional `do_classifier_free_guidance = guidance_scale > 1.0` + batch_size = prompt_ids.shape[0] + + max_length = prompt_ids.shape[-1] + + if neg_prompt_ids is None: + uncond_input = self.tokenizer( + [""] * batch_size, padding="max_length", max_length=max_length, return_tensors="np" + ).input_ids + else: + uncond_input = neg_prompt_ids + negative_prompt_embeds = self.text_encoder(uncond_input, params=params["text_encoder"])[0] + context = jnp.concatenate([negative_prompt_embeds, prompt_embeds]) + + latents_shape = ( + batch_size, + self.unet.in_channels, + height // self.vae_scale_factor, + width // self.vae_scale_factor, + ) + if noise is None: + noise = jax.random.normal(prng_seed, shape=latents_shape, dtype=jnp.float32) + else: + if noise.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {noise.shape}, expected {latents_shape}") + + # Create init_latents + init_latent_dist = self.vae.apply({"params": params["vae"]}, image, method=self.vae.encode).latent_dist + init_latents = init_latent_dist.sample(key=prng_seed).transpose((0, 3, 1, 2)) + init_latents = self.vae.config.scaling_factor * init_latents + + def loop_body(step, args): + latents, scheduler_state = args + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + latents_input = jnp.concatenate([latents] * 2) + + t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step] + timestep = jnp.broadcast_to(t, latents_input.shape[0]) + + latents_input = self.scheduler.scale_model_input(scheduler_state, latents_input, t) + + # predict the noise residual + noise_pred = self.unet.apply( + {"params": params["unet"]}, + jnp.array(latents_input), + jnp.array(timestep, dtype=jnp.int32), + encoder_hidden_states=context, + ).sample + # perform guidance + noise_pred_uncond, noise_prediction_text = jnp.split(noise_pred, 2, axis=0) + noise_pred = noise_pred_uncond + guidance_scale * (noise_prediction_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents, scheduler_state = self.scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple() + return latents, scheduler_state + + scheduler_state = self.scheduler.set_timesteps( + params["scheduler"], num_inference_steps=num_inference_steps, shape=latents_shape + ) + + latent_timestep = scheduler_state.timesteps[start_timestep : start_timestep + 1].repeat(batch_size) + + latents = self.scheduler.add_noise(params["scheduler"], init_latents, noise, latent_timestep) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * params["scheduler"].init_noise_sigma + + if DEBUG: + # run with python for loop + for i in range(start_timestep, num_inference_steps): + latents, scheduler_state = loop_body(i, (latents, scheduler_state)) + else: + latents, _ = jax.lax.fori_loop(start_timestep, num_inference_steps, loop_body, (latents, scheduler_state)) + + # scale and decode the image latents with vae + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.apply({"params": params["vae"]}, latents, method=self.vae.decode).sample + + image = (image / 2 + 0.5).clip(0, 1).transpose(0, 2, 3, 1) + return image + + def __call__( + self, + prompt_ids: jnp.array, + image: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + strength: float = 0.8, + num_inference_steps: int = 50, + height: Optional[int] = None, + width: Optional[int] = None, + guidance_scale: Union[float, jnp.array] = 7.5, + noise: jnp.array = None, + neg_prompt_ids: jnp.array = None, + return_dict: bool = True, + jit: bool = False, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt_ids (`jnp.array`): + The prompt or prompts to guide the image generation. + image (`jnp.array`): + Array representing an image batch, that will be used as the starting point for the process. + params (`Dict` or `FrozenDict`): Dictionary containing the model parameters/weights + prng_seed (`jax.random.KeyArray` or `jax.Array`): Array containing random number generator key + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + noise (`jnp.array`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. tensor will ge generated + by sampling using the supplied random `generator`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] instead of + a plain tuple. + jit (`bool`, defaults to `False`): + Whether to run `pmap` versions of the generation and safety scoring functions. NOTE: This argument + exists because `__call__` is not yet end-to-end pmap-able. It will be removed in a future release. + + Returns: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a + `tuple. When returning a tuple, the first element is a list with the generated images, and the second + element is a list of `bool`s denoting whether the corresponding generated image likely represents + "not-safe-for-work" (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + if isinstance(guidance_scale, float): + # Convert to a tensor so each device gets a copy. Follow the prompt_ids for + # shape information, as they may be sharded (when `jit` is `True`), or not. + guidance_scale = jnp.array([guidance_scale] * prompt_ids.shape[0]) + if len(prompt_ids.shape) > 2: + # Assume sharded + guidance_scale = guidance_scale[:, None] + + start_timestep = self.get_timestep_start(num_inference_steps, strength) + + if jit: + images = _p_generate( + self, + prompt_ids, + image, + params, + prng_seed, + start_timestep, + num_inference_steps, + height, + width, + guidance_scale, + noise, + neg_prompt_ids, + ) + else: + images = self._generate( + prompt_ids, + image, + params, + prng_seed, + start_timestep, + num_inference_steps, + height, + width, + guidance_scale, + noise, + neg_prompt_ids, + ) + + if self.safety_checker is not None: + safety_params = params["safety_checker"] + images_uint8_casted = (images * 255).round().astype("uint8") + num_devices, batch_size = images.shape[:2] + + images_uint8_casted = np.asarray(images_uint8_casted).reshape(num_devices * batch_size, height, width, 3) + images_uint8_casted, has_nsfw_concept = self._run_safety_checker(images_uint8_casted, safety_params, jit) + images = np.asarray(images) + + # block images + if any(has_nsfw_concept): + for i, is_nsfw in enumerate(has_nsfw_concept): + if is_nsfw: + images[i] = np.asarray(images_uint8_casted[i]) + + images = images.reshape(num_devices, batch_size, height, width, 3) + else: + images = np.asarray(images) + has_nsfw_concept = False + + if not return_dict: + return (images, has_nsfw_concept) + + return FlaxStableDiffusionPipelineOutput(images=images, nsfw_content_detected=has_nsfw_concept) + + +# Static argnums are pipe, start_timestep, num_inference_steps, height, width. A change would trigger recompilation. +# Non-static args are (sharded) input tensors mapped over their first dimension (hence, `0`). +@partial( + jax.pmap, + in_axes=(None, 0, 0, 0, 0, None, None, None, None, 0, 0, 0), + static_broadcasted_argnums=(0, 5, 6, 7, 8), +) +def _p_generate( + pipe, + prompt_ids, + image, + params, + prng_seed, + start_timestep, + num_inference_steps, + height, + width, + guidance_scale, + noise, + neg_prompt_ids, +): + return pipe._generate( + prompt_ids, + image, + params, + prng_seed, + start_timestep, + num_inference_steps, + height, + width, + guidance_scale, + noise, + neg_prompt_ids, + ) + + +@partial(jax.pmap, static_broadcasted_argnums=(0,)) +def _p_get_has_nsfw_concepts(pipe, features, params): + return pipe._get_has_nsfw_concepts(features, params) + + +def unshard(x: jnp.ndarray): + # einops.rearrange(x, 'd b ... -> (d b) ...') + num_devices, batch_size = x.shape[:2] + rest = x.shape[2:] + return x.reshape(num_devices * batch_size, *rest) + + +def preprocess(image, dtype): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = jnp.array(image).astype(dtype) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + return 2.0 * image - 1.0 diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_inpaint.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..1846b244d6cdbcc53180ea935ca56c4eccdffde3 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_flax_stable_diffusion_inpaint.py @@ -0,0 +1,523 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import warnings +from functools import partial +from typing import Dict, List, Optional, Union + +import jax +import jax.numpy as jnp +import numpy as np +from flax.core.frozen_dict import FrozenDict +from flax.jax_utils import unreplicate +from flax.training.common_utils import shard +from packaging import version +from PIL import Image +from transformers import CLIPFeatureExtractor, CLIPTokenizer, FlaxCLIPTextModel + +from ...models import FlaxAutoencoderKL, FlaxUNet2DConditionModel +from ...schedulers import ( + FlaxDDIMScheduler, + FlaxDPMSolverMultistepScheduler, + FlaxLMSDiscreteScheduler, + FlaxPNDMScheduler, +) +from ...utils import PIL_INTERPOLATION, deprecate, logging +from ..pipeline_flax_utils import FlaxDiffusionPipeline +from . import FlaxStableDiffusionPipelineOutput +from .safety_checker_flax import FlaxStableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +# Set to True to use python for loop instead of jax.fori_loop for easier debugging +DEBUG = False + + +class FlaxStableDiffusionInpaintPipeline(FlaxDiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`FlaxDiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`FlaxAutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`FlaxCLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.FlaxCLIPTextModel), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`FlaxUNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`FlaxDDIMScheduler`], [`FlaxLMSDiscreteScheduler`], [`FlaxPNDMScheduler`], or + [`FlaxDPMSolverMultistepScheduler`]. + safety_checker ([`FlaxStableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + def __init__( + self, + vae: FlaxAutoencoderKL, + text_encoder: FlaxCLIPTextModel, + tokenizer: CLIPTokenizer, + unet: FlaxUNet2DConditionModel, + scheduler: Union[ + FlaxDDIMScheduler, FlaxPNDMScheduler, FlaxLMSDiscreteScheduler, FlaxDPMSolverMultistepScheduler + ], + safety_checker: FlaxStableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + dtype: jnp.dtype = jnp.float32, + ): + super().__init__() + self.dtype = dtype + + if safety_checker is None: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + def prepare_inputs( + self, + prompt: Union[str, List[str]], + image: Union[Image.Image, List[Image.Image]], + mask: Union[Image.Image, List[Image.Image]], + ): + if not isinstance(prompt, (str, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if not isinstance(image, (Image.Image, list)): + raise ValueError(f"image has to be of type `PIL.Image.Image` or list but is {type(image)}") + + if isinstance(image, Image.Image): + image = [image] + + if not isinstance(mask, (Image.Image, list)): + raise ValueError(f"image has to be of type `PIL.Image.Image` or list but is {type(image)}") + + if isinstance(mask, Image.Image): + mask = [mask] + + processed_images = jnp.concatenate([preprocess_image(img, jnp.float32) for img in image]) + processed_masks = jnp.concatenate([preprocess_mask(m, jnp.float32) for m in mask]) + # processed_masks[processed_masks < 0.5] = 0 + processed_masks = processed_masks.at[processed_masks < 0.5].set(0) + # processed_masks[processed_masks >= 0.5] = 1 + processed_masks = processed_masks.at[processed_masks >= 0.5].set(1) + + processed_masked_images = processed_images * (processed_masks < 0.5) + + text_input = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + return text_input.input_ids, processed_masked_images, processed_masks + + def _get_has_nsfw_concepts(self, features, params): + has_nsfw_concepts = self.safety_checker(features, params) + return has_nsfw_concepts + + def _run_safety_checker(self, images, safety_model_params, jit=False): + # safety_model_params should already be replicated when jit is True + pil_images = [Image.fromarray(image) for image in images] + features = self.feature_extractor(pil_images, return_tensors="np").pixel_values + + if jit: + features = shard(features) + has_nsfw_concepts = _p_get_has_nsfw_concepts(self, features, safety_model_params) + has_nsfw_concepts = unshard(has_nsfw_concepts) + safety_model_params = unreplicate(safety_model_params) + else: + has_nsfw_concepts = self._get_has_nsfw_concepts(features, safety_model_params) + + images_was_copied = False + for idx, has_nsfw_concept in enumerate(has_nsfw_concepts): + if has_nsfw_concept: + if not images_was_copied: + images_was_copied = True + images = images.copy() + + images[idx] = np.zeros(images[idx].shape, dtype=np.uint8) # black image + + if any(has_nsfw_concepts): + warnings.warn( + "Potential NSFW content was detected in one or more images. A black image will be returned" + " instead. Try again with a different prompt and/or seed." + ) + + return images, has_nsfw_concepts + + def _generate( + self, + prompt_ids: jnp.array, + mask: jnp.array, + masked_image: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + num_inference_steps: int, + height: int, + width: int, + guidance_scale: float, + latents: Optional[jnp.array] = None, + neg_prompt_ids: Optional[jnp.array] = None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + # get prompt text embeddings + prompt_embeds = self.text_encoder(prompt_ids, params=params["text_encoder"])[0] + + # TODO: currently it is assumed `do_classifier_free_guidance = guidance_scale > 1.0` + # implement this conditional `do_classifier_free_guidance = guidance_scale > 1.0` + batch_size = prompt_ids.shape[0] + + max_length = prompt_ids.shape[-1] + + if neg_prompt_ids is None: + uncond_input = self.tokenizer( + [""] * batch_size, padding="max_length", max_length=max_length, return_tensors="np" + ).input_ids + else: + uncond_input = neg_prompt_ids + negative_prompt_embeds = self.text_encoder(uncond_input, params=params["text_encoder"])[0] + context = jnp.concatenate([negative_prompt_embeds, prompt_embeds]) + + latents_shape = ( + batch_size, + self.vae.config.latent_channels, + height // self.vae_scale_factor, + width // self.vae_scale_factor, + ) + if latents is None: + latents = jax.random.normal(prng_seed, shape=latents_shape, dtype=self.dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + + prng_seed, mask_prng_seed = jax.random.split(prng_seed) + + masked_image_latent_dist = self.vae.apply( + {"params": params["vae"]}, masked_image, method=self.vae.encode + ).latent_dist + masked_image_latents = masked_image_latent_dist.sample(key=mask_prng_seed).transpose((0, 3, 1, 2)) + masked_image_latents = self.vae.config.scaling_factor * masked_image_latents + del mask_prng_seed + + mask = jax.image.resize(mask, (*mask.shape[:-2], *masked_image_latents.shape[-2:]), method="nearest") + + # 8. Check that sizes of mask, masked image and latents match + num_channels_latents = self.vae.config.latent_channels + num_channels_mask = mask.shape[1] + num_channels_masked_image = masked_image_latents.shape[1] + if num_channels_latents + num_channels_mask + num_channels_masked_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_mask`: {num_channels_mask} + `num_channels_masked_image`: {num_channels_masked_image}" + f" = {num_channels_latents+num_channels_masked_image+num_channels_mask}. Please verify the config of" + " `pipeline.unet` or your `mask_image` or `image` input." + ) + + def loop_body(step, args): + latents, mask, masked_image_latents, scheduler_state = args + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + latents_input = jnp.concatenate([latents] * 2) + mask_input = jnp.concatenate([mask] * 2) + masked_image_latents_input = jnp.concatenate([masked_image_latents] * 2) + + t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step] + timestep = jnp.broadcast_to(t, latents_input.shape[0]) + + latents_input = self.scheduler.scale_model_input(scheduler_state, latents_input, t) + # concat latents, mask, masked_image_latents in the channel dimension + latents_input = jnp.concatenate([latents_input, mask_input, masked_image_latents_input], axis=1) + + # predict the noise residual + noise_pred = self.unet.apply( + {"params": params["unet"]}, + jnp.array(latents_input), + jnp.array(timestep, dtype=jnp.int32), + encoder_hidden_states=context, + ).sample + # perform guidance + noise_pred_uncond, noise_prediction_text = jnp.split(noise_pred, 2, axis=0) + noise_pred = noise_pred_uncond + guidance_scale * (noise_prediction_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents, scheduler_state = self.scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple() + return latents, mask, masked_image_latents, scheduler_state + + scheduler_state = self.scheduler.set_timesteps( + params["scheduler"], num_inference_steps=num_inference_steps, shape=latents.shape + ) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * params["scheduler"].init_noise_sigma + + if DEBUG: + # run with python for loop + for i in range(num_inference_steps): + latents, mask, masked_image_latents, scheduler_state = loop_body( + i, (latents, mask, masked_image_latents, scheduler_state) + ) + else: + latents, _, _, _ = jax.lax.fori_loop( + 0, num_inference_steps, loop_body, (latents, mask, masked_image_latents, scheduler_state) + ) + + # scale and decode the image latents with vae + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.apply({"params": params["vae"]}, latents, method=self.vae.decode).sample + + image = (image / 2 + 0.5).clip(0, 1).transpose(0, 2, 3, 1) + return image + + def __call__( + self, + prompt_ids: jnp.array, + mask: jnp.array, + masked_image: jnp.array, + params: Union[Dict, FrozenDict], + prng_seed: jax.random.KeyArray, + num_inference_steps: int = 50, + height: Optional[int] = None, + width: Optional[int] = None, + guidance_scale: Union[float, jnp.array] = 7.5, + latents: jnp.array = None, + neg_prompt_ids: jnp.array = None, + return_dict: bool = True, + jit: bool = False, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + latents (`jnp.array`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. tensor will ge generated + by sampling using the supplied random `generator`. + jit (`bool`, defaults to `False`): + Whether to run `pmap` versions of the generation and safety scoring functions. NOTE: This argument + exists because `__call__` is not yet end-to-end pmap-able. It will be removed in a future release. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] instead of + a plain tuple. + + Returns: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.FlaxStableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a + `tuple. When returning a tuple, the first element is a list with the generated images, and the second + element is a list of `bool`s denoting whether the corresponding generated image likely represents + "not-safe-for-work" (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + masked_image = jax.image.resize(masked_image, (*masked_image.shape[:-2], height, width), method="bicubic") + mask = jax.image.resize(mask, (*mask.shape[:-2], height, width), method="nearest") + + if isinstance(guidance_scale, float): + # Convert to a tensor so each device gets a copy. Follow the prompt_ids for + # shape information, as they may be sharded (when `jit` is `True`), or not. + guidance_scale = jnp.array([guidance_scale] * prompt_ids.shape[0]) + if len(prompt_ids.shape) > 2: + # Assume sharded + guidance_scale = guidance_scale[:, None] + + if jit: + images = _p_generate( + self, + prompt_ids, + mask, + masked_image, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + else: + images = self._generate( + prompt_ids, + mask, + masked_image, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + + if self.safety_checker is not None: + safety_params = params["safety_checker"] + images_uint8_casted = (images * 255).round().astype("uint8") + num_devices, batch_size = images.shape[:2] + + images_uint8_casted = np.asarray(images_uint8_casted).reshape(num_devices * batch_size, height, width, 3) + images_uint8_casted, has_nsfw_concept = self._run_safety_checker(images_uint8_casted, safety_params, jit) + images = np.asarray(images) + + # block images + if any(has_nsfw_concept): + for i, is_nsfw in enumerate(has_nsfw_concept): + if is_nsfw: + images[i] = np.asarray(images_uint8_casted[i]) + + images = images.reshape(num_devices, batch_size, height, width, 3) + else: + images = np.asarray(images) + has_nsfw_concept = False + + if not return_dict: + return (images, has_nsfw_concept) + + return FlaxStableDiffusionPipelineOutput(images=images, nsfw_content_detected=has_nsfw_concept) + + +# Static argnums are pipe, num_inference_steps, height, width. A change would trigger recompilation. +# Non-static args are (sharded) input tensors mapped over their first dimension (hence, `0`). +@partial( + jax.pmap, + in_axes=(None, 0, 0, 0, 0, 0, None, None, None, 0, 0, 0), + static_broadcasted_argnums=(0, 6, 7, 8), +) +def _p_generate( + pipe, + prompt_ids, + mask, + masked_image, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, +): + return pipe._generate( + prompt_ids, + mask, + masked_image, + params, + prng_seed, + num_inference_steps, + height, + width, + guidance_scale, + latents, + neg_prompt_ids, + ) + + +@partial(jax.pmap, static_broadcasted_argnums=(0,)) +def _p_get_has_nsfw_concepts(pipe, features, params): + return pipe._get_has_nsfw_concepts(features, params) + + +def unshard(x: jnp.ndarray): + # einops.rearrange(x, 'd b ... -> (d b) ...') + num_devices, batch_size = x.shape[:2] + rest = x.shape[2:] + return x.reshape(num_devices * batch_size, *rest) + + +def preprocess_image(image, dtype): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = jnp.array(image).astype(dtype) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + return 2.0 * image - 1.0 + + +def preprocess_mask(mask, dtype): + w, h = mask.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = mask.resize((w, h)) + mask = jnp.array(mask.convert("L")).astype(dtype) / 255.0 + mask = jnp.expand_dims(mask, axis=(0, 1)) + + return mask diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..6cfbca8eb129de92fe7b07f9fc592bd66494744e --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py @@ -0,0 +1,352 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import torch +from transformers import CLIPFeatureExtractor, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import deprecate, logging +from ..onnx_utils import ORT_TO_NP_TYPE, OnnxRuntimeModel +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput + + +logger = logging.get_logger(__name__) + + +class OnnxStableDiffusionPipeline(DiffusionPipeline): + vae_encoder: OnnxRuntimeModel + vae_decoder: OnnxRuntimeModel + text_encoder: OnnxRuntimeModel + tokenizer: CLIPTokenizer + unet: OnnxRuntimeModel + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler] + safety_checker: OnnxRuntimeModel + feature_extractor: CLIPFeatureExtractor + + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + self.register_modules( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def _encode_prompt(self, prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="np").input_ids + + if not np.array_equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder(input_ids=text_input_ids.astype(np.int32))[0] + prompt_embeds = np.repeat(prompt_embeds, num_images_per_prompt, axis=0) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] * batch_size + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="np", + ) + negative_prompt_embeds = self.text_encoder(input_ids=uncond_input.input_ids.astype(np.int32))[0] + negative_prompt_embeds = np.repeat(negative_prompt_embeds, num_images_per_prompt, axis=0) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = np.concatenate([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = 512, + width: Optional[int] = 512, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[np.random.RandomState] = None, + latents: Optional[np.ndarray] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + ): + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if generator is None: + generator = np.random + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + prompt_embeds = self._encode_prompt( + prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + # get the initial random noise unless the user supplied it + latents_dtype = prompt_embeds.dtype + latents_shape = (batch_size * num_images_per_prompt, 4, height // 8, width // 8) + if latents is None: + latents = generator.randn(*latents_shape).astype(latents_dtype) + elif latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + latents = latents * np.float64(self.scheduler.init_noise_sigma) + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + timestep_dtype = next( + (input.type for input in self.unet.model.get_inputs() if input.name == "timestep"), "tensor(float)" + ) + timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] + + for i, t in enumerate(self.progress_bar(self.scheduler.timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(torch.from_numpy(latent_model_input), t) + latent_model_input = latent_model_input.cpu().numpy() + + # predict the noise residual + timestep = np.array([t], dtype=timestep_dtype) + noise_pred = self.unet(sample=latent_model_input, timestep=timestep, encoder_hidden_states=prompt_embeds) + noise_pred = noise_pred[0] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + scheduler_output = self.scheduler.step( + torch.from_numpy(noise_pred), t, torch.from_numpy(latents), **extra_step_kwargs + ) + latents = scheduler_output.prev_sample.numpy() + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + # image = self.vae_decoder(latent_sample=latents)[0] + # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 + image = np.concatenate( + [self.vae_decoder(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])] + ) + + image = np.clip(image / 2 + 0.5, 0, 1) + image = image.transpose((0, 2, 3, 1)) + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor( + self.numpy_to_pil(image), return_tensors="np" + ).pixel_values.astype(image.dtype) + + image, has_nsfw_concepts = self.safety_checker(clip_input=safety_checker_input, images=image) + + # There will throw an error if use safety_checker batchsize>1 + images, has_nsfw_concept = [], [] + for i in range(image.shape[0]): + image_i, has_nsfw_concept_i = self.safety_checker( + clip_input=safety_checker_input[i : i + 1], images=image[i : i + 1] + ) + images.append(image_i) + has_nsfw_concept.append(has_nsfw_concept_i[0]) + image = np.concatenate(images) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) + + +class StableDiffusionOnnxPipeline(OnnxStableDiffusionPipeline): + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + ): + deprecation_message = "Please use `OnnxStableDiffusionPipeline` instead of `StableDiffusionOnnxPipeline`." + deprecate("StableDiffusionOnnxPipeline", "1.0.0", deprecation_message) + super().__init__( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_img2img.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..277025857e77256464cacf2b6638d986ee88adb3 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_img2img.py @@ -0,0 +1,470 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import PIL_INTERPOLATION, deprecate, logging +from ..onnx_utils import ORT_TO_NP_TYPE, OnnxRuntimeModel +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess with 8->64 +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class OnnxStableDiffusionImg2ImgPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image to image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + vae_encoder: OnnxRuntimeModel + vae_decoder: OnnxRuntimeModel + text_encoder: OnnxRuntimeModel + tokenizer: CLIPTokenizer + unet: OnnxRuntimeModel + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler] + safety_checker: OnnxRuntimeModel + feature_extractor: CLIPFeatureExtractor + + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + self.register_modules( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_onnx_stable_diffusion.OnnxStableDiffusionPipeline._encode_prompt + def _encode_prompt(self, prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="np").input_ids + + if not np.array_equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder(input_ids=text_input_ids.astype(np.int32))[0] + prompt_embeds = np.repeat(prompt_embeds, num_images_per_prompt, axis=0) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] * batch_size + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="np", + ) + negative_prompt_embeds = self.text_encoder(input_ids=uncond_input.input_ids.astype(np.int32))[0] + negative_prompt_embeds = np.repeat(negative_prompt_embeds, num_images_per_prompt, axis=0) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = np.concatenate([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[np.ndarray, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[np.random.RandomState] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`np.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`np.random.RandomState`, *optional*): + A np.random.RandomState to make generation deterministic. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if generator is None: + generator = np.random + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + image = preprocess(image).cpu().numpy() + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + prompt_embeds = self._encode_prompt( + prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + latents_dtype = prompt_embeds.dtype + image = image.astype(latents_dtype) + # encode the init image into latents and scale the latents + init_latents = self.vae_encoder(sample=image)[0] + init_latents = 0.18215 * init_latents + + if isinstance(prompt, str): + prompt = [prompt] + if len(prompt) > init_latents.shape[0] and len(prompt) % init_latents.shape[0] == 0: + # expand init_latents for batch_size + deprecation_message = ( + f"You have passed {len(prompt)} text prompts (`prompt`), but only {init_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = len(prompt) // init_latents.shape[0] + init_latents = np.concatenate([init_latents] * additional_image_per_prompt * num_images_per_prompt, axis=0) + elif len(prompt) > init_latents.shape[0] and len(prompt) % init_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {len(prompt)} text prompts." + ) + else: + init_latents = np.concatenate([init_latents] * num_images_per_prompt, axis=0) + + # get the original timestep using init_timestep + offset = self.scheduler.config.get("steps_offset", 0) + init_timestep = int(num_inference_steps * strength) + offset + init_timestep = min(init_timestep, num_inference_steps) + + timesteps = self.scheduler.timesteps.numpy()[-init_timestep] + timesteps = np.array([timesteps] * batch_size * num_images_per_prompt) + + # add noise to latents using the timesteps + noise = generator.randn(*init_latents.shape).astype(latents_dtype) + init_latents = self.scheduler.add_noise( + torch.from_numpy(init_latents), torch.from_numpy(noise), torch.from_numpy(timesteps) + ) + init_latents = init_latents.numpy() + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + latents = init_latents + + t_start = max(num_inference_steps - init_timestep + offset, 0) + timesteps = self.scheduler.timesteps[t_start:].numpy() + + timestep_dtype = next( + (input.type for input in self.unet.model.get_inputs() if input.name == "timestep"), "tensor(float)" + ) + timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] + + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(torch.from_numpy(latent_model_input), t) + latent_model_input = latent_model_input.cpu().numpy() + + # predict the noise residual + timestep = np.array([t], dtype=timestep_dtype) + noise_pred = self.unet(sample=latent_model_input, timestep=timestep, encoder_hidden_states=prompt_embeds)[ + 0 + ] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + scheduler_output = self.scheduler.step( + torch.from_numpy(noise_pred), t, torch.from_numpy(latents), **extra_step_kwargs + ) + latents = scheduler_output.prev_sample.numpy() + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + # image = self.vae_decoder(latent_sample=latents)[0] + # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 + image = np.concatenate( + [self.vae_decoder(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])] + ) + + image = np.clip(image / 2 + 0.5, 0, 1) + image = image.transpose((0, 2, 3, 1)) + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor( + self.numpy_to_pil(image), return_tensors="np" + ).pixel_values.astype(image.dtype) + # safety_checker does not support batched inputs yet + images, has_nsfw_concept = [], [] + for i in range(image.shape[0]): + image_i, has_nsfw_concept_i = self.safety_checker( + clip_input=safety_checker_input[i : i + 1], images=image[i : i + 1] + ) + images.append(image_i) + has_nsfw_concept.append(has_nsfw_concept_i[0]) + image = np.concatenate(images) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..e7e3dc0ae836fd69c0d347affbcb370f4fb4cf26 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint.py @@ -0,0 +1,477 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import PIL_INTERPOLATION, deprecate, logging +from ..onnx_utils import ORT_TO_NP_TYPE, OnnxRuntimeModel +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +NUM_UNET_INPUT_CHANNELS = 9 +NUM_LATENT_CHANNELS = 4 + + +def prepare_mask_and_masked_image(image, mask, latents_shape): + image = np.array(image.convert("RGB").resize((latents_shape[1] * 8, latents_shape[0] * 8))) + image = image[None].transpose(0, 3, 1, 2) + image = image.astype(np.float32) / 127.5 - 1.0 + + image_mask = np.array(mask.convert("L").resize((latents_shape[1] * 8, latents_shape[0] * 8))) + masked_image = image * (image_mask < 127.5) + + mask = mask.resize((latents_shape[1], latents_shape[0]), PIL_INTERPOLATION["nearest"]) + mask = np.array(mask.convert("L")) + mask = mask.astype(np.float32) / 255.0 + mask = mask[None, None] + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + + return mask, masked_image + + +class OnnxStableDiffusionInpaintPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + vae_encoder: OnnxRuntimeModel + vae_decoder: OnnxRuntimeModel + text_encoder: OnnxRuntimeModel + tokenizer: CLIPTokenizer + unet: OnnxRuntimeModel + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler] + safety_checker: OnnxRuntimeModel + feature_extractor: CLIPFeatureExtractor + + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + logger.info("`OnnxStableDiffusionInpaintPipeline` is experimental and will very likely change in the future.") + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + self.register_modules( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_onnx_stable_diffusion.OnnxStableDiffusionPipeline._encode_prompt + def _encode_prompt(self, prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="np").input_ids + + if not np.array_equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder(input_ids=text_input_ids.astype(np.int32))[0] + prompt_embeds = np.repeat(prompt_embeds, num_images_per_prompt, axis=0) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] * batch_size + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="np", + ) + negative_prompt_embeds = self.text_encoder(input_ids=uncond_input.input_ids.astype(np.int32))[0] + negative_prompt_embeds = np.repeat(negative_prompt_embeds, num_images_per_prompt, axis=0) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = np.concatenate([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: PIL.Image.Image, + mask_image: PIL.Image.Image, + height: Optional[int] = 512, + width: Optional[int] = 512, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[np.random.RandomState] = None, + latents: Optional[np.ndarray] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be inpainted, *i.e.* parts of the image will + be masked out with `mask_image` and repainted according to `prompt`. + mask_image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + repainted, while black pixels will be preserved. If `mask_image` is a PIL image, it will be converted + to a single channel (luminance) before use. If it's a tensor, it should contain one color channel (L) + instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to 512): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to 512): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`np.random.RandomState`, *optional*): + A np.random.RandomState to make generation deterministic. + latents (`np.ndarray`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if generator is None: + generator = np.random + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + prompt_embeds = self._encode_prompt( + prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + num_channels_latents = NUM_LATENT_CHANNELS + latents_shape = (batch_size * num_images_per_prompt, num_channels_latents, height // 8, width // 8) + latents_dtype = prompt_embeds.dtype + if latents is None: + latents = generator.randn(*latents_shape).astype(latents_dtype) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + + # prepare mask and masked_image + mask, masked_image = prepare_mask_and_masked_image(image, mask_image, latents_shape[-2:]) + mask = mask.astype(latents.dtype) + masked_image = masked_image.astype(latents.dtype) + + masked_image_latents = self.vae_encoder(sample=masked_image)[0] + masked_image_latents = 0.18215 * masked_image_latents + + # duplicate mask and masked_image_latents for each generation per prompt + mask = mask.repeat(batch_size * num_images_per_prompt, 0) + masked_image_latents = masked_image_latents.repeat(batch_size * num_images_per_prompt, 0) + + mask = np.concatenate([mask] * 2) if do_classifier_free_guidance else mask + masked_image_latents = ( + np.concatenate([masked_image_latents] * 2) if do_classifier_free_guidance else masked_image_latents + ) + + num_channels_mask = mask.shape[1] + num_channels_masked_image = masked_image_latents.shape[1] + + unet_input_channels = NUM_UNET_INPUT_CHANNELS + if num_channels_latents + num_channels_mask + num_channels_masked_image != unet_input_channels: + raise ValueError( + "Incorrect configuration settings! The config of `pipeline.unet` expects" + f" {unet_input_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_mask`: {num_channels_mask} + `num_channels_masked_image`: {num_channels_masked_image}" + f" = {num_channels_latents+num_channels_masked_image+num_channels_mask}. Please verify the config of" + " `pipeline.unet` or your `mask_image` or `image` input." + ) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * np.float64(self.scheduler.init_noise_sigma) + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + timestep_dtype = next( + (input.type for input in self.unet.model.get_inputs() if input.name == "timestep"), "tensor(float)" + ) + timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] + + for i, t in enumerate(self.progress_bar(self.scheduler.timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents + # concat latents, mask, masked_image_latnets in the channel dimension + latent_model_input = self.scheduler.scale_model_input(torch.from_numpy(latent_model_input), t) + latent_model_input = latent_model_input.cpu().numpy() + latent_model_input = np.concatenate([latent_model_input, mask, masked_image_latents], axis=1) + + # predict the noise residual + timestep = np.array([t], dtype=timestep_dtype) + noise_pred = self.unet(sample=latent_model_input, timestep=timestep, encoder_hidden_states=prompt_embeds)[ + 0 + ] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + scheduler_output = self.scheduler.step( + torch.from_numpy(noise_pred), t, torch.from_numpy(latents), **extra_step_kwargs + ) + latents = scheduler_output.prev_sample.numpy() + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + # image = self.vae_decoder(latent_sample=latents)[0] + # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 + image = np.concatenate( + [self.vae_decoder(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])] + ) + + image = np.clip(image / 2 + 0.5, 0, 1) + image = image.transpose((0, 2, 3, 1)) + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor( + self.numpy_to_pil(image), return_tensors="np" + ).pixel_values.astype(image.dtype) + # safety_checker does not support batched inputs yet + images, has_nsfw_concept = [], [] + for i in range(image.shape[0]): + image_i, has_nsfw_concept_i = self.safety_checker( + clip_input=safety_checker_input[i : i + 1], images=image[i : i + 1] + ) + images.append(image_i) + has_nsfw_concept.append(has_nsfw_concept_i[0]) + image = np.concatenate(images) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint_legacy.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint_legacy.py new file mode 100644 index 0000000000000000000000000000000000000000..df22652826aec60e44fbd5e46d2c0c3b96f89a95 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion_inpaint_legacy.py @@ -0,0 +1,465 @@ +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...schedulers import DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler +from ...utils import deprecate, logging +from ..onnx_utils import ORT_TO_NP_TYPE, OnnxRuntimeModel +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def preprocess(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL.Image.LANCZOS) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + return 2.0 * image - 1.0 + + +def preprocess_mask(mask, scale_factor=8): + mask = mask.convert("L") + w, h = mask.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = mask.resize((w // scale_factor, h // scale_factor), resample=PIL.Image.NEAREST) + mask = np.array(mask).astype(np.float32) / 255.0 + mask = np.tile(mask, (4, 1, 1)) + mask = mask[None].transpose(0, 1, 2, 3) # what does this step do? + mask = 1 - mask # repaint white, keep black + return mask + + +class OnnxStableDiffusionInpaintPipelineLegacy(DiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. This is a *legacy feature* for Onnx pipelines to + provide compatibility with StableDiffusionInpaintPipelineLegacy and may be removed in the future. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + vae_encoder: OnnxRuntimeModel + vae_decoder: OnnxRuntimeModel + text_encoder: OnnxRuntimeModel + tokenizer: CLIPTokenizer + unet: OnnxRuntimeModel + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler] + safety_checker: OnnxRuntimeModel + feature_extractor: CLIPFeatureExtractor + + def __init__( + self, + vae_encoder: OnnxRuntimeModel, + vae_decoder: OnnxRuntimeModel, + text_encoder: OnnxRuntimeModel, + tokenizer: CLIPTokenizer, + unet: OnnxRuntimeModel, + scheduler: Union[DDIMScheduler, PNDMScheduler, LMSDiscreteScheduler], + safety_checker: OnnxRuntimeModel, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + self.register_modules( + vae_encoder=vae_encoder, + vae_decoder=vae_decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_onnx_stable_diffusion.OnnxStableDiffusionPipeline._encode_prompt + def _encode_prompt(self, prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="np", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="np").input_ids + + if not np.array_equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder(input_ids=text_input_ids.astype(np.int32))[0] + prompt_embeds = np.repeat(prompt_embeds, num_images_per_prompt, axis=0) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] * batch_size + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="np", + ) + negative_prompt_embeds = self.text_encoder(input_ids=uncond_input.input_ids.astype(np.int32))[0] + negative_prompt_embeds = np.repeat(negative_prompt_embeds, num_images_per_prompt, axis=0) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = np.concatenate([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[np.ndarray, PIL.Image.Image] = None, + mask_image: Union[np.ndarray, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[np.random.RandomState] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, np.ndarray], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + image (`nd.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. This is the image whose masked region will be inpainted. + mask_image (`nd.ndarray` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If it's a tensor, it should + contain one color channel (L) instead of 3, so the expected shape would be `(B, H, W, 1)`.uu + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (?) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`np.random.RandomState`, *optional*): + A np.random.RandomState to make generation deterministic. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: np.ndarray)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if generator is None: + generator = np.random + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps) + + if isinstance(image, PIL.Image.Image): + image = preprocess(image) + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + prompt_embeds = self._encode_prompt( + prompt, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + latents_dtype = prompt_embeds.dtype + image = image.astype(latents_dtype) + + # encode the init image into latents and scale the latents + init_latents = self.vae_encoder(sample=image)[0] + init_latents = 0.18215 * init_latents + + # Expand init_latents for batch_size and num_images_per_prompt + init_latents = np.concatenate([init_latents] * num_images_per_prompt, axis=0) + init_latents_orig = init_latents + + # preprocess mask + if not isinstance(mask_image, np.ndarray): + mask_image = preprocess_mask(mask_image, 8) + mask_image = mask_image.astype(latents_dtype) + mask = np.concatenate([mask_image] * num_images_per_prompt, axis=0) + + # check sizes + if not mask.shape == init_latents.shape: + raise ValueError("The mask and image should be the same size!") + + # get the original timestep using init_timestep + offset = self.scheduler.config.get("steps_offset", 0) + init_timestep = int(num_inference_steps * strength) + offset + init_timestep = min(init_timestep, num_inference_steps) + + timesteps = self.scheduler.timesteps.numpy()[-init_timestep] + timesteps = np.array([timesteps] * batch_size * num_images_per_prompt) + + # add noise to latents using the timesteps + noise = generator.randn(*init_latents.shape).astype(latents_dtype) + init_latents = self.scheduler.add_noise( + torch.from_numpy(init_latents), torch.from_numpy(noise), torch.from_numpy(timesteps) + ) + init_latents = init_latents.numpy() + + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (?) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to ? in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + latents = init_latents + + t_start = max(num_inference_steps - init_timestep + offset, 0) + timesteps = self.scheduler.timesteps[t_start:].numpy() + timestep_dtype = next( + (input.type for input in self.unet.model.get_inputs() if input.name == "timestep"), "tensor(float)" + ) + timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] + + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + timestep = np.array([t], dtype=timestep_dtype) + noise_pred = self.unet(sample=latent_model_input, timestep=timestep, encoder_hidden_states=prompt_embeds)[ + 0 + ] + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step( + torch.from_numpy(noise_pred), t, torch.from_numpy(latents), **extra_step_kwargs + ).prev_sample + + latents = latents.numpy() + + init_latents_proper = self.scheduler.add_noise( + torch.from_numpy(init_latents_orig), torch.from_numpy(noise), torch.from_numpy(np.array([t])) + ) + + init_latents_proper = init_latents_proper.numpy() + + latents = (init_latents_proper * mask) + (latents * (1 - mask)) + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + latents = 1 / 0.18215 * latents + # image = self.vae_decoder(latent_sample=latents)[0] + # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 + image = np.concatenate( + [self.vae_decoder(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])] + ) + + image = np.clip(image / 2 + 0.5, 0, 1) + image = image.transpose((0, 2, 3, 1)) + + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor( + self.numpy_to_pil(image), return_tensors="np" + ).pixel_values.astype(image.dtype) + # There will throw an error if use safety_checker batchsize>1 + images, has_nsfw_concept = [], [] + for i in range(image.shape[0]): + image_i, has_nsfw_concept_i = self.safety_checker( + clip_input=safety_checker_input[i : i + 1], images=image[i : i + 1] + ) + images.append(image_i) + has_nsfw_concept.append(has_nsfw_concept_i[0]) + image = np.concatenate(images) + else: + has_nsfw_concept = None + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..2a4268d815a6087515f50b88d117cd9deefbbbfe --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py @@ -0,0 +1,653 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Any, Callable, Dict, List, Optional, Union + +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import deprecate, is_accelerate_available, logging, randn_tensor, replace_example_docstring +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +EXAMPLE_DOC_STRING = """ + Examples: + ```py + >>> import torch + >>> from diffusers import StableDiffusionPipeline + + >>> pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16) + >>> pipe = pipe.to("cuda") + + >>> prompt = "a photo of an astronaut riding a horse on mars" + >>> image = pipe(prompt).images[0] + ``` +""" + + +class StableDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. + + When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several + steps. This is useful to save some memory and allow larger batch sizes. + """ + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously invoked, this method will go back to + computing decoding in one step. + """ + self.vae.disable_slicing() + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + @replace_example_docstring(EXAMPLE_DOC_STRING) + def __call__( + self, + prompt: Union[str, List[str]] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttnProcessor` as defined under + `self.processor` in + [diffusers.cross_attention](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py). + + Examples: + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, height, width, callback_steps, negative_prompt, prompt_embeds, negative_prompt_embeds + ) + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + if output_type == "latent": + image = latents + has_nsfw_concept = None + elif output_type == "pil": + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 10. Convert to PIL + image = self.numpy_to_pil(image) + else: + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_depth2img.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_depth2img.py new file mode 100644 index 0000000000000000000000000000000000000000..9d663de47ff5ab401c02299838edd6e9e368505f --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_depth2img.py @@ -0,0 +1,676 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import contextlib +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPTextModel, CLIPTokenizer, DPTFeatureExtractor, DPTForDepthEstimation + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import PIL_INTERPOLATION, deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class StableDiffusionDepth2ImgPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image to image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + depth_estimator: DPTForDepthEstimation, + feature_extractor: DPTFeatureExtractor, + ): + super().__init__() + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + depth_estimator=depth_estimator, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae, self.depth_estimator]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.check_inputs + def check_inputs( + self, prompt, strength, callback_steps, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None + ): + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.get_timesteps + def get_timesteps(self, num_inference_steps, strength, device): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + timesteps = self.scheduler.timesteps[t_start:] + + return timesteps, num_inference_steps - t_start + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.prepare_latents + def prepare_latents(self, image, timestep, batch_size, num_images_per_prompt, dtype, device, generator=None): + if not isinstance(image, (torch.Tensor, PIL.Image.Image, list)): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or list but is {type(image)}" + ) + + image = image.to(device=device, dtype=dtype) + + batch_size = batch_size * num_images_per_prompt + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if isinstance(generator, list): + init_latents = [ + self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size) + ] + init_latents = torch.cat(init_latents, dim=0) + else: + init_latents = self.vae.encode(image).latent_dist.sample(generator) + + init_latents = self.vae.config.scaling_factor * init_latents + + if batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] == 0: + # expand init_latents for batch_size + deprecation_message = ( + f"You have passed {batch_size} text prompts (`prompt`), but only {init_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = batch_size // init_latents.shape[0] + init_latents = torch.cat([init_latents] * additional_image_per_prompt, dim=0) + elif batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts." + ) + else: + init_latents = torch.cat([init_latents], dim=0) + + shape = init_latents.shape + noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + + # get latents + init_latents = self.scheduler.add_noise(init_latents, noise, timestep) + latents = init_latents + + return latents + + def prepare_depth_map(self, image, depth_map, batch_size, do_classifier_free_guidance, dtype, device): + if isinstance(image, PIL.Image.Image): + image = [image] + else: + image = [img for img in image] + + if isinstance(image[0], PIL.Image.Image): + width, height = image[0].size + else: + width, height = image[0].shape[-2:] + + if depth_map is None: + pixel_values = self.feature_extractor(images=image, return_tensors="pt").pixel_values + pixel_values = pixel_values.to(device=device) + # The DPT-Hybrid model uses batch-norm layers which are not compatible with fp16. + # So we use `torch.autocast` here for half precision inference. + context_manger = torch.autocast("cuda", dtype=dtype) if device.type == "cuda" else contextlib.nullcontext() + with context_manger: + depth_map = self.depth_estimator(pixel_values).predicted_depth + else: + depth_map = depth_map.to(device=device, dtype=dtype) + + depth_map = torch.nn.functional.interpolate( + depth_map.unsqueeze(1), + size=(height // self.vae_scale_factor, width // self.vae_scale_factor), + mode="bicubic", + align_corners=False, + ) + + depth_min = torch.amin(depth_map, dim=[1, 2, 3], keepdim=True) + depth_max = torch.amax(depth_map, dim=[1, 2, 3], keepdim=True) + depth_map = 2.0 * (depth_map - depth_min) / (depth_max - depth_min) - 1.0 + depth_map = depth_map.to(dtype) + + # duplicate mask and masked_image_latents for each generation per prompt, using mps friendly method + if depth_map.shape[0] < batch_size: + depth_map = depth_map.repeat(batch_size, 1, 1, 1) + + depth_map = torch.cat([depth_map] * 2) if do_classifier_free_guidance else depth_map + return depth_map + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + depth_map: Optional[torch.FloatTensor] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> import torch + >>> import requests + >>> from PIL import Image + + >>> from diffusers import StableDiffusionDepth2ImgPipeline + + >>> pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + ... "stabilityai/stable-diffusion-2-depth", + ... torch_dtype=torch.float16, + ... ) + >>> pipe.to("cuda") + + + >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg" + >>> init_image = Image.open(requests.get(url, stream=True).raw) + >>> prompt = "two tigers" + >>> n_propmt = "bad, deformed, ugly, bad anotomy" + >>> image = pipe(prompt=prompt, image=init_image, negative_prompt=n_propmt, strength=0.7).images[0] + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 1. Check inputs + self.check_inputs(prompt, strength, callback_steps) + + if image is None: + raise ValueError("`image` input cannot be undefined.") + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Prepare depth mask + depth_mask = self.prepare_depth_map( + image, + depth_map, + batch_size * num_images_per_prompt, + do_classifier_free_guidance, + prompt_embeds.dtype, + device, + ) + + # 5. Preprocess image + image = preprocess(image) + + # 6. Set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 7. Prepare latent variables + latents = self.prepare_latents( + image, latent_timestep, batch_size, num_images_per_prompt, prompt_embeds.dtype, device, generator + ) + + # 8. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 9. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + latent_model_input = torch.cat([latent_model_input, depth_mask], dim=1) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 10. Post-processing + image = self.decode_latents(latents) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_image_variation.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..fb5d5da16688217b59a69cfb008a747da81bf365 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_image_variation.py @@ -0,0 +1,414 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPVisionModelWithProjection + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class StableDiffusionImageVariationPipeline(DiffusionPipeline): + r""" + Pipeline to generate variations from an input image using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + image_encoder ([`CLIPVisionModelWithProjection`]): + Frozen CLIP image-encoder. Stable Diffusion Image Variation uses the vision portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPVisionModelWithProjection), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + # TODO: feature_extractor is required to encode images (if they are in PIL format), + # we should give a descriptive message if the pipeline doesn't have one. + _optional_components = ["safety_checker"] + + def __init__( + self, + vae: AutoencoderKL, + image_encoder: CLIPVisionModelWithProjection, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if safety_checker is None and requires_safety_checker: + logger.warn( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + image_encoder=image_encoder, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.image_encoder, self.vae, self.safety_checker]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_image(self, image, device, num_images_per_prompt, do_classifier_free_guidance): + dtype = next(self.image_encoder.parameters()).dtype + + if not isinstance(image, torch.Tensor): + image = self.feature_extractor(images=image, return_tensors="pt").pixel_values + + image = image.to(device=device, dtype=dtype) + image_embeddings = self.image_encoder(image).image_embeds + image_embeddings = image_embeddings.unsqueeze(1) + + # duplicate image embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = image_embeddings.shape + image_embeddings = image_embeddings.repeat(1, num_images_per_prompt, 1) + image_embeddings = image_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + if do_classifier_free_guidance: + negative_prompt_embeds = torch.zeros_like(image_embeddings) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + image_embeddings = torch.cat([negative_prompt_embeds, image_embeddings]) + + return image_embeddings + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs(self, image, height, width, callback_steps): + if ( + not isinstance(image, torch.Tensor) + and not isinstance(image, PIL.Image.Image) + and not isinstance(image, list) + ): + raise ValueError( + "`image` has to be of type `torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]` but is" + f" {type(image)}" + ) + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + image: Union[PIL.Image.Image, List[PIL.Image.Image], torch.FloatTensor], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + image (`PIL.Image.Image` or `List[PIL.Image.Image]` or `torch.FloatTensor`): + The image or images to guide the image generation. If you provide a tensor, it needs to comply with the + configuration of + [this](https://huggingface.co/lambdalabs/sd-image-variations-diffusers/blob/main/feature_extractor/preprocessor_config.json) + `CLIPFeatureExtractor` + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(image, height, width, callback_steps) + + # 2. Define call parameters + if isinstance(image, PIL.Image.Image): + batch_size = 1 + elif isinstance(image, list): + batch_size = len(image) + else: + batch_size = image.shape[0] + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input image + image_embeddings = self._encode_image(image, device, num_images_per_prompt, do_classifier_free_guidance) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + image_embeddings.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=image_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, image_embeddings.dtype) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..e73c946133eeda76ef903e96a862014dc97867ff --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py @@ -0,0 +1,699 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import ( + PIL_INTERPOLATION, + deprecate, + is_accelerate_available, + logging, + randn_tensor, + replace_example_docstring, +) +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +EXAMPLE_DOC_STRING = """ + Examples: + ```py + >>> import requests + >>> import torch + >>> from PIL import Image + >>> from io import BytesIO + + >>> from diffusers import StableDiffusionImg2ImgPipeline + + >>> device = "cuda" + >>> model_id_or_path = "runwayml/stable-diffusion-v1-5" + >>> pipe = StableDiffusionImg2ImgPipeline.from_pretrained(model_id_or_path, torch_dtype=torch.float16) + >>> pipe = pipe.to(device) + + >>> url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg" + + >>> response = requests.get(url) + >>> init_image = Image.open(BytesIO(response.content)).convert("RGB") + >>> init_image = init_image.resize((768, 512)) + + >>> prompt = "A fantasy landscape, trending on artstation" + + >>> images = pipe(prompt=prompt, image=init_image, strength=0.75, guidance_scale=7.5).images + >>> images[0].save("fantasy_landscape.png") + ``` +""" + + +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class StableDiffusionImg2ImgPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image to image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.__init__ + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs( + self, prompt, strength, callback_steps, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None + ): + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + def get_timesteps(self, num_inference_steps, strength, device): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + timesteps = self.scheduler.timesteps[t_start:] + + return timesteps, num_inference_steps - t_start + + def prepare_latents(self, image, timestep, batch_size, num_images_per_prompt, dtype, device, generator=None): + if not isinstance(image, (torch.Tensor, PIL.Image.Image, list)): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or list but is {type(image)}" + ) + + image = image.to(device=device, dtype=dtype) + + batch_size = batch_size * num_images_per_prompt + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if isinstance(generator, list): + init_latents = [ + self.vae.encode(image[i : i + 1]).latent_dist.sample(generator[i]) for i in range(batch_size) + ] + init_latents = torch.cat(init_latents, dim=0) + else: + init_latents = self.vae.encode(image).latent_dist.sample(generator) + + init_latents = self.vae.config.scaling_factor * init_latents + + if batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] == 0: + # expand init_latents for batch_size + deprecation_message = ( + f"You have passed {batch_size} text prompts (`prompt`), but only {init_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = batch_size // init_latents.shape[0] + init_latents = torch.cat([init_latents] * additional_image_per_prompt, dim=0) + elif batch_size > init_latents.shape[0] and batch_size % init_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts." + ) + else: + init_latents = torch.cat([init_latents], dim=0) + + shape = init_latents.shape + noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + + # get latents + init_latents = self.scheduler.add_noise(init_latents, noise, timestep) + latents = init_latents + + return latents + + @torch.no_grad() + @replace_example_docstring(EXAMPLE_DOC_STRING) + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to transform the reference `image`. Must be between 0 and 1. `image` + will be used as a starting point, adding more noise to it the larger the `strength`. The number of + denoising steps depends on the amount of noise initially added. When `strength` is 1, added noise will + be maximum and the denoising process will run for the full number of iterations specified in + `num_inference_steps`. A value of 1, therefore, essentially ignores `image`. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. This parameter will be modulated by `strength`. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + Examples: + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, strength, callback_steps, negative_prompt, prompt_embeds, negative_prompt_embeds) + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Preprocess image + image = preprocess(image) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + latents = self.prepare_latents( + image, latent_timestep, batch_size, num_images_per_prompt, prompt_embeds.dtype, device, generator + ) + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..649530393909904d605ef06c2bcedaa720d839bc --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py @@ -0,0 +1,851 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def prepare_mask_and_masked_image(image, mask): + """ + Prepares a pair (image, mask) to be consumed by the Stable Diffusion pipeline. This means that those inputs will be + converted to ``torch.Tensor`` with shapes ``batch x channels x height x width`` where ``channels`` is ``3`` for the + ``image`` and ``1`` for the ``mask``. + + The ``image`` will be converted to ``torch.float32`` and normalized to be in ``[-1, 1]``. The ``mask`` will be + binarized (``mask > 0.5``) and cast to ``torch.float32`` too. + + Args: + image (Union[np.array, PIL.Image, torch.Tensor]): The image to inpaint. + It can be a ``PIL.Image``, or a ``height x width x 3`` ``np.array`` or a ``channels x height x width`` + ``torch.Tensor`` or a ``batch x channels x height x width`` ``torch.Tensor``. + mask (_type_): The mask to apply to the image, i.e. regions to inpaint. + It can be a ``PIL.Image``, or a ``height x width`` ``np.array`` or a ``1 x height x width`` + ``torch.Tensor`` or a ``batch x 1 x height x width`` ``torch.Tensor``. + + + Raises: + ValueError: ``torch.Tensor`` images should be in the ``[-1, 1]`` range. ValueError: ``torch.Tensor`` mask + should be in the ``[0, 1]`` range. ValueError: ``mask`` and ``image`` should have the same spatial dimensions. + TypeError: ``mask`` is a ``torch.Tensor`` but ``image`` is not + (ot the other way around). + + Returns: + tuple[torch.Tensor]: The pair (mask, masked_image) as ``torch.Tensor`` with 4 + dimensions: ``batch x channels x height x width``. + """ + if isinstance(image, torch.Tensor): + if not isinstance(mask, torch.Tensor): + raise TypeError(f"`image` is a torch.Tensor but `mask` (type: {type(mask)} is not") + + # Batch single image + if image.ndim == 3: + assert image.shape[0] == 3, "Image outside a batch should be of shape (3, H, W)" + image = image.unsqueeze(0) + + # Batch and add channel dim for single mask + if mask.ndim == 2: + mask = mask.unsqueeze(0).unsqueeze(0) + + # Batch single mask or add channel dim + if mask.ndim == 3: + # Single batched mask, no channel dim or single mask not batched but channel dim + if mask.shape[0] == 1: + mask = mask.unsqueeze(0) + + # Batched masks no channel dim + else: + mask = mask.unsqueeze(1) + + assert image.ndim == 4 and mask.ndim == 4, "Image and Mask must have 4 dimensions" + assert image.shape[-2:] == mask.shape[-2:], "Image and Mask must have the same spatial dimensions" + assert image.shape[0] == mask.shape[0], "Image and Mask must have the same batch size" + + # Check image is in [-1, 1] + if image.min() < -1 or image.max() > 1: + raise ValueError("Image should be in [-1, 1] range") + + # Check mask is in [0, 1] + if mask.min() < 0 or mask.max() > 1: + raise ValueError("Mask should be in [0, 1] range") + + # Binarize mask + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + + # Image as float32 + image = image.to(dtype=torch.float32) + elif isinstance(mask, torch.Tensor): + raise TypeError(f"`mask` is a torch.Tensor but `image` (type: {type(image)} is not") + else: + # preprocess image + if isinstance(image, (PIL.Image.Image, np.ndarray)): + image = [image] + + if isinstance(image, list) and isinstance(image[0], PIL.Image.Image): + image = [np.array(i.convert("RGB"))[None, :] for i in image] + image = np.concatenate(image, axis=0) + elif isinstance(image, list) and isinstance(image[0], np.ndarray): + image = np.concatenate([i[None, :] for i in image], axis=0) + + image = image.transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + + # preprocess mask + if isinstance(mask, (PIL.Image.Image, np.ndarray)): + mask = [mask] + + if isinstance(mask, list) and isinstance(mask[0], PIL.Image.Image): + mask = np.concatenate([np.array(m.convert("L"))[None, None, :] for m in mask], axis=0) + mask = mask.astype(np.float32) / 255.0 + elif isinstance(mask, list) and isinstance(mask[0], np.ndarray): + mask = np.concatenate([m[None, None, :] for m in mask], axis=0) + + mask[mask < 0.5] = 0 + mask[mask >= 0.5] = 1 + mask = torch.from_numpy(mask) + + masked_image = image * (mask < 0.5) + + return mask, masked_image + + +class StableDiffusionInpaintPipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "skip_prk_steps") and scheduler.config.skip_prk_steps is False: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration" + " `skip_prk_steps`. `skip_prk_steps` should be set to True in the configuration file. Please make" + " sure to update the config accordingly as not setting `skip_prk_steps` in the config might lead to" + " incorrect results in future versions. If you have downloaded this checkpoint from the Hugging Face" + " Hub, it would be very nice if you could open a Pull request for the" + " `scheduler/scheduler_config.json` file" + ) + deprecate("skip_prk_steps not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["skip_prk_steps"] = True + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.check_inputs + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + def prepare_mask_latents( + self, mask, masked_image, batch_size, height, width, dtype, device, generator, do_classifier_free_guidance + ): + # resize the mask to latents shape as we concatenate the mask to the latents + # we do that before converting to dtype to avoid breaking in case we're using cpu_offload + # and half precision + mask = torch.nn.functional.interpolate( + mask, size=(height // self.vae_scale_factor, width // self.vae_scale_factor) + ) + mask = mask.to(device=device, dtype=dtype) + + masked_image = masked_image.to(device=device, dtype=dtype) + + # encode the mask image into latents space so we can concatenate it to the latents + if isinstance(generator, list): + masked_image_latents = [ + self.vae.encode(masked_image[i : i + 1]).latent_dist.sample(generator=generator[i]) + for i in range(batch_size) + ] + masked_image_latents = torch.cat(masked_image_latents, dim=0) + else: + masked_image_latents = self.vae.encode(masked_image).latent_dist.sample(generator=generator) + masked_image_latents = self.vae.config.scaling_factor * masked_image_latents + + # duplicate mask and masked_image_latents for each generation per prompt, using mps friendly method + if mask.shape[0] < batch_size: + if not batch_size % mask.shape[0] == 0: + raise ValueError( + "The passed mask and the required batch size don't match. Masks are supposed to be duplicated to" + f" a total batch size of {batch_size}, but {mask.shape[0]} masks were passed. Make sure the number" + " of masks that you pass is divisible by the total requested batch size." + ) + mask = mask.repeat(batch_size // mask.shape[0], 1, 1, 1) + if masked_image_latents.shape[0] < batch_size: + if not batch_size % masked_image_latents.shape[0] == 0: + raise ValueError( + "The passed images and the required batch size don't match. Images are supposed to be duplicated" + f" to a total batch size of {batch_size}, but {masked_image_latents.shape[0]} images were passed." + " Make sure the number of images that you pass is divisible by the total requested batch size." + ) + masked_image_latents = masked_image_latents.repeat(batch_size // masked_image_latents.shape[0], 1, 1, 1) + + mask = torch.cat([mask] * 2) if do_classifier_free_guidance else mask + masked_image_latents = ( + torch.cat([masked_image_latents] * 2) if do_classifier_free_guidance else masked_image_latents + ) + + # aligning device to prevent device errors when concating it with the latent model input + masked_image_latents = masked_image_latents.to(device=device, dtype=dtype) + return mask, masked_image_latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + mask_image: Union[torch.FloatTensor, PIL.Image.Image] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be inpainted, *i.e.* parts of the image will + be masked out with `mask_image` and repainted according to `prompt`. + mask_image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + repainted, while black pixels will be preserved. If `mask_image` is a PIL image, it will be converted + to a single channel (luminance) before use. If it's a tensor, it should contain one color channel (L) + instead of 3, so the expected shape would be `(B, H, W, 1)`. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> import PIL + >>> import requests + >>> import torch + >>> from io import BytesIO + + >>> from diffusers import StableDiffusionInpaintPipeline + + + >>> def download_image(url): + ... response = requests.get(url) + ... return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + + >>> img_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png" + >>> mask_url = "https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png" + + >>> init_image = download_image(img_url).resize((512, 512)) + >>> mask_image = download_image(mask_url).resize((512, 512)) + + >>> pipe = StableDiffusionInpaintPipeline.from_pretrained( + ... "runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> prompt = "Face of a yellow cat, high resolution, sitting on a park bench" + >>> image = pipe(prompt=prompt, image=init_image, mask_image=mask_image).images[0] + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs + self.check_inputs( + prompt, + height, + width, + callback_steps, + negative_prompt, + prompt_embeds, + negative_prompt_embeds, + ) + + if image is None: + raise ValueError("`image` input cannot be undefined.") + + if mask_image is None: + raise ValueError("`mask_image` input cannot be undefined.") + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Preprocess mask and image + mask, masked_image = prepare_mask_and_masked_image(image, mask_image) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 6. Prepare latent variables + num_channels_latents = self.vae.config.latent_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 7. Prepare mask latent variables + mask, masked_image_latents = self.prepare_mask_latents( + mask, + masked_image, + batch_size * num_images_per_prompt, + height, + width, + prompt_embeds.dtype, + device, + generator, + do_classifier_free_guidance, + ) + + # 8. Check that sizes of mask, masked image and latents match + num_channels_mask = mask.shape[1] + num_channels_masked_image = masked_image_latents.shape[1] + if num_channels_latents + num_channels_mask + num_channels_masked_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_mask`: {num_channels_mask} + `num_channels_masked_image`: {num_channels_masked_image}" + f" = {num_channels_latents+num_channels_masked_image+num_channels_mask}. Please verify the config of" + " `pipeline.unet` or your `mask_image` or `image` input." + ) + + # 9. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 10. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + + # concat latents, mask, masked_image_latents in the channel dimension + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + latent_model_input = torch.cat([latent_model_input, mask, masked_image_latents], dim=1) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 11. Post-processing + image = self.decode_latents(latents) + + # 12. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 13. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint_legacy.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint_legacy.py new file mode 100644 index 0000000000000000000000000000000000000000..689886c51d45e3e49e61a41e4fb92bd9f75363c4 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint_legacy.py @@ -0,0 +1,672 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import PIL_INTERPOLATION, deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) + + +def preprocess_image(image): + w, h = image.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + image = image.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]) + image = np.array(image).astype(np.float32) / 255.0 + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image) + return 2.0 * image - 1.0 + + +def preprocess_mask(mask, scale_factor=8): + if not isinstance(mask, torch.FloatTensor): + mask = mask.convert("L") + w, h = mask.size + w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32 + mask = mask.resize((w // scale_factor, h // scale_factor), resample=PIL_INTERPOLATION["nearest"]) + mask = np.array(mask).astype(np.float32) / 255.0 + mask = np.tile(mask, (4, 1, 1)) + mask = mask[None].transpose(0, 1, 2, 3) # what does this step do? + mask = 1 - mask # repaint white, keep black + mask = torch.from_numpy(mask) + return mask + + else: + valid_mask_channel_sizes = [1, 3] + # if mask channel is fourth tensor dimension, permute dimensions to pytorch standard (B, C, H, W) + if mask.shape[3] in valid_mask_channel_sizes: + mask = mask.permute(0, 3, 1, 2) + elif mask.shape[1] not in valid_mask_channel_sizes: + raise ValueError( + f"Mask channel dimension of size in {valid_mask_channel_sizes} should be second or fourth dimension," + f" but received mask of shape {tuple(mask.shape)}" + ) + # (potentially) reduce mask channel dimension from 3 to 1 for broadcasting to latent shape + mask = mask.mean(dim=1, keepdim=True) + h, w = mask.shape[-2:] + h, w = map(lambda x: x - x % 32, (h, w)) # resize to integer multiple of 32 + mask = torch.nn.functional.interpolate(mask, (h // scale_factor, w // scale_factor)) + return mask + + +class StableDiffusionInpaintPipelineLegacy(DiffusionPipeline): + r""" + Pipeline for text-guided image inpainting using Stable Diffusion. *This is an experimental feature*. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["feature_extractor"] + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.__init__ + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely. If your checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.check_inputs + def check_inputs( + self, prompt, strength, callback_steps, negative_prompt=None, prompt_embeds=None, negative_prompt_embeds=None + ): + if strength < 0 or strength > 1: + raise ValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.get_timesteps + def get_timesteps(self, num_inference_steps, strength, device): + # get the original timestep using init_timestep + init_timestep = min(int(num_inference_steps * strength), num_inference_steps) + + t_start = max(num_inference_steps - init_timestep, 0) + timesteps = self.scheduler.timesteps[t_start:] + + return timesteps, num_inference_steps - t_start + + def prepare_latents(self, image, timestep, batch_size, num_images_per_prompt, dtype, device, generator): + image = image.to(device=self.device, dtype=dtype) + init_latent_dist = self.vae.encode(image).latent_dist + init_latents = init_latent_dist.sample(generator=generator) + init_latents = self.vae.config.scaling_factor * init_latents + + # Expand init_latents for batch_size and num_images_per_prompt + init_latents = torch.cat([init_latents] * batch_size * num_images_per_prompt, dim=0) + init_latents_orig = init_latents + + # add noise to latents using the timesteps + noise = randn_tensor(init_latents.shape, generator=generator, device=self.device, dtype=dtype) + init_latents = self.scheduler.add_noise(init_latents, noise, timestep) + latents = init_latents + return latents, init_latents_orig, noise + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + mask_image: Union[torch.FloatTensor, PIL.Image.Image] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + add_predicted_noise: Optional[bool] = False, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, that will be used as the starting point for the + process. This is the image whose masked region will be inpainted. + mask_image (`torch.FloatTensor` or `PIL.Image.Image`): + `Image`, or tensor representing an image batch, to mask `image`. White pixels in the mask will be + replaced by noise and therefore repainted, while black pixels will be preserved. If `mask_image` is a + PIL image, it will be converted to a single channel (luminance) before use. If mask is a tensor, the + expected shape should be either `(B, H, W, C)` or `(B, C, H, W)`, where C is 1 or 3. + strength (`float`, *optional*, defaults to 0.8): + Conceptually, indicates how much to inpaint the masked area. Must be between 0 and 1. When `strength` + is 1, the denoising process will be run on the masked area for the full number of iterations specified + in `num_inference_steps`. `image` will be used as a reference for the masked area, adding more noise to + that region the larger the `strength`. If `strength` is 0, no inpainting will occur. + num_inference_steps (`int`, *optional*, defaults to 50): + The reference number of denoising steps. More denoising steps usually lead to a higher quality image at + the expense of slower inference. This parameter will be modulated by `strength`, as explained above. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + add_predicted_noise (`bool`, *optional*, defaults to True): + Use predicted noise instead of random noise when constructing noisy versions of the original image in + the reverse diffusion process + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + message = "Please use `image` instead of `init_image`." + init_image = deprecate("init_image", "0.14.0", message, take_from=kwargs) + image = init_image or image + + # 1. Check inputs + self.check_inputs(prompt, strength, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Preprocess image and mask + if not isinstance(image, torch.FloatTensor): + image = preprocess_image(image) + + mask_image = preprocess_mask(mask_image, self.vae_scale_factor) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 6. Prepare latent variables + # encode the init image into latents and scale the latents + latents, init_latents_orig, noise = self.prepare_latents( + image, latent_timestep, batch_size, num_images_per_prompt, prompt_embeds.dtype, device, generator + ) + + # 7. Prepare mask latent + mask = mask_image.to(device=self.device, dtype=latents.dtype) + mask = torch.cat([mask] * batch_size * num_images_per_prompt) + + # 8. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 9. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + # masking + if add_predicted_noise: + init_latents_proper = self.scheduler.add_noise( + init_latents_orig, noise_pred_uncond, torch.tensor([t]) + ) + else: + init_latents_proper = self.scheduler.add_noise(init_latents_orig, noise, torch.tensor([t])) + + latents = (init_latents_proper * mask) + (latents * (1 - mask)) + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # use original latents corresponding to unmasked portions of the image + latents = (init_latents_orig * mask) + (latents * (1 - mask)) + + # 10. Post-processing + image = self.decode_latents(latents) + + # 11. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 12. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py new file mode 100644 index 0000000000000000000000000000000000000000..cff72536db7063a57256efe8ec6cec7c2a0b162e --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_instruct_pix2pix.py @@ -0,0 +1,672 @@ +# Copyright 2023 The InstructPix2Pix Authors and The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import PIL_INTERPOLATION, deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionPipelineOutput +from .safety_checker import StableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.preprocess +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 8, (w, h)) # resize to integer multiple of 8 + + image = [np.array(i.resize((w, h), resample=PIL_INTERPOLATION["lanczos"]))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class StableDiffusionInstructPix2PixPipeline(DiffusionPipeline): + r""" + Pipeline for pixel-level image editing by following text instructions. Based on Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: StableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image] = None, + num_inference_steps: int = 100, + guidance_scale: float = 7.5, + image_guidance_scale: float = 1.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`PIL.Image.Image`): + `Image`, or tensor representing an image batch which will be repainted according to `prompt`. + num_inference_steps (`int`, *optional*, defaults to 100): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. This pipeline requires a value of at least `1`. + image_guidance_scale (`float`, *optional*, defaults to 1.5): + Image guidance scale is to push the generated image towards the inital image `image`. Image guidance + scale is enabled by setting `image_guidance_scale > 1`. Higher image guidance scale encourages to + generate images that are closely linked to the source image `image`, usually at the expense of lower + image quality. This pipeline requires a value of at least `1`. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> import PIL + >>> import requests + >>> import torch + >>> from io import BytesIO + + >>> from diffusers import StableDiffusionInstructPix2PixPipeline + + + >>> def download_image(url): + ... response = requests.get(url) + ... return PIL.Image.open(BytesIO(response.content)).convert("RGB") + + + >>> img_url = "https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/mountain.png" + + >>> image = download_image(img_url).resize((512, 512)) + + >>> pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + ... "timbrooks/instruct-pix2pix", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> prompt = "make the mountains snowy" + >>> image = pipe(prompt=prompt, image=image).images[0] + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Check inputs + self.check_inputs(prompt, callback_steps) + + if image is None: + raise ValueError("`image` input cannot be undefined.") + + # 1. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 and image_guidance_scale >= 1.0 + # check if scheduler is in sigmas space + scheduler_is_in_sigma_space = hasattr(self.scheduler, "sigmas") + + # 2. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 3. Preprocess image + image = preprocess(image) + height, width = image.shape[-2:] + + # 4. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare Image latents + image_latents = self.prepare_image_latents( + image, + batch_size, + num_images_per_prompt, + prompt_embeds.dtype, + device, + do_classifier_free_guidance, + generator, + ) + + # 6. Prepare latent variables + num_channels_latents = self.vae.config.latent_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 7. Check that shapes of latents and image match the UNet channels + num_channels_image = image_latents.shape[1] + if num_channels_latents + num_channels_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_image`: {num_channels_image} " + f" = {num_channels_latents+num_channels_image}. Please verify the config of" + " `pipeline.unet` or your `image` input." + ) + + # 8. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 9. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # Expand the latents if we are doing classifier free guidance. + # The latents are expanded 3 times because for pix2pix the guidance\ + # is applied for both the text and the input image. + latent_model_input = torch.cat([latents] * 3) if do_classifier_free_guidance else latents + + # concat latents, image_latents in the channel dimension + scaled_latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + scaled_latent_model_input = torch.cat([scaled_latent_model_input, image_latents], dim=1) + + # predict the noise residual + noise_pred = self.unet(scaled_latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # Hack: + # For karras style schedulers the model does classifer free guidance using the + # predicted_original_sample instead of the noise_pred. So we need to compute the + # predicted_original_sample here if we are using a karras style scheduler. + if scheduler_is_in_sigma_space: + step_index = (self.scheduler.timesteps == t).nonzero().item() + sigma = self.scheduler.sigmas[step_index] + noise_pred = latent_model_input - sigma * noise_pred + + # perform guidance + if do_classifier_free_guidance: + noise_pred_text, noise_pred_image, noise_pred_uncond = noise_pred.chunk(3) + noise_pred = ( + noise_pred_uncond + + guidance_scale * (noise_pred_text - noise_pred_image) + + image_guidance_scale * (noise_pred_image - noise_pred_uncond) + ) + + # Hack: + # For karras style schedulers the model does classifer free guidance using the + # predicted_original_sample instead of the noise_pred. But the scheduler.step function + # expects the noise_pred and computes the predicted_original_sample internally. So we + # need to overwrite the noise_pred here such that the value of the computed + # predicted_original_sample is correct. + if scheduler_is_in_sigma_space: + noise_pred = (noise_pred - latents) / (-sigma) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 10. Post-processing + image = self.decode_latents(latents) + + # 11. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 12. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_ prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + # pix2pix has two negative embeddings, and unlike in other pipelines latents are ordered [prompt_embeds, negative_prompt_embeds, negative_prompt_embeds] + prompt_embeds = torch.cat([prompt_embeds, negative_prompt_embeds, negative_prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def check_inputs(self, prompt, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + def prepare_image_latents( + self, image, batch_size, num_images_per_prompt, dtype, device, do_classifier_free_guidance, generator=None + ): + if not isinstance(image, (torch.Tensor, PIL.Image.Image, list)): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or list but is {type(image)}" + ) + + image = image.to(device=device, dtype=dtype) + + batch_size = batch_size * num_images_per_prompt + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if isinstance(generator, list): + image_latents = [self.vae.encode(image[i : i + 1]).latent_dist.mode() for i in range(batch_size)] + image_latents = torch.cat(image_latents, dim=0) + else: + image_latents = self.vae.encode(image).latent_dist.mode() + + if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0: + # expand image_latents for batch_size + deprecation_message = ( + f"You have passed {batch_size} text prompts (`prompt`), but only {image_latents.shape[0]} initial" + " images (`image`). Initial images are now duplicating to match the number of text prompts. Note" + " that this behavior is deprecated and will be removed in a version 1.0.0. Please make sure to update" + " your script to pass as many initial images as text prompts to suppress this warning." + ) + deprecate("len(prompt) != len(image)", "1.0.0", deprecation_message, standard_warn=False) + additional_image_per_prompt = batch_size // image_latents.shape[0] + image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0) + elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0: + raise ValueError( + f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts." + ) + else: + image_latents = torch.cat([image_latents], dim=0) + + if do_classifier_free_guidance: + uncond_image_latents = torch.zeros_like(image_latents) + image_latents = torch.cat([image_latents, image_latents, uncond_image_latents], dim=0) + + return image_latents diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_k_diffusion.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_k_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..5d4f64cc96d76afd60ebab97dba48ddc87a82dd0 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_k_diffusion.py @@ -0,0 +1,508 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import importlib +from typing import Callable, List, Optional, Union + +import torch +from k_diffusion.external import CompVisDenoiser, CompVisVDenoiser + +from ...pipelines import DiffusionPipeline +from ...schedulers import LMSDiscreteScheduler +from ...utils import is_accelerate_available, logging, randn_tensor +from . import StableDiffusionPipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class ModelWrapper: + def __init__(self, model, alphas_cumprod): + self.model = model + self.alphas_cumprod = alphas_cumprod + + def apply_model(self, *args, **kwargs): + if len(args) == 3: + encoder_hidden_states = args[-1] + args = args[:2] + if kwargs.get("cond", None) is not None: + encoder_hidden_states = kwargs.pop("cond") + return self.model(*args, encoder_hidden_states=encoder_hidden_states, **kwargs).sample + + +class StableDiffusionKDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + + + This is an experimental pipeline and is likely to change in the future. + + + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae, + text_encoder, + tokenizer, + unet, + scheduler, + safety_checker, + feature_extractor, + requires_safety_checker: bool = True, + ): + super().__init__() + + logger.info( + f"{self.__class__} is an experimntal pipeline and is likely to change in the future. We recommend to use" + " this pipeline for fast experimentation / iteration if needed, but advice to rely on existing pipelines" + " as defined in https://huggingface.co/docs/diffusers/api/schedulers#implemented-schedulers for" + " production settings." + ) + + # get correct sigmas from LMS + scheduler = LMSDiscreteScheduler.from_config(scheduler.config) + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self.register_to_config(requires_safety_checker=requires_safety_checker) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + model = ModelWrapper(unet, scheduler.alphas_cumprod) + if scheduler.prediction_type == "v_prediction": + self.k_diffusion_model = CompVisVDenoiser(model) + else: + self.k_diffusion_model = CompVisDenoiser(model) + + def set_scheduler(self, scheduler_type: str): + library = importlib.import_module("k_diffusion") + sampling = getattr(library, "sampling") + self.sampler = getattr(sampling, scheduler_type) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_sequential_cpu_offload + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + cpu_offload(cpu_offloaded_model, device) + + if self.safety_checker is not None: + cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.run_safety_checker + def run_safety_checker(self, image, device, dtype): + if self.safety_checker is not None: + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + else: + has_nsfw_concept = None + return image, has_nsfw_concept + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def check_inputs(self, prompt, height, width, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = True + if guidance_scale <= 1.0: + raise ValueError("has to use guidance_scale") + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=prompt_embeds.device) + sigmas = self.scheduler.sigmas + sigmas = sigmas.to(prompt_embeds.dtype) + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + latents = latents * sigmas[0] + self.k_diffusion_model.sigmas = self.k_diffusion_model.sigmas.to(latents.device) + self.k_diffusion_model.log_sigmas = self.k_diffusion_model.log_sigmas.to(latents.device) + + # 6. Define model function + def model_fn(x, t): + latent_model_input = torch.cat([x] * 2) + t = torch.cat([t] * 2) + + noise_pred = self.k_diffusion_model(latent_model_input, t, cond=prompt_embeds) + + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + return noise_pred + + # 7. Run k-diffusion solver + latents = self.sampler(model_fn, latents, sigmas) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image, has_nsfw_concept) + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=has_nsfw_concept) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_latent_upscale.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_latent_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..f277e5a1eb9557b9038286676f432dfd24bc6d90 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_latent_upscale.py @@ -0,0 +1,518 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +import torch.nn.functional as F +from transformers import CLIPTextModel, CLIPTokenizer + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import EulerDiscreteScheduler +from ...utils import is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_upscale.preprocess +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64 + + image = [np.array(i.resize((w, h)))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class StableDiffusionLatentUpscalePipeline(DiffusionPipeline): + r""" + Pipeline to upscale the resolution of Stable Diffusion output images by a factor of 2. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/main/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`EulerDiscreteScheduler`]. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: EulerDiscreteScheduler, + ): + super().__init__() + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + ) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt(self, prompt, device, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `list(int)`): + prompt to be encoded + device: (`torch.device`): + torch device + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_length=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + text_encoder_out = self.text_encoder( + text_input_ids.to(device), + output_hidden_states=True, + ) + text_embeddings = text_encoder_out.hidden_states[-1] + text_pooler_out = text_encoder_out.pooler_output + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_length=True, + return_tensors="pt", + ) + + uncond_encoder_out = self.text_encoder( + uncond_input.input_ids.to(device), + output_hidden_states=True, + ) + + uncond_embeddings = uncond_encoder_out.hidden_states[-1] + uncond_pooler_out = uncond_encoder_out.pooler_output + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + text_embeddings = torch.cat([uncond_embeddings, text_embeddings]) + text_pooler_out = torch.cat([uncond_pooler_out, text_pooler_out]) + + return text_embeddings, text_pooler_out + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def check_inputs(self, prompt, image, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if ( + not isinstance(image, torch.Tensor) + and not isinstance(image, PIL.Image.Image) + and not isinstance(image, list) + ): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or `list` but is {type(image)}" + ) + + # verify batch size of prompt and image are same if image is a list or tensor + if isinstance(image, list) or isinstance(image, torch.Tensor): + if isinstance(prompt, str): + batch_size = 1 + else: + batch_size = len(prompt) + if isinstance(image, list): + image_batch_size = len(image) + else: + image_batch_size = image.shape[0] if image.ndim == 4 else 1 + if batch_size != image_batch_size: + raise ValueError( + f"`prompt` has batch size {batch_size} and `image` has batch size {image_batch_size}." + " Please make sure that passed `prompt` matches the batch size of `image`." + ) + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_upscale.StableDiffusionUpscalePipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height, width) + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + image: Union[torch.FloatTensor, PIL.Image.Image, List[PIL.Image.Image]], + num_inference_steps: int = 75, + guidance_scale: float = 9.0, + negative_prompt: Optional[Union[str, List[str]]] = None, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image upscaling. + image (`PIL.Image.Image` or List[`PIL.Image.Image`] or `torch.FloatTensor`): + `Image`, or tensor representing an image batch which will be upscaled. If it's a tensor, it can be + either a latent output from a stable diffusion model, or an image tensor in the range `[-1, 1]`. It + will be considered a `latent` if `image.shape[1]` is `4`; otherwise, it will be considered to be an + image representation and encoded using this pipeline's `vae` encoder. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + ```py + >>> from diffusers import StableDiffusionLatentUpscalePipeline, StableDiffusionPipeline + >>> import torch + + + >>> pipeline = StableDiffusionPipeline.from_pretrained( + ... "CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16 + ... ) + >>> pipeline.to("cuda") + + >>> model_id = "stabilityai/sd-x2-latent-upscaler" + >>> upscaler = StableDiffusionLatentUpscalePipeline.from_pretrained(model_id, torch_dtype=torch.float16) + >>> upscaler.to("cuda") + + >>> prompt = "a photo of an astronaut high resolution, unreal engine, ultra realistic" + >>> generator = torch.manual_seed(33) + + >>> low_res_latents = pipeline(prompt, generator=generator, output_type="latent").images + + >>> with torch.no_grad(): + ... image = pipeline.decode_latents(low_res_latents) + >>> image = pipeline.numpy_to_pil(image)[0] + + >>> image.save("../images/a1.png") + + >>> upscaled_image = upscaler( + ... prompt=prompt, + ... image=low_res_latents, + ... num_inference_steps=20, + ... guidance_scale=0, + ... generator=generator, + ... ).images[0] + + >>> upscaled_image.save("../images/a2.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + # 1. Check inputs + self.check_inputs(prompt, image, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + if guidance_scale == 0: + prompt = [""] * batch_size + + # 3. Encode input prompt + text_embeddings, text_pooler_out = self._encode_prompt( + prompt, device, do_classifier_free_guidance, negative_prompt + ) + + # 4. Preprocess image + image = preprocess(image) + image = image.to(dtype=text_embeddings.dtype, device=device) + if image.shape[1] == 3: + # encode image if not in latent-space yet + image = self.vae.encode(image).latent_dist.sample() * self.vae.config.scaling_factor + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + batch_multiplier = 2 if do_classifier_free_guidance else 1 + image = image[None, :] if image.ndim == 3 else image + image = torch.cat([image] * batch_multiplier) + + # 5. Add noise to image (set to be 0): + # (see below notes from the author): + # "the This step theoretically can make the model work better on out-of-distribution inputs, but mostly just seems to make it match the input less, so it's turned off by default." + noise_level = torch.tensor([0.0], dtype=torch.float32, device=device) + noise_level = torch.cat([noise_level] * image.shape[0]) + inv_noise_level = (noise_level**2 + 1) ** (-0.5) + + image_cond = F.interpolate(image, scale_factor=2, mode="nearest") * inv_noise_level[:, None, None, None] + image_cond = image_cond.to(text_embeddings.dtype) + + noise_level_embed = torch.cat( + [ + torch.ones(text_pooler_out.shape[0], 64, dtype=text_pooler_out.dtype, device=device), + torch.zeros(text_pooler_out.shape[0], 64, dtype=text_pooler_out.dtype, device=device), + ], + dim=1, + ) + + timestep_condition = torch.cat([noise_level_embed, text_pooler_out], dim=1) + + # 6. Prepare latent variables + height, width = image.shape[2:] + num_channels_latents = self.vae.config.latent_channels + latents = self.prepare_latents( + batch_size, + num_channels_latents, + height * 2, # 2x upscale + width * 2, + text_embeddings.dtype, + device, + generator, + latents, + ) + + # 7. Check that sizes of image and latents match + num_channels_image = image.shape[1] + if num_channels_latents + num_channels_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_image`: {num_channels_image} " + f" = {num_channels_latents+num_channels_image}. Please verify the config of" + " `pipeline.unet` or your `image` input." + ) + + # 9. Denoising loop + num_warmup_steps = 0 + + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + sigma = self.scheduler.sigmas[i] + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + scaled_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + scaled_model_input = torch.cat([scaled_model_input, image_cond], dim=1) + # preconditioning parameter based on Karras et al. (2022) (table 1) + timestep = torch.log(sigma) * 0.25 + + noise_pred = self.unet( + scaled_model_input, + timestep, + encoder_hidden_states=text_embeddings, + timestep_cond=timestep_condition, + ).sample + + # in original repo, the output contains a variance channel that's not used + noise_pred = noise_pred[:, :-1] + + # apply preconditioning, based on table 1 in Karras et al. (2022) + inv_sigma = 1 / (sigma**2 + 1) + noise_pred = inv_sigma * latent_model_input + self.scheduler.scale_model_input(sigma, t) * noise_pred + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 10. Post-processing + image = self.decode_latents(latents) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_upscale.py b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..07714d2c484ed86134fe8a023d5fb243769d719d --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_upscale.py @@ -0,0 +1,593 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +from transformers import CLIPTextModel, CLIPTokenizer + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import DDPMScheduler, KarrasDiffusionSchedulers +from ...utils import deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def preprocess(image): + if isinstance(image, torch.Tensor): + return image + elif isinstance(image, PIL.Image.Image): + image = [image] + + if isinstance(image[0], PIL.Image.Image): + w, h = image[0].size + w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64 + + image = [np.array(i.resize((w, h)))[None, :] for i in image] + image = np.concatenate(image, axis=0) + image = np.array(image).astype(np.float32) / 255.0 + image = image.transpose(0, 3, 1, 2) + image = 2.0 * image - 1.0 + image = torch.from_numpy(image) + elif isinstance(image[0], torch.Tensor): + image = torch.cat(image, dim=0) + return image + + +class StableDiffusionUpscalePipeline(DiffusionPipeline): + r""" + Pipeline for text-guided image super-resolution using Stable Diffusion 2. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + low_res_scheduler ([`SchedulerMixin`]): + A scheduler used to add initial noise to the low res conditioning image. It must be an instance of + [`DDPMScheduler`]. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + low_res_scheduler: DDPMScheduler, + scheduler: KarrasDiffusionSchedulers, + max_noise_level: int = 350, + ): + super().__init__() + + # check if vae has a config attribute `scaling_factor` and if it is set to 0.08333, else set it to 0.08333 and deprecate + is_vae_scaling_factor_set_to_0_08333 = ( + hasattr(vae.config, "scaling_factor") and vae.config.scaling_factor == 0.08333 + ) + if not is_vae_scaling_factor_set_to_0_08333: + deprecation_message = ( + "The configuration file of the vae does not contain `scaling_factor` or it is set to" + f" {vae.config.scaling_factor}, which seems highly unlikely. If your checkpoint is a fine-tuned" + " version of `stabilityai/stable-diffusion-x4-upscaler` you should change 'scaling_factor' to 0.08333" + " Please make sure to update the config accordingly, as not doing so might lead to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be" + " very nice if you could open a Pull Request for the `vae/config.json` file" + ) + deprecate("wrong scaling_factor", "1.0.0", deprecation_message, standard_warn=False) + vae.register_to_config(scaling_factor=0.08333) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + low_res_scheduler=low_res_scheduler, + scheduler=scheduler, + ) + self.register_to_config(max_noise_level=max_noise_level) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.unet, self.text_encoder]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt=None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`, *optional*): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. + Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + """ + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + if prompt_embeds is None: + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + bs_embed, seq_len, _ = prompt_embeds.shape + # duplicate text embeddings for each generation per prompt, using mps friendly method + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance and negative_prompt_embeds is None: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = prompt_embeds.shape[1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + if do_classifier_free_guidance: + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + + negative_prompt_embeds = negative_prompt_embeds.to(dtype=self.text_encoder.dtype, device=device) + + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + def check_inputs(self, prompt, image, noise_level, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if ( + not isinstance(image, torch.Tensor) + and not isinstance(image, PIL.Image.Image) + and not isinstance(image, list) + ): + raise ValueError( + f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or `list` but is {type(image)}" + ) + + # verify batch size of prompt and image are same if image is a list or tensor + if isinstance(image, list) or isinstance(image, torch.Tensor): + if isinstance(prompt, str): + batch_size = 1 + else: + batch_size = len(prompt) + if isinstance(image, list): + image_batch_size = len(image) + else: + image_batch_size = image.shape[0] + if batch_size != image_batch_size: + raise ValueError( + f"`prompt` has batch size {batch_size} and `image` has batch size {image_batch_size}." + " Please make sure that passed `prompt` matches the batch size of `image`." + ) + + # check noise level + if noise_level > self.config.max_noise_level: + raise ValueError(f"`noise_level` has to be <= {self.config.max_noise_level} but is {noise_level}") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height, width) + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: Union[torch.FloatTensor, PIL.Image.Image, List[PIL.Image.Image]] = None, + num_inference_steps: int = 75, + guidance_scale: float = 9.0, + noise_level: int = 20, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + prompt_embeds: Optional[torch.FloatTensor] = None, + negative_prompt_embeds: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`, *optional*): + The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`. + instead. + image (`PIL.Image.Image` or List[`PIL.Image.Image`] or `torch.FloatTensor`): + `Image`, or tensor representing an image batch which will be upscaled. * + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. If not defined, one has to pass + `negative_prompt_embeds`. instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` + is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not + provided, text embeddings will be generated from `prompt` input argument. + negative_prompt_embeds (`torch.FloatTensor`, *optional*): + Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt + weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input + argument. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + ```py + >>> import requests + >>> from PIL import Image + >>> from io import BytesIO + >>> from diffusers import StableDiffusionUpscalePipeline + >>> import torch + + >>> # load model and scheduler + >>> model_id = "stabilityai/stable-diffusion-x4-upscaler" + >>> pipeline = StableDiffusionUpscalePipeline.from_pretrained( + ... model_id, revision="fp16", torch_dtype=torch.float16 + ... ) + >>> pipeline = pipeline.to("cuda") + + >>> # let's download an image + >>> url = "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-upscale/low_res_cat.png" + >>> response = requests.get(url) + >>> low_res_img = Image.open(BytesIO(response.content)).convert("RGB") + >>> low_res_img = low_res_img.resize((128, 128)) + >>> prompt = "a white cat" + + >>> upscaled_image = pipeline(prompt=prompt, image=low_res_img).images[0] + >>> upscaled_image.save("upsampled_cat.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + + # 1. Check inputs + self.check_inputs(prompt, image, noise_level, callback_steps) + + if image is None: + raise ValueError("`image` input cannot be undefined.") + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + ) + + # 4. Preprocess image + image = preprocess(image) + image = image.to(dtype=prompt_embeds.dtype, device=device) + + # 5. set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Add noise to image + noise_level = torch.tensor([noise_level], dtype=torch.long, device=device) + noise = randn_tensor(image.shape, generator=generator, device=device, dtype=prompt_embeds.dtype) + image = self.low_res_scheduler.add_noise(image, noise, noise_level) + + batch_multiplier = 2 if do_classifier_free_guidance else 1 + image = torch.cat([image] * batch_multiplier * num_images_per_prompt) + noise_level = torch.cat([noise_level] * image.shape[0]) + + # 6. Prepare latent variables + height, width = image.shape[2:] + num_channels_latents = self.vae.config.latent_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 7. Check that sizes of image and latents match + num_channels_image = image.shape[1] + if num_channels_latents + num_channels_image != self.unet.config.in_channels: + raise ValueError( + f"Incorrect configuration settings! The config of `pipeline.unet`: {self.unet.config} expects" + f" {self.unet.config.in_channels} but received `num_channels_latents`: {num_channels_latents} +" + f" `num_channels_image`: {num_channels_image} " + f" = {num_channels_latents+num_channels_image}. Please verify the config of" + " `pipeline.unet` or your `image` input." + ) + + # 8. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 9. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + + # concat latents, mask, masked_image_latents in the channel dimension + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + latent_model_input = torch.cat([latent_model_input, image], dim=1) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input, t, encoder_hidden_states=prompt_embeds, class_labels=noise_level + ).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 10. Post-processing + # make sure the VAE is in float32 mode, as it overflows in float16 + self.vae.to(dtype=torch.float32) + image = self.decode_latents(latents.float()) + + # 11. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker.py b/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker.py new file mode 100644 index 0000000000000000000000000000000000000000..2e20c31b6466fb2fb534204f5e4392f156527e62 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker.py @@ -0,0 +1,122 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import numpy as np +import torch +import torch.nn as nn +from transformers import CLIPConfig, CLIPVisionModel, PreTrainedModel + +from ...utils import logging + + +logger = logging.get_logger(__name__) + + +def cosine_distance(image_embeds, text_embeds): + normalized_image_embeds = nn.functional.normalize(image_embeds) + normalized_text_embeds = nn.functional.normalize(text_embeds) + return torch.mm(normalized_image_embeds, normalized_text_embeds.t()) + + +class StableDiffusionSafetyChecker(PreTrainedModel): + config_class = CLIPConfig + + _no_split_modules = ["CLIPEncoderLayer"] + + def __init__(self, config: CLIPConfig): + super().__init__(config) + + self.vision_model = CLIPVisionModel(config.vision_config) + self.visual_projection = nn.Linear(config.vision_config.hidden_size, config.projection_dim, bias=False) + + self.concept_embeds = nn.Parameter(torch.ones(17, config.projection_dim), requires_grad=False) + self.special_care_embeds = nn.Parameter(torch.ones(3, config.projection_dim), requires_grad=False) + + self.concept_embeds_weights = nn.Parameter(torch.ones(17), requires_grad=False) + self.special_care_embeds_weights = nn.Parameter(torch.ones(3), requires_grad=False) + + @torch.no_grad() + def forward(self, clip_input, images): + pooled_output = self.vision_model(clip_input)[1] # pooled_output + image_embeds = self.visual_projection(pooled_output) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + special_cos_dist = cosine_distance(image_embeds, self.special_care_embeds).cpu().float().numpy() + cos_dist = cosine_distance(image_embeds, self.concept_embeds).cpu().float().numpy() + + result = [] + batch_size = image_embeds.shape[0] + for i in range(batch_size): + result_img = {"special_scores": {}, "special_care": [], "concept_scores": {}, "bad_concepts": []} + + # increase this value to create a stronger `nfsw` filter + # at the cost of increasing the possibility of filtering benign images + adjustment = 0.0 + + for concept_idx in range(len(special_cos_dist[0])): + concept_cos = special_cos_dist[i][concept_idx] + concept_threshold = self.special_care_embeds_weights[concept_idx].item() + result_img["special_scores"][concept_idx] = round(concept_cos - concept_threshold + adjustment, 3) + if result_img["special_scores"][concept_idx] > 0: + result_img["special_care"].append({concept_idx, result_img["special_scores"][concept_idx]}) + adjustment = 0.01 + + for concept_idx in range(len(cos_dist[0])): + concept_cos = cos_dist[i][concept_idx] + concept_threshold = self.concept_embeds_weights[concept_idx].item() + result_img["concept_scores"][concept_idx] = round(concept_cos - concept_threshold + adjustment, 3) + if result_img["concept_scores"][concept_idx] > 0: + result_img["bad_concepts"].append(concept_idx) + + result.append(result_img) + + has_nsfw_concepts = [len(res["bad_concepts"]) > 0 for res in result] + + for idx, has_nsfw_concept in enumerate(has_nsfw_concepts): + if has_nsfw_concept: + images[idx] = np.zeros(images[idx].shape) # black image + + if any(has_nsfw_concepts): + logger.warning( + "Potential NSFW content was detected in one or more images. A black image will be returned instead." + " Try again with a different prompt and/or seed." + ) + + return images, has_nsfw_concepts + + @torch.no_grad() + def forward_onnx(self, clip_input: torch.FloatTensor, images: torch.FloatTensor): + pooled_output = self.vision_model(clip_input)[1] # pooled_output + image_embeds = self.visual_projection(pooled_output) + + special_cos_dist = cosine_distance(image_embeds, self.special_care_embeds) + cos_dist = cosine_distance(image_embeds, self.concept_embeds) + + # increase this value to create a stronger `nsfw` filter + # at the cost of increasing the possibility of filtering benign images + adjustment = 0.0 + + special_scores = special_cos_dist - self.special_care_embeds_weights + adjustment + # special_scores = special_scores.round(decimals=3) + special_care = torch.any(special_scores > 0, dim=1) + special_adjustment = special_care * 0.01 + special_adjustment = special_adjustment.unsqueeze(1).expand(-1, cos_dist.shape[1]) + + concept_scores = (cos_dist - self.concept_embeds_weights) + special_adjustment + # concept_scores = concept_scores.round(decimals=3) + has_nsfw_concepts = torch.any(concept_scores > 0, dim=1) + + images[has_nsfw_concepts] = 0.0 # black image + + return images, has_nsfw_concepts diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker_flax.py b/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..71b7306134a5c7818a62689b96680a356d01d864 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion/safety_checker_flax.py @@ -0,0 +1,112 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import Optional, Tuple + +import jax +import jax.numpy as jnp +from flax import linen as nn +from flax.core.frozen_dict import FrozenDict +from transformers import CLIPConfig, FlaxPreTrainedModel +from transformers.models.clip.modeling_flax_clip import FlaxCLIPVisionModule + + +def jax_cosine_distance(emb_1, emb_2, eps=1e-12): + norm_emb_1 = jnp.divide(emb_1.T, jnp.clip(jnp.linalg.norm(emb_1, axis=1), a_min=eps)).T + norm_emb_2 = jnp.divide(emb_2.T, jnp.clip(jnp.linalg.norm(emb_2, axis=1), a_min=eps)).T + return jnp.matmul(norm_emb_1, norm_emb_2.T) + + +class FlaxStableDiffusionSafetyCheckerModule(nn.Module): + config: CLIPConfig + dtype: jnp.dtype = jnp.float32 + + def setup(self): + self.vision_model = FlaxCLIPVisionModule(self.config.vision_config) + self.visual_projection = nn.Dense(self.config.projection_dim, use_bias=False, dtype=self.dtype) + + self.concept_embeds = self.param("concept_embeds", jax.nn.initializers.ones, (17, self.config.projection_dim)) + self.special_care_embeds = self.param( + "special_care_embeds", jax.nn.initializers.ones, (3, self.config.projection_dim) + ) + + self.concept_embeds_weights = self.param("concept_embeds_weights", jax.nn.initializers.ones, (17,)) + self.special_care_embeds_weights = self.param("special_care_embeds_weights", jax.nn.initializers.ones, (3,)) + + def __call__(self, clip_input): + pooled_output = self.vision_model(clip_input)[1] + image_embeds = self.visual_projection(pooled_output) + + special_cos_dist = jax_cosine_distance(image_embeds, self.special_care_embeds) + cos_dist = jax_cosine_distance(image_embeds, self.concept_embeds) + + # increase this value to create a stronger `nfsw` filter + # at the cost of increasing the possibility of filtering benign image inputs + adjustment = 0.0 + + special_scores = special_cos_dist - self.special_care_embeds_weights[None, :] + adjustment + special_scores = jnp.round(special_scores, 3) + is_special_care = jnp.any(special_scores > 0, axis=1, keepdims=True) + # Use a lower threshold if an image has any special care concept + special_adjustment = is_special_care * 0.01 + + concept_scores = cos_dist - self.concept_embeds_weights[None, :] + special_adjustment + concept_scores = jnp.round(concept_scores, 3) + has_nsfw_concepts = jnp.any(concept_scores > 0, axis=1) + + return has_nsfw_concepts + + +class FlaxStableDiffusionSafetyChecker(FlaxPreTrainedModel): + config_class = CLIPConfig + main_input_name = "clip_input" + module_class = FlaxStableDiffusionSafetyCheckerModule + + def __init__( + self, + config: CLIPConfig, + input_shape: Optional[Tuple] = None, + seed: int = 0, + dtype: jnp.dtype = jnp.float32, + _do_init: bool = True, + **kwargs, + ): + if input_shape is None: + input_shape = (1, 224, 224, 3) + module = self.module_class(config=config, dtype=dtype, **kwargs) + super().__init__(config, module, input_shape=input_shape, seed=seed, dtype=dtype, _do_init=_do_init) + + def init_weights(self, rng: jax.random.KeyArray, input_shape: Tuple, params: FrozenDict = None) -> FrozenDict: + # init input tensor + clip_input = jax.random.normal(rng, input_shape) + + params_rng, dropout_rng = jax.random.split(rng) + rngs = {"params": params_rng, "dropout": dropout_rng} + + random_params = self.module.init(rngs, clip_input)["params"] + + return random_params + + def __call__( + self, + clip_input, + params: dict = None, + ): + clip_input = jnp.transpose(clip_input, (0, 2, 3, 1)) + + return self.module.apply( + {"params": params or self.params}, + jnp.array(clip_input, dtype=jnp.float32), + rngs={}, + ) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion_safe/__init__.py b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5aecfeac112e53b2fc49278c1acaa95a6c0c7257 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/__init__.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional, Union + +import numpy as np +import PIL +from PIL import Image + +from ...utils import BaseOutput, is_torch_available, is_transformers_available + + +@dataclass +class SafetyConfig(object): + WEAK = { + "sld_warmup_steps": 15, + "sld_guidance_scale": 20, + "sld_threshold": 0.0, + "sld_momentum_scale": 0.0, + "sld_mom_beta": 0.0, + } + MEDIUM = { + "sld_warmup_steps": 10, + "sld_guidance_scale": 1000, + "sld_threshold": 0.01, + "sld_momentum_scale": 0.3, + "sld_mom_beta": 0.4, + } + STRONG = { + "sld_warmup_steps": 7, + "sld_guidance_scale": 2000, + "sld_threshold": 0.025, + "sld_momentum_scale": 0.5, + "sld_mom_beta": 0.7, + } + MAX = { + "sld_warmup_steps": 0, + "sld_guidance_scale": 5000, + "sld_threshold": 1.0, + "sld_momentum_scale": 0.5, + "sld_mom_beta": 0.7, + } + + +@dataclass +class StableDiffusionSafePipelineOutput(BaseOutput): + """ + Output class for Safe Stable Diffusion pipelines. + + Args: + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width, + num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline. + nsfw_content_detected (`List[bool]`) + List of flags denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, or `None` if safety checking could not be performed. + images (`List[PIL.Image.Image]` or `np.ndarray`) + List of denoised PIL images that were flagged by the safety checker any may contain "not-safe-for-work" + (nsfw) content, or `None` if no safety check was performed or no images were flagged. + applied_safety_concept (`str`) + The safety concept that was applied for safety guidance, or `None` if safety guidance was disabled + """ + + images: Union[List[PIL.Image.Image], np.ndarray] + nsfw_content_detected: Optional[List[bool]] + unsafe_images: Optional[Union[List[PIL.Image.Image], np.ndarray]] + applied_safety_concept: Optional[str] + + +if is_transformers_available() and is_torch_available(): + from .pipeline_stable_diffusion_safe import StableDiffusionPipelineSafe + from .safety_checker import SafeStableDiffusionSafetyChecker diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion_safe/pipeline_stable_diffusion_safe.py b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/pipeline_stable_diffusion_safe.py new file mode 100644 index 0000000000000000000000000000000000000000..e27cab7a92966a82dc7d425b72870779c1646900 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/pipeline_stable_diffusion_safe.py @@ -0,0 +1,736 @@ +import inspect +import warnings +from typing import Callable, List, Optional, Union + +import numpy as np +import torch +from packaging import version +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import FrozenDict +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import deprecate, is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline +from . import StableDiffusionSafePipelineOutput +from .safety_checker import SafeStableDiffusionSafetyChecker + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class StableDiffusionPipelineSafe(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Safe Latent Diffusion. + + The implementation is based on the [`StableDiffusionPipeline`] + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + _optional_components = ["safety_checker", "feature_extractor"] + + def __init__( + self, + vae: AutoencoderKL, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + unet: UNet2DConditionModel, + scheduler: KarrasDiffusionSchedulers, + safety_checker: SafeStableDiffusionSafetyChecker, + feature_extractor: CLIPFeatureExtractor, + requires_safety_checker: bool = True, + ): + super().__init__() + safety_concept: Optional[str] = ( + "an image showing hate, harassment, violence, suffering, humiliation, harm, suicide, sexual, nudity," + " bodily fluids, blood, obscene gestures, illegal activity, drug use, theft, vandalism, weapons, child" + " abuse, brutality, cruelty" + ) + + if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`" + f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " + "to update the config accordingly as leaving `steps_offset` might led to incorrect results" + " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub," + " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`" + " file" + ) + deprecate("steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["steps_offset"] = 1 + scheduler._internal_dict = FrozenDict(new_config) + + if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: + deprecation_message = ( + f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`." + " `clip_sample` should be set to False in the configuration file. Please make sure to update the" + " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in" + " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very" + " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file" + ) + deprecate("clip_sample not set", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(scheduler.config) + new_config["clip_sample"] = False + scheduler._internal_dict = FrozenDict(new_config) + + if safety_checker is None and requires_safety_checker: + logger.warning( + f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`. Ensure" + " that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered" + " results in services or applications open to the public. Both the diffusers team and Hugging Face" + " strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling" + " it only for use-cases that involve analyzing network behavior or auditing its results. For more" + " information, please have a look at https://github.com/huggingface/diffusers/pull/254 ." + ) + + if safety_checker is not None and feature_extractor is None: + raise ValueError( + "Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety" + " checker. If you do not want to use the safety checker, you can pass `'safety_checker=None'` instead." + ) + + is_unet_version_less_0_9_0 = hasattr(unet.config, "_diffusers_version") and version.parse( + version.parse(unet.config._diffusers_version).base_version + ) < version.parse("0.9.0.dev0") + is_unet_sample_size_less_64 = hasattr(unet.config, "sample_size") and unet.config.sample_size < 64 + if is_unet_version_less_0_9_0 and is_unet_sample_size_less_64: + deprecation_message = ( + "The configuration file of the unet has set the default `sample_size` to smaller than" + " 64 which seems highly unlikely .If you're checkpoint is a fine-tuned version of any of the" + " following: \n- CompVis/stable-diffusion-v1-4 \n- CompVis/stable-diffusion-v1-3 \n-" + " CompVis/stable-diffusion-v1-2 \n- CompVis/stable-diffusion-v1-1 \n- runwayml/stable-diffusion-v1-5" + " \n- runwayml/stable-diffusion-inpainting \n you should change 'sample_size' to 64 in the" + " configuration file. Please make sure to update the config accordingly as leaving `sample_size=32`" + " in the config might lead to incorrect results in future versions. If you have downloaded this" + " checkpoint from the Hugging Face Hub, it would be very nice if you could open a Pull request for" + " the `unet/config.json` file" + ) + deprecate("sample_size<64", "1.0.0", deprecation_message, standard_warn=False) + new_config = dict(unet.config) + new_config["sample_size"] = 64 + unet._internal_dict = FrozenDict(new_config) + + self.register_modules( + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + unet=unet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + self._safety_text_concept = safety_concept + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.register_to_config(requires_safety_checker=requires_safety_checker) + + @property + def safety_concept(self): + r""" + Getter method for the safety concept used with SLD + + Returns: + `str`: The text describing the safety concept + """ + return self._safety_text_concept + + @safety_concept.setter + def safety_concept(self, concept): + r""" + Setter method for the safety concept used with SLD + + Args: + concept (`str`): + The text of the new safety concept + """ + self._safety_text_concept = concept + + def enable_sequential_cpu_offload(self): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device("cuda") + + for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae, self.safety_checker]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"): + return self.device + for module in self.unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + negative_prompt, + enable_safety_guidance, + ): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="pt").input_ids + + if not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = prompt_embeds[0] + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = prompt_embeds.shape + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = negative_prompt_embeds[0] + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # Encode the safety concept text + if enable_safety_guidance: + safety_concept_input = self.tokenizer( + [self._safety_text_concept], + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + safety_embeddings = self.text_encoder(safety_concept_input.input_ids.to(self.device))[0] + + # duplicate safety embeddings for each generation per prompt, using mps friendly method + seq_len = safety_embeddings.shape[1] + safety_embeddings = safety_embeddings.repeat(batch_size, num_images_per_prompt, 1) + safety_embeddings = safety_embeddings.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance + sld, we need to do three forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing three forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds, safety_embeddings]) + + else: + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def run_safety_checker(self, image, device, dtype, enable_safety_guidance): + if self.safety_checker is not None: + images = image.copy() + safety_checker_input = self.feature_extractor(self.numpy_to_pil(image), return_tensors="pt").to(device) + image, has_nsfw_concept = self.safety_checker( + images=image, clip_input=safety_checker_input.pixel_values.to(dtype) + ) + flagged_images = np.zeros((2, *image.shape[1:])) + if any(has_nsfw_concept): + logger.warning( + "Potential NSFW content was detected in one or more images. A black image will be returned" + " instead." + f"{'You may look at this images in the `unsafe_images` variable of the output at your own discretion.' if enable_safety_guidance else 'Try again with a different prompt and/or seed.'}" + ) + for idx, has_nsfw_concept in enumerate(has_nsfw_concept): + if has_nsfw_concept: + flagged_images[idx] = images[idx] + image[idx] = np.zeros(image[idx].shape) # black image + else: + has_nsfw_concept = None + flagged_images = None + return image, has_nsfw_concept, flagged_images + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.check_inputs + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + def perform_safety_guidance( + self, + enable_safety_guidance, + safety_momentum, + noise_guidance, + noise_pred_out, + i, + sld_guidance_scale, + sld_warmup_steps, + sld_threshold, + sld_momentum_scale, + sld_mom_beta, + ): + # Perform SLD guidance + if enable_safety_guidance: + if safety_momentum is None: + safety_momentum = torch.zeros_like(noise_guidance) + noise_pred_text, noise_pred_uncond = noise_pred_out[0], noise_pred_out[1] + noise_pred_safety_concept = noise_pred_out[2] + + # Equation 6 + scale = torch.clamp(torch.abs((noise_pred_text - noise_pred_safety_concept)) * sld_guidance_scale, max=1.0) + + # Equation 6 + safety_concept_scale = torch.where( + (noise_pred_text - noise_pred_safety_concept) >= sld_threshold, torch.zeros_like(scale), scale + ) + + # Equation 4 + noise_guidance_safety = torch.mul((noise_pred_safety_concept - noise_pred_uncond), safety_concept_scale) + + # Equation 7 + noise_guidance_safety = noise_guidance_safety + sld_momentum_scale * safety_momentum + + # Equation 8 + safety_momentum = sld_mom_beta * safety_momentum + (1 - sld_mom_beta) * noise_guidance_safety + + if i >= sld_warmup_steps: # Warmup + # Equation 3 + noise_guidance = noise_guidance - noise_guidance_safety + return noise_guidance, safety_momentum + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + sld_guidance_scale: Optional[float] = 1000, + sld_warmup_steps: Optional[int] = 10, + sld_threshold: Optional[float] = 0.01, + sld_momentum_scale: Optional[float] = 0.3, + sld_mom_beta: Optional[float] = 0.4, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + sld_guidance_scale (`float`, *optional*, defaults to 1000): + Safe latent guidance as defined in [Safe Latent Diffusion](https://arxiv.org/abs/2211.05105). + `sld_guidance_scale` is defined as sS of Eq. 6. If set to be less than 1, safety guidance will be + disabled. + sld_warmup_steps (`int`, *optional*, defaults to 10): + Number of warmup steps for safety guidance. SLD will only be applied for diffusion steps greater than + `sld_warmup_steps`. `sld_warmup_steps` is defined as `delta` of [Safe Latent + Diffusion](https://arxiv.org/abs/2211.05105). + sld_threshold (`float`, *optional*, defaults to 0.01): + Threshold that separates the hyperplane between appropriate and inappropriate images. `sld_threshold` + is defined as `lamda` of Eq. 5 in [Safe Latent Diffusion](https://arxiv.org/abs/2211.05105). + sld_momentum_scale (`float`, *optional*, defaults to 0.3): + Scale of the SLD momentum to be added to the safety guidance at each diffusion step. If set to 0.0 + momentum will be disabled. Momentum is already built up during warmup, i.e. for diffusion steps smaller + than `sld_warmup_steps`. `sld_momentum_scale` is defined as `sm` of Eq. 7 in [Safe Latent + Diffusion](https://arxiv.org/abs/2211.05105). + sld_mom_beta (`float`, *optional*, defaults to 0.4): + Defines how safety guidance momentum builds up. `sld_mom_beta` indicates how much of the previous + momentum will be kept. Momentum is already built up during warmup, i.e. for diffusion steps smaller + than `sld_warmup_steps`. `sld_mom_beta` is defined as `beta m` of Eq. 8 in [Safe Latent + Diffusion](https://arxiv.org/abs/2211.05105). + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.unet.config.sample_size * self.vae_scale_factor + width = width or self.unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + enable_safety_guidance = sld_guidance_scale > 1.0 and do_classifier_free_guidance + if not enable_safety_guidance: + warnings.warn("Safety checker disabled!") + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt, enable_safety_guidance + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + safety_momentum = None + + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + # expand the latents if we are doing classifier free guidance + latent_model_input = ( + torch.cat([latents] * (3 if enable_safety_guidance else 2)) + if do_classifier_free_guidance + else latents + ) + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_out = noise_pred.chunk((3 if enable_safety_guidance else 2)) + noise_pred_uncond, noise_pred_text = noise_pred_out[0], noise_pred_out[1] + + # default classifier free guidance + noise_guidance = noise_pred_text - noise_pred_uncond + + # Perform SLD guidance + if enable_safety_guidance: + if safety_momentum is None: + safety_momentum = torch.zeros_like(noise_guidance) + noise_pred_safety_concept = noise_pred_out[2] + + # Equation 6 + scale = torch.clamp( + torch.abs((noise_pred_text - noise_pred_safety_concept)) * sld_guidance_scale, max=1.0 + ) + + # Equation 6 + safety_concept_scale = torch.where( + (noise_pred_text - noise_pred_safety_concept) >= sld_threshold, + torch.zeros_like(scale), + scale, + ) + + # Equation 4 + noise_guidance_safety = torch.mul( + (noise_pred_safety_concept - noise_pred_uncond), safety_concept_scale + ) + + # Equation 7 + noise_guidance_safety = noise_guidance_safety + sld_momentum_scale * safety_momentum + + # Equation 8 + safety_momentum = sld_mom_beta * safety_momentum + (1 - sld_mom_beta) * noise_guidance_safety + + if i >= sld_warmup_steps: # Warmup + # Equation 3 + noise_guidance = noise_guidance - noise_guidance_safety + + noise_pred = noise_pred_uncond + guidance_scale * noise_guidance + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Run safety checker + image, has_nsfw_concept, flagged_images = self.run_safety_checker( + image, device, prompt_embeds.dtype, enable_safety_guidance + ) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + if flagged_images is not None: + flagged_images = self.numpy_to_pil(flagged_images) + + if not return_dict: + return ( + image, + has_nsfw_concept, + self._safety_text_concept if enable_safety_guidance else None, + flagged_images, + ) + + return StableDiffusionSafePipelineOutput( + images=image, + nsfw_content_detected=has_nsfw_concept, + applied_safety_concept=self._safety_text_concept if enable_safety_guidance else None, + unsafe_images=flagged_images, + ) diff --git a/diffusers/src/diffusers/pipelines/stable_diffusion_safe/safety_checker.py b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/safety_checker.py new file mode 100644 index 0000000000000000000000000000000000000000..6fc11b0985472f103ecf11040e4f48285f47192e --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stable_diffusion_safe/safety_checker.py @@ -0,0 +1,109 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import torch +import torch.nn as nn +from transformers import CLIPConfig, CLIPVisionModel, PreTrainedModel + +from ...utils import logging + + +logger = logging.get_logger(__name__) + + +def cosine_distance(image_embeds, text_embeds): + normalized_image_embeds = nn.functional.normalize(image_embeds) + normalized_text_embeds = nn.functional.normalize(text_embeds) + return torch.mm(normalized_image_embeds, normalized_text_embeds.t()) + + +class SafeStableDiffusionSafetyChecker(PreTrainedModel): + config_class = CLIPConfig + + _no_split_modules = ["CLIPEncoderLayer"] + + def __init__(self, config: CLIPConfig): + super().__init__(config) + + self.vision_model = CLIPVisionModel(config.vision_config) + self.visual_projection = nn.Linear(config.vision_config.hidden_size, config.projection_dim, bias=False) + + self.concept_embeds = nn.Parameter(torch.ones(17, config.projection_dim), requires_grad=False) + self.special_care_embeds = nn.Parameter(torch.ones(3, config.projection_dim), requires_grad=False) + + self.concept_embeds_weights = nn.Parameter(torch.ones(17), requires_grad=False) + self.special_care_embeds_weights = nn.Parameter(torch.ones(3), requires_grad=False) + + @torch.no_grad() + def forward(self, clip_input, images): + pooled_output = self.vision_model(clip_input)[1] # pooled_output + image_embeds = self.visual_projection(pooled_output) + + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + special_cos_dist = cosine_distance(image_embeds, self.special_care_embeds).cpu().float().numpy() + cos_dist = cosine_distance(image_embeds, self.concept_embeds).cpu().float().numpy() + + result = [] + batch_size = image_embeds.shape[0] + for i in range(batch_size): + result_img = {"special_scores": {}, "special_care": [], "concept_scores": {}, "bad_concepts": []} + + # increase this value to create a stronger `nfsw` filter + # at the cost of increasing the possibility of filtering benign images + adjustment = 0.0 + + for concept_idx in range(len(special_cos_dist[0])): + concept_cos = special_cos_dist[i][concept_idx] + concept_threshold = self.special_care_embeds_weights[concept_idx].item() + result_img["special_scores"][concept_idx] = round(concept_cos - concept_threshold + adjustment, 3) + if result_img["special_scores"][concept_idx] > 0: + result_img["special_care"].append({concept_idx, result_img["special_scores"][concept_idx]}) + adjustment = 0.01 + + for concept_idx in range(len(cos_dist[0])): + concept_cos = cos_dist[i][concept_idx] + concept_threshold = self.concept_embeds_weights[concept_idx].item() + result_img["concept_scores"][concept_idx] = round(concept_cos - concept_threshold + adjustment, 3) + if result_img["concept_scores"][concept_idx] > 0: + result_img["bad_concepts"].append(concept_idx) + + result.append(result_img) + + has_nsfw_concepts = [len(res["bad_concepts"]) > 0 for res in result] + + return images, has_nsfw_concepts + + @torch.no_grad() + def forward_onnx(self, clip_input: torch.FloatTensor, images: torch.FloatTensor): + pooled_output = self.vision_model(clip_input)[1] # pooled_output + image_embeds = self.visual_projection(pooled_output) + + special_cos_dist = cosine_distance(image_embeds, self.special_care_embeds) + cos_dist = cosine_distance(image_embeds, self.concept_embeds) + + # increase this value to create a stronger `nsfw` filter + # at the cost of increasing the possibility of filtering benign images + adjustment = 0.0 + + special_scores = special_cos_dist - self.special_care_embeds_weights + adjustment + # special_scores = special_scores.round(decimals=3) + special_care = torch.any(special_scores > 0, dim=1) + special_adjustment = special_care * 0.01 + special_adjustment = special_adjustment.unsqueeze(1).expand(-1, cos_dist.shape[1]) + + concept_scores = (cos_dist - self.concept_embeds_weights) + special_adjustment + # concept_scores = concept_scores.round(decimals=3) + has_nsfw_concepts = torch.any(concept_scores > 0, dim=1) + + return images, has_nsfw_concepts diff --git a/diffusers/src/diffusers/pipelines/stochastic_karras_ve/__init__.py b/diffusers/src/diffusers/pipelines/stochastic_karras_ve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5a63c1d24afb2c4f36b0e284f0985a3ff508f4c7 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stochastic_karras_ve/__init__.py @@ -0,0 +1 @@ +from .pipeline_stochastic_karras_ve import KarrasVePipeline diff --git a/diffusers/src/diffusers/pipelines/stochastic_karras_ve/pipeline_stochastic_karras_ve.py b/diffusers/src/diffusers/pipelines/stochastic_karras_ve/pipeline_stochastic_karras_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..60e2a942437c794f67835390685f4ddef0c9b30d --- /dev/null +++ b/diffusers/src/diffusers/pipelines/stochastic_karras_ve/pipeline_stochastic_karras_ve.py @@ -0,0 +1,128 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import torch + +from ...models import UNet2DModel +from ...schedulers import KarrasVeScheduler +from ...utils import randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class KarrasVePipeline(DiffusionPipeline): + r""" + Stochastic sampling from Karras et al. [1] tailored to the Variance-Expanding (VE) models [2]. Use Algorithm 2 and + the VE column of Table 1 from [1] for reference. + + [1] Karras, Tero, et al. "Elucidating the Design Space of Diffusion-Based Generative Models." + https://arxiv.org/abs/2206.00364 [2] Song, Yang, et al. "Score-based generative modeling through stochastic + differential equations." https://arxiv.org/abs/2011.13456 + + Parameters: + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`KarrasVeScheduler`]): + Scheduler for the diffusion process to be used in combination with `unet` to denoise the encoded image. + """ + + # add type hints for linting + unet: UNet2DModel + scheduler: KarrasVeScheduler + + def __init__(self, unet: UNet2DModel, scheduler: KarrasVeScheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + num_inference_steps: int = 50, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[Tuple, ImagePipelineOutput]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if `return_dict` is + True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + + img_size = self.unet.config.sample_size + shape = (batch_size, 3, img_size, img_size) + + model = self.unet + + # sample x_0 ~ N(0, sigma_0^2 * I) + sample = randn_tensor(shape, generator=generator, device=self.device) * self.scheduler.init_noise_sigma + + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # here sigma_t == t_i from the paper + sigma = self.scheduler.schedule[t] + sigma_prev = self.scheduler.schedule[t - 1] if t > 0 else 0 + + # 1. Select temporarily increased noise level sigma_hat + # 2. Add new noise to move from sample_i to sample_hat + sample_hat, sigma_hat = self.scheduler.add_noise_to_input(sample, sigma, generator=generator) + + # 3. Predict the noise residual given the noise magnitude `sigma_hat` + # The model inputs and output are adjusted by following eq. (213) in [1]. + model_output = (sigma_hat / 2) * model((sample_hat + 1) / 2, sigma_hat / 2).sample + + # 4. Evaluate dx/dt at sigma_hat + # 5. Take Euler step from sigma to sigma_prev + step_output = self.scheduler.step(model_output, sigma_hat, sigma_prev, sample_hat) + + if sigma_prev != 0: + # 6. Apply 2nd order correction + # The model inputs and output are adjusted by following eq. (213) in [1]. + model_output = (sigma_prev / 2) * model((step_output.prev_sample + 1) / 2, sigma_prev / 2).sample + step_output = self.scheduler.step_correct( + model_output, + sigma_hat, + sigma_prev, + sample_hat, + step_output.prev_sample, + step_output["derivative"], + ) + sample = step_output.prev_sample + + sample = (sample / 2 + 0.5).clamp(0, 1) + image = sample.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(sample) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/unclip/__init__.py b/diffusers/src/diffusers/pipelines/unclip/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..075e66bb680aca294b36aa7ad0abb8d0f651cd92 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/unclip/__init__.py @@ -0,0 +1,17 @@ +from ...utils import ( + OptionalDependencyNotAvailable, + is_torch_available, + is_transformers_available, + is_transformers_version, +) + + +try: + if not (is_transformers_available() and is_torch_available() and is_transformers_version(">=", "4.25.0")): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ...utils.dummy_torch_and_transformers_objects import UnCLIPImageVariationPipeline, UnCLIPPipeline +else: + from .pipeline_unclip import UnCLIPPipeline + from .pipeline_unclip_image_variation import UnCLIPImageVariationPipeline + from .text_proj import UnCLIPTextProjModel diff --git a/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip.py b/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip.py new file mode 100644 index 0000000000000000000000000000000000000000..5f516e76dea6bb6d52f27f7aff5f25964dbea785 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip.py @@ -0,0 +1,534 @@ +# Copyright 2022 Kakao Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import List, Optional, Tuple, Union + +import torch +from torch.nn import functional as F +from transformers import CLIPTextModelWithProjection, CLIPTokenizer +from transformers.models.clip.modeling_clip import CLIPTextModelOutput + +from ...models import PriorTransformer, UNet2DConditionModel, UNet2DModel +from ...pipelines import DiffusionPipeline +from ...pipelines.pipeline_utils import ImagePipelineOutput +from ...schedulers import UnCLIPScheduler +from ...utils import is_accelerate_available, logging, randn_tensor +from .text_proj import UnCLIPTextProjModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class UnCLIPPipeline(DiffusionPipeline): + """ + Pipeline for text-to-image generation using unCLIP + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + text_encoder ([`CLIPTextModelWithProjection`]): + Frozen text-encoder. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + prior ([`PriorTransformer`]): + The canonincal unCLIP prior to approximate the image embedding from the text embedding. + text_proj ([`UnCLIPTextProjModel`]): + Utility class to prepare and combine the embeddings before they are passed to the decoder. + decoder ([`UNet2DConditionModel`]): + The decoder to invert the image embedding into an image. + super_res_first ([`UNet2DModel`]): + Super resolution unet. Used in all but the last step of the super resolution diffusion process. + super_res_last ([`UNet2DModel`]): + Super resolution unet. Used in the last step of the super resolution diffusion process. + prior_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the prior denoising process. Just a modified DDPMScheduler. + decoder_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the decoder denoising process. Just a modified DDPMScheduler. + super_res_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the super resolution denoising process. Just a modified DDPMScheduler. + + """ + + prior: PriorTransformer + decoder: UNet2DConditionModel + text_proj: UnCLIPTextProjModel + text_encoder: CLIPTextModelWithProjection + tokenizer: CLIPTokenizer + super_res_first: UNet2DModel + super_res_last: UNet2DModel + + prior_scheduler: UnCLIPScheduler + decoder_scheduler: UnCLIPScheduler + super_res_scheduler: UnCLIPScheduler + + def __init__( + self, + prior: PriorTransformer, + decoder: UNet2DConditionModel, + text_encoder: CLIPTextModelWithProjection, + tokenizer: CLIPTokenizer, + text_proj: UnCLIPTextProjModel, + super_res_first: UNet2DModel, + super_res_last: UNet2DModel, + prior_scheduler: UnCLIPScheduler, + decoder_scheduler: UnCLIPScheduler, + super_res_scheduler: UnCLIPScheduler, + ): + super().__init__() + + self.register_modules( + prior=prior, + decoder=decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + text_proj=text_proj, + super_res_first=super_res_first, + super_res_last=super_res_last, + prior_scheduler=prior_scheduler, + decoder_scheduler=decoder_scheduler, + super_res_scheduler=super_res_scheduler, + ) + + def prepare_latents(self, shape, dtype, device, generator, latents, scheduler): + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + latents = latents * scheduler.init_noise_sigma + return latents + + def _encode_prompt( + self, + prompt, + device, + num_images_per_prompt, + do_classifier_free_guidance, + text_model_output: Optional[Union[CLIPTextModelOutput, Tuple]] = None, + text_attention_mask: Optional[torch.Tensor] = None, + ): + if text_model_output is None: + batch_size = len(prompt) if isinstance(prompt, list) else 1 + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + text_mask = text_inputs.attention_mask.bool().to(device) + + untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal( + text_input_ids, untruncated_ids + ): + removed_text = self.tokenizer.batch_decode( + untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + + text_encoder_output = self.text_encoder(text_input_ids.to(device)) + + prompt_embeds = text_encoder_output.text_embeds + text_encoder_hidden_states = text_encoder_output.last_hidden_state + + else: + batch_size = text_model_output[0].shape[0] + prompt_embeds, text_encoder_hidden_states = text_model_output[0], text_model_output[1] + text_mask = text_attention_mask + + prompt_embeds = prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) + text_encoder_hidden_states = text_encoder_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + text_mask = text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + if do_classifier_free_guidance: + uncond_tokens = [""] * batch_size + + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + uncond_text_mask = uncond_input.attention_mask.bool().to(device) + negative_prompt_embeds_text_encoder_output = self.text_encoder(uncond_input.input_ids.to(device)) + + negative_prompt_embeds = negative_prompt_embeds_text_encoder_output.text_embeds + uncond_text_encoder_hidden_states = negative_prompt_embeds_text_encoder_output.last_hidden_state + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len) + + seq_len = uncond_text_encoder_hidden_states.shape[1] + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.repeat(1, num_images_per_prompt, 1) + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.view( + batch_size * num_images_per_prompt, seq_len, -1 + ) + uncond_text_mask = uncond_text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + # done duplicates + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + text_encoder_hidden_states = torch.cat([uncond_text_encoder_hidden_states, text_encoder_hidden_states]) + + text_mask = torch.cat([uncond_text_mask, text_mask]) + + return prompt_embeds, text_encoder_hidden_states, text_mask + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, the pipeline's + models have their state dicts saved to CPU and then are moved to a `torch.device('meta') and loaded to GPU only + when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + # TODO: self.prior.post_process_latents is not covered by the offload hooks, so it fails if added to the list + models = [ + self.decoder, + self.text_proj, + self.text_encoder, + self.super_res_first, + self.super_res_last, + ] + for cpu_offloaded_model in models: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.decoder, "_hf_hook"): + return self.device + for module in self.decoder.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + @torch.no_grad() + def __call__( + self, + prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: int = 1, + prior_num_inference_steps: int = 25, + decoder_num_inference_steps: int = 25, + super_res_num_inference_steps: int = 7, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prior_latents: Optional[torch.FloatTensor] = None, + decoder_latents: Optional[torch.FloatTensor] = None, + super_res_latents: Optional[torch.FloatTensor] = None, + text_model_output: Optional[Union[CLIPTextModelOutput, Tuple]] = None, + text_attention_mask: Optional[torch.Tensor] = None, + prior_guidance_scale: float = 4.0, + decoder_guidance_scale: float = 8.0, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ): + """ + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. This can only be left undefined if + `text_model_output` and `text_attention_mask` is passed. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + prior_num_inference_steps (`int`, *optional*, defaults to 25): + The number of denoising steps for the prior. More denoising steps usually lead to a higher quality + image at the expense of slower inference. + decoder_num_inference_steps (`int`, *optional*, defaults to 25): + The number of denoising steps for the decoder. More denoising steps usually lead to a higher quality + image at the expense of slower inference. + super_res_num_inference_steps (`int`, *optional*, defaults to 7): + The number of denoising steps for super resolution. More denoising steps usually lead to a higher + quality image at the expense of slower inference. + generator (`torch.Generator` or `List[torch.Generator]`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + prior_latents (`torch.FloatTensor` of shape (batch size, embeddings dimension), *optional*): + Pre-generated noisy latents to be used as inputs for the prior. + decoder_latents (`torch.FloatTensor` of shape (batch size, channels, height, width), *optional*): + Pre-generated noisy latents to be used as inputs for the decoder. + super_res_latents (`torch.FloatTensor` of shape (batch size, channels, super res height, super res width), *optional*): + Pre-generated noisy latents to be used as inputs for the decoder. + prior_guidance_scale (`float`, *optional*, defaults to 4.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + decoder_guidance_scale (`float`, *optional*, defaults to 4.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + text_model_output (`CLIPTextModelOutput`, *optional*): + Pre-defined CLIPTextModel outputs that can be derived from the text encoder. Pre-defined text outputs + can be passed for tasks like text embedding interpolations. Make sure to also pass + `text_attention_mask` in this case. `prompt` can the be left to `None`. + text_attention_mask (`torch.Tensor`, *optional*): + Pre-defined CLIP text attention mask that can be derived from the tokenizer. Pre-defined text attention + masks are necessary when passing `text_model_output`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + """ + if prompt is not None: + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + else: + batch_size = text_model_output[0].shape[0] + + device = self._execution_device + + batch_size = batch_size * num_images_per_prompt + + do_classifier_free_guidance = prior_guidance_scale > 1.0 or decoder_guidance_scale > 1.0 + + prompt_embeds, text_encoder_hidden_states, text_mask = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, text_model_output, text_attention_mask + ) + + # prior + + self.prior_scheduler.set_timesteps(prior_num_inference_steps, device=device) + prior_timesteps_tensor = self.prior_scheduler.timesteps + + embedding_dim = self.prior.config.embedding_dim + + prior_latents = self.prepare_latents( + (batch_size, embedding_dim), + prompt_embeds.dtype, + device, + generator, + prior_latents, + self.prior_scheduler, + ) + + for i, t in enumerate(self.progress_bar(prior_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([prior_latents] * 2) if do_classifier_free_guidance else prior_latents + + predicted_image_embedding = self.prior( + latent_model_input, + timestep=t, + proj_embedding=prompt_embeds, + encoder_hidden_states=text_encoder_hidden_states, + attention_mask=text_mask, + ).predicted_image_embedding + + if do_classifier_free_guidance: + predicted_image_embedding_uncond, predicted_image_embedding_text = predicted_image_embedding.chunk(2) + predicted_image_embedding = predicted_image_embedding_uncond + prior_guidance_scale * ( + predicted_image_embedding_text - predicted_image_embedding_uncond + ) + + if i + 1 == prior_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = prior_timesteps_tensor[i + 1] + + prior_latents = self.prior_scheduler.step( + predicted_image_embedding, + timestep=t, + sample=prior_latents, + generator=generator, + prev_timestep=prev_timestep, + ).prev_sample + + prior_latents = self.prior.post_process_latents(prior_latents) + + image_embeddings = prior_latents + + # done prior + + # decoder + + text_encoder_hidden_states, additive_clip_time_embeddings = self.text_proj( + image_embeddings=image_embeddings, + prompt_embeds=prompt_embeds, + text_encoder_hidden_states=text_encoder_hidden_states, + do_classifier_free_guidance=do_classifier_free_guidance, + ) + + if device.type == "mps": + # HACK: MPS: There is a panic when padding bool tensors, + # so cast to int tensor for the pad and back to bool afterwards + text_mask = text_mask.type(torch.int) + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=1) + decoder_text_mask = decoder_text_mask.type(torch.bool) + else: + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=True) + + self.decoder_scheduler.set_timesteps(decoder_num_inference_steps, device=device) + decoder_timesteps_tensor = self.decoder_scheduler.timesteps + + num_channels_latents = self.decoder.in_channels + height = self.decoder.sample_size + width = self.decoder.sample_size + + decoder_latents = self.prepare_latents( + (batch_size, num_channels_latents, height, width), + text_encoder_hidden_states.dtype, + device, + generator, + decoder_latents, + self.decoder_scheduler, + ) + + for i, t in enumerate(self.progress_bar(decoder_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([decoder_latents] * 2) if do_classifier_free_guidance else decoder_latents + + noise_pred = self.decoder( + sample=latent_model_input, + timestep=t, + encoder_hidden_states=text_encoder_hidden_states, + class_labels=additive_clip_time_embeddings, + attention_mask=decoder_text_mask, + ).sample + + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred_uncond, _ = noise_pred_uncond.split(latent_model_input.shape[1], dim=1) + noise_pred_text, predicted_variance = noise_pred_text.split(latent_model_input.shape[1], dim=1) + noise_pred = noise_pred_uncond + decoder_guidance_scale * (noise_pred_text - noise_pred_uncond) + noise_pred = torch.cat([noise_pred, predicted_variance], dim=1) + + if i + 1 == decoder_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = decoder_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + decoder_latents = self.decoder_scheduler.step( + noise_pred, t, decoder_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + decoder_latents = decoder_latents.clamp(-1, 1) + + image_small = decoder_latents + + # done decoder + + # super res + + self.super_res_scheduler.set_timesteps(super_res_num_inference_steps, device=device) + super_res_timesteps_tensor = self.super_res_scheduler.timesteps + + channels = self.super_res_first.in_channels // 2 + height = self.super_res_first.sample_size + width = self.super_res_first.sample_size + + super_res_latents = self.prepare_latents( + (batch_size, channels, height, width), + image_small.dtype, + device, + generator, + super_res_latents, + self.super_res_scheduler, + ) + + if device.type == "mps": + # MPS does not support many interpolations + image_upscaled = F.interpolate(image_small, size=[height, width]) + else: + interpolate_antialias = {} + if "antialias" in inspect.signature(F.interpolate).parameters: + interpolate_antialias["antialias"] = True + + image_upscaled = F.interpolate( + image_small, size=[height, width], mode="bicubic", align_corners=False, **interpolate_antialias + ) + + for i, t in enumerate(self.progress_bar(super_res_timesteps_tensor)): + # no classifier free guidance + + if i == super_res_timesteps_tensor.shape[0] - 1: + unet = self.super_res_last + else: + unet = self.super_res_first + + latent_model_input = torch.cat([super_res_latents, image_upscaled], dim=1) + + noise_pred = unet( + sample=latent_model_input, + timestep=t, + ).sample + + if i + 1 == super_res_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = super_res_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + super_res_latents = self.super_res_scheduler.step( + noise_pred, t, super_res_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + image = super_res_latents + # done super res + + # post processing + + image = image * 0.5 + 0.5 + image = image.clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip_image_variation.py b/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..9a76b9f1fb84d318701e23f93adf159b314f6f3b --- /dev/null +++ b/diffusers/src/diffusers/pipelines/unclip/pipeline_unclip_image_variation.py @@ -0,0 +1,463 @@ +# Copyright 2022 Kakao Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import List, Optional, Union + +import PIL +import torch +from torch.nn import functional as F +from transformers import ( + CLIPFeatureExtractor, + CLIPTextModelWithProjection, + CLIPTokenizer, + CLIPVisionModelWithProjection, +) + +from ...models import UNet2DConditionModel, UNet2DModel +from ...pipelines import DiffusionPipeline, ImagePipelineOutput +from ...schedulers import UnCLIPScheduler +from ...utils import is_accelerate_available, logging, randn_tensor +from .text_proj import UnCLIPTextProjModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class UnCLIPImageVariationPipeline(DiffusionPipeline): + """ + Pipeline to generate variations from an input image using unCLIP + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + text_encoder ([`CLIPTextModelWithProjection`]): + Frozen text-encoder. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `image_encoder`. + image_encoder ([`CLIPVisionModelWithProjection`]): + Frozen CLIP image-encoder. unCLIP Image Variation uses the vision portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPVisionModelWithProjection), + specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + text_proj ([`UnCLIPTextProjModel`]): + Utility class to prepare and combine the embeddings before they are passed to the decoder. + decoder ([`UNet2DConditionModel`]): + The decoder to invert the image embedding into an image. + super_res_first ([`UNet2DModel`]): + Super resolution unet. Used in all but the last step of the super resolution diffusion process. + super_res_last ([`UNet2DModel`]): + Super resolution unet. Used in the last step of the super resolution diffusion process. + decoder_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the decoder denoising process. Just a modified DDPMScheduler. + super_res_scheduler ([`UnCLIPScheduler`]): + Scheduler used in the super resolution denoising process. Just a modified DDPMScheduler. + + """ + + decoder: UNet2DConditionModel + text_proj: UnCLIPTextProjModel + text_encoder: CLIPTextModelWithProjection + tokenizer: CLIPTokenizer + feature_extractor: CLIPFeatureExtractor + image_encoder: CLIPVisionModelWithProjection + super_res_first: UNet2DModel + super_res_last: UNet2DModel + + decoder_scheduler: UnCLIPScheduler + super_res_scheduler: UnCLIPScheduler + + def __init__( + self, + decoder: UNet2DConditionModel, + text_encoder: CLIPTextModelWithProjection, + tokenizer: CLIPTokenizer, + text_proj: UnCLIPTextProjModel, + feature_extractor: CLIPFeatureExtractor, + image_encoder: CLIPVisionModelWithProjection, + super_res_first: UNet2DModel, + super_res_last: UNet2DModel, + decoder_scheduler: UnCLIPScheduler, + super_res_scheduler: UnCLIPScheduler, + ): + super().__init__() + + self.register_modules( + decoder=decoder, + text_encoder=text_encoder, + tokenizer=tokenizer, + text_proj=text_proj, + feature_extractor=feature_extractor, + image_encoder=image_encoder, + super_res_first=super_res_first, + super_res_last=super_res_last, + decoder_scheduler=decoder_scheduler, + super_res_scheduler=super_res_scheduler, + ) + + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline.prepare_latents + def prepare_latents(self, shape, dtype, device, generator, latents, scheduler): + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(device) + + latents = latents * scheduler.init_noise_sigma + return latents + + def _encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance): + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + text_mask = text_inputs.attention_mask.bool().to(device) + text_encoder_output = self.text_encoder(text_input_ids.to(device)) + + prompt_embeds = text_encoder_output.text_embeds + text_encoder_hidden_states = text_encoder_output.last_hidden_state + + prompt_embeds = prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) + text_encoder_hidden_states = text_encoder_hidden_states.repeat_interleave(num_images_per_prompt, dim=0) + text_mask = text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + if do_classifier_free_guidance: + uncond_tokens = [""] * batch_size + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + uncond_text_mask = uncond_input.attention_mask.bool().to(device) + negative_prompt_embeds_text_encoder_output = self.text_encoder(uncond_input.input_ids.to(device)) + + negative_prompt_embeds = negative_prompt_embeds_text_encoder_output.text_embeds + uncond_text_encoder_hidden_states = negative_prompt_embeds_text_encoder_output.last_hidden_state + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len) + + seq_len = uncond_text_encoder_hidden_states.shape[1] + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.repeat(1, num_images_per_prompt, 1) + uncond_text_encoder_hidden_states = uncond_text_encoder_hidden_states.view( + batch_size * num_images_per_prompt, seq_len, -1 + ) + uncond_text_mask = uncond_text_mask.repeat_interleave(num_images_per_prompt, dim=0) + + # done duplicates + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + text_encoder_hidden_states = torch.cat([uncond_text_encoder_hidden_states, text_encoder_hidden_states]) + + text_mask = torch.cat([uncond_text_mask, text_mask]) + + return prompt_embeds, text_encoder_hidden_states, text_mask + + def _encode_image(self, image, device, num_images_per_prompt, image_embeddings: Optional[torch.Tensor] = None): + dtype = next(self.image_encoder.parameters()).dtype + + if image_embeddings is None: + if not isinstance(image, torch.Tensor): + image = self.feature_extractor(images=image, return_tensors="pt").pixel_values + + image = image.to(device=device, dtype=dtype) + image_embeddings = self.image_encoder(image).image_embeds + + image_embeddings = image_embeddings.repeat_interleave(num_images_per_prompt, dim=0) + + return image_embeddings + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, the pipeline's + models have their state dicts saved to CPU and then are moved to a `torch.device('meta') and loaded to GPU only + when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + models = [ + self.decoder, + self.text_proj, + self.text_encoder, + self.super_res_first, + self.super_res_last, + ] + for cpu_offloaded_model in models: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.unclip.pipeline_unclip.UnCLIPPipeline._execution_device + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.decoder, "_hf_hook"): + return self.device + for module in self.decoder.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + @torch.no_grad() + def __call__( + self, + image: Optional[Union[PIL.Image.Image, List[PIL.Image.Image], torch.FloatTensor]] = None, + num_images_per_prompt: int = 1, + decoder_num_inference_steps: int = 25, + super_res_num_inference_steps: int = 7, + generator: Optional[torch.Generator] = None, + decoder_latents: Optional[torch.FloatTensor] = None, + super_res_latents: Optional[torch.FloatTensor] = None, + image_embeddings: Optional[torch.Tensor] = None, + decoder_guidance_scale: float = 8.0, + output_type: Optional[str] = "pil", + return_dict: bool = True, + ): + """ + Function invoked when calling the pipeline for generation. + + Args: + image (`PIL.Image.Image` or `List[PIL.Image.Image]` or `torch.FloatTensor`): + The image or images to guide the image generation. If you provide a tensor, it needs to comply with the + configuration of + [this](https://huggingface.co/fusing/karlo-image-variations-diffusers/blob/main/feature_extractor/preprocessor_config.json) + `CLIPFeatureExtractor`. Can be left to `None` only when `image_embeddings` are passed. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + decoder_num_inference_steps (`int`, *optional*, defaults to 25): + The number of denoising steps for the decoder. More denoising steps usually lead to a higher quality + image at the expense of slower inference. + super_res_num_inference_steps (`int`, *optional*, defaults to 7): + The number of denoising steps for super resolution. More denoising steps usually lead to a higher + quality image at the expense of slower inference. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + decoder_latents (`torch.FloatTensor` of shape (batch size, channels, height, width), *optional*): + Pre-generated noisy latents to be used as inputs for the decoder. + super_res_latents (`torch.FloatTensor` of shape (batch size, channels, super res height, super res width), *optional*): + Pre-generated noisy latents to be used as inputs for the decoder. + decoder_guidance_scale (`float`, *optional*, defaults to 4.0): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + image_embeddings (`torch.Tensor`, *optional*): + Pre-defined image embeddings that can be derived from the image encoder. Pre-defined image embeddings + can be passed for tasks like image interpolations. `image` can the be left to `None`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + """ + if image is not None: + if isinstance(image, PIL.Image.Image): + batch_size = 1 + elif isinstance(image, list): + batch_size = len(image) + else: + batch_size = image.shape[0] + else: + batch_size = image_embeddings.shape[0] + + prompt = [""] * batch_size + + device = self._execution_device + + batch_size = batch_size * num_images_per_prompt + + do_classifier_free_guidance = decoder_guidance_scale > 1.0 + + prompt_embeds, text_encoder_hidden_states, text_mask = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance + ) + + image_embeddings = self._encode_image(image, device, num_images_per_prompt, image_embeddings) + + # decoder + text_encoder_hidden_states, additive_clip_time_embeddings = self.text_proj( + image_embeddings=image_embeddings, + prompt_embeds=prompt_embeds, + text_encoder_hidden_states=text_encoder_hidden_states, + do_classifier_free_guidance=do_classifier_free_guidance, + ) + + if device.type == "mps": + # HACK: MPS: There is a panic when padding bool tensors, + # so cast to int tensor for the pad and back to bool afterwards + text_mask = text_mask.type(torch.int) + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=1) + decoder_text_mask = decoder_text_mask.type(torch.bool) + else: + decoder_text_mask = F.pad(text_mask, (self.text_proj.clip_extra_context_tokens, 0), value=True) + + self.decoder_scheduler.set_timesteps(decoder_num_inference_steps, device=device) + decoder_timesteps_tensor = self.decoder_scheduler.timesteps + + num_channels_latents = self.decoder.in_channels + height = self.decoder.sample_size + width = self.decoder.sample_size + + if decoder_latents is None: + decoder_latents = self.prepare_latents( + (batch_size, num_channels_latents, height, width), + text_encoder_hidden_states.dtype, + device, + generator, + decoder_latents, + self.decoder_scheduler, + ) + + for i, t in enumerate(self.progress_bar(decoder_timesteps_tensor)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([decoder_latents] * 2) if do_classifier_free_guidance else decoder_latents + + noise_pred = self.decoder( + sample=latent_model_input, + timestep=t, + encoder_hidden_states=text_encoder_hidden_states, + class_labels=additive_clip_time_embeddings, + attention_mask=decoder_text_mask, + ).sample + + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred_uncond, _ = noise_pred_uncond.split(latent_model_input.shape[1], dim=1) + noise_pred_text, predicted_variance = noise_pred_text.split(latent_model_input.shape[1], dim=1) + noise_pred = noise_pred_uncond + decoder_guidance_scale * (noise_pred_text - noise_pred_uncond) + noise_pred = torch.cat([noise_pred, predicted_variance], dim=1) + + if i + 1 == decoder_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = decoder_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + decoder_latents = self.decoder_scheduler.step( + noise_pred, t, decoder_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + decoder_latents = decoder_latents.clamp(-1, 1) + + image_small = decoder_latents + + # done decoder + + # super res + + self.super_res_scheduler.set_timesteps(super_res_num_inference_steps, device=device) + super_res_timesteps_tensor = self.super_res_scheduler.timesteps + + channels = self.super_res_first.in_channels // 2 + height = self.super_res_first.sample_size + width = self.super_res_first.sample_size + + if super_res_latents is None: + super_res_latents = self.prepare_latents( + (batch_size, channels, height, width), + image_small.dtype, + device, + generator, + super_res_latents, + self.super_res_scheduler, + ) + + if device.type == "mps": + # MPS does not support many interpolations + image_upscaled = F.interpolate(image_small, size=[height, width]) + else: + interpolate_antialias = {} + if "antialias" in inspect.signature(F.interpolate).parameters: + interpolate_antialias["antialias"] = True + + image_upscaled = F.interpolate( + image_small, size=[height, width], mode="bicubic", align_corners=False, **interpolate_antialias + ) + + for i, t in enumerate(self.progress_bar(super_res_timesteps_tensor)): + # no classifier free guidance + + if i == super_res_timesteps_tensor.shape[0] - 1: + unet = self.super_res_last + else: + unet = self.super_res_first + + latent_model_input = torch.cat([super_res_latents, image_upscaled], dim=1) + + noise_pred = unet( + sample=latent_model_input, + timestep=t, + ).sample + + if i + 1 == super_res_timesteps_tensor.shape[0]: + prev_timestep = None + else: + prev_timestep = super_res_timesteps_tensor[i + 1] + + # compute the previous noisy sample x_t -> x_t-1 + super_res_latents = self.super_res_scheduler.step( + noise_pred, t, super_res_latents, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + image = super_res_latents + + # done super res + + # post processing + + image = image * 0.5 + 0.5 + image = image.clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/unclip/text_proj.py b/diffusers/src/diffusers/pipelines/unclip/text_proj.py new file mode 100644 index 0000000000000000000000000000000000000000..a98cfbebdb9069d8e0827e6174b4f74cbee8f90f --- /dev/null +++ b/diffusers/src/diffusers/pipelines/unclip/text_proj.py @@ -0,0 +1,86 @@ +# Copyright 2022 Kakao Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +import torch +from torch import nn + +from ...configuration_utils import ConfigMixin, register_to_config +from ...models import ModelMixin + + +class UnCLIPTextProjModel(ModelMixin, ConfigMixin): + """ + Utility class for CLIP embeddings. Used to combine the image and text embeddings into a format usable by the + decoder. + + For more details, see the original paper: https://arxiv.org/abs/2204.06125 section 2.1 + """ + + @register_to_config + def __init__( + self, + *, + clip_extra_context_tokens: int = 4, + clip_embeddings_dim: int = 768, + time_embed_dim: int, + cross_attention_dim, + ): + super().__init__() + + self.learned_classifier_free_guidance_embeddings = nn.Parameter(torch.zeros(clip_embeddings_dim)) + + # parameters for additional clip time embeddings + self.embedding_proj = nn.Linear(clip_embeddings_dim, time_embed_dim) + self.clip_image_embeddings_project_to_time_embeddings = nn.Linear(clip_embeddings_dim, time_embed_dim) + + # parameters for encoder hidden states + self.clip_extra_context_tokens = clip_extra_context_tokens + self.clip_extra_context_tokens_proj = nn.Linear( + clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim + ) + self.encoder_hidden_states_proj = nn.Linear(clip_embeddings_dim, cross_attention_dim) + self.text_encoder_hidden_states_norm = nn.LayerNorm(cross_attention_dim) + + def forward(self, *, image_embeddings, prompt_embeds, text_encoder_hidden_states, do_classifier_free_guidance): + if do_classifier_free_guidance: + # Add the classifier free guidance embeddings to the image embeddings + image_embeddings_batch_size = image_embeddings.shape[0] + classifier_free_guidance_embeddings = self.learned_classifier_free_guidance_embeddings.unsqueeze(0) + classifier_free_guidance_embeddings = classifier_free_guidance_embeddings.expand( + image_embeddings_batch_size, -1 + ) + image_embeddings = torch.cat([classifier_free_guidance_embeddings, image_embeddings], dim=0) + + # The image embeddings batch size and the text embeddings batch size are equal + assert image_embeddings.shape[0] == prompt_embeds.shape[0] + + batch_size = prompt_embeds.shape[0] + + # "Specifically, we modify the architecture described in Nichol et al. (2021) by projecting and + # adding CLIP embeddings to the existing timestep embedding, ... + time_projected_prompt_embeds = self.embedding_proj(prompt_embeds) + time_projected_image_embeddings = self.clip_image_embeddings_project_to_time_embeddings(image_embeddings) + additive_clip_time_embeddings = time_projected_image_embeddings + time_projected_prompt_embeds + + # ... and by projecting CLIP embeddings into four + # extra tokens of context that are concatenated to the sequence of outputs from the GLIDE text encoder" + clip_extra_context_tokens = self.clip_extra_context_tokens_proj(image_embeddings) + clip_extra_context_tokens = clip_extra_context_tokens.reshape(batch_size, -1, self.clip_extra_context_tokens) + + text_encoder_hidden_states = self.encoder_hidden_states_proj(text_encoder_hidden_states) + text_encoder_hidden_states = self.text_encoder_hidden_states_norm(text_encoder_hidden_states) + text_encoder_hidden_states = text_encoder_hidden_states.permute(0, 2, 1) + text_encoder_hidden_states = torch.cat([clip_extra_context_tokens, text_encoder_hidden_states], dim=2) + + return text_encoder_hidden_states, additive_clip_time_embeddings diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..abf9dcff59dbc922dcc7063a1e73560679a23696 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/__init__.py @@ -0,0 +1,24 @@ +from ...utils import ( + OptionalDependencyNotAvailable, + is_torch_available, + is_transformers_available, + is_transformers_version, +) + + +try: + if not (is_transformers_available() and is_torch_available() and is_transformers_version(">=", "4.25.0")): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ...utils.dummy_torch_and_transformers_objects import ( + VersatileDiffusionDualGuidedPipeline, + VersatileDiffusionImageVariationPipeline, + VersatileDiffusionPipeline, + VersatileDiffusionTextToImagePipeline, + ) +else: + from .modeling_text_unet import UNetFlatConditionModel + from .pipeline_versatile_diffusion import VersatileDiffusionPipeline + from .pipeline_versatile_diffusion_dual_guided import VersatileDiffusionDualGuidedPipeline + from .pipeline_versatile_diffusion_image_variation import VersatileDiffusionImageVariationPipeline + from .pipeline_versatile_diffusion_text_to_image import VersatileDiffusionTextToImagePipeline diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/modeling_text_unet.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/modeling_text_unet.py new file mode 100644 index 0000000000000000000000000000000000000000..2c5b717ac8612535238d5470375aeed96b1dc384 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/modeling_text_unet.py @@ -0,0 +1,1441 @@ +from typing import Any, Dict, List, Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn as nn + +from ...configuration_utils import ConfigMixin, register_to_config +from ...models import ModelMixin +from ...models.attention import CrossAttention +from ...models.cross_attention import AttnProcessor, CrossAttnAddedKVProcessor +from ...models.dual_transformer_2d import DualTransformer2DModel +from ...models.embeddings import GaussianFourierProjection, TimestepEmbedding, Timesteps +from ...models.transformer_2d import Transformer2DModel +from ...models.unet_2d_condition import UNet2DConditionOutput +from ...utils import logging + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def get_down_block( + down_block_type, + num_layers, + in_channels, + out_channels, + temb_channels, + add_downsample, + resnet_eps, + resnet_act_fn, + attn_num_head_channels, + resnet_groups=None, + cross_attention_dim=None, + downsample_padding=None, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + resnet_time_scale_shift="default", +): + down_block_type = down_block_type[7:] if down_block_type.startswith("UNetRes") else down_block_type + if down_block_type == "DownBlockFlat": + return DownBlockFlat( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "CrossAttnDownBlockFlat": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnDownBlockFlat") + return CrossAttnDownBlockFlat( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + raise ValueError(f"{down_block_type} is not supported.") + + +def get_up_block( + up_block_type, + num_layers, + in_channels, + out_channels, + prev_output_channel, + temb_channels, + add_upsample, + resnet_eps, + resnet_act_fn, + attn_num_head_channels, + resnet_groups=None, + cross_attention_dim=None, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + resnet_time_scale_shift="default", +): + up_block_type = up_block_type[7:] if up_block_type.startswith("UNetRes") else up_block_type + if up_block_type == "UpBlockFlat": + return UpBlockFlat( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "CrossAttnUpBlockFlat": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnUpBlockFlat") + return CrossAttnUpBlockFlat( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attn_num_head_channels, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + raise ValueError(f"{up_block_type} is not supported.") + + +# Copied from diffusers.models.unet_2d_condition.UNet2DConditionModel with UNet2DConditionModel->UNetFlatConditionModel, nn.Conv2d->LinearMultiDim, Block2D->BlockFlat +class UNetFlatConditionModel(ModelMixin, ConfigMixin): + r""" + UNetFlatConditionModel is a conditional 2D UNet model that takes in a noisy sample, conditional state, and a + timestep and returns sample shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for the generic methods the library + implements for all the models (such as downloading or saving, etc.) + + Parameters: + sample_size (`int` or `Tuple[int, int]`, *optional*, defaults to `None`): + Height and width of input/output sample. + in_channels (`int`, *optional*, defaults to 4): The number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 4): The number of channels in the output. + center_input_sample (`bool`, *optional*, defaults to `False`): Whether to center the input sample. + flip_sin_to_cos (`bool`, *optional*, defaults to `False`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, *optional*, defaults to 0): The frequency shift to apply to the time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to `("CrossAttnDownBlockFlat", "CrossAttnDownBlockFlat", "CrossAttnDownBlockFlat", "DownBlockFlat")`): + The tuple of downsample blocks to use. + mid_block_type (`str`, *optional*, defaults to `"UNetMidBlockFlatCrossAttn"`): + The mid block type. Choose from `UNetMidBlockFlatCrossAttn` or `UNetMidBlockFlatSimpleCrossAttn`, will skip + the mid block layer if `None`. + up_block_types (`Tuple[str]`, *optional*, defaults to `("UpBlockFlat", "CrossAttnUpBlockFlat", "CrossAttnUpBlockFlat", "CrossAttnUpBlockFlat",)`): + The tuple of upsample blocks to use. + only_cross_attention(`bool` or `Tuple[bool]`, *optional*, default to `False`): + Whether to include self-attention in the basic transformer blocks, see + [`~models.attention.BasicTransformerBlock`]. + block_out_channels (`Tuple[int]`, *optional*, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, *optional*, defaults to 2): The number of layers per block. + downsample_padding (`int`, *optional*, defaults to 1): The padding to use for the downsampling convolution. + mid_block_scale_factor (`float`, *optional*, defaults to 1.0): The scale factor to use for the mid block. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + norm_num_groups (`int`, *optional*, defaults to 32): The number of groups to use for the normalization. + If `None`, it will skip the normalization and activation layers in post-processing + norm_eps (`float`, *optional*, defaults to 1e-5): The epsilon to use for the normalization. + cross_attention_dim (`int`, *optional*, defaults to 1280): The dimension of the cross attention features. + attention_head_dim (`int`, *optional*, defaults to 8): The dimension of the attention heads. + resnet_time_scale_shift (`str`, *optional*, defaults to `"default"`): Time scale shift config + for resnet blocks, see [`~models.resnet.ResnetBlockFlat`]. Choose from `default` or `scale_shift`. + class_embed_type (`str`, *optional*, defaults to None): The type of class embedding to use which is ultimately + summed with the time embeddings. Choose from `None`, `"timestep"`, or `"identity"`. + num_class_embeds (`int`, *optional*, defaults to None): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + time_embedding_type (`str`, *optional*, default to `positional`): + The type of position embedding to use for timesteps. Choose from `positional` or `fourier`. + timestep_post_act (`str, *optional*, default to `None`): + The second activation function to use in timestep embedding. Choose from `silu`, `mish` and `gelu`. + time_cond_proj_dim (`int`, *optional*, default to `None`): + The dimension of `cond_proj` layer in timestep embedding. + conv_in_kernel (`int`, *optional*, default to `3`): The kernel size of `conv_in` layer. + conv_out_kernel (`int`, *optional*, default to `3`): the Kernel size of `conv_out` layer. + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + sample_size: Optional[int] = None, + in_channels: int = 4, + out_channels: int = 4, + center_input_sample: bool = False, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str] = ( + "CrossAttnDownBlockFlat", + "CrossAttnDownBlockFlat", + "CrossAttnDownBlockFlat", + "DownBlockFlat", + ), + mid_block_type: Optional[str] = "UNetMidBlockFlatCrossAttn", + up_block_types: Tuple[str] = ( + "UpBlockFlat", + "CrossAttnUpBlockFlat", + "CrossAttnUpBlockFlat", + "CrossAttnUpBlockFlat", + ), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int] = (320, 640, 1280, 1280), + layers_per_block: int = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: int = 1280, + attention_head_dim: Union[int, Tuple[int]] = 8, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + time_embedding_type: str = "positional", # fourier, positional + timestep_post_act: Optional[str] = None, + time_cond_proj_dim: Optional[int] = None, + conv_in_kernel: int = 3, + conv_out_kernel: int = 3, + ): + super().__init__() + + self.sample_size = sample_size + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + "Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`:" + f" {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + "Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`:" + f" {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + "Must provide the same number of `only_cross_attention` as `down_block_types`." + f" `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(attention_head_dim, int) and len(attention_head_dim) != len(down_block_types): + raise ValueError( + "Must provide the same number of `attention_head_dim` as `down_block_types`. `attention_head_dim`:" + f" {attention_head_dim}. `down_block_types`: {down_block_types}." + ) + + # input + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in = LinearMultiDim( + in_channels, block_out_channels[0], kernel_size=conv_in_kernel, padding=conv_in_padding + ) + + # time + if time_embedding_type == "fourier": + time_embed_dim = block_out_channels[0] * 2 + if time_embed_dim % 2 != 0: + raise ValueError(f"`time_embed_dim` should be divisible by 2, but is {time_embed_dim}.") + self.time_proj = GaussianFourierProjection( + time_embed_dim // 2, set_W_to_weight=False, log=False, flip_sin_to_cos=flip_sin_to_cos + ) + timestep_input_dim = time_embed_dim + elif time_embedding_type == "positional": + time_embed_dim = block_out_channels[0] * 4 + + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + else: + raise ValueError( + f"{time_embedding_type} does not exist. Pleaes make sure to use one of `fourier` or `positional`." + ) + + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + post_act_fn=timestep_post_act, + cond_proj_dim=time_cond_proj_dim, + ) + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + else: + self.class_embedding = None + + self.down_blocks = nn.ModuleList([]) + self.up_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[i], + downsample_padding=downsample_padding, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + # mid + if mid_block_type == "UNetMidBlockFlatCrossAttn": + self.mid_block = UNetMidBlockFlatCrossAttn( + in_channels=block_out_channels[-1], + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[-1], + resnet_groups=norm_num_groups, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + elif mid_block_type == "UNetMidBlockFlatSimpleCrossAttn": + self.mid_block = UNetMidBlockFlatSimpleCrossAttn( + in_channels=block_out_channels[-1], + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=attention_head_dim[-1], + resnet_groups=norm_num_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif mid_block_type is None: + self.mid_block = None + else: + raise ValueError(f"unknown mid_block_type : {mid_block_type}") + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_attention_head_dim = list(reversed(attention_head_dim)) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + attn_num_head_channels=reversed_attention_head_dim[i], + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + if norm_num_groups is not None: + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + self.conv_act = nn.SiLU() + else: + self.conv_norm_out = None + self.conv_act = None + + conv_out_padding = (conv_out_kernel - 1) // 2 + self.conv_out = LinearMultiDim( + block_out_channels[0], out_channels, kernel_size=conv_out_kernel, padding=conv_out_padding + ) + + @property + def attn_processors(self) -> Dict[str, AttnProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttnProcessor]): + if hasattr(module, "set_processor"): + processors[f"{name}.processor"] = module.processor + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + def set_attn_processor(self, processor: Union[AttnProcessor, Dict[str, AttnProcessor]]): + r""" + Parameters: + `processor (`dict` of `AttnProcessor` or `AttnProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + of **all** `CrossAttention` layers. + In case `processor` is a dict, the key needs to define the path to the corresponding cross attention processor. This is strongly recommended when setting trainablae attention processors.: + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + def set_attention_slice(self, slice_size): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module will split the input tensor in slices, to compute attention + in several steps. This is useful to save some memory in exchange for a small speed decrease. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, halves the input to the attention heads, so attention will be computed in two steps. If + `"max"`, maxium amount of memory will be saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_slicable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_slicable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_slicable_dims(module) + + num_slicable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_slicable_layers * [1] + + slice_size = num_slicable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value=False): + if isinstance(module, (CrossAttnDownBlockFlat, DownBlockFlat, CrossAttnUpBlockFlat, UpBlockFlat)): + module.gradient_checkpointing = value + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + return_dict: bool = True, + ) -> Union[UNet2DConditionOutput, Tuple]: + r""" + Args: + sample (`torch.FloatTensor`): (batch, channel, height, width) noisy inputs tensor + timestep (`torch.FloatTensor` or `float` or `int`): (batch) timesteps + encoder_hidden_states (`torch.FloatTensor`): (batch, sequence_length, feature_dim) encoder hidden states + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain tuple. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttnProcessor` as defined under + `self.processor` in + [diffusers.cross_attention](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/cross_attention.py). + + Returns: + [`~models.unet_2d_condition.UNet2DConditionOutput`] or `tuple`: + [`~models.unet_2d_condition.UNet2DConditionOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layears). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2**self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + if any(s % default_overall_up_factor != 0 for s in sample.shape[-2:]): + logger.info("Forward upsample size to force interpolation output size.") + forward_upsample_size = True + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=self.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + # 2. pre-process + sample = self.conv_in(sample) + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + + down_block_res_samples += res_samples + + # 4. mid + if self.mid_block is not None: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + ) + else: + sample = upsample_block( + hidden_states=sample, temb=emb, res_hidden_states_tuple=res_samples, upsample_size=upsample_size + ) + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if not return_dict: + return (sample,) + + return UNet2DConditionOutput(sample=sample) + + +class LinearMultiDim(nn.Linear): + def __init__(self, in_features, out_features=None, second_dim=4, *args, **kwargs): + in_features = [in_features, second_dim, 1] if isinstance(in_features, int) else list(in_features) + if out_features is None: + out_features = in_features + out_features = [out_features, second_dim, 1] if isinstance(out_features, int) else list(out_features) + self.in_features_multidim = in_features + self.out_features_multidim = out_features + super().__init__(np.array(in_features).prod(), np.array(out_features).prod()) + + def forward(self, input_tensor, *args, **kwargs): + shape = input_tensor.shape + n_dim = len(self.in_features_multidim) + input_tensor = input_tensor.reshape(*shape[0:-n_dim], self.in_features) + output_tensor = super().forward(input_tensor) + output_tensor = output_tensor.view(*shape[0:-n_dim], *self.out_features_multidim) + return output_tensor + + +class ResnetBlockFlat(nn.Module): + def __init__( + self, + *, + in_channels, + out_channels=None, + dropout=0.0, + temb_channels=512, + groups=32, + groups_out=None, + pre_norm=True, + eps=1e-6, + time_embedding_norm="default", + use_in_shortcut=None, + second_dim=4, + **kwargs, + ): + super().__init__() + self.pre_norm = pre_norm + self.pre_norm = True + + in_channels = [in_channels, second_dim, 1] if isinstance(in_channels, int) else list(in_channels) + self.in_channels_prod = np.array(in_channels).prod() + self.channels_multidim = in_channels + + if out_channels is not None: + out_channels = [out_channels, second_dim, 1] if isinstance(out_channels, int) else list(out_channels) + out_channels_prod = np.array(out_channels).prod() + self.out_channels_multidim = out_channels + else: + out_channels_prod = self.in_channels_prod + self.out_channels_multidim = self.channels_multidim + self.time_embedding_norm = time_embedding_norm + + if groups_out is None: + groups_out = groups + + self.norm1 = torch.nn.GroupNorm(num_groups=groups, num_channels=self.in_channels_prod, eps=eps, affine=True) + self.conv1 = torch.nn.Conv2d(self.in_channels_prod, out_channels_prod, kernel_size=1, padding=0) + + if temb_channels is not None: + self.time_emb_proj = torch.nn.Linear(temb_channels, out_channels_prod) + else: + self.time_emb_proj = None + + self.norm2 = torch.nn.GroupNorm(num_groups=groups_out, num_channels=out_channels_prod, eps=eps, affine=True) + self.dropout = torch.nn.Dropout(dropout) + self.conv2 = torch.nn.Conv2d(out_channels_prod, out_channels_prod, kernel_size=1, padding=0) + + self.nonlinearity = nn.SiLU() + + self.use_in_shortcut = ( + self.in_channels_prod != out_channels_prod if use_in_shortcut is None else use_in_shortcut + ) + + self.conv_shortcut = None + if self.use_in_shortcut: + self.conv_shortcut = torch.nn.Conv2d( + self.in_channels_prod, out_channels_prod, kernel_size=1, stride=1, padding=0 + ) + + def forward(self, input_tensor, temb): + shape = input_tensor.shape + n_dim = len(self.channels_multidim) + input_tensor = input_tensor.reshape(*shape[0:-n_dim], self.in_channels_prod, 1, 1) + input_tensor = input_tensor.view(-1, self.in_channels_prod, 1, 1) + + hidden_states = input_tensor + + hidden_states = self.norm1(hidden_states) + hidden_states = self.nonlinearity(hidden_states) + hidden_states = self.conv1(hidden_states) + + if temb is not None: + temb = self.time_emb_proj(self.nonlinearity(temb))[:, :, None, None] + hidden_states = hidden_states + temb + + hidden_states = self.norm2(hidden_states) + hidden_states = self.nonlinearity(hidden_states) + + hidden_states = self.dropout(hidden_states) + hidden_states = self.conv2(hidden_states) + + if self.conv_shortcut is not None: + input_tensor = self.conv_shortcut(input_tensor) + + output_tensor = input_tensor + hidden_states + + output_tensor = output_tensor.view(*shape[0:-n_dim], -1) + output_tensor = output_tensor.view(*shape[0:-n_dim], *self.out_channels_multidim) + + return output_tensor + + +# Copied from diffusers.models.unet_2d_blocks.DownBlock2D with DownBlock2D->DownBlockFlat, ResnetBlock2D->ResnetBlockFlat, Downsample2D->LinearMultiDim +class DownBlockFlat(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_downsample=True, + downsample_padding=1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlockFlat( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + LinearMultiDim( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, temb=None): + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +# Copied from diffusers.models.unet_2d_blocks.CrossAttnDownBlock2D with CrossAttnDownBlock2D->CrossAttnDownBlockFlat, ResnetBlock2D->ResnetBlockFlat, Downsample2D->LinearMultiDim +class CrossAttnDownBlockFlat(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + downsample_padding=1, + add_downsample=True, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlockFlat( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + LinearMultiDim( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + # TODO(Patrick, William) - attention mask is not used + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + cross_attention_kwargs, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +# Copied from diffusers.models.unet_2d_blocks.UpBlock2D with UpBlock2D->UpBlockFlat, ResnetBlock2D->ResnetBlockFlat, Upsample2D->LinearMultiDim +class UpBlockFlat(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor=1.0, + add_upsample=True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlockFlat( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([LinearMultiDim(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward(self, hidden_states, res_hidden_states_tuple, temb=None, upsample_size=None): + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + + return hidden_states + + +# Copied from diffusers.models.unet_2d_blocks.CrossAttnUpBlock2D with CrossAttnUpBlock2D->CrossAttnUpBlockFlat, ResnetBlock2D->ResnetBlockFlat, Upsample2D->LinearMultiDim +class CrossAttnUpBlockFlat(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + cross_attention_dim=1280, + output_scale_factor=1.0, + add_upsample=True, + dual_cross_attention=False, + use_linear_projection=False, + only_cross_attention=False, + upcast_attention=False, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlockFlat( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + out_channels // attn_num_head_channels, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([LinearMultiDim(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states, + res_hidden_states_tuple, + temb=None, + encoder_hidden_states=None, + cross_attention_kwargs=None, + upsample_size=None, + attention_mask=None, + ): + # TODO(Patrick, William) - attention mask is not used + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(attn, return_dict=False), + hidden_states, + encoder_hidden_states, + cross_attention_kwargs, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + + return hidden_states + + +# Copied from diffusers.models.unet_2d_blocks.UNetMidBlock2DCrossAttn with UNetMidBlock2DCrossAttn->UNetMidBlockFlatCrossAttn, ResnetBlock2D->ResnetBlockFlat +class UNetMidBlockFlatCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + cross_attention_dim=1280, + dual_cross_attention=False, + use_linear_projection=False, + upcast_attention=False, + ): + super().__init__() + + self.has_cross_attention = True + self.attn_num_head_channels = attn_num_head_channels + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + # there is always at least one resnet + resnets = [ + ResnetBlockFlat( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for _ in range(num_layers): + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + attn_num_head_channels, + in_channels // attn_num_head_channels, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + attn_num_head_channels, + in_channels // attn_num_head_channels, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + resnets.append( + ResnetBlockFlat( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + ).sample + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +# Copied from diffusers.models.unet_2d_blocks.UNetMidBlock2DSimpleCrossAttn with UNetMidBlock2DSimpleCrossAttn->UNetMidBlockFlatSimpleCrossAttn, ResnetBlock2D->ResnetBlockFlat +class UNetMidBlockFlatSimpleCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attn_num_head_channels=1, + output_scale_factor=1.0, + cross_attention_dim=1280, + ): + super().__init__() + + self.has_cross_attention = True + + self.attn_num_head_channels = attn_num_head_channels + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + self.num_heads = in_channels // self.attn_num_head_channels + + # there is always at least one resnet + resnets = [ + ResnetBlockFlat( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for _ in range(num_layers): + attentions.append( + CrossAttention( + query_dim=in_channels, + cross_attention_dim=in_channels, + heads=self.num_heads, + dim_head=attn_num_head_channels, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + processor=CrossAttnAddedKVProcessor(), + ) + ) + resnets.append( + ResnetBlockFlat( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, hidden_states, temb=None, encoder_hidden_states=None, attention_mask=None, cross_attention_kwargs=None + ): + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + + # resnet + hidden_states = resnet(hidden_states, temb) + + return hidden_states diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..84e46217878bc17f3a133b71f1fbaff1901a6261 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion.py @@ -0,0 +1,434 @@ +import inspect +from typing import Callable, List, Optional, Union + +import PIL.Image +import torch +from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer, CLIPVisionModel + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import logging +from ..pipeline_utils import DiffusionPipeline +from .pipeline_versatile_diffusion_dual_guided import VersatileDiffusionDualGuidedPipeline +from .pipeline_versatile_diffusion_image_variation import VersatileDiffusionImageVariationPipeline +from .pipeline_versatile_diffusion_text_to_image import VersatileDiffusionTextToImagePipeline + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class VersatileDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using Stable Diffusion. + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vae ([`AutoencoderKL`]): + Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. Stable Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + safety_checker ([`StableDiffusionMegaSafetyChecker`]): + Classification module that estimates whether generated images could be considered offensive or harmful. + Please, refer to the [model card](https://huggingface.co/runwayml/stable-diffusion-v1-5) for details. + feature_extractor ([`CLIPFeatureExtractor`]): + Model that extracts features from generated images to be used as inputs for the `safety_checker`. + """ + + tokenizer: CLIPTokenizer + image_feature_extractor: CLIPFeatureExtractor + text_encoder: CLIPTextModel + image_encoder: CLIPVisionModel + image_unet: UNet2DConditionModel + text_unet: UNet2DConditionModel + vae: AutoencoderKL + scheduler: KarrasDiffusionSchedulers + + def __init__( + self, + tokenizer: CLIPTokenizer, + image_feature_extractor: CLIPFeatureExtractor, + text_encoder: CLIPTextModel, + image_encoder: CLIPVisionModel, + image_unet: UNet2DConditionModel, + text_unet: UNet2DConditionModel, + vae: AutoencoderKL, + scheduler: KarrasDiffusionSchedulers, + ): + super().__init__() + + self.register_modules( + tokenizer=tokenizer, + image_feature_extractor=image_feature_extractor, + text_encoder=text_encoder, + image_encoder=image_encoder, + image_unet=image_unet, + text_unet=text_unet, + vae=vae, + scheduler=scheduler, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + @torch.no_grad() + def image_variation( + self, + image: Union[torch.FloatTensor, PIL.Image.Image], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + image (`PIL.Image.Image`, `List[PIL.Image.Image]` or `torch.Tensor`): + The image prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionPipeline + >>> import torch + >>> import requests + >>> from io import BytesIO + >>> from PIL import Image + + >>> # let's download an initial image + >>> url = "https://huggingface.co/datasets/diffusers/images/resolve/main/benz.jpg" + + >>> response = requests.get(url) + >>> image = Image.open(BytesIO(response.content)).convert("RGB") + + >>> pipe = VersatileDiffusionPipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> image = pipe.image_variation(image, generator=generator).images[0] + >>> image.save("./car_variation.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + expected_components = inspect.signature(VersatileDiffusionImageVariationPipeline.__init__).parameters.keys() + components = {name: component for name, component in self.components.items() if name in expected_components} + return VersatileDiffusionImageVariationPipeline(**components)( + image=image, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) + + @torch.no_grad() + def text_to_image( + self, + prompt: Union[str, List[str]], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionPipeline + >>> import torch + + >>> pipe = VersatileDiffusionPipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> image = pipe.text_to_image("an astronaut riding on a horse on mars", generator=generator).images[0] + >>> image.save("./astronaut.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + expected_components = inspect.signature(VersatileDiffusionTextToImagePipeline.__init__).parameters.keys() + components = {name: component for name, component in self.components.items() if name in expected_components} + temp_pipeline = VersatileDiffusionTextToImagePipeline(**components) + output = temp_pipeline( + prompt=prompt, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + negative_prompt=negative_prompt, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) + # swap the attention blocks back to the original state + temp_pipeline._swap_unet_attention_blocks() + + return output + + @torch.no_grad() + def dual_guided( + self, + prompt: Union[PIL.Image.Image, List[PIL.Image.Image]], + image: Union[str, List[str]], + text_to_image_strength: float = 0.5, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionPipeline + >>> import torch + >>> import requests + >>> from io import BytesIO + >>> from PIL import Image + + >>> # let's download an initial image + >>> url = "https://huggingface.co/datasets/diffusers/images/resolve/main/benz.jpg" + + >>> response = requests.get(url) + >>> image = Image.open(BytesIO(response.content)).convert("RGB") + >>> text = "a red car in the sun" + + >>> pipe = VersatileDiffusionPipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> text_to_image_strength = 0.75 + + >>> image = pipe.dual_guided( + ... prompt=text, image=image, text_to_image_strength=text_to_image_strength, generator=generator + ... ).images[0] + >>> image.save("./car_variation.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.ImagePipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.ImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple. When + returning a tuple, the first element is a list with the generated images. + """ + + expected_components = inspect.signature(VersatileDiffusionDualGuidedPipeline.__init__).parameters.keys() + components = {name: component for name, component in self.components.items() if name in expected_components} + temp_pipeline = VersatileDiffusionDualGuidedPipeline(**components) + output = temp_pipeline( + prompt=prompt, + image=image, + text_to_image_strength=text_to_image_strength, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + num_images_per_prompt=num_images_per_prompt, + eta=eta, + generator=generator, + latents=latents, + output_type=output_type, + return_dict=return_dict, + callback=callback, + callback_steps=callback_steps, + ) + temp_pipeline._revert_dual_attention() + + return output diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_dual_guided.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_dual_guided.py new file mode 100644 index 0000000000000000000000000000000000000000..24ca9666e2729cf672acaa840f8bb509d336a3b9 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_dual_guided.py @@ -0,0 +1,585 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Tuple, Union + +import numpy as np +import PIL +import torch +import torch.utils.checkpoint +from transformers import ( + CLIPFeatureExtractor, + CLIPTextModelWithProjection, + CLIPTokenizer, + CLIPVisionModelWithProjection, +) + +from ...models import AutoencoderKL, DualTransformer2DModel, Transformer2DModel, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput +from .modeling_text_unet import UNetFlatConditionModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class VersatileDiffusionDualGuidedPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) Model to encode and decode images to and from latent representations. + bert ([`LDMBertModel`]): + Text-encoder model based on [BERT](https://huggingface.co/docs/transformers/model_doc/bert) architecture. + tokenizer (`transformers.BertTokenizer`): + Tokenizer of class + [BertTokenizer](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + tokenizer: CLIPTokenizer + image_feature_extractor: CLIPFeatureExtractor + text_encoder: CLIPTextModelWithProjection + image_encoder: CLIPVisionModelWithProjection + image_unet: UNet2DConditionModel + text_unet: UNetFlatConditionModel + vae: AutoencoderKL + scheduler: KarrasDiffusionSchedulers + + _optional_components = ["text_unet"] + + def __init__( + self, + tokenizer: CLIPTokenizer, + image_feature_extractor: CLIPFeatureExtractor, + text_encoder: CLIPTextModelWithProjection, + image_encoder: CLIPVisionModelWithProjection, + image_unet: UNet2DConditionModel, + text_unet: UNetFlatConditionModel, + vae: AutoencoderKL, + scheduler: KarrasDiffusionSchedulers, + ): + super().__init__() + self.register_modules( + tokenizer=tokenizer, + image_feature_extractor=image_feature_extractor, + text_encoder=text_encoder, + image_encoder=image_encoder, + image_unet=image_unet, + text_unet=text_unet, + vae=vae, + scheduler=scheduler, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + if self.text_unet is not None and ( + "dual_cross_attention" not in self.image_unet.config or not self.image_unet.config.dual_cross_attention + ): + # if loading from a universal checkpoint rather than a saved dual-guided pipeline + self._convert_to_dual_attention() + + def remove_unused_weights(self): + self.register_modules(text_unet=None) + + def _convert_to_dual_attention(self): + """ + Replace image_unet's `Transformer2DModel` blocks with `DualTransformer2DModel` that contains transformer blocks + from both `image_unet` and `text_unet` + """ + for name, module in self.image_unet.named_modules(): + if isinstance(module, Transformer2DModel): + parent_name, index = name.rsplit(".", 1) + index = int(index) + + image_transformer = self.image_unet.get_submodule(parent_name)[index] + text_transformer = self.text_unet.get_submodule(parent_name)[index] + + config = image_transformer.config + dual_transformer = DualTransformer2DModel( + num_attention_heads=config.num_attention_heads, + attention_head_dim=config.attention_head_dim, + in_channels=config.in_channels, + num_layers=config.num_layers, + dropout=config.dropout, + norm_num_groups=config.norm_num_groups, + cross_attention_dim=config.cross_attention_dim, + attention_bias=config.attention_bias, + sample_size=config.sample_size, + num_vector_embeds=config.num_vector_embeds, + activation_fn=config.activation_fn, + num_embeds_ada_norm=config.num_embeds_ada_norm, + ) + dual_transformer.transformers[0] = image_transformer + dual_transformer.transformers[1] = text_transformer + + self.image_unet.get_submodule(parent_name)[index] = dual_transformer + self.image_unet.register_to_config(dual_cross_attention=True) + + def _revert_dual_attention(self): + """ + Revert the image_unet `DualTransformer2DModel` blocks back to `Transformer2DModel` with image_unet weights Call + this function if you reuse `image_unet` in another pipeline, e.g. `VersatileDiffusionPipeline` + """ + for name, module in self.image_unet.named_modules(): + if isinstance(module, DualTransformer2DModel): + parent_name, index = name.rsplit(".", 1) + index = int(index) + self.image_unet.get_submodule(parent_name)[index] = module.transformers[0] + + self.image_unet.register_to_config(dual_cross_attention=False) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.image_unet, self.text_unet, self.text_encoder, self.vae]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device with unet->image_unet + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.image_unet, "_hf_hook"): + return self.device + for module in self.image_unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_text_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + """ + + def normalize_embeddings(encoder_output): + embeds = self.text_encoder.text_projection(encoder_output.last_hidden_state) + embeds_pooled = encoder_output.text_embeds + embeds = embeds / torch.norm(embeds_pooled.unsqueeze(1), dim=-1, keepdim=True) + return embeds + + batch_size = len(prompt) + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="pt").input_ids + + if not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = normalize_embeddings(prompt_embeds) + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = prompt_embeds.shape + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens = [""] * batch_size + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = normalize_embeddings(negative_prompt_embeds) + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + def _encode_image_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + """ + + def normalize_embeddings(encoder_output): + embeds = self.image_encoder.vision_model.post_layernorm(encoder_output.last_hidden_state) + embeds = self.image_encoder.visual_projection(embeds) + embeds_pooled = embeds[:, 0:1] + embeds = embeds / torch.norm(embeds_pooled, dim=-1, keepdim=True) + return embeds + + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + image_input = self.image_feature_extractor(images=prompt, return_tensors="pt") + pixel_values = image_input.pixel_values.to(device).to(self.image_encoder.dtype) + image_embeddings = self.image_encoder(pixel_values) + image_embeddings = normalize_embeddings(image_embeddings) + + # duplicate image embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = image_embeddings.shape + image_embeddings = image_embeddings.repeat(1, num_images_per_prompt, 1) + image_embeddings = image_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_images = [np.zeros((512, 512, 3)) + 0.5] * batch_size + uncond_images = self.image_feature_extractor(images=uncond_images, return_tensors="pt") + pixel_values = uncond_images.pixel_values.to(device).to(self.image_encoder.dtype) + negative_prompt_embeds = self.image_encoder(pixel_values) + negative_prompt_embeds = normalize_embeddings(negative_prompt_embeds) + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and conditional embeddings into a single batch + # to avoid doing two forward passes + image_embeddings = torch.cat([negative_prompt_embeds, image_embeddings]) + + return image_embeddings + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + def check_inputs(self, prompt, image, height, width, callback_steps): + if not isinstance(prompt, str) and not isinstance(prompt, PIL.Image.Image) and not isinstance(prompt, list): + raise ValueError(f"`prompt` has to be of type `str` `PIL.Image` or `list` but is {type(prompt)}") + if not isinstance(image, str) and not isinstance(image, PIL.Image.Image) and not isinstance(image, list): + raise ValueError(f"`image` has to be of type `str` `PIL.Image` or `list` but is {type(image)}") + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + def set_transformer_params(self, mix_ratio: float = 0.5, condition_types: Tuple = ("text", "image")): + for name, module in self.image_unet.named_modules(): + if isinstance(module, DualTransformer2DModel): + module.mix_ratio = mix_ratio + + for i, type in enumerate(condition_types): + if type == "text": + module.condition_lengths[i] = self.text_encoder.config.max_position_embeddings + module.transformer_index_for_condition[i] = 1 # use the second (text) transformer + else: + module.condition_lengths[i] = 257 + module.transformer_index_for_condition[i] = 0 # use the first (image) transformer + + @torch.no_grad() + def __call__( + self, + prompt: Union[PIL.Image.Image, List[PIL.Image.Image]], + image: Union[str, List[str]], + text_to_image_strength: float = 0.5, + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionDualGuidedPipeline + >>> import torch + >>> import requests + >>> from io import BytesIO + >>> from PIL import Image + + >>> # let's download an initial image + >>> url = "https://huggingface.co/datasets/diffusers/images/resolve/main/benz.jpg" + + >>> response = requests.get(url) + >>> image = Image.open(BytesIO(response.content)).convert("RGB") + >>> text = "a red car in the sun" + + >>> pipe = VersatileDiffusionDualGuidedPipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe.remove_unused_weights() + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> text_to_image_strength = 0.75 + + >>> image = pipe( + ... prompt=text, image=image, text_to_image_strength=text_to_image_strength, generator=generator + ... ).images[0] + >>> image.save("./car_variation.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.ImagePipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.ImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple. When + returning a tuple, the first element is a list with the generated images. + """ + # 0. Default height and width to unet + height = height or self.image_unet.config.sample_size * self.vae_scale_factor + width = width or self.image_unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, image, height, width, callback_steps) + + # 2. Define call parameters + prompt = [prompt] if not isinstance(prompt, list) else prompt + image = [image] if not isinstance(image, list) else image + batch_size = len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompts + prompt_embeds = self._encode_text_prompt(prompt, device, num_images_per_prompt, do_classifier_free_guidance) + image_embeddings = self._encode_image_prompt(image, device, num_images_per_prompt, do_classifier_free_guidance) + dual_prompt_embeddings = torch.cat([prompt_embeds, image_embeddings], dim=1) + prompt_types = ("text", "image") + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.image_unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + dual_prompt_embeddings.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Combine the attention blocks of the image and text UNets + self.set_transformer_params(text_to_image_strength, prompt_types) + + # 8. Denoising loop + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.image_unet(latent_model_input, t, encoder_hidden_states=dual_prompt_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_image_variation.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..b0865915f033bb03884c2d5082fd229b9ec982c7 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_image_variation.py @@ -0,0 +1,427 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import numpy as np +import PIL +import torch +import torch.utils.checkpoint +from transformers import CLIPFeatureExtractor, CLIPVisionModelWithProjection + +from ...models import AutoencoderKL, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class VersatileDiffusionImageVariationPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) Model to encode and decode images to and from latent representations. + bert ([`LDMBertModel`]): + Text-encoder model based on [BERT](https://huggingface.co/docs/transformers/model_doc/bert) architecture. + tokenizer (`transformers.BertTokenizer`): + Tokenizer of class + [BertTokenizer](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + image_feature_extractor: CLIPFeatureExtractor + image_encoder: CLIPVisionModelWithProjection + image_unet: UNet2DConditionModel + vae: AutoencoderKL + scheduler: KarrasDiffusionSchedulers + + def __init__( + self, + image_feature_extractor: CLIPFeatureExtractor, + image_encoder: CLIPVisionModelWithProjection, + image_unet: UNet2DConditionModel, + vae: AutoencoderKL, + scheduler: KarrasDiffusionSchedulers, + ): + super().__init__() + self.register_modules( + image_feature_extractor=image_feature_extractor, + image_encoder=image_encoder, + image_unet=image_unet, + vae=vae, + scheduler=scheduler, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.image_unet, self.text_unet, self.text_encoder, self.vae]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device with unet->image_unet + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.image_unet, "_hf_hook"): + return self.device + for module in self.image_unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + + def normalize_embeddings(encoder_output): + embeds = self.image_encoder.vision_model.post_layernorm(encoder_output.last_hidden_state) + embeds = self.image_encoder.visual_projection(embeds) + embeds_pooled = embeds[:, 0:1] + embeds = embeds / torch.norm(embeds_pooled, dim=-1, keepdim=True) + return embeds + + if isinstance(prompt, torch.Tensor) and len(prompt.shape) == 4: + prompt = [p for p in prompt] + + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + image_input = self.image_feature_extractor(images=prompt, return_tensors="pt") + pixel_values = image_input.pixel_values.to(device).to(self.image_encoder.dtype) + image_embeddings = self.image_encoder(pixel_values) + image_embeddings = normalize_embeddings(image_embeddings) + + # duplicate image embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = image_embeddings.shape + image_embeddings = image_embeddings.repeat(1, num_images_per_prompt, 1) + image_embeddings = image_embeddings.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_images: List[str] + if negative_prompt is None: + uncond_images = [np.zeros((512, 512, 3)) + 0.5] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, PIL.Image.Image): + uncond_images = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_images = negative_prompt + + uncond_images = self.image_feature_extractor(images=uncond_images, return_tensors="pt") + pixel_values = uncond_images.pixel_values.to(device).to(self.image_encoder.dtype) + negative_prompt_embeds = self.image_encoder(pixel_values) + negative_prompt_embeds = normalize_embeddings(negative_prompt_embeds) + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and conditional embeddings into a single batch + # to avoid doing two forward passes + image_embeddings = torch.cat([negative_prompt_embeds, image_embeddings]) + + return image_embeddings + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_image_variation.StableDiffusionImageVariationPipeline.check_inputs + def check_inputs(self, image, height, width, callback_steps): + if ( + not isinstance(image, torch.Tensor) + and not isinstance(image, PIL.Image.Image) + and not isinstance(image, list) + ): + raise ValueError( + "`image` has to be of type `torch.FloatTensor` or `PIL.Image.Image` or `List[PIL.Image.Image]` but is" + f" {type(image)}" + ) + + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + image: Union[PIL.Image.Image, List[PIL.Image.Image], torch.Tensor], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + image (`PIL.Image.Image`, `List[PIL.Image.Image]` or `torch.Tensor`): + The image prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionImageVariationPipeline + >>> import torch + >>> import requests + >>> from io import BytesIO + >>> from PIL import Image + + >>> # let's download an initial image + >>> url = "https://huggingface.co/datasets/diffusers/images/resolve/main/benz.jpg" + + >>> response = requests.get(url) + >>> image = Image.open(BytesIO(response.content)).convert("RGB") + + >>> pipe = VersatileDiffusionImageVariationPipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> image = pipe(image, generator=generator).images[0] + >>> image.save("./car_variation.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.image_unet.config.sample_size * self.vae_scale_factor + width = width or self.image_unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(image, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(image, PIL.Image.Image) else len(image) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + image_embeddings = self._encode_prompt( + image, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.image_unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + image_embeddings.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.image_unet(latent_model_input, t, encoder_hidden_states=image_embeddings).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 8. Post-processing + image = self.decode_latents(latents) + + # 9. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_text_to_image.py b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_text_to_image.py new file mode 100644 index 0000000000000000000000000000000000000000..c52509c528699b4ec65401adda662341e978aeb0 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/versatile_diffusion/pipeline_versatile_diffusion_text_to_image.py @@ -0,0 +1,501 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import inspect +from typing import Callable, List, Optional, Union + +import torch +import torch.utils.checkpoint +from transformers import CLIPFeatureExtractor, CLIPTextModelWithProjection, CLIPTokenizer + +from ...models import AutoencoderKL, Transformer2DModel, UNet2DConditionModel +from ...schedulers import KarrasDiffusionSchedulers +from ...utils import is_accelerate_available, logging, randn_tensor +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput +from .modeling_text_unet import UNetFlatConditionModel + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class VersatileDiffusionTextToImagePipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + vqvae ([`VQModel`]): + Vector-quantized (VQ) Model to encode and decode images to and from latent representations. + bert ([`LDMBertModel`]): + Text-encoder model based on [BERT](https://huggingface.co/docs/transformers/model_doc/bert) architecture. + tokenizer (`transformers.BertTokenizer`): + Tokenizer of class + [BertTokenizer](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertTokenizer). + unet ([`UNet2DConditionModel`]): Conditional U-Net architecture to denoise the encoded image latents. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image latents. Can be one of + [`DDIMScheduler`], [`LMSDiscreteScheduler`], or [`PNDMScheduler`]. + """ + tokenizer: CLIPTokenizer + image_feature_extractor: CLIPFeatureExtractor + text_encoder: CLIPTextModelWithProjection + image_unet: UNet2DConditionModel + text_unet: UNetFlatConditionModel + vae: AutoencoderKL + scheduler: KarrasDiffusionSchedulers + + _optional_components = ["text_unet"] + + def __init__( + self, + tokenizer: CLIPTokenizer, + text_encoder: CLIPTextModelWithProjection, + image_unet: UNet2DConditionModel, + text_unet: UNetFlatConditionModel, + vae: AutoencoderKL, + scheduler: KarrasDiffusionSchedulers, + ): + super().__init__() + self.register_modules( + tokenizer=tokenizer, + text_encoder=text_encoder, + image_unet=image_unet, + text_unet=text_unet, + vae=vae, + scheduler=scheduler, + ) + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + + if self.text_unet is not None: + self._swap_unet_attention_blocks() + + def _swap_unet_attention_blocks(self): + """ + Swap the `Transformer2DModel` blocks between the image and text UNets + """ + for name, module in self.image_unet.named_modules(): + if isinstance(module, Transformer2DModel): + parent_name, index = name.rsplit(".", 1) + index = int(index) + self.image_unet.get_submodule(parent_name)[index], self.text_unet.get_submodule(parent_name)[index] = ( + self.text_unet.get_submodule(parent_name)[index], + self.image_unet.get_submodule(parent_name)[index], + ) + + def remove_unused_weights(self): + self.register_modules(text_unet=None) + + def enable_sequential_cpu_offload(self, gpu_id=0): + r""" + Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet, + text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a + `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called. + """ + if is_accelerate_available(): + from accelerate import cpu_offload + else: + raise ImportError("Please install accelerate via `pip install accelerate`") + + device = torch.device(f"cuda:{gpu_id}") + + for cpu_offloaded_model in [self.image_unet, self.text_unet, self.text_encoder, self.vae]: + if cpu_offloaded_model is not None: + cpu_offload(cpu_offloaded_model, device) + + @property + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device with unet->image_unet + def _execution_device(self): + r""" + Returns the device on which the pipeline's models will be executed. After calling + `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module + hooks. + """ + if self.device != torch.device("meta") or not hasattr(self.image_unet, "_hf_hook"): + return self.device + for module in self.image_unet.modules(): + if ( + hasattr(module, "_hf_hook") + and hasattr(module._hf_hook, "execution_device") + and module._hf_hook.execution_device is not None + ): + return torch.device(module._hf_hook.execution_device) + return self.device + + def _encode_prompt(self, prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt): + r""" + Encodes the prompt into text encoder hidden states. + + Args: + prompt (`str` or `List[str]`): + prompt to be encoded + device: (`torch.device`): + torch device + num_images_per_prompt (`int`): + number of images that should be generated per prompt + do_classifier_free_guidance (`bool`): + whether to use classifier free guidance or not + negative_prompt (`str` or `List[str]`): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + """ + + def normalize_embeddings(encoder_output): + embeds = self.text_encoder.text_projection(encoder_output.last_hidden_state) + embeds_pooled = encoder_output.text_embeds + embeds = embeds / torch.norm(embeds_pooled.unsqueeze(1), dim=-1, keepdim=True) + return embeds + + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = self.tokenizer(prompt, padding="max_length", return_tensors="pt").input_ids + + if not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = text_inputs.attention_mask.to(device) + else: + attention_mask = None + + prompt_embeds = self.text_encoder( + text_input_ids.to(device), + attention_mask=attention_mask, + ) + prompt_embeds = normalize_embeddings(prompt_embeds) + + # duplicate text embeddings for each generation per prompt, using mps friendly method + bs_embed, seq_len, _ = prompt_embeds.shape + prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1) + prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1) + + # get unconditional embeddings for classifier free guidance + if do_classifier_free_guidance: + uncond_tokens: List[str] + if negative_prompt is None: + uncond_tokens = [""] * batch_size + elif type(prompt) is not type(negative_prompt): + raise TypeError( + f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" + f" {type(prompt)}." + ) + elif isinstance(negative_prompt, str): + uncond_tokens = [negative_prompt] + elif batch_size != len(negative_prompt): + raise ValueError( + f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" + f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" + " the batch size of `prompt`." + ) + else: + uncond_tokens = negative_prompt + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + + if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask: + attention_mask = uncond_input.attention_mask.to(device) + else: + attention_mask = None + + negative_prompt_embeds = self.text_encoder( + uncond_input.input_ids.to(device), + attention_mask=attention_mask, + ) + negative_prompt_embeds = normalize_embeddings(negative_prompt_embeds) + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.decode_latents + def decode_latents(self, latents): + latents = 1 / self.vae.config.scaling_factor * latents + image = self.vae.decode(latents).sample + image = (image / 2 + 0.5).clamp(0, 1) + # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16 + image = image.cpu().permute(0, 2, 3, 1).float().numpy() + return image + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_extra_step_kwargs + def prepare_extra_step_kwargs(self, generator, eta): + # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature + # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers. + # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502 + # and should be between [0, 1] + + accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()) + extra_step_kwargs = {} + if accepts_eta: + extra_step_kwargs["eta"] = eta + + # check if the scheduler accepts generator + accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()) + if accepts_generator: + extra_step_kwargs["generator"] = generator + return extra_step_kwargs + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.check_inputs + def check_inputs( + self, + prompt, + height, + width, + callback_steps, + negative_prompt=None, + prompt_embeds=None, + negative_prompt_embeds=None, + ): + if height % 8 != 0 or width % 8 != 0: + raise ValueError(f"`height` and `width` have to be divisible by 8 but are {height} and {width}.") + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + if prompt is not None and prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" + " only forward one of the two." + ) + elif prompt is None and prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + if negative_prompt is not None and negative_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" + f" {negative_prompt_embeds}. Please make sure to only forward one of the two." + ) + + if prompt_embeds is not None and negative_prompt_embeds is not None: + if prompt_embeds.shape != negative_prompt_embeds.shape: + raise ValueError( + "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" + f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" + f" {negative_prompt_embeds.shape}." + ) + + # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.prepare_latents + def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None): + shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor) + if isinstance(generator, list) and len(generator) != batch_size: + raise ValueError( + f"You have passed a list of generators of length {len(generator)}, but requested an effective batch" + f" size of {batch_size}. Make sure the batch size matches the length of the generators." + ) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype) + else: + latents = latents.to(device) + + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * self.scheduler.init_noise_sigma + return latents + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + height: Optional[int] = None, + width: Optional[int] = None, + num_inference_steps: int = 50, + guidance_scale: float = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + num_images_per_prompt: Optional[int] = 1, + eta: float = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + **kwargs, + ): + r""" + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + height (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The height in pixels of the generated image. + width (`int`, *optional*, defaults to self.image_unet.config.sample_size * self.vae_scale_factor): + The width in pixels of the generated image. + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + negative_prompt (`str` or `List[str]`, *optional*): + The prompt or prompts not to guide the image generation. Ignored when not using guidance (i.e., ignored + if `guidance_scale` is less than `1`). + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + eta (`float`, *optional*, defaults to 0.0): + Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to + [`schedulers.DDIMScheduler`], will be ignored for others. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor`, *optional*): + Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image + generation. Can be used to tweak the same generation with different prompts. If not provided, a latents + tensor will ge generated by sampling using the supplied random `generator`. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a + plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Examples: + + ```py + >>> from diffusers import VersatileDiffusionTextToImagePipeline + >>> import torch + + >>> pipe = VersatileDiffusionTextToImagePipeline.from_pretrained( + ... "shi-labs/versatile-diffusion", torch_dtype=torch.float16 + ... ) + >>> pipe.remove_unused_weights() + >>> pipe = pipe.to("cuda") + + >>> generator = torch.Generator(device="cuda").manual_seed(0) + >>> image = pipe("an astronaut riding on a horse on mars", generator=generator).images[0] + >>> image.save("./astronaut.png") + ``` + + Returns: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`: + [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] if `return_dict` is True, otherwise a `tuple. + When returning a tuple, the first element is a list with the generated images, and the second element is a + list of `bool`s denoting whether the corresponding generated image likely represents "not-safe-for-work" + (nsfw) content, according to the `safety_checker`. + """ + # 0. Default height and width to unet + height = height or self.image_unet.config.sample_size * self.vae_scale_factor + width = width or self.image_unet.config.sample_size * self.vae_scale_factor + + # 1. Check inputs. Raise error if not correct + self.check_inputs(prompt, height, width, callback_steps) + + # 2. Define call parameters + batch_size = 1 if isinstance(prompt, str) else len(prompt) + device = self._execution_device + # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` + # corresponds to doing no classifier free guidance. + do_classifier_free_guidance = guidance_scale > 1.0 + + # 3. Encode input prompt + prompt_embeds = self._encode_prompt( + prompt, device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + # 4. Prepare timesteps + self.scheduler.set_timesteps(num_inference_steps, device=device) + timesteps = self.scheduler.timesteps + + # 5. Prepare latent variables + num_channels_latents = self.image_unet.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + prompt_embeds.dtype, + device, + generator, + latents, + ) + + # 6. Prepare extra step kwargs. + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7. Denoising loop + for i, t in enumerate(self.progress_bar(timesteps)): + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.image_unet(latent_model_input, t, encoder_hidden_states=prompt_embeds).sample + + # perform guidance + if do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, latents) + + # 9. Post-processing + image = self.decode_latents(latents) + + # 10. Convert to PIL + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) diff --git a/diffusers/src/diffusers/pipelines/vq_diffusion/__init__.py b/diffusers/src/diffusers/pipelines/vq_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8c9f14f000648347fe75a5bec0cb45d08c7d2ff9 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/vq_diffusion/__init__.py @@ -0,0 +1,5 @@ +from ...utils import is_torch_available, is_transformers_available + + +if is_transformers_available() and is_torch_available(): + from .pipeline_vq_diffusion import LearnedClassifierFreeSamplingEmbeddings, VQDiffusionPipeline diff --git a/diffusers/src/diffusers/pipelines/vq_diffusion/pipeline_vq_diffusion.py b/diffusers/src/diffusers/pipelines/vq_diffusion/pipeline_vq_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..a26704e60f8bcd6685bd934a357ce6c6a7736456 --- /dev/null +++ b/diffusers/src/diffusers/pipelines/vq_diffusion/pipeline_vq_diffusion.py @@ -0,0 +1,330 @@ +# Copyright 2022 Microsoft and The HuggingFace Team. All rights reserved. +# +# 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. + +from typing import Callable, List, Optional, Tuple, Union + +import torch +from transformers import CLIPTextModel, CLIPTokenizer + +from ...configuration_utils import ConfigMixin, register_to_config +from ...models import ModelMixin, Transformer2DModel, VQModel +from ...schedulers import VQDiffusionScheduler +from ...utils import logging +from ..pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class LearnedClassifierFreeSamplingEmbeddings(ModelMixin, ConfigMixin): + """ + Utility class for storing learned text embeddings for classifier free sampling + """ + + @register_to_config + def __init__(self, learnable: bool, hidden_size: Optional[int] = None, length: Optional[int] = None): + super().__init__() + + self.learnable = learnable + + if self.learnable: + assert hidden_size is not None, "learnable=True requires `hidden_size` to be set" + assert length is not None, "learnable=True requires `length` to be set" + + embeddings = torch.zeros(length, hidden_size) + else: + embeddings = None + + self.embeddings = torch.nn.Parameter(embeddings) + + +class VQDiffusionPipeline(DiffusionPipeline): + r""" + Pipeline for text-to-image generation using VQ Diffusion + + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Args: + vqvae ([`VQModel`]): + Vector Quantized Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent + representations. + text_encoder ([`CLIPTextModel`]): + Frozen text-encoder. VQ Diffusion uses the text portion of + [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel), specifically + the [clip-vit-base-patch32](https://huggingface.co/openai/clip-vit-base-patch32) variant. + tokenizer (`CLIPTokenizer`): + Tokenizer of class + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer). + transformer ([`Transformer2DModel`]): + Conditional transformer to denoise the encoded image latents. + scheduler ([`VQDiffusionScheduler`]): + A scheduler to be used in combination with `transformer` to denoise the encoded image latents. + """ + + vqvae: VQModel + text_encoder: CLIPTextModel + tokenizer: CLIPTokenizer + transformer: Transformer2DModel + learned_classifier_free_sampling_embeddings: LearnedClassifierFreeSamplingEmbeddings + scheduler: VQDiffusionScheduler + + def __init__( + self, + vqvae: VQModel, + text_encoder: CLIPTextModel, + tokenizer: CLIPTokenizer, + transformer: Transformer2DModel, + scheduler: VQDiffusionScheduler, + learned_classifier_free_sampling_embeddings: LearnedClassifierFreeSamplingEmbeddings, + ): + super().__init__() + + self.register_modules( + vqvae=vqvae, + transformer=transformer, + text_encoder=text_encoder, + tokenizer=tokenizer, + scheduler=scheduler, + learned_classifier_free_sampling_embeddings=learned_classifier_free_sampling_embeddings, + ) + + def _encode_prompt(self, prompt, num_images_per_prompt, do_classifier_free_guidance): + batch_size = len(prompt) if isinstance(prompt, list) else 1 + + # get prompt text embeddings + text_inputs = self.tokenizer( + prompt, + padding="max_length", + max_length=self.tokenizer.model_max_length, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + + if text_input_ids.shape[-1] > self.tokenizer.model_max_length: + removed_text = self.tokenizer.batch_decode(text_input_ids[:, self.tokenizer.model_max_length :]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {self.tokenizer.model_max_length} tokens: {removed_text}" + ) + text_input_ids = text_input_ids[:, : self.tokenizer.model_max_length] + prompt_embeds = self.text_encoder(text_input_ids.to(self.device))[0] + + # NOTE: This additional step of normalizing the text embeddings is from VQ-Diffusion. + # While CLIP does normalize the pooled output of the text transformer when combining + # the image and text embeddings, CLIP does not directly normalize the last hidden state. + # + # CLIP normalizing the pooled output. + # https://github.com/huggingface/transformers/blob/d92e22d1f28324f513f3080e5c47c071a3916721/src/transformers/models/clip/modeling_clip.py#L1052-L1053 + prompt_embeds = prompt_embeds / prompt_embeds.norm(dim=-1, keepdim=True) + + # duplicate text embeddings for each generation per prompt + prompt_embeds = prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) + + if do_classifier_free_guidance: + if self.learned_classifier_free_sampling_embeddings.learnable: + negative_prompt_embeds = self.learned_classifier_free_sampling_embeddings.embeddings + negative_prompt_embeds = negative_prompt_embeds.unsqueeze(0).repeat(batch_size, 1, 1) + else: + uncond_tokens = [""] * batch_size + + max_length = text_input_ids.shape[-1] + uncond_input = self.tokenizer( + uncond_tokens, + padding="max_length", + max_length=max_length, + truncation=True, + return_tensors="pt", + ) + negative_prompt_embeds = self.text_encoder(uncond_input.input_ids.to(self.device))[0] + # See comment for normalizing text embeddings + negative_prompt_embeds = negative_prompt_embeds / negative_prompt_embeds.norm(dim=-1, keepdim=True) + + # duplicate unconditional embeddings for each generation per prompt, using mps friendly method + seq_len = negative_prompt_embeds.shape[1] + negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1) + + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + return prompt_embeds + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]], + num_inference_steps: int = 100, + guidance_scale: float = 5.0, + truncation_rate: float = 1.0, + num_images_per_prompt: int = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.FloatTensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None, + callback_steps: Optional[int] = 1, + ) -> Union[ImagePipelineOutput, Tuple]: + """ + Function invoked when calling the pipeline for generation. + + Args: + prompt (`str` or `List[str]`): + The prompt or prompts to guide the image generation. + num_inference_steps (`int`, *optional*, defaults to 100): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + guidance_scale (`float`, *optional*, defaults to 7.5): + Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598). + `guidance_scale` is defined as `w` of equation 2. of [Imagen + Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale > + 1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`, + usually at the expense of lower image quality. + truncation_rate (`float`, *optional*, defaults to 1.0 (equivalent to no truncation)): + Used to "truncate" the predicted classes for x_0 such that the cumulative probability for a pixel is at + most `truncation_rate`. The lowest probabilities that would increase the cumulative probability above + `truncation_rate` are set to zero. + num_images_per_prompt (`int`, *optional*, defaults to 1): + The number of images to generate per prompt. + generator (`torch.Generator`, *optional*): + One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html) + to make generation deterministic. + latents (`torch.FloatTensor` of shape (batch), *optional*): + Pre-generated noisy latents to be used as inputs for image generation. Must be valid embedding indices. + Can be used to tweak the same generation with different prompts. If not provided, a latents tensor will + be generated of completely masked latent pixels. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generated image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + callback (`Callable`, *optional*): + A function that will be called every `callback_steps` steps during inference. The function will be + called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`. + callback_steps (`int`, *optional*, defaults to 1): + The frequency at which the `callback` function will be called. If not specified, the callback will be + called at every step. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~ pipeline_utils.ImagePipelineOutput `] if `return_dict` + is True, otherwise a `tuple. When returning a tuple, the first element is a list with the generated images. + """ + if isinstance(prompt, str): + batch_size = 1 + elif isinstance(prompt, list): + batch_size = len(prompt) + else: + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + + batch_size = batch_size * num_images_per_prompt + + do_classifier_free_guidance = guidance_scale > 1.0 + + prompt_embeds = self._encode_prompt(prompt, num_images_per_prompt, do_classifier_free_guidance) + + if (callback_steps is None) or ( + callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0) + ): + raise ValueError( + f"`callback_steps` has to be a positive integer but is {callback_steps} of type" + f" {type(callback_steps)}." + ) + + # get the initial completely masked latents unless the user supplied it + + latents_shape = (batch_size, self.transformer.num_latent_pixels) + if latents is None: + mask_class = self.transformer.num_vector_embeds - 1 + latents = torch.full(latents_shape, mask_class).to(self.device) + else: + if latents.shape != latents_shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") + if (latents < 0).any() or (latents >= self.transformer.num_vector_embeds).any(): + raise ValueError( + "Unexpected latents value(s). All latents be valid embedding indices i.e. in the range 0," + f" {self.transformer.num_vector_embeds - 1} (inclusive)." + ) + latents = latents.to(self.device) + + # set timesteps + self.scheduler.set_timesteps(num_inference_steps, device=self.device) + + timesteps_tensor = self.scheduler.timesteps.to(self.device) + + sample = latents + + for i, t in enumerate(self.progress_bar(timesteps_tensor)): + # expand the sample if we are doing classifier free guidance + latent_model_input = torch.cat([sample] * 2) if do_classifier_free_guidance else sample + + # predict the un-noised image + # model_output == `log_p_x_0` + model_output = self.transformer(latent_model_input, encoder_hidden_states=prompt_embeds, timestep=t).sample + + if do_classifier_free_guidance: + model_output_uncond, model_output_text = model_output.chunk(2) + model_output = model_output_uncond + guidance_scale * (model_output_text - model_output_uncond) + model_output -= torch.logsumexp(model_output, dim=1, keepdim=True) + + model_output = self.truncate(model_output, truncation_rate) + + # remove `log(0)`'s (`-inf`s) + model_output = model_output.clamp(-70) + + # compute the previous noisy sample x_t -> x_t-1 + sample = self.scheduler.step(model_output, timestep=t, sample=sample, generator=generator).prev_sample + + # call the callback, if provided + if callback is not None and i % callback_steps == 0: + callback(i, t, sample) + + embedding_channels = self.vqvae.config.vq_embed_dim + embeddings_shape = (batch_size, self.transformer.height, self.transformer.width, embedding_channels) + embeddings = self.vqvae.quantize.get_codebook_entry(sample, shape=embeddings_shape) + image = self.vqvae.decode(embeddings, force_not_quantize=True).sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,) + + return ImagePipelineOutput(images=image) + + def truncate(self, log_p_x_0: torch.FloatTensor, truncation_rate: float) -> torch.FloatTensor: + """ + Truncates log_p_x_0 such that for each column vector, the total cumulative probability is `truncation_rate` The + lowest probabilities that would increase the cumulative probability above `truncation_rate` are set to zero. + """ + sorted_log_p_x_0, indices = torch.sort(log_p_x_0, 1, descending=True) + sorted_p_x_0 = torch.exp(sorted_log_p_x_0) + keep_mask = sorted_p_x_0.cumsum(dim=1) < truncation_rate + + # Ensure that at least the largest probability is not zeroed out + all_true = torch.full_like(keep_mask[:, 0:1, :], True) + keep_mask = torch.cat((all_true, keep_mask), dim=1) + keep_mask = keep_mask[:, :-1, :] + + keep_mask = keep_mask.gather(1, indices.argsort(1)) + + rv = log_p_x_0.clone() + + rv[~keep_mask] = -torch.inf # -inf = log(0) + + return rv diff --git a/diffusers/src/diffusers/schedulers/README.md b/diffusers/src/diffusers/schedulers/README.md new file mode 100644 index 0000000000000000000000000000000000000000..31ad27793e34783faabc222adf98691fb396a0d8 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/README.md @@ -0,0 +1,3 @@ +# Schedulers + +For more information on the schedulers, please refer to the [docs](https://huggingface.co/docs/diffusers/api/schedulers/overview). \ No newline at end of file diff --git a/diffusers/src/diffusers/schedulers/__init__.py b/diffusers/src/diffusers/schedulers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3746acd5b5760d3c52192d1dc4e8ac0a599f562b --- /dev/null +++ b/diffusers/src/diffusers/schedulers/__init__.py @@ -0,0 +1,72 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from ..utils import OptionalDependencyNotAvailable, is_flax_available, is_scipy_available, is_torch_available + + +try: + if not is_torch_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_pt_objects import * # noqa F403 +else: + from .scheduling_ddim import DDIMScheduler + from .scheduling_ddpm import DDPMScheduler + from .scheduling_deis_multistep import DEISMultistepScheduler + from .scheduling_dpmsolver_multistep import DPMSolverMultistepScheduler + from .scheduling_dpmsolver_singlestep import DPMSolverSinglestepScheduler + from .scheduling_euler_ancestral_discrete import EulerAncestralDiscreteScheduler + from .scheduling_euler_discrete import EulerDiscreteScheduler + from .scheduling_heun_discrete import HeunDiscreteScheduler + from .scheduling_ipndm import IPNDMScheduler + from .scheduling_k_dpm_2_ancestral_discrete import KDPM2AncestralDiscreteScheduler + from .scheduling_k_dpm_2_discrete import KDPM2DiscreteScheduler + from .scheduling_karras_ve import KarrasVeScheduler + from .scheduling_pndm import PNDMScheduler + from .scheduling_repaint import RePaintScheduler + from .scheduling_sde_ve import ScoreSdeVeScheduler + from .scheduling_sde_vp import ScoreSdeVpScheduler + from .scheduling_unclip import UnCLIPScheduler + from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + from .scheduling_vq_diffusion import VQDiffusionScheduler + +try: + if not is_flax_available(): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_flax_objects import * # noqa F403 +else: + from .scheduling_ddim_flax import FlaxDDIMScheduler + from .scheduling_ddpm_flax import FlaxDDPMScheduler + from .scheduling_dpmsolver_multistep_flax import FlaxDPMSolverMultistepScheduler + from .scheduling_karras_ve_flax import FlaxKarrasVeScheduler + from .scheduling_lms_discrete_flax import FlaxLMSDiscreteScheduler + from .scheduling_pndm_flax import FlaxPNDMScheduler + from .scheduling_sde_ve_flax import FlaxScoreSdeVeScheduler + from .scheduling_utils_flax import ( + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + broadcast_to_shape_from_left, + ) + + +try: + if not (is_torch_available() and is_scipy_available()): + raise OptionalDependencyNotAvailable() +except OptionalDependencyNotAvailable: + from ..utils.dummy_torch_and_scipy_objects import * # noqa F403 +else: + from .scheduling_lms_discrete import LMSDiscreteScheduler diff --git a/diffusers/src/diffusers/schedulers/scheduling_ddim.py b/diffusers/src/diffusers/schedulers/scheduling_ddim.py new file mode 100644 index 0000000000000000000000000000000000000000..4eeb67f6b182c18113ffdbeeab6902df7ebbef4c --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_ddim.py @@ -0,0 +1,372 @@ +# Copyright 2022 Stanford University Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This code is strongly influenced by https://github.com/pesser/pytorch_diffusion +# and https://github.com/hojonathanho/diffusion + +import math +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + + +@dataclass +# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->DDIM +class DDIMSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999) -> torch.Tensor: + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas) + + +class DDIMScheduler(SchedulerMixin, ConfigMixin): + """ + Denoising diffusion implicit models is a scheduler that extends the denoising procedure introduced in denoising + diffusion probabilistic models (DDPMs) with non-Markovian guidance. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2010.02502 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + clip_sample (`bool`, default `True`): + option to clip predicted sample between -1 and 1 for numerical stability. + set_alpha_to_one (`bool`, default `True`): + each diffusion step uses the value of alphas product at that step and at the previous one. For the final + step there is no previous alpha. When this option is `True` the previous alpha product is fixed to `1`, + otherwise it uses the value of alpha at step 0. + steps_offset (`int`, default `0`): + an offset added to the inference steps. You can use a combination of `offset=1` and + `set_alpha_to_one=False`, to make the last step use step 0 for the previous alpha product, as done in + stable diffusion. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + clip_sample: bool = True, + set_alpha_to_one: bool = True, + steps_offset: int = 0, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + # At every step in ddim, we are looking into the previous alphas_cumprod + # For the final step, there is no previous alphas_cumprod because we are already at 0 + # `set_alpha_to_one` decides whether we set this parameter simply to one or + # whether we use the final alpha of the "non-previous" one. + self.final_alpha_cumprod = torch.tensor(1.0) if set_alpha_to_one else self.alphas_cumprod[0] + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # setable values + self.num_inference_steps = None + self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy().astype(np.int64)) + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def _get_variance(self, timestep, prev_timestep): + alpha_prod_t = self.alphas_cumprod[timestep] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + variance = (beta_prod_t_prev / beta_prod_t) * (1 - alpha_prod_t / alpha_prod_t_prev) + + return variance + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + + if num_inference_steps > self.config.num_train_timesteps: + raise ValueError( + f"`num_inference_steps`: {num_inference_steps} cannot be larger than `self.config.train_timesteps`:" + f" {self.config.num_train_timesteps} as the unet model trained with this scheduler can only handle" + f" maximal {self.config.num_train_timesteps} timesteps." + ) + + self.num_inference_steps = num_inference_steps + step_ratio = self.config.num_train_timesteps // self.num_inference_steps + # creates integer timesteps by multiplying by ratio + # casting to int to avoid issues when num_inference_step is power of 3 + timesteps = (np.arange(0, num_inference_steps) * step_ratio).round()[::-1].copy().astype(np.int64) + self.timesteps = torch.from_numpy(timesteps).to(device) + self.timesteps += self.config.steps_offset + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + eta: float = 0.0, + use_clipped_model_output: bool = False, + generator=None, + variance_noise: Optional[torch.FloatTensor] = None, + return_dict: bool = True, + ) -> Union[DDIMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + eta (`float`): weight of noise for added noise in diffusion step. + use_clipped_model_output (`bool`): if `True`, compute "corrected" `model_output` from the clipped + predicted original sample. Necessary because predicted original sample is clipped to [-1, 1] when + `self.config.clip_sample` is `True`. If no clipping has happened, "corrected" `model_output` would + coincide with the one provided as input and `use_clipped_model_output` will have not effect. + generator: random number generator. + variance_noise (`torch.FloatTensor`): instead of generating noise for the variance using `generator`, we + can directly provide the noise for the variance itself. This is useful for methods such as + CycleDiffusion. (https://arxiv.org/abs/2210.05559) + return_dict (`bool`): option for returning tuple rather than DDIMSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.DDIMSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.DDIMSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + # See formulas (12) and (16) of DDIM paper https://arxiv.org/pdf/2010.02502.pdf + # Ideally, read DDIM paper in-detail understanding + + # Notation ( -> + # - pred_noise_t -> e_theta(x_t, t) + # - pred_original_sample -> f_theta(x_t, t) or x_0 + # - std_dev_t -> sigma_t + # - eta -> η + # - pred_sample_direction -> "direction pointing to x_t" + # - pred_prev_sample -> "x_t-1" + + # 1. get previous step value (=t-1) + prev_timestep = timestep - self.config.num_train_timesteps // self.num_inference_steps + + # 2. compute alphas, betas + alpha_prod_t = self.alphas_cumprod[timestep] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + + beta_prod_t = 1 - alpha_prod_t + + # 3. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + if self.config.prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif self.config.prediction_type == "sample": + pred_original_sample = model_output + elif self.config.prediction_type == "v_prediction": + pred_original_sample = (alpha_prod_t**0.5) * sample - (beta_prod_t**0.5) * model_output + # predict V + model_output = (alpha_prod_t**0.5) * model_output + (beta_prod_t**0.5) * sample + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction`" + ) + + # 4. Clip "predicted x_0" + if self.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -1, 1) + + # 5. compute variance: "sigma_t(η)" -> see formula (16) + # σ_t = sqrt((1 − α_t−1)/(1 − α_t)) * sqrt(1 − α_t/α_t−1) + variance = self._get_variance(timestep, prev_timestep) + std_dev_t = eta * variance ** (0.5) + + if use_clipped_model_output: + # the model_output is always re-derived from the clipped x_0 in Glide + model_output = (sample - alpha_prod_t ** (0.5) * pred_original_sample) / beta_prod_t ** (0.5) + + # 6. compute "direction pointing to x_t" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * model_output + + # 7. compute x_t without "random noise" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction + + if eta > 0: + device = model_output.device + if variance_noise is not None and generator is not None: + raise ValueError( + "Cannot pass both generator and variance_noise. Please make sure that either `generator` or" + " `variance_noise` stays `None`." + ) + + if variance_noise is None: + variance_noise = randn_tensor( + model_output.shape, generator=generator, device=device, dtype=model_output.dtype + ) + variance = std_dev_t * variance_noise + + prev_sample = prev_sample + variance + + if not return_dict: + return (prev_sample,) + + return DDIMSchedulerOutput(prev_sample=prev_sample, pred_original_sample=pred_original_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def get_velocity( + self, sample: torch.FloatTensor, noise: torch.FloatTensor, timesteps: torch.IntTensor + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as sample + self.alphas_cumprod = self.alphas_cumprod.to(device=sample.device, dtype=sample.dtype) + timesteps = timesteps.to(sample.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(sample.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(sample.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + velocity = sqrt_alpha_prod * noise - sqrt_one_minus_alpha_prod * sample + return velocity + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_ddim_flax.py b/diffusers/src/diffusers/schedulers/scheduling_ddim_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..565b7ff3c9c283f79c0b64f68fee2c3fdc178577 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_ddim_flax.py @@ -0,0 +1,304 @@ +# Copyright 2022 Stanford University Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This code is strongly influenced by https://github.com/pesser/pytorch_diffusion +# and https://github.com/hojonathanho/diffusion + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax.numpy as jnp + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import ( + CommonSchedulerState, + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + add_noise_common, + get_velocity_common, +) + + +@flax.struct.dataclass +class DDIMSchedulerState: + common: CommonSchedulerState + final_alpha_cumprod: jnp.ndarray + + # setable values + init_noise_sigma: jnp.ndarray + timesteps: jnp.ndarray + num_inference_steps: Optional[int] = None + + @classmethod + def create( + cls, + common: CommonSchedulerState, + final_alpha_cumprod: jnp.ndarray, + init_noise_sigma: jnp.ndarray, + timesteps: jnp.ndarray, + ): + return cls( + common=common, + final_alpha_cumprod=final_alpha_cumprod, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + +@dataclass +class FlaxDDIMSchedulerOutput(FlaxSchedulerOutput): + state: DDIMSchedulerState + + +class FlaxDDIMScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + Denoising diffusion implicit models is a scheduler that extends the denoising procedure introduced in denoising + diffusion probabilistic models (DDPMs) with non-Markovian guidance. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2010.02502 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`jnp.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + clip_sample (`bool`, default `True`): + option to clip predicted sample between -1 and 1 for numerical stability. + set_alpha_to_one (`bool`, default `True`): + each diffusion step uses the value of alphas product at that step and at the previous one. For the final + step there is no previous alpha. When this option is `True` the previous alpha product is fixed to `1`, + otherwise it uses the value of alpha at step 0. + steps_offset (`int`, default `0`): + an offset added to the inference steps. You can use a combination of `offset=1` and + `set_alpha_to_one=False`, to make the last step use step 0 for the previous alpha product, as done in + stable diffusion. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the samples. One of `epsilon`, `sample`. + `v-prediction` is not supported for this scheduler. + dtype (`jnp.dtype`, *optional*, defaults to `jnp.float32`): + the `dtype` used for params and computation. + """ + + _compatibles = [e.name for e in FlaxKarrasDiffusionSchedulers] + + dtype: jnp.dtype + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[jnp.ndarray] = None, + set_alpha_to_one: bool = True, + steps_offset: int = 0, + prediction_type: str = "epsilon", + dtype: jnp.dtype = jnp.float32, + ): + self.dtype = dtype + + def create_state(self, common: Optional[CommonSchedulerState] = None) -> DDIMSchedulerState: + if common is None: + common = CommonSchedulerState.create(self) + + # At every step in ddim, we are looking into the previous alphas_cumprod + # For the final step, there is no previous alphas_cumprod because we are already at 0 + # `set_alpha_to_one` decides whether we set this parameter simply to one or + # whether we use the final alpha of the "non-previous" one. + final_alpha_cumprod = ( + jnp.array(1.0, dtype=self.dtype) if self.config.set_alpha_to_one else common.alphas_cumprod[0] + ) + + # standard deviation of the initial noise distribution + init_noise_sigma = jnp.array(1.0, dtype=self.dtype) + + timesteps = jnp.arange(0, self.config.num_train_timesteps).round()[::-1] + + return DDIMSchedulerState.create( + common=common, + final_alpha_cumprod=final_alpha_cumprod, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + def scale_model_input( + self, state: DDIMSchedulerState, sample: jnp.ndarray, timestep: Optional[int] = None + ) -> jnp.ndarray: + """ + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + sample (`jnp.ndarray`): input sample + timestep (`int`, optional): current timestep + + Returns: + `jnp.ndarray`: scaled input sample + """ + return sample + + def set_timesteps( + self, state: DDIMSchedulerState, num_inference_steps: int, shape: Tuple = () + ) -> DDIMSchedulerState: + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`DDIMSchedulerState`): + the `FlaxDDIMScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + step_ratio = self.config.num_train_timesteps // num_inference_steps + # creates integer timesteps by multiplying by ratio + # rounding to avoid issues when num_inference_step is power of 3 + timesteps = (jnp.arange(0, num_inference_steps) * step_ratio).round()[::-1] + self.config.steps_offset + + return state.replace( + num_inference_steps=num_inference_steps, + timesteps=timesteps, + ) + + def _get_variance(self, state: DDIMSchedulerState, timestep, prev_timestep): + alpha_prod_t = state.common.alphas_cumprod[timestep] + alpha_prod_t_prev = jnp.where( + prev_timestep >= 0, state.common.alphas_cumprod[prev_timestep], state.final_alpha_cumprod + ) + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + variance = (beta_prod_t_prev / beta_prod_t) * (1 - alpha_prod_t / alpha_prod_t_prev) + + return variance + + def step( + self, + state: DDIMSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + eta: float = 0.0, + return_dict: bool = True, + ) -> Union[FlaxDDIMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + state (`DDIMSchedulerState`): the `FlaxDDIMScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than FlaxDDIMSchedulerOutput class + + Returns: + [`FlaxDDIMSchedulerOutput`] or `tuple`: [`FlaxDDIMSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + # See formulas (12) and (16) of DDIM paper https://arxiv.org/pdf/2010.02502.pdf + # Ideally, read DDIM paper in-detail understanding + + # Notation ( -> + # - pred_noise_t -> e_theta(x_t, t) + # - pred_original_sample -> f_theta(x_t, t) or x_0 + # - std_dev_t -> sigma_t + # - eta -> η + # - pred_sample_direction -> "direction pointing to x_t" + # - pred_prev_sample -> "x_t-1" + + # 1. get previous step value (=t-1) + prev_timestep = timestep - self.config.num_train_timesteps // state.num_inference_steps + + alphas_cumprod = state.common.alphas_cumprod + final_alpha_cumprod = state.final_alpha_cumprod + + # 2. compute alphas, betas + alpha_prod_t = alphas_cumprod[timestep] + alpha_prod_t_prev = jnp.where(prev_timestep >= 0, alphas_cumprod[prev_timestep], final_alpha_cumprod) + + beta_prod_t = 1 - alpha_prod_t + + # 3. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + if self.config.prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif self.config.prediction_type == "sample": + pred_original_sample = model_output + elif self.config.prediction_type == "v_prediction": + pred_original_sample = (alpha_prod_t**0.5) * sample - (beta_prod_t**0.5) * model_output + # predict V + model_output = (alpha_prod_t**0.5) * model_output + (beta_prod_t**0.5) * sample + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction`" + ) + + # 4. compute variance: "sigma_t(η)" -> see formula (16) + # σ_t = sqrt((1 − α_t−1)/(1 − α_t)) * sqrt(1 − α_t/α_t−1) + variance = self._get_variance(state, timestep, prev_timestep) + std_dev_t = eta * variance ** (0.5) + + # 5. compute "direction pointing to x_t" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * model_output + + # 6. compute x_t without "random noise" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction + + if not return_dict: + return (prev_sample, state) + + return FlaxDDIMSchedulerOutput(prev_sample=prev_sample, state=state) + + def add_noise( + self, + state: DDIMSchedulerState, + original_samples: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return add_noise_common(state.common, original_samples, noise, timesteps) + + def get_velocity( + self, + state: DDIMSchedulerState, + sample: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return get_velocity_common(state.common, sample, noise, timesteps) + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_ddpm.py b/diffusers/src/diffusers/schedulers/scheduling_ddpm.py new file mode 100644 index 0000000000000000000000000000000000000000..9d8aa6fa5b2f1e46fea85ea3a38ceaf2cb7088ef --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_ddpm.py @@ -0,0 +1,364 @@ +# Copyright 2022 UC Berkeley Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/ermongroup/ddim + +import math +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + + +@dataclass +class DDPMSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class DDPMScheduler(SchedulerMixin, ConfigMixin): + """ + Denoising diffusion probabilistic models (DDPMs) explores the connections between denoising score matching and + Langevin dynamics sampling. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2006.11239 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + variance_type (`str`): + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + clip_sample (`bool`, default `True`): + option to clip predicted sample between -1 and 1 for numerical stability. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + variance_type: str = "fixed_small", + clip_sample: bool = True, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + elif beta_schedule == "sigmoid": + # GeoDiff sigmoid schedule + betas = torch.linspace(-6, 6, num_train_timesteps) + self.betas = torch.sigmoid(betas) * (beta_end - beta_start) + beta_start + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.one = torch.tensor(1.0) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # setable values + self.num_inference_steps = None + self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy()) + + self.variance_type = variance_type + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + + if num_inference_steps > self.config.num_train_timesteps: + raise ValueError( + f"`num_inference_steps`: {num_inference_steps} cannot be larger than `self.config.train_timesteps`:" + f" {self.config.num_train_timesteps} as the unet model trained with this scheduler can only handle" + f" maximal {self.config.num_train_timesteps} timesteps." + ) + + self.num_inference_steps = num_inference_steps + + step_ratio = self.config.num_train_timesteps // self.num_inference_steps + timesteps = (np.arange(0, num_inference_steps) * step_ratio).round()[::-1].copy().astype(np.int64) + self.timesteps = torch.from_numpy(timesteps).to(device) + + def _get_variance(self, t, predicted_variance=None, variance_type=None): + num_inference_steps = self.num_inference_steps if self.num_inference_steps else self.config.num_train_timesteps + prev_t = t - self.config.num_train_timesteps // num_inference_steps + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_t] if prev_t >= 0 else self.one + current_beta_t = 1 - alpha_prod_t / alpha_prod_t_prev + + # For t > 0, compute predicted variance βt (see formula (6) and (7) from https://arxiv.org/pdf/2006.11239.pdf) + # and sample from it to get previous sample + # x_{t-1} ~ N(pred_prev_sample, variance) == add variance to pred_sample + variance = (1 - alpha_prod_t_prev) / (1 - alpha_prod_t) * current_beta_t + + if variance_type is None: + variance_type = self.config.variance_type + + # hacks - were probably added for training stability + if variance_type == "fixed_small": + variance = torch.clamp(variance, min=1e-20) + # for rl-diffuser https://arxiv.org/abs/2205.09991 + elif variance_type == "fixed_small_log": + variance = torch.log(torch.clamp(variance, min=1e-20)) + variance = torch.exp(0.5 * variance) + elif variance_type == "fixed_large": + variance = current_beta_t + elif variance_type == "fixed_large_log": + # Glide max_log + variance = torch.log(current_beta_t) + elif variance_type == "learned": + return predicted_variance + elif variance_type == "learned_range": + min_log = torch.log(variance) + max_log = torch.log(self.betas[t]) + frac = (predicted_variance + 1) / 2 + variance = frac * max_log + (1 - frac) * min_log + + return variance + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + generator=None, + return_dict: bool = True, + ) -> Union[DDPMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than DDPMSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.DDPMSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.DDPMSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + t = timestep + num_inference_steps = self.num_inference_steps if self.num_inference_steps else self.config.num_train_timesteps + prev_t = timestep - self.config.num_train_timesteps // num_inference_steps + + if model_output.shape[1] == sample.shape[1] * 2 and self.variance_type in ["learned", "learned_range"]: + model_output, predicted_variance = torch.split(model_output, sample.shape[1], dim=1) + else: + predicted_variance = None + + # 1. compute alphas, betas + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_t] if prev_t >= 0 else self.one + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + current_alpha_t = alpha_prod_t / alpha_prod_t_prev + current_beta_t = 1 - current_alpha_t + + # 2. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf + if self.config.prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif self.config.prediction_type == "sample": + pred_original_sample = model_output + elif self.config.prediction_type == "v_prediction": + pred_original_sample = (alpha_prod_t**0.5) * sample - (beta_prod_t**0.5) * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample` or" + " `v_prediction` for the DDPMScheduler." + ) + + # 3. Clip "predicted x_0" + if self.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -1, 1) + + # 4. Compute coefficients for pred_original_sample x_0 and current sample x_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * current_beta_t) / beta_prod_t + current_sample_coeff = current_alpha_t ** (0.5) * beta_prod_t_prev / beta_prod_t + + # 5. Compute predicted previous sample µ_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_prev_sample = pred_original_sample_coeff * pred_original_sample + current_sample_coeff * sample + + # 6. Add noise + variance = 0 + if t > 0: + device = model_output.device + variance_noise = randn_tensor( + model_output.shape, generator=generator, device=device, dtype=model_output.dtype + ) + if self.variance_type == "fixed_small_log": + variance = self._get_variance(t, predicted_variance=predicted_variance) * variance_noise + elif self.variance_type == "learned_range": + variance = self._get_variance(t, predicted_variance=predicted_variance) + variance = torch.exp(0.5 * variance) * variance_noise + else: + variance = (self._get_variance(t, predicted_variance=predicted_variance) ** 0.5) * variance_noise + + pred_prev_sample = pred_prev_sample + variance + + if not return_dict: + return (pred_prev_sample,) + + return DDPMSchedulerOutput(prev_sample=pred_prev_sample, pred_original_sample=pred_original_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def get_velocity( + self, sample: torch.FloatTensor, noise: torch.FloatTensor, timesteps: torch.IntTensor + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as sample + self.alphas_cumprod = self.alphas_cumprod.to(device=sample.device, dtype=sample.dtype) + timesteps = timesteps.to(sample.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(sample.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(sample.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + velocity = sqrt_alpha_prod * noise - sqrt_one_minus_alpha_prod * sample + return velocity + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_ddpm_flax.py b/diffusers/src/diffusers/schedulers/scheduling_ddpm_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..3179538e8394be93c45a10445a70e05b263a14b3 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_ddpm_flax.py @@ -0,0 +1,299 @@ +# Copyright 2022 UC Berkeley Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/ermongroup/ddim + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax +import jax.numpy as jnp + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import ( + CommonSchedulerState, + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + add_noise_common, + get_velocity_common, +) + + +@flax.struct.dataclass +class DDPMSchedulerState: + common: CommonSchedulerState + + # setable values + init_noise_sigma: jnp.ndarray + timesteps: jnp.ndarray + num_inference_steps: Optional[int] = None + + @classmethod + def create(cls, common: CommonSchedulerState, init_noise_sigma: jnp.ndarray, timesteps: jnp.ndarray): + return cls(common=common, init_noise_sigma=init_noise_sigma, timesteps=timesteps) + + +@dataclass +class FlaxDDPMSchedulerOutput(FlaxSchedulerOutput): + state: DDPMSchedulerState + + +class FlaxDDPMScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + Denoising diffusion probabilistic models (DDPMs) explores the connections between denoising score matching and + Langevin dynamics sampling. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2006.11239 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + variance_type (`str`): + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + clip_sample (`bool`, default `True`): + option to clip predicted sample between -1 and 1 for numerical stability. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the samples. One of `epsilon`, `sample`. + `v-prediction` is not supported for this scheduler. + dtype (`jnp.dtype`, *optional*, defaults to `jnp.float32`): + the `dtype` used for params and computation. + """ + + _compatibles = [e.name for e in FlaxKarrasDiffusionSchedulers] + + dtype: jnp.dtype + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[jnp.ndarray] = None, + variance_type: str = "fixed_small", + clip_sample: bool = True, + prediction_type: str = "epsilon", + dtype: jnp.dtype = jnp.float32, + ): + self.dtype = dtype + + def create_state(self, common: Optional[CommonSchedulerState] = None) -> DDPMSchedulerState: + if common is None: + common = CommonSchedulerState.create(self) + + # standard deviation of the initial noise distribution + init_noise_sigma = jnp.array(1.0, dtype=self.dtype) + + timesteps = jnp.arange(0, self.config.num_train_timesteps).round()[::-1] + + return DDPMSchedulerState.create( + common=common, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + def scale_model_input( + self, state: DDPMSchedulerState, sample: jnp.ndarray, timestep: Optional[int] = None + ) -> jnp.ndarray: + """ + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + sample (`jnp.ndarray`): input sample + timestep (`int`, optional): current timestep + + Returns: + `jnp.ndarray`: scaled input sample + """ + return sample + + def set_timesteps( + self, state: DDPMSchedulerState, num_inference_steps: int, shape: Tuple = () + ) -> DDPMSchedulerState: + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`DDIMSchedulerState`): + the `FlaxDDPMScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + + step_ratio = self.config.num_train_timesteps // num_inference_steps + # creates integer timesteps by multiplying by ratio + # rounding to avoid issues when num_inference_step is power of 3 + timesteps = (jnp.arange(0, num_inference_steps) * step_ratio).round()[::-1] + + return state.replace( + num_inference_steps=num_inference_steps, + timesteps=timesteps, + ) + + def _get_variance(self, state: DDPMSchedulerState, t, predicted_variance=None, variance_type=None): + alpha_prod_t = state.common.alphas_cumprod[t] + alpha_prod_t_prev = jnp.where(t > 0, state.common.alphas_cumprod[t - 1], jnp.array(1.0, dtype=self.dtype)) + + # For t > 0, compute predicted variance βt (see formula (6) and (7) from https://arxiv.org/pdf/2006.11239.pdf) + # and sample from it to get previous sample + # x_{t-1} ~ N(pred_prev_sample, variance) == add variance to pred_sample + variance = (1 - alpha_prod_t_prev) / (1 - alpha_prod_t) * state.common.betas[t] + + if variance_type is None: + variance_type = self.config.variance_type + + # hacks - were probably added for training stability + if variance_type == "fixed_small": + variance = jnp.clip(variance, a_min=1e-20) + # for rl-diffuser https://arxiv.org/abs/2205.09991 + elif variance_type == "fixed_small_log": + variance = jnp.log(jnp.clip(variance, a_min=1e-20)) + elif variance_type == "fixed_large": + variance = state.common.betas[t] + elif variance_type == "fixed_large_log": + # Glide max_log + variance = jnp.log(state.common.betas[t]) + elif variance_type == "learned": + return predicted_variance + elif variance_type == "learned_range": + min_log = variance + max_log = state.common.betas[t] + frac = (predicted_variance + 1) / 2 + variance = frac * max_log + (1 - frac) * min_log + + return variance + + def step( + self, + state: DDPMSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + key: Optional[jax.random.KeyArray] = None, + return_dict: bool = True, + ) -> Union[FlaxDDPMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + state (`DDPMSchedulerState`): the `FlaxDDPMScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + key (`jax.random.KeyArray`): a PRNG key. + return_dict (`bool`): option for returning tuple rather than FlaxDDPMSchedulerOutput class + + Returns: + [`FlaxDDPMSchedulerOutput`] or `tuple`: [`FlaxDDPMSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + t = timestep + + if key is None: + key = jax.random.PRNGKey(0) + + if model_output.shape[1] == sample.shape[1] * 2 and self.config.variance_type in ["learned", "learned_range"]: + model_output, predicted_variance = jnp.split(model_output, sample.shape[1], axis=1) + else: + predicted_variance = None + + # 1. compute alphas, betas + alpha_prod_t = state.common.alphas_cumprod[t] + alpha_prod_t_prev = jnp.where(t > 0, state.common.alphas_cumprod[t - 1], jnp.array(1.0, dtype=self.dtype)) + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + # 2. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf + if self.config.prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif self.config.prediction_type == "sample": + pred_original_sample = model_output + elif self.config.prediction_type == "v_prediction": + pred_original_sample = (alpha_prod_t**0.5) * sample - (beta_prod_t**0.5) * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample` " + " for the FlaxDDPMScheduler." + ) + + # 3. Clip "predicted x_0" + if self.config.clip_sample: + pred_original_sample = jnp.clip(pred_original_sample, -1, 1) + + # 4. Compute coefficients for pred_original_sample x_0 and current sample x_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * state.common.betas[t]) / beta_prod_t + current_sample_coeff = state.common.alphas[t] ** (0.5) * beta_prod_t_prev / beta_prod_t + + # 5. Compute predicted previous sample µ_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_prev_sample = pred_original_sample_coeff * pred_original_sample + current_sample_coeff * sample + + # 6. Add noise + def random_variance(): + split_key = jax.random.split(key, num=1) + noise = jax.random.normal(split_key, shape=model_output.shape, dtype=self.dtype) + return (self._get_variance(state, t, predicted_variance=predicted_variance) ** 0.5) * noise + + variance = jnp.where(t > 0, random_variance(), jnp.zeros(model_output.shape, dtype=self.dtype)) + + pred_prev_sample = pred_prev_sample + variance + + if not return_dict: + return (pred_prev_sample, state) + + return FlaxDDPMSchedulerOutput(prev_sample=pred_prev_sample, state=state) + + def add_noise( + self, + state: DDPMSchedulerState, + original_samples: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return add_noise_common(state.common, original_samples, noise, timesteps) + + def get_velocity( + self, + state: DDPMSchedulerState, + sample: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return get_velocity_common(state.common, sample, noise, timesteps) + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_deis_multistep.py b/diffusers/src/diffusers/schedulers/scheduling_deis_multistep.py new file mode 100644 index 0000000000000000000000000000000000000000..1ad5480b78783a76cc23674bcb4357b1cf039b0f --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_deis_multistep.py @@ -0,0 +1,480 @@ +# Copyright 2022 FLAIR Lab and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: check https://arxiv.org/abs/2204.13902 and https://github.com/qsh-zh/deis for more info +# The codebase is modified based on https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class DEISMultistepScheduler(SchedulerMixin, ConfigMixin): + """ + DEIS (https://arxiv.org/abs/2204.13902) is a fast high order solver for diffusion ODEs. We slightly modify the + polynomial fitting formula in log-rho space instead of the original linear t space in DEIS paper. The modification + enjoys closed-form coefficients for exponential multistep update instead of replying on the numerical solver. More + variants of DEIS can be found in https://github.com/qsh-zh/deis. + + Currently, we support the log-rho multistep DEIS. We recommend to use `solver_order=2 / 3` while `solver_order=1` + reduces to DDIM. + + We also support the "dynamic thresholding" method in Imagen (https://arxiv.org/abs/2205.11487). For pixel-space + diffusion models, you can set `thresholding=True` to use the dynamic thresholding. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + solver_order (`int`, default `2`): + the order of DEIS; can be `1` or `2` or `3`. We recommend to use `solver_order=2` for guided sampling, and + `solver_order=3` for unconditional sampling. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the data / `x0`. One of `epsilon`, `sample`, + or `v-prediction`. + thresholding (`bool`, default `False`): + whether to use the "dynamic thresholding" method (introduced by Imagen, https://arxiv.org/abs/2205.11487). + Note that the thresholding method is unsuitable for latent-space diffusion models (such as + stable-diffusion). + dynamic_thresholding_ratio (`float`, default `0.995`): + the ratio for the dynamic thresholding method. Default is `0.995`, the same as Imagen + (https://arxiv.org/abs/2205.11487). + sample_max_value (`float`, default `1.0`): + the threshold value for dynamic thresholding. Valid woks when `thresholding=True` + algorithm_type (`str`, default `deis`): + the algorithm type for the solver. current we support multistep deis, we will add other variants of DEIS in + the future + lower_order_final (`bool`, default `True`): + whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. We empirically + find this trick can stabilize the sampling of DEIS for steps < 15, especially for steps <= 10. + + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[np.ndarray] = None, + solver_order: int = 2, + prediction_type: str = "epsilon", + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + algorithm_type: str = "deis", + solver_type: str = "logrho", + lower_order_final: bool = True, + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + # Currently we only support VP-type noise schedule + self.alpha_t = torch.sqrt(self.alphas_cumprod) + self.sigma_t = torch.sqrt(1 - self.alphas_cumprod) + self.lambda_t = torch.log(self.alpha_t) - torch.log(self.sigma_t) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # settings for DEIS + if algorithm_type not in ["deis"]: + if algorithm_type in ["dpmsolver", "dpmsolver++"]: + algorithm_type = "deis" + else: + raise NotImplementedError(f"{algorithm_type} does is not implemented for {self.__class__}") + + if solver_type not in ["logrho"]: + if solver_type in ["midpoint", "heun"]: + solver_type = "logrho" + else: + raise NotImplementedError(f"solver type {solver_type} does is not implemented for {self.__class__}") + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.model_outputs = [None] * solver_order + self.lower_order_nums = 0 + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + timesteps = ( + np.linspace(0, self.num_train_timesteps - 1, num_inference_steps + 1) + .round()[::-1][:-1] + .copy() + .astype(np.int64) + ) + self.timesteps = torch.from_numpy(timesteps).to(device) + self.model_outputs = [ + None, + ] * self.config.solver_order + self.lower_order_nums = 0 + + def convert_model_output( + self, model_output: torch.FloatTensor, timestep: int, sample: torch.FloatTensor + ) -> torch.FloatTensor: + """ + Convert the model output to the corresponding type that the algorithm DEIS needs. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the converted model output. + """ + if self.config.prediction_type == "epsilon": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = (sample - sigma_t * model_output) / alpha_t + elif self.config.prediction_type == "sample": + x0_pred = model_output + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = alpha_t * sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction` for the DEISMultistepScheduler." + ) + + if self.config.thresholding: + # Dynamic thresholding in https://arxiv.org/abs/2205.11487 + orig_dtype = x0_pred.dtype + if orig_dtype not in [torch.float, torch.double]: + x0_pred = x0_pred.float() + dynamic_max_val = torch.quantile( + torch.abs(x0_pred).reshape((x0_pred.shape[0], -1)), self.config.dynamic_thresholding_ratio, dim=1 + ) + dynamic_max_val = torch.maximum( + dynamic_max_val, + self.config.sample_max_value * torch.ones_like(dynamic_max_val).to(dynamic_max_val.device), + )[(...,) + (None,) * (x0_pred.ndim - 1)] + x0_pred = torch.clamp(x0_pred, -dynamic_max_val, dynamic_max_val) / dynamic_max_val + x0_pred = x0_pred.type(orig_dtype) + + if self.config.algorithm_type == "deis": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + return (sample - alpha_t * x0_pred) / sigma_t + else: + raise NotImplementedError("only support log-rho multistep deis now") + + def deis_first_order_update( + self, + model_output: torch.FloatTensor, + timestep: int, + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the first-order DEIS (equivalent to DDIM). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + lambda_t, lambda_s = self.lambda_t[prev_timestep], self.lambda_t[timestep] + alpha_t, alpha_s = self.alpha_t[prev_timestep], self.alpha_t[timestep] + sigma_t, _ = self.sigma_t[prev_timestep], self.sigma_t[timestep] + h = lambda_t - lambda_s + if self.config.algorithm_type == "deis": + x_t = (alpha_t / alpha_s) * sample - (sigma_t * (torch.exp(h) - 1.0)) * model_output + else: + raise NotImplementedError("only support log-rho multistep deis now") + return x_t + + def multistep_deis_second_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the second-order multistep DEIS. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1 = prev_timestep, timestep_list[-1], timestep_list[-2] + m0, m1 = model_output_list[-1], model_output_list[-2] + alpha_t, alpha_s0, alpha_s1 = self.alpha_t[t], self.alpha_t[s0], self.alpha_t[s1] + sigma_t, sigma_s0, sigma_s1 = self.sigma_t[t], self.sigma_t[s0], self.sigma_t[s1] + + rho_t, rho_s0, rho_s1 = sigma_t / alpha_t, sigma_s0 / alpha_s0, sigma_s1 / alpha_s1 + + if self.config.algorithm_type == "deis": + + def ind_fn(t, b, c): + # Integrate[(log(t) - log(c)) / (log(b) - log(c)), {t}] + return t * (-np.log(c) + np.log(t) - 1) / (np.log(b) - np.log(c)) + + coef1 = ind_fn(rho_t, rho_s0, rho_s1) - ind_fn(rho_s0, rho_s0, rho_s1) + coef2 = ind_fn(rho_t, rho_s1, rho_s0) - ind_fn(rho_s0, rho_s1, rho_s0) + + x_t = alpha_t * (sample / alpha_s0 + coef1 * m0 + coef2 * m1) + return x_t + else: + raise NotImplementedError("only support log-rho multistep deis now") + + def multistep_deis_third_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the third-order multistep DEIS. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1, s2 = prev_timestep, timestep_list[-1], timestep_list[-2], timestep_list[-3] + m0, m1, m2 = model_output_list[-1], model_output_list[-2], model_output_list[-3] + alpha_t, alpha_s0, alpha_s1, alpha_s2 = self.alpha_t[t], self.alpha_t[s0], self.alpha_t[s1], self.alpha_t[s2] + sigma_t, sigma_s0, sigma_s1, simga_s2 = self.sigma_t[t], self.sigma_t[s0], self.sigma_t[s1], self.sigma_t[s2] + rho_t, rho_s0, rho_s1, rho_s2 = ( + sigma_t / alpha_t, + sigma_s0 / alpha_s0, + sigma_s1 / alpha_s1, + simga_s2 / alpha_s2, + ) + + if self.config.algorithm_type == "deis": + + def ind_fn(t, b, c, d): + # Integrate[(log(t) - log(c))(log(t) - log(d)) / (log(b) - log(c))(log(b) - log(d)), {t}] + numerator = t * ( + np.log(c) * (np.log(d) - np.log(t) + 1) + - np.log(d) * np.log(t) + + np.log(d) + + np.log(t) ** 2 + - 2 * np.log(t) + + 2 + ) + denominator = (np.log(b) - np.log(c)) * (np.log(b) - np.log(d)) + return numerator / denominator + + coef1 = ind_fn(rho_t, rho_s0, rho_s1, rho_s2) - ind_fn(rho_s0, rho_s0, rho_s1, rho_s2) + coef2 = ind_fn(rho_t, rho_s1, rho_s2, rho_s0) - ind_fn(rho_s0, rho_s1, rho_s2, rho_s0) + coef3 = ind_fn(rho_t, rho_s2, rho_s0, rho_s1) - ind_fn(rho_s0, rho_s2, rho_s0, rho_s1) + + x_t = alpha_t * (sample / alpha_s0 + coef1 * m0 + coef2 * m1 + coef3 * m2) + + return x_t + else: + raise NotImplementedError("only support log-rho multistep deis now") + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the multistep DEIS. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero() + if len(step_index) == 0: + step_index = len(self.timesteps) - 1 + else: + step_index = step_index.item() + prev_timestep = 0 if step_index == len(self.timesteps) - 1 else self.timesteps[step_index + 1] + lower_order_final = ( + (step_index == len(self.timesteps) - 1) and self.config.lower_order_final and len(self.timesteps) < 15 + ) + lower_order_second = ( + (step_index == len(self.timesteps) - 2) and self.config.lower_order_final and len(self.timesteps) < 15 + ) + + model_output = self.convert_model_output(model_output, timestep, sample) + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self.model_outputs[-1] = model_output + + if self.config.solver_order == 1 or self.lower_order_nums < 1 or lower_order_final: + prev_sample = self.deis_first_order_update(model_output, timestep, prev_timestep, sample) + elif self.config.solver_order == 2 or self.lower_order_nums < 2 or lower_order_second: + timestep_list = [self.timesteps[step_index - 1], timestep] + prev_sample = self.multistep_deis_second_order_update( + self.model_outputs, timestep_list, prev_timestep, sample + ) + else: + timestep_list = [self.timesteps[step_index - 2], self.timesteps[step_index - 1], timestep] + prev_sample = self.multistep_deis_third_order_update( + self.model_outputs, timestep_list, prev_timestep, sample + ) + + if self.lower_order_nums < self.config.solver_order: + self.lower_order_nums += 1 + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py new file mode 100644 index 0000000000000000000000000000000000000000..0630ea1d1fe7fcef48255438b7d379e5aea336a4 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py @@ -0,0 +1,528 @@ +# Copyright 2022 TSAIL Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/LuChengTHU/dpm-solver + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class DPMSolverMultistepScheduler(SchedulerMixin, ConfigMixin): + """ + DPM-Solver (and the improved version DPM-Solver++) is a fast dedicated high-order solver for diffusion ODEs with + the convergence order guarantee. Empirically, sampling by DPM-Solver with only 20 steps can generate high-quality + samples, and it can generate quite good samples even in only 10 steps. + + For more details, see the original paper: https://arxiv.org/abs/2206.00927 and https://arxiv.org/abs/2211.01095 + + Currently, we support the multistep DPM-Solver for both noise prediction models and data prediction models. We + recommend to use `solver_order=2` for guided sampling, and `solver_order=3` for unconditional sampling. + + We also support the "dynamic thresholding" method in Imagen (https://arxiv.org/abs/2205.11487). For pixel-space + diffusion models, you can set both `algorithm_type="dpmsolver++"` and `thresholding=True` to use the dynamic + thresholding. Note that the thresholding method is unsuitable for latent-space diffusion models (such as + stable-diffusion). + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + solver_order (`int`, default `2`): + the order of DPM-Solver; can be `1` or `2` or `3`. We recommend to use `solver_order=2` for guided + sampling, and `solver_order=3` for unconditional sampling. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + thresholding (`bool`, default `False`): + whether to use the "dynamic thresholding" method (introduced by Imagen, https://arxiv.org/abs/2205.11487). + For pixel-space diffusion models, you can set both `algorithm_type=dpmsolver++` and `thresholding=True` to + use the dynamic thresholding. Note that the thresholding method is unsuitable for latent-space diffusion + models (such as stable-diffusion). + dynamic_thresholding_ratio (`float`, default `0.995`): + the ratio for the dynamic thresholding method. Default is `0.995`, the same as Imagen + (https://arxiv.org/abs/2205.11487). + sample_max_value (`float`, default `1.0`): + the threshold value for dynamic thresholding. Valid only when `thresholding=True` and + `algorithm_type="dpmsolver++`. + algorithm_type (`str`, default `dpmsolver++`): + the algorithm type for the solver. Either `dpmsolver` or `dpmsolver++`. The `dpmsolver` type implements the + algorithms in https://arxiv.org/abs/2206.00927, and the `dpmsolver++` type implements the algorithms in + https://arxiv.org/abs/2211.01095. We recommend to use `dpmsolver++` with `solver_order=2` for guided + sampling (e.g. stable-diffusion). + solver_type (`str`, default `midpoint`): + the solver type for the second-order solver. Either `midpoint` or `heun`. The solver type slightly affects + the sample quality, especially for small number of steps. We empirically find that `midpoint` solvers are + slightly better, so we recommend to use the `midpoint` type. + lower_order_final (`bool`, default `True`): + whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. We empirically + find this trick can stabilize the sampling of DPM-Solver for steps < 15, especially for steps <= 10. + + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + solver_order: int = 2, + prediction_type: str = "epsilon", + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + algorithm_type: str = "dpmsolver++", + solver_type: str = "midpoint", + lower_order_final: bool = True, + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + # Currently we only support VP-type noise schedule + self.alpha_t = torch.sqrt(self.alphas_cumprod) + self.sigma_t = torch.sqrt(1 - self.alphas_cumprod) + self.lambda_t = torch.log(self.alpha_t) - torch.log(self.sigma_t) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # settings for DPM-Solver + if algorithm_type not in ["dpmsolver", "dpmsolver++"]: + if algorithm_type == "deis": + algorithm_type = "dpmsolver++" + else: + raise NotImplementedError(f"{algorithm_type} does is not implemented for {self.__class__}") + if solver_type not in ["midpoint", "heun"]: + if solver_type == "logrho": + solver_type = "midpoint" + else: + raise NotImplementedError(f"{solver_type} does is not implemented for {self.__class__}") + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.model_outputs = [None] * solver_order + self.lower_order_nums = 0 + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + timesteps = ( + np.linspace(0, self.num_train_timesteps - 1, num_inference_steps + 1) + .round()[::-1][:-1] + .copy() + .astype(np.int64) + ) + self.timesteps = torch.from_numpy(timesteps).to(device) + self.model_outputs = [ + None, + ] * self.config.solver_order + self.lower_order_nums = 0 + + def convert_model_output( + self, model_output: torch.FloatTensor, timestep: int, sample: torch.FloatTensor + ) -> torch.FloatTensor: + """ + Convert the model output to the corresponding type that the algorithm (DPM-Solver / DPM-Solver++) needs. + + DPM-Solver is designed to discretize an integral of the noise prediction model, and DPM-Solver++ is designed to + discretize an integral of the data prediction model. So we need to first convert the model output to the + corresponding type to match the algorithm. + + Note that the algorithm type and the model type is decoupled. That is to say, we can use either DPM-Solver or + DPM-Solver++ for both noise prediction model and data prediction model. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the converted model output. + """ + # DPM-Solver++ needs to solve an integral of the data prediction model. + if self.config.algorithm_type == "dpmsolver++": + if self.config.prediction_type == "epsilon": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = (sample - sigma_t * model_output) / alpha_t + elif self.config.prediction_type == "sample": + x0_pred = model_output + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = alpha_t * sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction` for the DPMSolverMultistepScheduler." + ) + + if self.config.thresholding: + # Dynamic thresholding in https://arxiv.org/abs/2205.11487 + orig_dtype = x0_pred.dtype + if orig_dtype not in [torch.float, torch.double]: + x0_pred = x0_pred.float() + dynamic_max_val = torch.quantile( + torch.abs(x0_pred).reshape((x0_pred.shape[0], -1)), self.config.dynamic_thresholding_ratio, dim=1 + ) + dynamic_max_val = torch.maximum( + dynamic_max_val, + self.config.sample_max_value * torch.ones_like(dynamic_max_val).to(dynamic_max_val.device), + )[(...,) + (None,) * (x0_pred.ndim - 1)] + x0_pred = torch.clamp(x0_pred, -dynamic_max_val, dynamic_max_val) / dynamic_max_val + x0_pred = x0_pred.type(orig_dtype) + return x0_pred + # DPM-Solver needs to solve an integral of the noise prediction model. + elif self.config.algorithm_type == "dpmsolver": + if self.config.prediction_type == "epsilon": + return model_output + elif self.config.prediction_type == "sample": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + epsilon = (sample - alpha_t * model_output) / sigma_t + return epsilon + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + epsilon = alpha_t * model_output + sigma_t * sample + return epsilon + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction` for the DPMSolverMultistepScheduler." + ) + + def dpm_solver_first_order_update( + self, + model_output: torch.FloatTensor, + timestep: int, + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the first-order DPM-Solver (equivalent to DDIM). + + See https://arxiv.org/abs/2206.00927 for the detailed derivation. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + lambda_t, lambda_s = self.lambda_t[prev_timestep], self.lambda_t[timestep] + alpha_t, alpha_s = self.alpha_t[prev_timestep], self.alpha_t[timestep] + sigma_t, sigma_s = self.sigma_t[prev_timestep], self.sigma_t[timestep] + h = lambda_t - lambda_s + if self.config.algorithm_type == "dpmsolver++": + x_t = (sigma_t / sigma_s) * sample - (alpha_t * (torch.exp(-h) - 1.0)) * model_output + elif self.config.algorithm_type == "dpmsolver": + x_t = (alpha_t / alpha_s) * sample - (sigma_t * (torch.exp(h) - 1.0)) * model_output + return x_t + + def multistep_dpm_solver_second_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the second-order multistep DPM-Solver. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1 = prev_timestep, timestep_list[-1], timestep_list[-2] + m0, m1 = model_output_list[-1], model_output_list[-2] + lambda_t, lambda_s0, lambda_s1 = self.lambda_t[t], self.lambda_t[s0], self.lambda_t[s1] + alpha_t, alpha_s0 = self.alpha_t[t], self.alpha_t[s0] + sigma_t, sigma_s0 = self.sigma_t[t], self.sigma_t[s0] + h, h_0 = lambda_t - lambda_s0, lambda_s0 - lambda_s1 + r0 = h_0 / h + D0, D1 = m0, (1.0 / r0) * (m0 - m1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2211.01095 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + - 0.5 * (alpha_t * (torch.exp(-h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + + (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - 0.5 * (sigma_t * (torch.exp(h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1 + ) + return x_t + + def multistep_dpm_solver_third_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the third-order multistep DPM-Solver. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1, s2 = prev_timestep, timestep_list[-1], timestep_list[-2], timestep_list[-3] + m0, m1, m2 = model_output_list[-1], model_output_list[-2], model_output_list[-3] + lambda_t, lambda_s0, lambda_s1, lambda_s2 = ( + self.lambda_t[t], + self.lambda_t[s0], + self.lambda_t[s1], + self.lambda_t[s2], + ) + alpha_t, alpha_s0 = self.alpha_t[t], self.alpha_t[s0] + sigma_t, sigma_s0 = self.sigma_t[t], self.sigma_t[s0] + h, h_0, h_1 = lambda_t - lambda_s0, lambda_s0 - lambda_s1, lambda_s1 - lambda_s2 + r0, r1 = h_0 / h, h_1 / h + D0 = m0 + D1_0, D1_1 = (1.0 / r0) * (m0 - m1), (1.0 / r1) * (m1 - m2) + D1 = D1_0 + (r0 / (r0 + r1)) * (D1_0 - D1_1) + D2 = (1.0 / (r0 + r1)) * (D1_0 - D1_1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + + (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1 + - (alpha_t * ((torch.exp(-h) - 1.0 + h) / h**2 - 0.5)) * D2 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1 + - (sigma_t * ((torch.exp(h) - 1.0 - h) / h**2 - 0.5)) * D2 + ) + return x_t + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the multistep DPM-Solver. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero() + if len(step_index) == 0: + step_index = len(self.timesteps) - 1 + else: + step_index = step_index.item() + prev_timestep = 0 if step_index == len(self.timesteps) - 1 else self.timesteps[step_index + 1] + lower_order_final = ( + (step_index == len(self.timesteps) - 1) and self.config.lower_order_final and len(self.timesteps) < 15 + ) + lower_order_second = ( + (step_index == len(self.timesteps) - 2) and self.config.lower_order_final and len(self.timesteps) < 15 + ) + + model_output = self.convert_model_output(model_output, timestep, sample) + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self.model_outputs[-1] = model_output + + if self.config.solver_order == 1 or self.lower_order_nums < 1 or lower_order_final: + prev_sample = self.dpm_solver_first_order_update(model_output, timestep, prev_timestep, sample) + elif self.config.solver_order == 2 or self.lower_order_nums < 2 or lower_order_second: + timestep_list = [self.timesteps[step_index - 1], timestep] + prev_sample = self.multistep_dpm_solver_second_order_update( + self.model_outputs, timestep_list, prev_timestep, sample + ) + else: + timestep_list = [self.timesteps[step_index - 2], self.timesteps[step_index - 1], timestep] + prev_sample = self.multistep_dpm_solver_third_order_update( + self.model_outputs, timestep_list, prev_timestep, sample + ) + + if self.lower_order_nums < self.config.solver_order: + self.lower_order_nums += 1 + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep_flax.py b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..cadf782fb3ae8c1837579127f5d505e39c62aedc --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_multistep_flax.py @@ -0,0 +1,622 @@ +# Copyright 2022 TSAIL Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/LuChengTHU/dpm-solver + +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import flax +import jax +import jax.numpy as jnp + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import ( + CommonSchedulerState, + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + add_noise_common, +) + + +@flax.struct.dataclass +class DPMSolverMultistepSchedulerState: + common: CommonSchedulerState + alpha_t: jnp.ndarray + sigma_t: jnp.ndarray + lambda_t: jnp.ndarray + + # setable values + init_noise_sigma: jnp.ndarray + timesteps: jnp.ndarray + num_inference_steps: Optional[int] = None + + # running values + model_outputs: Optional[jnp.ndarray] = None + lower_order_nums: Optional[jnp.int32] = None + prev_timestep: Optional[jnp.int32] = None + cur_sample: Optional[jnp.ndarray] = None + + @classmethod + def create( + cls, + common: CommonSchedulerState, + alpha_t: jnp.ndarray, + sigma_t: jnp.ndarray, + lambda_t: jnp.ndarray, + init_noise_sigma: jnp.ndarray, + timesteps: jnp.ndarray, + ): + return cls( + common=common, + alpha_t=alpha_t, + sigma_t=sigma_t, + lambda_t=lambda_t, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + +@dataclass +class FlaxDPMSolverMultistepSchedulerOutput(FlaxSchedulerOutput): + state: DPMSolverMultistepSchedulerState + + +class FlaxDPMSolverMultistepScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + DPM-Solver (and the improved version DPM-Solver++) is a fast dedicated high-order solver for diffusion ODEs with + the convergence order guarantee. Empirically, sampling by DPM-Solver with only 20 steps can generate high-quality + samples, and it can generate quite good samples even in only 10 steps. + + For more details, see the original paper: https://arxiv.org/abs/2206.00927 and https://arxiv.org/abs/2211.01095 + + Currently, we support the multistep DPM-Solver for both noise prediction models and data prediction models. We + recommend to use `solver_order=2` for guided sampling, and `solver_order=3` for unconditional sampling. + + We also support the "dynamic thresholding" method in Imagen (https://arxiv.org/abs/2205.11487). For pixel-space + diffusion models, you can set both `algorithm_type="dpmsolver++"` and `thresholding=True` to use the dynamic + thresholding. Note that the thresholding method is unsuitable for latent-space diffusion models (such as + stable-diffusion). + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2206.00927 and https://arxiv.org/abs/2211.01095 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + solver_order (`int`, default `2`): + the order of DPM-Solver; can be `1` or `2` or `3`. We recommend to use `solver_order=2` for guided + sampling, and `solver_order=3` for unconditional sampling. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the data / `x0`. One of `epsilon`, `sample`, + or `v-prediction`. + thresholding (`bool`, default `False`): + whether to use the "dynamic thresholding" method (introduced by Imagen, https://arxiv.org/abs/2205.11487). + For pixel-space diffusion models, you can set both `algorithm_type=dpmsolver++` and `thresholding=True` to + use the dynamic thresholding. Note that the thresholding method is unsuitable for latent-space diffusion + models (such as stable-diffusion). + dynamic_thresholding_ratio (`float`, default `0.995`): + the ratio for the dynamic thresholding method. Default is `0.995`, the same as Imagen + (https://arxiv.org/abs/2205.11487). + sample_max_value (`float`, default `1.0`): + the threshold value for dynamic thresholding. Valid only when `thresholding=True` and + `algorithm_type="dpmsolver++`. + algorithm_type (`str`, default `dpmsolver++`): + the algorithm type for the solver. Either `dpmsolver` or `dpmsolver++`. The `dpmsolver` type implements the + algorithms in https://arxiv.org/abs/2206.00927, and the `dpmsolver++` type implements the algorithms in + https://arxiv.org/abs/2211.01095. We recommend to use `dpmsolver++` with `solver_order=2` for guided + sampling (e.g. stable-diffusion). + solver_type (`str`, default `midpoint`): + the solver type for the second-order solver. Either `midpoint` or `heun`. The solver type slightly affects + the sample quality, especially for small number of steps. We empirically find that `midpoint` solvers are + slightly better, so we recommend to use the `midpoint` type. + lower_order_final (`bool`, default `True`): + whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. We empirically + find this trick can stabilize the sampling of DPM-Solver for steps < 15, especially for steps <= 10. + dtype (`jnp.dtype`, *optional*, defaults to `jnp.float32`): + the `dtype` used for params and computation. + """ + + _compatibles = [e.name for e in FlaxKarrasDiffusionSchedulers] + + dtype: jnp.dtype + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[jnp.ndarray] = None, + solver_order: int = 2, + prediction_type: str = "epsilon", + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + algorithm_type: str = "dpmsolver++", + solver_type: str = "midpoint", + lower_order_final: bool = True, + dtype: jnp.dtype = jnp.float32, + ): + self.dtype = dtype + + def create_state(self, common: Optional[CommonSchedulerState] = None) -> DPMSolverMultistepSchedulerState: + if common is None: + common = CommonSchedulerState.create(self) + + # Currently we only support VP-type noise schedule + alpha_t = jnp.sqrt(common.alphas_cumprod) + sigma_t = jnp.sqrt(1 - common.alphas_cumprod) + lambda_t = jnp.log(alpha_t) - jnp.log(sigma_t) + + # settings for DPM-Solver + if self.config.algorithm_type not in ["dpmsolver", "dpmsolver++"]: + raise NotImplementedError(f"{self.config.algorithm_type} does is not implemented for {self.__class__}") + if self.config.solver_type not in ["midpoint", "heun"]: + raise NotImplementedError(f"{self.config.solver_type} does is not implemented for {self.__class__}") + + # standard deviation of the initial noise distribution + init_noise_sigma = jnp.array(1.0, dtype=self.dtype) + + timesteps = jnp.arange(0, self.config.num_train_timesteps).round()[::-1] + + return DPMSolverMultistepSchedulerState.create( + common=common, + alpha_t=alpha_t, + sigma_t=sigma_t, + lambda_t=lambda_t, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + def set_timesteps( + self, state: DPMSolverMultistepSchedulerState, num_inference_steps: int, shape: Tuple + ) -> DPMSolverMultistepSchedulerState: + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`DPMSolverMultistepSchedulerState`): + the `FlaxDPMSolverMultistepScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + shape (`Tuple`): + the shape of the samples to be generated. + """ + + timesteps = ( + jnp.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps + 1) + .round()[::-1][:-1] + .astype(jnp.int32) + ) + + # initial running values + + model_outputs = jnp.zeros((self.config.solver_order,) + shape, dtype=self.dtype) + lower_order_nums = jnp.int32(0) + prev_timestep = jnp.int32(-1) + cur_sample = jnp.zeros(shape, dtype=self.dtype) + + return state.replace( + num_inference_steps=num_inference_steps, + timesteps=timesteps, + model_outputs=model_outputs, + lower_order_nums=lower_order_nums, + prev_timestep=prev_timestep, + cur_sample=cur_sample, + ) + + def convert_model_output( + self, + state: DPMSolverMultistepSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + ) -> jnp.ndarray: + """ + Convert the model output to the corresponding type that the algorithm (DPM-Solver / DPM-Solver++) needs. + + DPM-Solver is designed to discretize an integral of the noise prediction model, and DPM-Solver++ is designed to + discretize an integral of the data prediction model. So we need to first convert the model output to the + corresponding type to match the algorithm. + + Note that the algorithm type and the model type is decoupled. That is to say, we can use either DPM-Solver or + DPM-Solver++ for both noise prediction model and data prediction model. + + Args: + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + + Returns: + `jnp.ndarray`: the converted model output. + """ + # DPM-Solver++ needs to solve an integral of the data prediction model. + if self.config.algorithm_type == "dpmsolver++": + if self.config.prediction_type == "epsilon": + alpha_t, sigma_t = state.alpha_t[timestep], state.sigma_t[timestep] + x0_pred = (sample - sigma_t * model_output) / alpha_t + elif self.config.prediction_type == "sample": + x0_pred = model_output + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = state.alpha_t[timestep], state.sigma_t[timestep] + x0_pred = alpha_t * sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, " + " or `v_prediction` for the FlaxDPMSolverMultistepScheduler." + ) + + if self.config.thresholding: + # Dynamic thresholding in https://arxiv.org/abs/2205.11487 + dynamic_max_val = jnp.percentile( + jnp.abs(x0_pred), self.config.dynamic_thresholding_ratio, axis=tuple(range(1, x0_pred.ndim)) + ) + dynamic_max_val = jnp.maximum( + dynamic_max_val, self.config.sample_max_value * jnp.ones_like(dynamic_max_val) + ) + x0_pred = jnp.clip(x0_pred, -dynamic_max_val, dynamic_max_val) / dynamic_max_val + return x0_pred + # DPM-Solver needs to solve an integral of the noise prediction model. + elif self.config.algorithm_type == "dpmsolver": + if self.config.prediction_type == "epsilon": + return model_output + elif self.config.prediction_type == "sample": + alpha_t, sigma_t = state.alpha_t[timestep], state.sigma_t[timestep] + epsilon = (sample - alpha_t * model_output) / sigma_t + return epsilon + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = state.alpha_t[timestep], state.sigma_t[timestep] + epsilon = alpha_t * model_output + sigma_t * sample + return epsilon + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, " + " or `v_prediction` for the FlaxDPMSolverMultistepScheduler." + ) + + def dpm_solver_first_order_update( + self, + state: DPMSolverMultistepSchedulerState, + model_output: jnp.ndarray, + timestep: int, + prev_timestep: int, + sample: jnp.ndarray, + ) -> jnp.ndarray: + """ + One step for the first-order DPM-Solver (equivalent to DDIM). + + See https://arxiv.org/abs/2206.00927 for the detailed derivation. + + Args: + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + + Returns: + `jnp.ndarray`: the sample tensor at the previous timestep. + """ + t, s0 = prev_timestep, timestep + m0 = model_output + lambda_t, lambda_s = state.lambda_t[t], state.lambda_t[s0] + alpha_t, alpha_s = state.alpha_t[t], state.alpha_t[s0] + sigma_t, sigma_s = state.sigma_t[t], state.sigma_t[s0] + h = lambda_t - lambda_s + if self.config.algorithm_type == "dpmsolver++": + x_t = (sigma_t / sigma_s) * sample - (alpha_t * (jnp.exp(-h) - 1.0)) * m0 + elif self.config.algorithm_type == "dpmsolver": + x_t = (alpha_t / alpha_s) * sample - (sigma_t * (jnp.exp(h) - 1.0)) * m0 + return x_t + + def multistep_dpm_solver_second_order_update( + self, + state: DPMSolverMultistepSchedulerState, + model_output_list: jnp.ndarray, + timestep_list: List[int], + prev_timestep: int, + sample: jnp.ndarray, + ) -> jnp.ndarray: + """ + One step for the second-order multistep DPM-Solver. + + Args: + model_output_list (`List[jnp.ndarray]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + + Returns: + `jnp.ndarray`: the sample tensor at the previous timestep. + """ + t, s0, s1 = prev_timestep, timestep_list[-1], timestep_list[-2] + m0, m1 = model_output_list[-1], model_output_list[-2] + lambda_t, lambda_s0, lambda_s1 = state.lambda_t[t], state.lambda_t[s0], state.lambda_t[s1] + alpha_t, alpha_s0 = state.alpha_t[t], state.alpha_t[s0] + sigma_t, sigma_s0 = state.sigma_t[t], state.sigma_t[s0] + h, h_0 = lambda_t - lambda_s0, lambda_s0 - lambda_s1 + r0 = h_0 / h + D0, D1 = m0, (1.0 / r0) * (m0 - m1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2211.01095 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (jnp.exp(-h) - 1.0)) * D0 + - 0.5 * (alpha_t * (jnp.exp(-h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (jnp.exp(-h) - 1.0)) * D0 + + (alpha_t * ((jnp.exp(-h) - 1.0) / h + 1.0)) * D1 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (jnp.exp(h) - 1.0)) * D0 + - 0.5 * (sigma_t * (jnp.exp(h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (jnp.exp(h) - 1.0)) * D0 + - (sigma_t * ((jnp.exp(h) - 1.0) / h - 1.0)) * D1 + ) + return x_t + + def multistep_dpm_solver_third_order_update( + self, + state: DPMSolverMultistepSchedulerState, + model_output_list: jnp.ndarray, + timestep_list: List[int], + prev_timestep: int, + sample: jnp.ndarray, + ) -> jnp.ndarray: + """ + One step for the third-order multistep DPM-Solver. + + Args: + model_output_list (`List[jnp.ndarray]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + + Returns: + `jnp.ndarray`: the sample tensor at the previous timestep. + """ + t, s0, s1, s2 = prev_timestep, timestep_list[-1], timestep_list[-2], timestep_list[-3] + m0, m1, m2 = model_output_list[-1], model_output_list[-2], model_output_list[-3] + lambda_t, lambda_s0, lambda_s1, lambda_s2 = ( + state.lambda_t[t], + state.lambda_t[s0], + state.lambda_t[s1], + state.lambda_t[s2], + ) + alpha_t, alpha_s0 = state.alpha_t[t], state.alpha_t[s0] + sigma_t, sigma_s0 = state.sigma_t[t], state.sigma_t[s0] + h, h_0, h_1 = lambda_t - lambda_s0, lambda_s0 - lambda_s1, lambda_s1 - lambda_s2 + r0, r1 = h_0 / h, h_1 / h + D0 = m0 + D1_0, D1_1 = (1.0 / r0) * (m0 - m1), (1.0 / r1) * (m1 - m2) + D1 = D1_0 + (r0 / (r0 + r1)) * (D1_0 - D1_1) + D2 = (1.0 / (r0 + r1)) * (D1_0 - D1_1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + x_t = ( + (sigma_t / sigma_s0) * sample + - (alpha_t * (jnp.exp(-h) - 1.0)) * D0 + + (alpha_t * ((jnp.exp(-h) - 1.0) / h + 1.0)) * D1 + - (alpha_t * ((jnp.exp(-h) - 1.0 + h) / h**2 - 0.5)) * D2 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + x_t = ( + (alpha_t / alpha_s0) * sample + - (sigma_t * (jnp.exp(h) - 1.0)) * D0 + - (sigma_t * ((jnp.exp(h) - 1.0) / h - 1.0)) * D1 + - (sigma_t * ((jnp.exp(h) - 1.0 - h) / h**2 - 0.5)) * D2 + ) + return x_t + + def step( + self, + state: DPMSolverMultistepSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + return_dict: bool = True, + ) -> Union[FlaxDPMSolverMultistepSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by DPM-Solver. Core function to propagate the diffusion process + from the learned model outputs (most often the predicted noise). + + Args: + state (`DPMSolverMultistepSchedulerState`): + the `FlaxDPMSolverMultistepScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than FlaxDPMSolverMultistepSchedulerOutput class + + Returns: + [`FlaxDPMSolverMultistepSchedulerOutput`] or `tuple`: [`FlaxDPMSolverMultistepSchedulerOutput`] if + `return_dict` is True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + (step_index,) = jnp.where(state.timesteps == timestep, size=1) + step_index = step_index[0] + + prev_timestep = jax.lax.select(step_index == len(state.timesteps) - 1, 0, state.timesteps[step_index + 1]) + + model_output = self.convert_model_output(state, model_output, timestep, sample) + + model_outputs_new = jnp.roll(state.model_outputs, -1, axis=0) + model_outputs_new = model_outputs_new.at[-1].set(model_output) + state = state.replace( + model_outputs=model_outputs_new, + prev_timestep=prev_timestep, + cur_sample=sample, + ) + + def step_1(state: DPMSolverMultistepSchedulerState) -> jnp.ndarray: + return self.dpm_solver_first_order_update( + state, + state.model_outputs[-1], + state.timesteps[step_index], + state.prev_timestep, + state.cur_sample, + ) + + def step_23(state: DPMSolverMultistepSchedulerState) -> jnp.ndarray: + def step_2(state: DPMSolverMultistepSchedulerState) -> jnp.ndarray: + timestep_list = jnp.array([state.timesteps[step_index - 1], state.timesteps[step_index]]) + return self.multistep_dpm_solver_second_order_update( + state, + state.model_outputs, + timestep_list, + state.prev_timestep, + state.cur_sample, + ) + + def step_3(state: DPMSolverMultistepSchedulerState) -> jnp.ndarray: + timestep_list = jnp.array( + [ + state.timesteps[step_index - 2], + state.timesteps[step_index - 1], + state.timesteps[step_index], + ] + ) + return self.multistep_dpm_solver_third_order_update( + state, + state.model_outputs, + timestep_list, + state.prev_timestep, + state.cur_sample, + ) + + step_2_output = step_2(state) + step_3_output = step_3(state) + + if self.config.solver_order == 2: + return step_2_output + elif self.config.lower_order_final and len(state.timesteps) < 15: + return jax.lax.select( + state.lower_order_nums < 2, + step_2_output, + jax.lax.select( + step_index == len(state.timesteps) - 2, + step_2_output, + step_3_output, + ), + ) + else: + return jax.lax.select( + state.lower_order_nums < 2, + step_2_output, + step_3_output, + ) + + step_1_output = step_1(state) + step_23_output = step_23(state) + + if self.config.solver_order == 1: + prev_sample = step_1_output + + elif self.config.lower_order_final and len(state.timesteps) < 15: + prev_sample = jax.lax.select( + state.lower_order_nums < 1, + step_1_output, + jax.lax.select( + step_index == len(state.timesteps) - 1, + step_1_output, + step_23_output, + ), + ) + + else: + prev_sample = jax.lax.select( + state.lower_order_nums < 1, + step_1_output, + step_23_output, + ) + + state = state.replace( + lower_order_nums=jnp.minimum(state.lower_order_nums + 1, self.config.solver_order), + ) + + if not return_dict: + return (prev_sample, state) + + return FlaxDPMSolverMultistepSchedulerOutput(prev_sample=prev_sample, state=state) + + def scale_model_input( + self, state: DPMSolverMultistepSchedulerState, sample: jnp.ndarray, timestep: Optional[int] = None + ) -> jnp.ndarray: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + state (`DPMSolverMultistepSchedulerState`): + the `FlaxDPMSolverMultistepScheduler` state data class instance. + sample (`jnp.ndarray`): input sample + timestep (`int`, optional): current timestep + + Returns: + `jnp.ndarray`: scaled input sample + """ + return sample + + def add_noise( + self, + state: DPMSolverMultistepSchedulerState, + original_samples: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return add_noise_common(state.common, original_samples, noise, timesteps) + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_singlestep.py b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_singlestep.py new file mode 100644 index 0000000000000000000000000000000000000000..0225d8027bc3e24cd99e96fae49bf57fbf61a730 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_dpmsolver_singlestep.py @@ -0,0 +1,604 @@ +# Copyright 2022 TSAIL Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/LuChengTHU/dpm-solver + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class DPMSolverSinglestepScheduler(SchedulerMixin, ConfigMixin): + """ + DPM-Solver (and the improved version DPM-Solver++) is a fast dedicated high-order solver for diffusion ODEs with + the convergence order guarantee. Empirically, sampling by DPM-Solver with only 20 steps can generate high-quality + samples, and it can generate quite good samples even in only 10 steps. + + For more details, see the original paper: https://arxiv.org/abs/2206.00927 and https://arxiv.org/abs/2211.01095 + + Currently, we support the singlestep DPM-Solver for both noise prediction models and data prediction models. We + recommend to use `solver_order=2` for guided sampling, and `solver_order=3` for unconditional sampling. + + We also support the "dynamic thresholding" method in Imagen (https://arxiv.org/abs/2205.11487). For pixel-space + diffusion models, you can set both `algorithm_type="dpmsolver++"` and `thresholding=True` to use the dynamic + thresholding. Note that the thresholding method is unsuitable for latent-space diffusion models (such as + stable-diffusion). + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + solver_order (`int`, default `2`): + the order of DPM-Solver; can be `1` or `2` or `3`. We recommend to use `solver_order=2` for guided + sampling, and `solver_order=3` for unconditional sampling. + prediction_type (`str`, default `epsilon`): + indicates whether the model predicts the noise (epsilon), or the data / `x0`. One of `epsilon`, `sample`, + or `v-prediction`. + thresholding (`bool`, default `False`): + whether to use the "dynamic thresholding" method (introduced by Imagen, https://arxiv.org/abs/2205.11487). + For pixel-space diffusion models, you can set both `algorithm_type=dpmsolver++` and `thresholding=True` to + use the dynamic thresholding. Note that the thresholding method is unsuitable for latent-space diffusion + models (such as stable-diffusion). + dynamic_thresholding_ratio (`float`, default `0.995`): + the ratio for the dynamic thresholding method. Default is `0.995`, the same as Imagen + (https://arxiv.org/abs/2205.11487). + sample_max_value (`float`, default `1.0`): + the threshold value for dynamic thresholding. Valid only when `thresholding=True` and + `algorithm_type="dpmsolver++`. + algorithm_type (`str`, default `dpmsolver++`): + the algorithm type for the solver. Either `dpmsolver` or `dpmsolver++`. The `dpmsolver` type implements the + algorithms in https://arxiv.org/abs/2206.00927, and the `dpmsolver++` type implements the algorithms in + https://arxiv.org/abs/2211.01095. We recommend to use `dpmsolver++` with `solver_order=2` for guided + sampling (e.g. stable-diffusion). + solver_type (`str`, default `midpoint`): + the solver type for the second-order solver. Either `midpoint` or `heun`. The solver type slightly affects + the sample quality, especially for small number of steps. We empirically find that `midpoint` solvers are + slightly better, so we recommend to use the `midpoint` type. + lower_order_final (`bool`, default `True`): + whether to use lower-order solvers in the final steps. For singlestep schedulers, we recommend to enable + this to use up all the function evaluations. + + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[np.ndarray] = None, + solver_order: int = 2, + prediction_type: str = "epsilon", + thresholding: bool = False, + dynamic_thresholding_ratio: float = 0.995, + sample_max_value: float = 1.0, + algorithm_type: str = "dpmsolver++", + solver_type: str = "midpoint", + lower_order_final: bool = True, + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + # Currently we only support VP-type noise schedule + self.alpha_t = torch.sqrt(self.alphas_cumprod) + self.sigma_t = torch.sqrt(1 - self.alphas_cumprod) + self.lambda_t = torch.log(self.alpha_t) - torch.log(self.sigma_t) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # settings for DPM-Solver + if algorithm_type not in ["dpmsolver", "dpmsolver++"]: + if algorithm_type == "deis": + algorithm_type = "dpmsolver++" + else: + raise NotImplementedError(f"{algorithm_type} does is not implemented for {self.__class__}") + if solver_type not in ["midpoint", "heun"]: + if solver_type == "logrho": + solver_type = "midpoint" + else: + raise NotImplementedError(f"{solver_type} does is not implemented for {self.__class__}") + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.model_outputs = [None] * solver_order + self.sample = None + self.order_list = self.get_order_list(num_train_timesteps) + + def get_order_list(self, num_inference_steps: int) -> List[int]: + """ + Computes the solver order at each time step. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + steps = num_inference_steps + order = self.solver_order + if self.lower_order_final: + if order == 3: + if steps % 3 == 0: + orders = [1, 2, 3] * (steps // 3 - 1) + [1, 2] + [1] + elif steps % 3 == 1: + orders = [1, 2, 3] * (steps // 3) + [1] + else: + orders = [1, 2, 3] * (steps // 3) + [1, 2] + elif order == 2: + if steps % 2 == 0: + orders = [1, 2] * (steps // 2) + else: + orders = [1, 2] * (steps // 2) + [1] + elif order == 1: + orders = [1] * steps + else: + if order == 3: + orders = [1, 2, 3] * (steps // 3) + elif order == 2: + orders = [1, 2] * (steps // 2) + elif order == 1: + orders = [1] * steps + return orders + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + timesteps = ( + np.linspace(0, self.num_train_timesteps - 1, num_inference_steps + 1) + .round()[::-1][:-1] + .copy() + .astype(np.int64) + ) + self.timesteps = torch.from_numpy(timesteps).to(device) + self.model_outputs = [None] * self.config.solver_order + self.sample = None + self.orders = self.get_order_list(num_inference_steps) + + def convert_model_output( + self, model_output: torch.FloatTensor, timestep: int, sample: torch.FloatTensor + ) -> torch.FloatTensor: + """ + Convert the model output to the corresponding type that the algorithm (DPM-Solver / DPM-Solver++) needs. + + DPM-Solver is designed to discretize an integral of the noise prediction model, and DPM-Solver++ is designed to + discretize an integral of the data prediction model. So we need to first convert the model output to the + corresponding type to match the algorithm. + + Note that the algorithm type and the model type is decoupled. That is to say, we can use either DPM-Solver or + DPM-Solver++ for both noise prediction model and data prediction model. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the converted model output. + """ + # DPM-Solver++ needs to solve an integral of the data prediction model. + if self.config.algorithm_type == "dpmsolver++": + if self.config.prediction_type == "epsilon": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = (sample - sigma_t * model_output) / alpha_t + elif self.config.prediction_type == "sample": + x0_pred = model_output + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + x0_pred = alpha_t * sample - sigma_t * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction` for the DPMSolverSinglestepScheduler." + ) + + if self.config.thresholding: + # Dynamic thresholding in https://arxiv.org/abs/2205.11487 + dtype = x0_pred.dtype + dynamic_max_val = torch.quantile( + torch.abs(x0_pred).reshape((x0_pred.shape[0], -1)).float(), + self.config.dynamic_thresholding_ratio, + dim=1, + ) + dynamic_max_val = torch.maximum( + dynamic_max_val, + self.config.sample_max_value * torch.ones_like(dynamic_max_val).to(dynamic_max_val.device), + )[(...,) + (None,) * (x0_pred.ndim - 1)] + x0_pred = torch.clamp(x0_pred, -dynamic_max_val, dynamic_max_val) / dynamic_max_val + x0_pred = x0_pred.to(dtype) + return x0_pred + # DPM-Solver needs to solve an integral of the noise prediction model. + elif self.config.algorithm_type == "dpmsolver": + if self.config.prediction_type == "epsilon": + return model_output + elif self.config.prediction_type == "sample": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + epsilon = (sample - alpha_t * model_output) / sigma_t + return epsilon + elif self.config.prediction_type == "v_prediction": + alpha_t, sigma_t = self.alpha_t[timestep], self.sigma_t[timestep] + epsilon = alpha_t * model_output + sigma_t * sample + return epsilon + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`, or" + " `v_prediction` for the DPMSolverSinglestepScheduler." + ) + + def dpm_solver_first_order_update( + self, + model_output: torch.FloatTensor, + timestep: int, + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the first-order DPM-Solver (equivalent to DDIM). + + See https://arxiv.org/abs/2206.00927 for the detailed derivation. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + lambda_t, lambda_s = self.lambda_t[prev_timestep], self.lambda_t[timestep] + alpha_t, alpha_s = self.alpha_t[prev_timestep], self.alpha_t[timestep] + sigma_t, sigma_s = self.sigma_t[prev_timestep], self.sigma_t[timestep] + h = lambda_t - lambda_s + if self.config.algorithm_type == "dpmsolver++": + x_t = (sigma_t / sigma_s) * sample - (alpha_t * (torch.exp(-h) - 1.0)) * model_output + elif self.config.algorithm_type == "dpmsolver": + x_t = (alpha_t / alpha_s) * sample - (sigma_t * (torch.exp(h) - 1.0)) * model_output + return x_t + + def singlestep_dpm_solver_second_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the second-order singlestep DPM-Solver. + + It computes the solution at time `prev_timestep` from the time `timestep_list[-2]`. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1 = prev_timestep, timestep_list[-1], timestep_list[-2] + m0, m1 = model_output_list[-1], model_output_list[-2] + lambda_t, lambda_s0, lambda_s1 = self.lambda_t[t], self.lambda_t[s0], self.lambda_t[s1] + alpha_t, alpha_s1 = self.alpha_t[t], self.alpha_t[s1] + sigma_t, sigma_s1 = self.sigma_t[t], self.sigma_t[s1] + h, h_0 = lambda_t - lambda_s1, lambda_s0 - lambda_s1 + r0 = h_0 / h + D0, D1 = m1, (1.0 / r0) * (m0 - m1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2211.01095 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (sigma_t / sigma_s1) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + - 0.5 * (alpha_t * (torch.exp(-h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (sigma_t / sigma_s1) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + + (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (alpha_t / alpha_s1) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - 0.5 * (sigma_t * (torch.exp(h) - 1.0)) * D1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (alpha_t / alpha_s1) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1 + ) + return x_t + + def singlestep_dpm_solver_third_order_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + ) -> torch.FloatTensor: + """ + One step for the third-order singlestep DPM-Solver. + + It computes the solution at time `prev_timestep` from the time `timestep_list[-3]`. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + t, s0, s1, s2 = prev_timestep, timestep_list[-1], timestep_list[-2], timestep_list[-3] + m0, m1, m2 = model_output_list[-1], model_output_list[-2], model_output_list[-3] + lambda_t, lambda_s0, lambda_s1, lambda_s2 = ( + self.lambda_t[t], + self.lambda_t[s0], + self.lambda_t[s1], + self.lambda_t[s2], + ) + alpha_t, alpha_s2 = self.alpha_t[t], self.alpha_t[s2] + sigma_t, sigma_s2 = self.sigma_t[t], self.sigma_t[s2] + h, h_0, h_1 = lambda_t - lambda_s2, lambda_s0 - lambda_s2, lambda_s1 - lambda_s2 + r0, r1 = h_0 / h, h_1 / h + D0 = m2 + D1_0, D1_1 = (1.0 / r1) * (m1 - m2), (1.0 / r0) * (m0 - m2) + D1 = (r0 * D1_0 - r1 * D1_1) / (r0 - r1) + D2 = 2.0 * (D1_1 - D1_0) / (r0 - r1) + if self.config.algorithm_type == "dpmsolver++": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (sigma_t / sigma_s2) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + + (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1_1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (sigma_t / sigma_s2) * sample + - (alpha_t * (torch.exp(-h) - 1.0)) * D0 + + (alpha_t * ((torch.exp(-h) - 1.0) / h + 1.0)) * D1 + - (alpha_t * ((torch.exp(-h) - 1.0 + h) / h**2 - 0.5)) * D2 + ) + elif self.config.algorithm_type == "dpmsolver": + # See https://arxiv.org/abs/2206.00927 for detailed derivations + if self.config.solver_type == "midpoint": + x_t = ( + (alpha_t / alpha_s2) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1_1 + ) + elif self.config.solver_type == "heun": + x_t = ( + (alpha_t / alpha_s2) * sample + - (sigma_t * (torch.exp(h) - 1.0)) * D0 + - (sigma_t * ((torch.exp(h) - 1.0) / h - 1.0)) * D1 + - (sigma_t * ((torch.exp(h) - 1.0 - h) / h**2 - 0.5)) * D2 + ) + return x_t + + def singlestep_dpm_solver_update( + self, + model_output_list: List[torch.FloatTensor], + timestep_list: List[int], + prev_timestep: int, + sample: torch.FloatTensor, + order: int, + ) -> torch.FloatTensor: + """ + One step for the singlestep DPM-Solver. + + Args: + model_output_list (`List[torch.FloatTensor]`): + direct outputs from learned diffusion model at current and latter timesteps. + timestep (`int`): current and latter discrete timestep in the diffusion chain. + prev_timestep (`int`): previous discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + order (`int`): + the solver order at this step. + + Returns: + `torch.FloatTensor`: the sample tensor at the previous timestep. + """ + if order == 1: + return self.dpm_solver_first_order_update(model_output_list[-1], timestep_list[-1], prev_timestep, sample) + elif order == 2: + return self.singlestep_dpm_solver_second_order_update( + model_output_list, timestep_list, prev_timestep, sample + ) + elif order == 3: + return self.singlestep_dpm_solver_third_order_update( + model_output_list, timestep_list, prev_timestep, sample + ) + else: + raise ValueError(f"Order must be 1, 2, 3, got {order}") + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the singlestep DPM-Solver. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero() + if len(step_index) == 0: + step_index = len(self.timesteps) - 1 + else: + step_index = step_index.item() + prev_timestep = 0 if step_index == len(self.timesteps) - 1 else self.timesteps[step_index + 1] + + model_output = self.convert_model_output(model_output, timestep, sample) + for i in range(self.config.solver_order - 1): + self.model_outputs[i] = self.model_outputs[i + 1] + self.model_outputs[-1] = model_output + + order = self.order_list[step_index] + # For single-step solvers, we use the initial value at each time with order = 1. + if order == 1: + self.sample = sample + + timestep_list = [self.timesteps[step_index - i] for i in range(order - 1, 0, -1)] + [timestep] + prev_sample = self.singlestep_dpm_solver_update( + self.model_outputs, timestep_list, prev_timestep, self.sample, order + ) + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_euler_ancestral_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_euler_ancestral_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..45f939aafe702ff375afeb0c8561e7c7110c4f92 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_euler_ancestral_discrete.py @@ -0,0 +1,273 @@ +# Copyright 2022 Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# 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. + +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, logging, randn_tensor +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->EulerAncestralDiscrete +class EulerAncestralDiscreteSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +class EulerAncestralDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Ancestral sampling with Euler method steps. Based on the original k-diffusion implementation by Katherine Crowson: + https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L72 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.concatenate([sigmas[::-1], [0.0]]).astype(np.float32) + self.sigmas = torch.from_numpy(sigmas) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=float)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.is_scale_input_called = False + + def scale_model_input( + self, sample: torch.FloatTensor, timestep: Union[float, torch.FloatTensor] + ) -> torch.FloatTensor: + """ + Scales the denoising model input by `(sigma**2 + 1) ** 0.5` to match the Euler algorithm. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`float` or `torch.FloatTensor`): the current timestep in the diffusion chain + + Returns: + `torch.FloatTensor`: scaled input sample + """ + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + sample = sample / ((sigma**2 + 1) ** 0.5) + self.is_scale_input_called = True + return sample + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + timesteps = np.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + self.sigmas = torch.from_numpy(sigmas).to(device=device) + if str(device).startswith("mps"): + # mps does not support float64 + self.timesteps = torch.from_numpy(timesteps).to(device, dtype=torch.float32) + else: + self.timesteps = torch.from_numpy(timesteps).to(device=device) + + def step( + self, + model_output: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + sample: torch.FloatTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[EulerAncestralDiscreteSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`float`): current timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + generator (`torch.Generator`, optional): Random number generator. + return_dict (`bool`): option for returning tuple rather than EulerAncestralDiscreteSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.EulerAncestralDiscreteSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.EulerAncestralDiscreteSchedulerOutput`] if `return_dict` is True, otherwise + a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + + if ( + isinstance(timestep, int) + or isinstance(timestep, torch.IntTensor) + or isinstance(timestep, torch.LongTensor) + ): + raise ValueError( + ( + "Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to" + " `EulerDiscreteScheduler.step()` is not supported. Make sure to pass" + " one of the `scheduler.timesteps` as a timestep." + ), + ) + + if not self.is_scale_input_called: + logger.warning( + "The `scale_model_input` function should be called before `step` to ensure correct denoising. " + "See `StableDiffusionPipeline` for a usage example." + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + pred_original_sample = sample - sigma * model_output + elif self.config.prediction_type == "v_prediction": + # * c_out + input * c_skip + pred_original_sample = model_output * (-sigma / (sigma**2 + 1) ** 0.5) + (sample / (sigma**2 + 1)) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + sigma_from = self.sigmas[step_index] + sigma_to = self.sigmas[step_index + 1] + sigma_up = (sigma_to**2 * (sigma_from**2 - sigma_to**2) / sigma_from**2) ** 0.5 + sigma_down = (sigma_to**2 - sigma_up**2) ** 0.5 + + # 2. Convert to an ODE derivative + derivative = (sample - pred_original_sample) / sigma + + dt = sigma_down - sigma + + prev_sample = sample + derivative * dt + + device = model_output.device + noise = randn_tensor(model_output.shape, dtype=model_output.dtype, device=device, generator=generator) + + prev_sample = prev_sample + noise * sigma_up + + if not return_dict: + return (prev_sample,) + + return EulerAncestralDiscreteSchedulerOutput( + prev_sample=prev_sample, pred_original_sample=pred_original_sample + ) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + self.sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + self.timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + self.timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + schedule_timesteps = self.timesteps + step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps] + + sigma = self.sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_euler_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_euler_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..1a7a46bc5d3201889b226d0932ed720c3f9e97a9 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_euler_discrete.py @@ -0,0 +1,299 @@ +# Copyright 2022 Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# 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. + +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, logging, randn_tensor +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->EulerDiscrete +class EulerDiscreteSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +class EulerDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Euler scheduler (Algorithm 2) from Karras et al. (2022) https://arxiv.org/abs/2206.00364. . Based on the original + k-diffusion implementation by Katherine Crowson: + https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L51 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + prediction_type (`str`, default `"epsilon"`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + interpolation_type (`str`, default `"linear"`, optional): + interpolation type to compute intermediate sigmas for the scheduler denoising steps. Should be one of + [`"linear"`, `"log_linear"`]. + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + interpolation_type: str = "linear", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.concatenate([sigmas[::-1], [0.0]]).astype(np.float32) + self.sigmas = torch.from_numpy(sigmas) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=float)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.is_scale_input_called = False + + def scale_model_input( + self, sample: torch.FloatTensor, timestep: Union[float, torch.FloatTensor] + ) -> torch.FloatTensor: + """ + Scales the denoising model input by `(sigma**2 + 1) ** 0.5` to match the Euler algorithm. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`float` or `torch.FloatTensor`): the current timestep in the diffusion chain + + Returns: + `torch.FloatTensor`: scaled input sample + """ + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + + sample = sample / ((sigma**2 + 1) ** 0.5) + + self.is_scale_input_called = True + return sample + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + timesteps = np.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + + if self.config.interpolation_type == "linear": + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + elif self.config.interpolation_type == "log_linear": + sigmas = torch.linspace(np.log(sigmas[-1]), np.log(sigmas[0]), num_inference_steps + 1).exp() + else: + raise ValueError( + f"{self.config.interpolation_type} is not implemented. Please specify interpolation_type to either" + " 'linear' or 'log_linear'" + ) + + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + self.sigmas = torch.from_numpy(sigmas).to(device=device) + if str(device).startswith("mps"): + # mps does not support float64 + self.timesteps = torch.from_numpy(timesteps).to(device, dtype=torch.float32) + else: + self.timesteps = torch.from_numpy(timesteps).to(device=device) + + def step( + self, + model_output: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + sample: torch.FloatTensor, + s_churn: float = 0.0, + s_tmin: float = 0.0, + s_tmax: float = float("inf"), + s_noise: float = 1.0, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[EulerDiscreteSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`float`): current timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + s_churn (`float`) + s_tmin (`float`) + s_tmax (`float`) + s_noise (`float`) + generator (`torch.Generator`, optional): Random number generator. + return_dict (`bool`): option for returning tuple rather than EulerDiscreteSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.EulerDiscreteSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.EulerDiscreteSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + + if ( + isinstance(timestep, int) + or isinstance(timestep, torch.IntTensor) + or isinstance(timestep, torch.LongTensor) + ): + raise ValueError( + ( + "Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to" + " `EulerDiscreteScheduler.step()` is not supported. Make sure to pass" + " one of the `scheduler.timesteps` as a timestep." + ), + ) + + if not self.is_scale_input_called: + logger.warning( + "The `scale_model_input` function should be called before `step` to ensure correct denoising. " + "See `StableDiffusionPipeline` for a usage example." + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + + gamma = min(s_churn / (len(self.sigmas) - 1), 2**0.5 - 1) if s_tmin <= sigma <= s_tmax else 0.0 + + noise = randn_tensor( + model_output.shape, dtype=model_output.dtype, device=model_output.device, generator=generator + ) + + eps = noise * s_noise + sigma_hat = sigma * (gamma + 1) + + if gamma > 0: + sample = sample + eps * (sigma_hat**2 - sigma**2) ** 0.5 + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "original_sample": + pred_original_sample = model_output + elif self.config.prediction_type == "epsilon": + pred_original_sample = sample - sigma_hat * model_output + elif self.config.prediction_type == "v_prediction": + # * c_out + input * c_skip + pred_original_sample = model_output * (-sigma / (sigma**2 + 1) ** 0.5) + (sample / (sigma**2 + 1)) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + # 2. Convert to an ODE derivative + derivative = (sample - pred_original_sample) / sigma_hat + + dt = self.sigmas[step_index + 1] - sigma_hat + + prev_sample = sample + derivative * dt + + if not return_dict: + return (prev_sample,) + + return EulerDiscreteSchedulerOutput(prev_sample=prev_sample, pred_original_sample=pred_original_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + self.sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + self.timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + self.timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + schedule_timesteps = self.timesteps + step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps] + + sigma = self.sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_heun_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_heun_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..0dea944b6fef5466c759b8c763852ae546001a09 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_heun_discrete.py @@ -0,0 +1,263 @@ +# Copyright 2022 Katherine Crowson, The HuggingFace Team and hlky. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +class HeunDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Implements Algorithm 2 (Heun steps) from Karras et al. (2022). for discrete beta schedules. Based on the original + k-diffusion implementation by Katherine Crowson: + https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L90 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. beta_start (`float`): the + starting `beta` value of inference. beta_end (`float`): the final `beta` value. beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 2 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.00085, # sensible defaults + beta_end: float = 0.012, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + # set all values + self.set_timesteps(num_train_timesteps, None, num_train_timesteps) + + def index_for_timestep(self, timestep): + indices = (self.timesteps == timestep).nonzero() + if self.state_in_first_order: + pos = -1 + else: + pos = 0 + return indices[pos].item() + + def scale_model_input( + self, + sample: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + ) -> torch.FloatTensor: + """ + Args: + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + sample (`torch.FloatTensor`): input sample timestep (`int`, optional): current timestep + Returns: + `torch.FloatTensor`: scaled input sample + """ + step_index = self.index_for_timestep(timestep) + + sigma = self.sigmas[step_index] + sample = sample / ((sigma**2 + 1) ** 0.5) + return sample + + def set_timesteps( + self, + num_inference_steps: int, + device: Union[str, torch.device] = None, + num_train_timesteps: Optional[int] = None, + ): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + num_train_timesteps = num_train_timesteps or self.config.num_train_timesteps + + timesteps = np.linspace(0, num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + sigmas = torch.from_numpy(sigmas).to(device=device) + self.sigmas = torch.cat([sigmas[:1], sigmas[1:-1].repeat_interleave(2), sigmas[-1:]]) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + timesteps = torch.from_numpy(timesteps) + timesteps = torch.cat([timesteps[:1], timesteps[1:].repeat_interleave(2)]) + + if str(device).startswith("mps"): + # mps does not support float64 + self.timesteps = timesteps.to(device, dtype=torch.float32) + else: + self.timesteps = timesteps.to(device=device) + + # empty dt and derivative + self.prev_derivative = None + self.dt = None + + @property + def state_in_first_order(self): + return self.dt is None + + def step( + self, + model_output: Union[torch.FloatTensor, np.ndarray], + timestep: Union[float, torch.FloatTensor], + sample: Union[torch.FloatTensor, np.ndarray], + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Args: + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + model_output (`torch.FloatTensor` or `np.ndarray`): direct output from learned diffusion model. timestep + (`int`): current discrete timestep in the diffusion chain. sample (`torch.FloatTensor` or `np.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.SchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + step_index = self.index_for_timestep(timestep) + + if self.state_in_first_order: + sigma = self.sigmas[step_index] + sigma_next = self.sigmas[step_index + 1] + else: + # 2nd order / Heun's method + sigma = self.sigmas[step_index - 1] + sigma_next = self.sigmas[step_index] + + # currently only gamma=0 is supported. This usually works best anyways. + # We can support gamma in the future but then need to scale the timestep before + # passing it to the model which requires a change in API + gamma = 0 + sigma_hat = sigma * (gamma + 1) # Note: sigma_hat == sigma for now + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + sigma_input = sigma_hat if self.state_in_first_order else sigma_next + pred_original_sample = sample - sigma_input * model_output + elif self.config.prediction_type == "v_prediction": + sigma_input = sigma_hat if self.state_in_first_order else sigma_next + pred_original_sample = model_output * (-sigma_input / (sigma_input**2 + 1) ** 0.5) + ( + sample / (sigma_input**2 + 1) + ) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + if self.state_in_first_order: + # 2. Convert to an ODE derivative for 1st order + derivative = (sample - pred_original_sample) / sigma_hat + # 3. delta timestep + dt = sigma_next - sigma_hat + + # store for 2nd order step + self.prev_derivative = derivative + self.dt = dt + self.sample = sample + else: + # 2. 2nd order / Heun's method + derivative = (sample - pred_original_sample) / sigma_next + derivative = (self.prev_derivative + derivative) / 2 + + # 3. take prev timestep & sample + dt = self.dt + sample = self.sample + + # free dt and derivative + # Note, this puts the scheduler in "first order mode" + self.prev_derivative = None + self.dt = None + self.sample = None + + prev_sample = sample + derivative * dt + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + self.sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + self.timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + self.timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + step_indices = [self.index_for_timestep(t) for t in timesteps] + + sigma = self.sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_ipndm.py b/diffusers/src/diffusers/schedulers/scheduling_ipndm.py new file mode 100644 index 0000000000000000000000000000000000000000..f22261d3ecd258485d21a77a49e105cb02af15f5 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_ipndm.py @@ -0,0 +1,161 @@ +# Copyright 2022 Zhejiang University Team and The HuggingFace Team. All rights reserved. +# +# 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. + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import SchedulerMixin, SchedulerOutput + + +class IPNDMScheduler(SchedulerMixin, ConfigMixin): + """ + Improved Pseudo numerical methods for diffusion models (iPNDM) ported from @crowsonkb's amazing k-diffusion + [library](https://github.com/crowsonkb/v-diffusion-pytorch/blob/987f8985e38208345c1959b0ea767a625831cc9b/diffusion/sampling.py#L296) + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2202.09778 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + """ + + order = 1 + + @register_to_config + def __init__( + self, num_train_timesteps: int = 1000, trained_betas: Optional[Union[np.ndarray, List[float]]] = None + ): + # set `betas`, `alphas`, `timesteps` + self.set_timesteps(num_train_timesteps) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # For now we only support F-PNDM, i.e. the runge-kutta method + # For more information on the algorithm please take a look at the paper: https://arxiv.org/pdf/2202.09778.pdf + # mainly at formula (9), (12), (13) and the Algorithm 2. + self.pndm_order = 4 + + # running values + self.ets = [] + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + self.num_inference_steps = num_inference_steps + steps = torch.linspace(1, 0, num_inference_steps + 1)[:-1] + steps = torch.cat([steps, torch.tensor([0.0])]) + + if self.config.trained_betas is not None: + self.betas = torch.tensor(self.config.trained_betas, dtype=torch.float32) + else: + self.betas = torch.sin(steps * math.pi / 2) ** 2 + + self.alphas = (1.0 - self.betas**2) ** 0.5 + + timesteps = (torch.atan2(self.betas, self.alphas) / math.pi * 2)[:-1] + self.timesteps = timesteps.to(device) + + self.ets = [] + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the linear multi-step method. This has one forward pass with multiple + times to approximate the solution. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + timestep_index = (self.timesteps == timestep).nonzero().item() + prev_timestep_index = timestep_index + 1 + + ets = sample * self.betas[timestep_index] + model_output * self.alphas[timestep_index] + self.ets.append(ets) + + if len(self.ets) == 1: + ets = self.ets[-1] + elif len(self.ets) == 2: + ets = (3 * self.ets[-1] - self.ets[-2]) / 2 + elif len(self.ets) == 3: + ets = (23 * self.ets[-1] - 16 * self.ets[-2] + 5 * self.ets[-3]) / 12 + else: + ets = (1 / 24) * (55 * self.ets[-1] - 59 * self.ets[-2] + 37 * self.ets[-3] - 9 * self.ets[-4]) + + prev_sample = self._get_prev_sample(sample, timestep_index, prev_timestep_index, ets) + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def _get_prev_sample(self, sample, timestep_index, prev_timestep_index, ets): + alpha = self.alphas[timestep_index] + sigma = self.betas[timestep_index] + + next_alpha = self.alphas[prev_timestep_index] + next_sigma = self.betas[prev_timestep_index] + + pred = (sample - sigma * ets) / max(alpha, 1e-8) + prev_sample = next_alpha * pred + ets * next_sigma + + return prev_sample + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_ancestral_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_ancestral_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..711bdf2d5ef078a8d7d477ae8ae3d8933043091a --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_ancestral_discrete.py @@ -0,0 +1,316 @@ +# Copyright 2022 Katherine Crowson, The HuggingFace Team and hlky. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import randn_tensor +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +class KDPM2AncestralDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Scheduler created by @crowsonkb in [k_diffusion](https://github.com/crowsonkb/k-diffusion), see: + https://github.com/crowsonkb/k-diffusion/blob/5b3af030dd83e0297272d861c19477735d0317ec/k_diffusion/sampling.py#L188 + + Scheduler inspired by DPM-Solver-2 and Algorthim 2 from Karras et al. (2022). + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. beta_start (`float`): the + starting `beta` value of inference. beta_end (`float`): the final `beta` value. beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 2 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.00085, # sensible defaults + beta_end: float = 0.012, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + # set all values + self.set_timesteps(num_train_timesteps, None, num_train_timesteps) + + def index_for_timestep(self, timestep): + indices = (self.timesteps == timestep).nonzero() + if self.state_in_first_order: + pos = -1 + else: + pos = 0 + return indices[pos].item() + + def scale_model_input( + self, + sample: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + ) -> torch.FloatTensor: + """ + Args: + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + sample (`torch.FloatTensor`): input sample timestep (`int`, optional): current timestep + Returns: + `torch.FloatTensor`: scaled input sample + """ + step_index = self.index_for_timestep(timestep) + + if self.state_in_first_order: + sigma = self.sigmas[step_index] + else: + sigma = self.sigmas_interpol[step_index - 1] + + sample = sample / ((sigma**2 + 1) ** 0.5) + return sample + + def set_timesteps( + self, + num_inference_steps: int, + device: Union[str, torch.device] = None, + num_train_timesteps: Optional[int] = None, + ): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + num_train_timesteps = num_train_timesteps or self.config.num_train_timesteps + + timesteps = np.linspace(0, num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + self.log_sigmas = torch.from_numpy(np.log(sigmas)).to(device) + + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + sigmas = torch.from_numpy(sigmas).to(device=device) + + # compute up and down sigmas + sigmas_next = sigmas.roll(-1) + sigmas_next[-1] = 0.0 + sigmas_up = (sigmas_next**2 * (sigmas**2 - sigmas_next**2) / sigmas**2) ** 0.5 + sigmas_down = (sigmas_next**2 - sigmas_up**2) ** 0.5 + sigmas_down[-1] = 0.0 + + # compute interpolated sigmas + sigmas_interpol = sigmas.log().lerp(sigmas_down.log(), 0.5).exp() + sigmas_interpol[-2:] = 0.0 + + # set sigmas + self.sigmas = torch.cat([sigmas[:1], sigmas[1:].repeat_interleave(2), sigmas[-1:]]) + self.sigmas_interpol = torch.cat( + [sigmas_interpol[:1], sigmas_interpol[1:].repeat_interleave(2), sigmas_interpol[-1:]] + ) + self.sigmas_up = torch.cat([sigmas_up[:1], sigmas_up[1:].repeat_interleave(2), sigmas_up[-1:]]) + self.sigmas_down = torch.cat([sigmas_down[:1], sigmas_down[1:].repeat_interleave(2), sigmas_down[-1:]]) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + if str(device).startswith("mps"): + # mps does not support float64 + timesteps = torch.from_numpy(timesteps).to(device, dtype=torch.float32) + else: + timesteps = torch.from_numpy(timesteps).to(device) + + timesteps_interpol = self.sigma_to_t(sigmas_interpol).to(device) + interleaved_timesteps = torch.stack((timesteps_interpol[:-2, None], timesteps[1:, None]), dim=-1).flatten() + + self.timesteps = torch.cat([timesteps[:1], interleaved_timesteps]) + + self.sample = None + + def sigma_to_t(self, sigma): + # get log sigma + log_sigma = sigma.log() + + # get distribution + dists = log_sigma - self.log_sigmas[:, None] + + # get sigmas range + low_idx = dists.ge(0).cumsum(dim=0).argmax(dim=0).clamp(max=self.log_sigmas.shape[0] - 2) + high_idx = low_idx + 1 + + low = self.log_sigmas[low_idx] + high = self.log_sigmas[high_idx] + + # interpolate sigmas + w = (low - log_sigma) / (low - high) + w = w.clamp(0, 1) + + # transform interpolation to time range + t = (1 - w) * low_idx + w * high_idx + t = t.view(sigma.shape) + return t + + @property + def state_in_first_order(self): + return self.sample is None + + def step( + self, + model_output: Union[torch.FloatTensor, np.ndarray], + timestep: Union[float, torch.FloatTensor], + sample: Union[torch.FloatTensor, np.ndarray], + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Args: + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + model_output (`torch.FloatTensor` or `np.ndarray`): direct output from learned diffusion model. timestep + (`int`): current discrete timestep in the diffusion chain. sample (`torch.FloatTensor` or `np.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.SchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + step_index = self.index_for_timestep(timestep) + + if self.state_in_first_order: + sigma = self.sigmas[step_index] + sigma_interpol = self.sigmas_interpol[step_index] + sigma_up = self.sigmas_up[step_index] + sigma_down = self.sigmas_down[step_index - 1] + else: + # 2nd order / KPDM2's method + sigma = self.sigmas[step_index - 1] + sigma_interpol = self.sigmas_interpol[step_index - 1] + sigma_up = self.sigmas_up[step_index - 1] + sigma_down = self.sigmas_down[step_index - 1] + + # currently only gamma=0 is supported. This usually works best anyways. + # We can support gamma in the future but then need to scale the timestep before + # passing it to the model which requires a change in API + gamma = 0 + sigma_hat = sigma * (gamma + 1) # Note: sigma_hat == sigma for now + + device = model_output.device + noise = randn_tensor(model_output.shape, dtype=model_output.dtype, device=device, generator=generator) + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + sigma_input = sigma_hat if self.state_in_first_order else sigma_interpol + pred_original_sample = sample - sigma_input * model_output + elif self.config.prediction_type == "v_prediction": + sigma_input = sigma_hat if self.state_in_first_order else sigma_interpol + pred_original_sample = model_output * (-sigma_input / (sigma_input**2 + 1) ** 0.5) + ( + sample / (sigma_input**2 + 1) + ) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + if self.state_in_first_order: + # 2. Convert to an ODE derivative for 1st order + derivative = (sample - pred_original_sample) / sigma_hat + # 3. delta timestep + dt = sigma_interpol - sigma_hat + + # store for 2nd order step + self.sample = sample + self.dt = dt + prev_sample = sample + derivative * dt + else: + # DPM-Solver-2 + # 2. Convert to an ODE derivative for 2nd order + derivative = (sample - pred_original_sample) / sigma_interpol + # 3. delta timestep + dt = sigma_down - sigma_hat + + sample = self.sample + self.sample = None + + prev_sample = sample + derivative * dt + prev_sample = prev_sample + noise * sigma_up + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + self.sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + self.timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + self.timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + step_indices = [self.index_for_timestep(t) for t in timesteps] + + sigma = self.sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..a46cc060522c7baefee9ff012e6e1c085eecb357 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_k_dpm_2_discrete.py @@ -0,0 +1,297 @@ +# Copyright 2022 Katherine Crowson, The HuggingFace Team and hlky. All rights reserved. +# +# 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. + +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +class KDPM2DiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Scheduler created by @crowsonkb in [k_diffusion](https://github.com/crowsonkb/k-diffusion), see: + https://github.com/crowsonkb/k-diffusion/blob/5b3af030dd83e0297272d861c19477735d0317ec/k_diffusion/sampling.py#L188 + + Scheduler inspired by DPM-Solver-2 and Algorthim 2 from Karras et al. (2022). + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. beta_start (`float`): the + starting `beta` value of inference. beta_end (`float`): the final `beta` value. beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 2 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.00085, # sensible defaults + beta_end: float = 0.012, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + # set all values + self.set_timesteps(num_train_timesteps, None, num_train_timesteps) + + def index_for_timestep(self, timestep): + indices = (self.timesteps == timestep).nonzero() + if self.state_in_first_order: + pos = -1 + else: + pos = 0 + return indices[pos].item() + + def scale_model_input( + self, + sample: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + ) -> torch.FloatTensor: + """ + Args: + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + sample (`torch.FloatTensor`): input sample timestep (`int`, optional): current timestep + Returns: + `torch.FloatTensor`: scaled input sample + """ + step_index = self.index_for_timestep(timestep) + + if self.state_in_first_order: + sigma = self.sigmas[step_index] + else: + sigma = self.sigmas_interpol[step_index] + + sample = sample / ((sigma**2 + 1) ** 0.5) + return sample + + def set_timesteps( + self, + num_inference_steps: int, + device: Union[str, torch.device] = None, + num_train_timesteps: Optional[int] = None, + ): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + num_train_timesteps = num_train_timesteps or self.config.num_train_timesteps + + timesteps = np.linspace(0, num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + self.log_sigmas = torch.from_numpy(np.log(sigmas)).to(device) + + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + sigmas = torch.from_numpy(sigmas).to(device=device) + + # interpolate sigmas + sigmas_interpol = sigmas.log().lerp(sigmas.roll(1).log(), 0.5).exp() + + self.sigmas = torch.cat([sigmas[:1], sigmas[1:].repeat_interleave(2), sigmas[-1:]]) + self.sigmas_interpol = torch.cat( + [sigmas_interpol[:1], sigmas_interpol[1:].repeat_interleave(2), sigmas_interpol[-1:]] + ) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + if str(device).startswith("mps"): + # mps does not support float64 + timesteps = torch.from_numpy(timesteps).to(device, dtype=torch.float32) + else: + timesteps = torch.from_numpy(timesteps).to(device) + + # interpolate timesteps + timesteps_interpol = self.sigma_to_t(sigmas_interpol).to(device) + interleaved_timesteps = torch.stack((timesteps_interpol[1:-1, None], timesteps[1:, None]), dim=-1).flatten() + + self.timesteps = torch.cat([timesteps[:1], interleaved_timesteps]) + + self.sample = None + + def sigma_to_t(self, sigma): + # get log sigma + log_sigma = sigma.log() + + # get distribution + dists = log_sigma - self.log_sigmas[:, None] + + # get sigmas range + low_idx = dists.ge(0).cumsum(dim=0).argmax(dim=0).clamp(max=self.log_sigmas.shape[0] - 2) + high_idx = low_idx + 1 + + low = self.log_sigmas[low_idx] + high = self.log_sigmas[high_idx] + + # interpolate sigmas + w = (low - log_sigma) / (low - high) + w = w.clamp(0, 1) + + # transform interpolation to time range + t = (1 - w) * low_idx + w * high_idx + t = t.view(sigma.shape) + return t + + @property + def state_in_first_order(self): + return self.sample is None + + def step( + self, + model_output: Union[torch.FloatTensor, np.ndarray], + timestep: Union[float, torch.FloatTensor], + sample: Union[torch.FloatTensor, np.ndarray], + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Args: + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + model_output (`torch.FloatTensor` or `np.ndarray`): direct output from learned diffusion model. timestep + (`int`): current discrete timestep in the diffusion chain. sample (`torch.FloatTensor` or `np.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.SchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + """ + step_index = self.index_for_timestep(timestep) + + if self.state_in_first_order: + sigma = self.sigmas[step_index] + sigma_interpol = self.sigmas_interpol[step_index + 1] + sigma_next = self.sigmas[step_index + 1] + else: + # 2nd order / KDPM2's method + sigma = self.sigmas[step_index - 1] + sigma_interpol = self.sigmas_interpol[step_index] + sigma_next = self.sigmas[step_index] + + # currently only gamma=0 is supported. This usually works best anyways. + # We can support gamma in the future but then need to scale the timestep before + # passing it to the model which requires a change in API + gamma = 0 + sigma_hat = sigma * (gamma + 1) # Note: sigma_hat == sigma for now + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + sigma_input = sigma_hat if self.state_in_first_order else sigma_interpol + pred_original_sample = sample - sigma_input * model_output + elif self.config.prediction_type == "v_prediction": + sigma_input = sigma_hat if self.state_in_first_order else sigma_interpol + pred_original_sample = model_output * (-sigma_input / (sigma_input**2 + 1) ** 0.5) + ( + sample / (sigma_input**2 + 1) + ) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + if self.state_in_first_order: + # 2. Convert to an ODE derivative for 1st order + derivative = (sample - pred_original_sample) / sigma_hat + # 3. delta timestep + dt = sigma_interpol - sigma_hat + + # store for 2nd order step + self.sample = sample + else: + # DPM-Solver-2 + # 2. Convert to an ODE derivative for 2nd order + derivative = (sample - pred_original_sample) / sigma_interpol + + # 3. delta timestep + dt = sigma_next - sigma_hat + + sample = self.sample + self.sample = None + + prev_sample = sample + derivative * dt + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + self.sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + self.timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + self.timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + step_indices = [self.index_for_timestep(t) for t in timesteps] + + sigma = self.sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_karras_ve.py b/diffusers/src/diffusers/schedulers/scheduling_karras_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..b60b4a7180300854a7b23c40af027c470f8d5ed5 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_karras_ve.py @@ -0,0 +1,232 @@ +# Copyright 2022 NVIDIA and The HuggingFace Team. All rights reserved. +# +# 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. + + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import SchedulerMixin + + +@dataclass +class KarrasVeOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + derivative (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Derivative of predicted original image sample (x_0). + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + derivative: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +class KarrasVeScheduler(SchedulerMixin, ConfigMixin): + """ + Stochastic sampling from Karras et al. [1] tailored to the Variance-Expanding (VE) models [2]. Use Algorithm 2 and + the VE column of Table 1 from [1] for reference. + + [1] Karras, Tero, et al. "Elucidating the Design Space of Diffusion-Based Generative Models." + https://arxiv.org/abs/2206.00364 [2] Song, Yang, et al. "Score-based generative modeling through stochastic + differential equations." https://arxiv.org/abs/2011.13456 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details on the parameters, see the original paper's Appendix E.: "Elucidating the Design Space of + Diffusion-Based Generative Models." https://arxiv.org/abs/2206.00364. The grid search values used to find the + optimal {s_noise, s_churn, s_min, s_max} for a specific model are described in Table 5 of the paper. + + Args: + sigma_min (`float`): minimum noise magnitude + sigma_max (`float`): maximum noise magnitude + s_noise (`float`): the amount of additional noise to counteract loss of detail during sampling. + A reasonable range is [1.000, 1.011]. + s_churn (`float`): the parameter controlling the overall amount of stochasticity. + A reasonable range is [0, 100]. + s_min (`float`): the start value of the sigma range where we add noise (enable stochasticity). + A reasonable range is [0, 10]. + s_max (`float`): the end value of the sigma range where we add noise. + A reasonable range is [0.2, 80]. + + """ + + order = 2 + + @register_to_config + def __init__( + self, + sigma_min: float = 0.02, + sigma_max: float = 100, + s_noise: float = 1.007, + s_churn: float = 80, + s_min: float = 0.05, + s_max: float = 50, + ): + # standard deviation of the initial noise distribution + self.init_noise_sigma = sigma_max + + # setable values + self.num_inference_steps: int = None + self.timesteps: np.IntTensor = None + self.schedule: torch.FloatTensor = None # sigma(t_i) + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the continuous timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + + """ + self.num_inference_steps = num_inference_steps + timesteps = np.arange(0, self.num_inference_steps)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps).to(device) + schedule = [ + ( + self.config.sigma_max**2 + * (self.config.sigma_min**2 / self.config.sigma_max**2) ** (i / (num_inference_steps - 1)) + ) + for i in self.timesteps + ] + self.schedule = torch.tensor(schedule, dtype=torch.float32, device=device) + + def add_noise_to_input( + self, sample: torch.FloatTensor, sigma: float, generator: Optional[torch.Generator] = None + ) -> Tuple[torch.FloatTensor, float]: + """ + Explicit Langevin-like "churn" step of adding noise to the sample according to a factor gamma_i ≥ 0 to reach a + higher noise level sigma_hat = sigma_i + gamma_i*sigma_i. + + TODO Args: + """ + if self.config.s_min <= sigma <= self.config.s_max: + gamma = min(self.config.s_churn / self.num_inference_steps, 2**0.5 - 1) + else: + gamma = 0 + + # sample eps ~ N(0, S_noise^2 * I) + eps = self.config.s_noise * randn_tensor(sample.shape, generator=generator).to(sample.device) + sigma_hat = sigma + gamma * sigma + sample_hat = sample + ((sigma_hat**2 - sigma**2) ** 0.5 * eps) + + return sample_hat, sigma_hat + + def step( + self, + model_output: torch.FloatTensor, + sigma_hat: float, + sigma_prev: float, + sample_hat: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[KarrasVeOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + sigma_hat (`float`): TODO + sigma_prev (`float`): TODO + sample_hat (`torch.FloatTensor`): TODO + return_dict (`bool`): option for returning tuple rather than KarrasVeOutput class + + KarrasVeOutput: updated sample in the diffusion chain and derivative (TODO double check). + Returns: + [`~schedulers.scheduling_karras_ve.KarrasVeOutput`] or `tuple`: + [`~schedulers.scheduling_karras_ve.KarrasVeOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + + pred_original_sample = sample_hat + sigma_hat * model_output + derivative = (sample_hat - pred_original_sample) / sigma_hat + sample_prev = sample_hat + (sigma_prev - sigma_hat) * derivative + + if not return_dict: + return (sample_prev, derivative) + + return KarrasVeOutput( + prev_sample=sample_prev, derivative=derivative, pred_original_sample=pred_original_sample + ) + + def step_correct( + self, + model_output: torch.FloatTensor, + sigma_hat: float, + sigma_prev: float, + sample_hat: torch.FloatTensor, + sample_prev: torch.FloatTensor, + derivative: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[KarrasVeOutput, Tuple]: + """ + Correct the predicted sample based on the output model_output of the network. TODO complete description + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + sigma_hat (`float`): TODO + sigma_prev (`float`): TODO + sample_hat (`torch.FloatTensor`): TODO + sample_prev (`torch.FloatTensor`): TODO + derivative (`torch.FloatTensor`): TODO + return_dict (`bool`): option for returning tuple rather than KarrasVeOutput class + + Returns: + prev_sample (TODO): updated sample in the diffusion chain. derivative (TODO): TODO + + """ + pred_original_sample = sample_prev + sigma_prev * model_output + derivative_corr = (sample_prev - pred_original_sample) / sigma_prev + sample_prev = sample_hat + (sigma_prev - sigma_hat) * (0.5 * derivative + 0.5 * derivative_corr) + + if not return_dict: + return (sample_prev, derivative) + + return KarrasVeOutput( + prev_sample=sample_prev, derivative=derivative, pred_original_sample=pred_original_sample + ) + + def add_noise(self, original_samples, noise, timesteps): + raise NotImplementedError() diff --git a/diffusers/src/diffusers/schedulers/scheduling_karras_ve_flax.py b/diffusers/src/diffusers/schedulers/scheduling_karras_ve_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..08d41d006ca40c5568836f433bb4468cf4593ed8 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_karras_ve_flax.py @@ -0,0 +1,237 @@ +# Copyright 2022 NVIDIA and The HuggingFace Team. All rights reserved. +# +# 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. + + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax.numpy as jnp +from jax import random + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .scheduling_utils_flax import FlaxSchedulerMixin + + +@flax.struct.dataclass +class KarrasVeSchedulerState: + # setable values + num_inference_steps: Optional[int] = None + timesteps: Optional[jnp.ndarray] = None + schedule: Optional[jnp.ndarray] = None # sigma(t_i) + + @classmethod + def create(cls): + return cls() + + +@dataclass +class FlaxKarrasVeOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + derivative (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)` for images): + Derivative of predicted original image sample (x_0). + state (`KarrasVeSchedulerState`): the `FlaxKarrasVeScheduler` state data class. + """ + + prev_sample: jnp.ndarray + derivative: jnp.ndarray + state: KarrasVeSchedulerState + + +class FlaxKarrasVeScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + Stochastic sampling from Karras et al. [1] tailored to the Variance-Expanding (VE) models [2]. Use Algorithm 2 and + the VE column of Table 1 from [1] for reference. + + [1] Karras, Tero, et al. "Elucidating the Design Space of Diffusion-Based Generative Models." + https://arxiv.org/abs/2206.00364 [2] Song, Yang, et al. "Score-based generative modeling through stochastic + differential equations." https://arxiv.org/abs/2011.13456 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details on the parameters, see the original paper's Appendix E.: "Elucidating the Design Space of + Diffusion-Based Generative Models." https://arxiv.org/abs/2206.00364. The grid search values used to find the + optimal {s_noise, s_churn, s_min, s_max} for a specific model are described in Table 5 of the paper. + + Args: + sigma_min (`float`): minimum noise magnitude + sigma_max (`float`): maximum noise magnitude + s_noise (`float`): the amount of additional noise to counteract loss of detail during sampling. + A reasonable range is [1.000, 1.011]. + s_churn (`float`): the parameter controlling the overall amount of stochasticity. + A reasonable range is [0, 100]. + s_min (`float`): the start value of the sigma range where we add noise (enable stochasticity). + A reasonable range is [0, 10]. + s_max (`float`): the end value of the sigma range where we add noise. + A reasonable range is [0.2, 80]. + """ + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + sigma_min: float = 0.02, + sigma_max: float = 100, + s_noise: float = 1.007, + s_churn: float = 80, + s_min: float = 0.05, + s_max: float = 50, + ): + pass + + def create_state(self): + return KarrasVeSchedulerState.create() + + def set_timesteps( + self, state: KarrasVeSchedulerState, num_inference_steps: int, shape: Tuple = () + ) -> KarrasVeSchedulerState: + """ + Sets the continuous timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`KarrasVeSchedulerState`): + the `FlaxKarrasVeScheduler` state data class. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + + """ + timesteps = jnp.arange(0, num_inference_steps)[::-1].copy() + schedule = [ + ( + self.config.sigma_max**2 + * (self.config.sigma_min**2 / self.config.sigma_max**2) ** (i / (num_inference_steps - 1)) + ) + for i in timesteps + ] + + return state.replace( + num_inference_steps=num_inference_steps, + schedule=jnp.array(schedule, dtype=jnp.float32), + timesteps=timesteps, + ) + + def add_noise_to_input( + self, + state: KarrasVeSchedulerState, + sample: jnp.ndarray, + sigma: float, + key: random.KeyArray, + ) -> Tuple[jnp.ndarray, float]: + """ + Explicit Langevin-like "churn" step of adding noise to the sample according to a factor gamma_i ≥ 0 to reach a + higher noise level sigma_hat = sigma_i + gamma_i*sigma_i. + + TODO Args: + """ + if self.config.s_min <= sigma <= self.config.s_max: + gamma = min(self.config.s_churn / state.num_inference_steps, 2**0.5 - 1) + else: + gamma = 0 + + # sample eps ~ N(0, S_noise^2 * I) + key = random.split(key, num=1) + eps = self.config.s_noise * random.normal(key=key, shape=sample.shape) + sigma_hat = sigma + gamma * sigma + sample_hat = sample + ((sigma_hat**2 - sigma**2) ** 0.5 * eps) + + return sample_hat, sigma_hat + + def step( + self, + state: KarrasVeSchedulerState, + model_output: jnp.ndarray, + sigma_hat: float, + sigma_prev: float, + sample_hat: jnp.ndarray, + return_dict: bool = True, + ) -> Union[FlaxKarrasVeOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + state (`KarrasVeSchedulerState`): the `FlaxKarrasVeScheduler` state data class. + model_output (`torch.FloatTensor` or `np.ndarray`): direct output from learned diffusion model. + sigma_hat (`float`): TODO + sigma_prev (`float`): TODO + sample_hat (`torch.FloatTensor` or `np.ndarray`): TODO + return_dict (`bool`): option for returning tuple rather than FlaxKarrasVeOutput class + + Returns: + [`~schedulers.scheduling_karras_ve_flax.FlaxKarrasVeOutput`] or `tuple`: Updated sample in the diffusion + chain and derivative. [`~schedulers.scheduling_karras_ve_flax.FlaxKarrasVeOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + """ + + pred_original_sample = sample_hat + sigma_hat * model_output + derivative = (sample_hat - pred_original_sample) / sigma_hat + sample_prev = sample_hat + (sigma_prev - sigma_hat) * derivative + + if not return_dict: + return (sample_prev, derivative, state) + + return FlaxKarrasVeOutput(prev_sample=sample_prev, derivative=derivative, state=state) + + def step_correct( + self, + state: KarrasVeSchedulerState, + model_output: jnp.ndarray, + sigma_hat: float, + sigma_prev: float, + sample_hat: jnp.ndarray, + sample_prev: jnp.ndarray, + derivative: jnp.ndarray, + return_dict: bool = True, + ) -> Union[FlaxKarrasVeOutput, Tuple]: + """ + Correct the predicted sample based on the output model_output of the network. TODO complete description + + Args: + state (`KarrasVeSchedulerState`): the `FlaxKarrasVeScheduler` state data class. + model_output (`torch.FloatTensor` or `np.ndarray`): direct output from learned diffusion model. + sigma_hat (`float`): TODO + sigma_prev (`float`): TODO + sample_hat (`torch.FloatTensor` or `np.ndarray`): TODO + sample_prev (`torch.FloatTensor` or `np.ndarray`): TODO + derivative (`torch.FloatTensor` or `np.ndarray`): TODO + return_dict (`bool`): option for returning tuple rather than FlaxKarrasVeOutput class + + Returns: + prev_sample (TODO): updated sample in the diffusion chain. derivative (TODO): TODO + + """ + pred_original_sample = sample_prev + sigma_prev * model_output + derivative_corr = (sample_prev - pred_original_sample) / sigma_prev + sample_prev = sample_hat + (sigma_prev - sigma_hat) * (0.5 * derivative + 0.5 * derivative_corr) + + if not return_dict: + return (sample_prev, derivative, state) + + return FlaxKarrasVeOutput(prev_sample=sample_prev, derivative=derivative, state=state) + + def add_noise(self, state: KarrasVeSchedulerState, original_samples, noise, timesteps): + raise NotImplementedError() diff --git a/diffusers/src/diffusers/schedulers/scheduling_lms_discrete.py b/diffusers/src/diffusers/schedulers/scheduling_lms_discrete.py new file mode 100644 index 0000000000000000000000000000000000000000..88537a32df53c987c1186fe2be9977d6e2d51695 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_lms_discrete.py @@ -0,0 +1,277 @@ +# Copyright 2022 Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# 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. +import warnings +from dataclasses import dataclass +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from scipy import integrate + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin + + +@dataclass +# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->LMSDiscrete +class LMSDiscreteSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +class LMSDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Linear Multistep Scheduler for discrete beta schedules. Based on the original k-diffusion implementation by + Katherine Crowson: + https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L181 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + prediction_type: str = "epsilon", + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.concatenate([sigmas[::-1], [0.0]]).astype(np.float32) + self.sigmas = torch.from_numpy(sigmas) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = self.sigmas.max() + + # setable values + self.num_inference_steps = None + timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=float)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps) + self.derivatives = [] + self.is_scale_input_called = False + + def scale_model_input( + self, sample: torch.FloatTensor, timestep: Union[float, torch.FloatTensor] + ) -> torch.FloatTensor: + """ + Scales the denoising model input by `(sigma**2 + 1) ** 0.5` to match the K-LMS algorithm. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`float` or `torch.FloatTensor`): the current timestep in the diffusion chain + + Returns: + `torch.FloatTensor`: scaled input sample + """ + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + sample = sample / ((sigma**2 + 1) ** 0.5) + self.is_scale_input_called = True + return sample + + def get_lms_coefficient(self, order, t, current_order): + """ + Compute a linear multistep coefficient. + + Args: + order (TODO): + t (TODO): + current_order (TODO): + """ + + def lms_derivative(tau): + prod = 1.0 + for k in range(order): + if current_order == k: + continue + prod *= (tau - self.sigmas[t - k]) / (self.sigmas[t - current_order] - self.sigmas[t - k]) + return prod + + integrated_coeff = integrate.quad(lms_derivative, self.sigmas[t], self.sigmas[t + 1], epsrel=1e-4)[0] + + return integrated_coeff + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, optional): + the device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + """ + self.num_inference_steps = num_inference_steps + + timesteps = np.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps, dtype=float)[::-1].copy() + sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5) + sigmas = np.interp(timesteps, np.arange(0, len(sigmas)), sigmas) + sigmas = np.concatenate([sigmas, [0.0]]).astype(np.float32) + + self.sigmas = torch.from_numpy(sigmas).to(device=device) + if str(device).startswith("mps"): + # mps does not support float64 + self.timesteps = torch.from_numpy(timesteps).to(device, dtype=torch.float32) + else: + self.timesteps = torch.from_numpy(timesteps).to(device=device) + + self.derivatives = [] + + def step( + self, + model_output: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + sample: torch.FloatTensor, + order: int = 4, + return_dict: bool = True, + ) -> Union[LMSDiscreteSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`float`): current timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + order: coefficient for multi-step inference. + return_dict (`bool`): option for returning tuple rather than LMSDiscreteSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.LMSDiscreteSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.LMSDiscreteSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. + When returning a tuple, the first element is the sample tensor. + + """ + if not self.is_scale_input_called: + warnings.warn( + "The `scale_model_input` function should be called before `step` to ensure correct denoising. " + "See `StableDiffusionPipeline` for a usage example." + ) + + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + step_index = (self.timesteps == timestep).nonzero().item() + sigma = self.sigmas[step_index] + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + pred_original_sample = sample - sigma * model_output + elif self.config.prediction_type == "v_prediction": + # * c_out + input * c_skip + pred_original_sample = model_output * (-sigma / (sigma**2 + 1) ** 0.5) + (sample / (sigma**2 + 1)) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + # 2. Convert to an ODE derivative + derivative = (sample - pred_original_sample) / sigma + self.derivatives.append(derivative) + if len(self.derivatives) > order: + self.derivatives.pop(0) + + # 3. Compute linear multistep coefficients + order = min(step_index + 1, order) + lms_coeffs = [self.get_lms_coefficient(order, step_index, curr_order) for curr_order in range(order)] + + # 4. Compute previous sample based on the derivatives path + prev_sample = sample + sum( + coeff * derivative for coeff, derivative in zip(lms_coeffs, reversed(self.derivatives)) + ) + + if not return_dict: + return (prev_sample,) + + return LMSDiscreteSchedulerOutput(prev_sample=prev_sample, pred_original_sample=pred_original_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype) + if original_samples.device.type == "mps" and torch.is_floating_point(timesteps): + # mps does not support float64 + schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32) + timesteps = timesteps.to(original_samples.device, dtype=torch.float32) + else: + schedule_timesteps = self.timesteps.to(original_samples.device) + timesteps = timesteps.to(original_samples.device) + + step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps] + + sigma = sigmas[step_indices].flatten() + while len(sigma.shape) < len(original_samples.shape): + sigma = sigma.unsqueeze(-1) + + noisy_samples = original_samples + noise * sigma + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_lms_discrete_flax.py b/diffusers/src/diffusers/schedulers/scheduling_lms_discrete_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..e105ded997d219c4f08f227dea7445275daaf387 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_lms_discrete_flax.py @@ -0,0 +1,283 @@ +# Copyright 2022 Katherine Crowson and The HuggingFace Team. All rights reserved. +# +# 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. + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax.numpy as jnp +from scipy import integrate + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import ( + CommonSchedulerState, + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + broadcast_to_shape_from_left, +) + + +@flax.struct.dataclass +class LMSDiscreteSchedulerState: + common: CommonSchedulerState + + # setable values + init_noise_sigma: jnp.ndarray + timesteps: jnp.ndarray + sigmas: jnp.ndarray + num_inference_steps: Optional[int] = None + + # running values + derivatives: Optional[jnp.ndarray] = None + + @classmethod + def create( + cls, common: CommonSchedulerState, init_noise_sigma: jnp.ndarray, timesteps: jnp.ndarray, sigmas: jnp.ndarray + ): + return cls(common=common, init_noise_sigma=init_noise_sigma, timesteps=timesteps, sigmas=sigmas) + + +@dataclass +class FlaxLMSSchedulerOutput(FlaxSchedulerOutput): + state: LMSDiscreteSchedulerState + + +class FlaxLMSDiscreteScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + Linear Multistep Scheduler for discrete beta schedules. Based on the original k-diffusion implementation by + Katherine Crowson: + https://github.com/crowsonkb/k-diffusion/blob/481677d114f6ea445aa009cf5bd7a9cdee909e47/k_diffusion/sampling.py#L181 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear` or `scaled_linear`. + trained_betas (`jnp.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + dtype (`jnp.dtype`, *optional*, defaults to `jnp.float32`): + the `dtype` used for params and computation. + """ + + _compatibles = [e.name for e in FlaxKarrasDiffusionSchedulers] + + dtype: jnp.dtype + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[jnp.ndarray] = None, + prediction_type: str = "epsilon", + dtype: jnp.dtype = jnp.float32, + ): + self.dtype = dtype + + def create_state(self, common: Optional[CommonSchedulerState] = None) -> LMSDiscreteSchedulerState: + if common is None: + common = CommonSchedulerState.create(self) + + timesteps = jnp.arange(0, self.config.num_train_timesteps).round()[::-1] + sigmas = ((1 - common.alphas_cumprod) / common.alphas_cumprod) ** 0.5 + + # standard deviation of the initial noise distribution + init_noise_sigma = sigmas.max() + + return LMSDiscreteSchedulerState.create( + common=common, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + sigmas=sigmas, + ) + + def scale_model_input(self, state: LMSDiscreteSchedulerState, sample: jnp.ndarray, timestep: int) -> jnp.ndarray: + """ + Scales the denoising model input by `(sigma**2 + 1) ** 0.5` to match the K-LMS algorithm. + + Args: + state (`LMSDiscreteSchedulerState`): + the `FlaxLMSDiscreteScheduler` state data class instance. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + timestep (`int`): + current discrete timestep in the diffusion chain. + + Returns: + `jnp.ndarray`: scaled input sample + """ + (step_index,) = jnp.where(state.timesteps == timestep, size=1) + step_index = step_index[0] + + sigma = state.sigmas[step_index] + sample = sample / ((sigma**2 + 1) ** 0.5) + return sample + + def get_lms_coefficient(self, state: LMSDiscreteSchedulerState, order, t, current_order): + """ + Compute a linear multistep coefficient. + + Args: + order (TODO): + t (TODO): + current_order (TODO): + """ + + def lms_derivative(tau): + prod = 1.0 + for k in range(order): + if current_order == k: + continue + prod *= (tau - state.sigmas[t - k]) / (state.sigmas[t - current_order] - state.sigmas[t - k]) + return prod + + integrated_coeff = integrate.quad(lms_derivative, state.sigmas[t], state.sigmas[t + 1], epsrel=1e-4)[0] + + return integrated_coeff + + def set_timesteps( + self, state: LMSDiscreteSchedulerState, num_inference_steps: int, shape: Tuple = () + ) -> LMSDiscreteSchedulerState: + """ + Sets the timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`LMSDiscreteSchedulerState`): + the `FlaxLMSDiscreteScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + + timesteps = jnp.linspace(self.config.num_train_timesteps - 1, 0, num_inference_steps, dtype=self.dtype) + + low_idx = jnp.floor(timesteps).astype(jnp.int32) + high_idx = jnp.ceil(timesteps).astype(jnp.int32) + + frac = jnp.mod(timesteps, 1.0) + + sigmas = ((1 - state.common.alphas_cumprod) / state.common.alphas_cumprod) ** 0.5 + sigmas = (1 - frac) * sigmas[low_idx] + frac * sigmas[high_idx] + sigmas = jnp.concatenate([sigmas, jnp.array([0.0], dtype=self.dtype)]) + + timesteps = timesteps.astype(jnp.int32) + + # initial running values + derivatives = jnp.zeros((0,) + shape, dtype=self.dtype) + + return state.replace( + timesteps=timesteps, + sigmas=sigmas, + num_inference_steps=num_inference_steps, + derivatives=derivatives, + ) + + def step( + self, + state: LMSDiscreteSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + order: int = 4, + return_dict: bool = True, + ) -> Union[FlaxLMSSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + state (`LMSDiscreteSchedulerState`): the `FlaxLMSDiscreteScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + order: coefficient for multi-step inference. + return_dict (`bool`): option for returning tuple rather than FlaxLMSSchedulerOutput class + + Returns: + [`FlaxLMSSchedulerOutput`] or `tuple`: [`FlaxLMSSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + sigma = state.sigmas[timestep] + + # 1. compute predicted original sample (x_0) from sigma-scaled predicted noise + if self.config.prediction_type == "epsilon": + pred_original_sample = sample - sigma * model_output + elif self.config.prediction_type == "v_prediction": + # * c_out + input * c_skip + pred_original_sample = model_output * (-sigma / (sigma**2 + 1) ** 0.5) + (sample / (sigma**2 + 1)) + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, or `v_prediction`" + ) + + # 2. Convert to an ODE derivative + derivative = (sample - pred_original_sample) / sigma + state = state.replace(derivatives=jnp.append(state.derivatives, derivative)) + if len(state.derivatives) > order: + state = state.replace(derivatives=jnp.delete(state.derivatives, 0)) + + # 3. Compute linear multistep coefficients + order = min(timestep + 1, order) + lms_coeffs = [self.get_lms_coefficient(state, order, timestep, curr_order) for curr_order in range(order)] + + # 4. Compute previous sample based on the derivatives path + prev_sample = sample + sum( + coeff * derivative for coeff, derivative in zip(lms_coeffs, reversed(state.derivatives)) + ) + + if not return_dict: + return (prev_sample, state) + + return FlaxLMSSchedulerOutput(prev_sample=prev_sample, state=state) + + def add_noise( + self, + state: LMSDiscreteSchedulerState, + original_samples: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + sigma = state.sigmas[timesteps].flatten() + sigma = broadcast_to_shape_from_left(sigma, noise.shape) + + noisy_samples = original_samples + noise * sigma + + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_pndm.py b/diffusers/src/diffusers/schedulers/scheduling_pndm.py new file mode 100644 index 0000000000000000000000000000000000000000..065a07e955f8ed2ed8a6a915e8e10f29a0e51a62 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_pndm.py @@ -0,0 +1,423 @@ +# Copyright 2022 Zhejiang University Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/ermongroup/ddim + +import math +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class PNDMScheduler(SchedulerMixin, ConfigMixin): + """ + Pseudo numerical methods for diffusion models (PNDM) proposes using more advanced ODE integration techniques, + namely Runge-Kutta method and a linear multi-step method. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2202.09778 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + skip_prk_steps (`bool`): + allows the scheduler to skip the Runge-Kutta steps that are defined in the original paper as being required + before plms steps; defaults to `False`. + set_alpha_to_one (`bool`, default `False`): + each diffusion step uses the value of alphas product at that step and at the previous one. For the final + step there is no previous alpha. When this option is `True` the previous alpha product is fixed to `1`, + otherwise it uses the value of alpha at step 0. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion process) + or `v_prediction` (see section 2.4 https://imagen.research.google/video/paper.pdf) + steps_offset (`int`, default `0`): + an offset added to the inference steps. You can use a combination of `offset=1` and + `set_alpha_to_one=False`, to make the last step use step 0 for the previous alpha product, as done in + stable diffusion. + + """ + + _compatibles = [e.name for e in KarrasDiffusionSchedulers] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[Union[np.ndarray, List[float]]] = None, + skip_prk_steps: bool = False, + set_alpha_to_one: bool = False, + prediction_type: str = "epsilon", + steps_offset: int = 0, + ): + if trained_betas is not None: + self.betas = torch.tensor(trained_betas, dtype=torch.float32) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + + self.final_alpha_cumprod = torch.tensor(1.0) if set_alpha_to_one else self.alphas_cumprod[0] + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # For now we only support F-PNDM, i.e. the runge-kutta method + # For more information on the algorithm please take a look at the paper: https://arxiv.org/pdf/2202.09778.pdf + # mainly at formula (9), (12), (13) and the Algorithm 2. + self.pndm_order = 4 + + # running values + self.cur_model_output = 0 + self.counter = 0 + self.cur_sample = None + self.ets = [] + + # setable values + self.num_inference_steps = None + self._timesteps = np.arange(0, num_train_timesteps)[::-1].copy() + self.prk_timesteps = None + self.plms_timesteps = None + self.timesteps = None + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + + self.num_inference_steps = num_inference_steps + step_ratio = self.config.num_train_timesteps // self.num_inference_steps + # creates integer timesteps by multiplying by ratio + # casting to int to avoid issues when num_inference_step is power of 3 + self._timesteps = (np.arange(0, num_inference_steps) * step_ratio).round() + self._timesteps += self.config.steps_offset + + if self.config.skip_prk_steps: + # for some models like stable diffusion the prk steps can/should be skipped to + # produce better results. When using PNDM with `self.config.skip_prk_steps` the implementation + # is based on crowsonkb's PLMS sampler implementation: https://github.com/CompVis/latent-diffusion/pull/51 + self.prk_timesteps = np.array([]) + self.plms_timesteps = np.concatenate([self._timesteps[:-1], self._timesteps[-2:-1], self._timesteps[-1:]])[ + ::-1 + ].copy() + else: + prk_timesteps = np.array(self._timesteps[-self.pndm_order :]).repeat(2) + np.tile( + np.array([0, self.config.num_train_timesteps // num_inference_steps // 2]), self.pndm_order + ) + self.prk_timesteps = (prk_timesteps[:-1].repeat(2)[1:-1])[::-1].copy() + self.plms_timesteps = self._timesteps[:-3][ + ::-1 + ].copy() # we copy to avoid having negative strides which are not supported by torch.from_numpy + + timesteps = np.concatenate([self.prk_timesteps, self.plms_timesteps]).astype(np.int64) + self.timesteps = torch.from_numpy(timesteps).to(device) + + self.ets = [] + self.counter = 0 + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + This function calls `step_prk()` or `step_plms()` depending on the internal variable `counter`. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.SchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + if self.counter < len(self.prk_timesteps) and not self.config.skip_prk_steps: + return self.step_prk(model_output=model_output, timestep=timestep, sample=sample, return_dict=return_dict) + else: + return self.step_plms(model_output=model_output, timestep=timestep, sample=sample, return_dict=return_dict) + + def step_prk( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the Runge-Kutta method. RK takes 4 forward passes to approximate the + solution to the differential equation. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + diff_to_prev = 0 if self.counter % 2 else self.config.num_train_timesteps // self.num_inference_steps // 2 + prev_timestep = timestep - diff_to_prev + timestep = self.prk_timesteps[self.counter // 4 * 4] + + if self.counter % 4 == 0: + self.cur_model_output += 1 / 6 * model_output + self.ets.append(model_output) + self.cur_sample = sample + elif (self.counter - 1) % 4 == 0: + self.cur_model_output += 1 / 3 * model_output + elif (self.counter - 2) % 4 == 0: + self.cur_model_output += 1 / 3 * model_output + elif (self.counter - 3) % 4 == 0: + model_output = self.cur_model_output + 1 / 6 * model_output + self.cur_model_output = 0 + + # cur_sample should not be `None` + cur_sample = self.cur_sample if self.cur_sample is not None else sample + + prev_sample = self._get_prev_sample(cur_sample, timestep, prev_timestep, model_output) + self.counter += 1 + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def step_plms( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Step function propagating the sample with the linear multi-step method. This has one forward pass with multiple + times to approximate the solution. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~scheduling_utils.SchedulerOutput`] or `tuple`: [`~scheduling_utils.SchedulerOutput`] if `return_dict` is + True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if not self.config.skip_prk_steps and len(self.ets) < 3: + raise ValueError( + f"{self.__class__} can only be run AFTER scheduler has been run " + "in 'prk' mode for at least 12 iterations " + "See: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/pipeline_pndm.py " + "for more information." + ) + + prev_timestep = timestep - self.config.num_train_timesteps // self.num_inference_steps + + if self.counter != 1: + self.ets = self.ets[-3:] + self.ets.append(model_output) + else: + prev_timestep = timestep + timestep = timestep + self.config.num_train_timesteps // self.num_inference_steps + + if len(self.ets) == 1 and self.counter == 0: + model_output = model_output + self.cur_sample = sample + elif len(self.ets) == 1 and self.counter == 1: + model_output = (model_output + self.ets[-1]) / 2 + sample = self.cur_sample + self.cur_sample = None + elif len(self.ets) == 2: + model_output = (3 * self.ets[-1] - self.ets[-2]) / 2 + elif len(self.ets) == 3: + model_output = (23 * self.ets[-1] - 16 * self.ets[-2] + 5 * self.ets[-3]) / 12 + else: + model_output = (1 / 24) * (55 * self.ets[-1] - 59 * self.ets[-2] + 37 * self.ets[-3] - 9 * self.ets[-4]) + + prev_sample = self._get_prev_sample(sample, timestep, prev_timestep, model_output) + self.counter += 1 + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def scale_model_input(self, sample: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def _get_prev_sample(self, sample, timestep, prev_timestep, model_output): + # See formula (9) of PNDM paper https://arxiv.org/pdf/2202.09778.pdf + # this function computes x_(t−δ) using the formula of (9) + # Note that x_t needs to be added to both sides of the equation + + # Notation ( -> + # alpha_prod_t -> α_t + # alpha_prod_t_prev -> α_(t−δ) + # beta_prod_t -> (1 - α_t) + # beta_prod_t_prev -> (1 - α_(t−δ)) + # sample -> x_t + # model_output -> e_θ(x_t, t) + # prev_sample -> x_(t−δ) + alpha_prod_t = self.alphas_cumprod[timestep] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + if self.config.prediction_type == "v_prediction": + model_output = (alpha_prod_t**0.5) * model_output + (beta_prod_t**0.5) * sample + elif self.config.prediction_type != "epsilon": + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon` or `v_prediction`" + ) + + # corresponds to (α_(t−δ) - α_t) divided by + # denominator of x_t in formula (9) and plus 1 + # Note: (α_(t−δ) - α_t) / (sqrt(α_t) * (sqrt(α_(t−δ)) + sqr(α_t))) = + # sqrt(α_(t−δ)) / sqrt(α_t)) + sample_coeff = (alpha_prod_t_prev / alpha_prod_t) ** (0.5) + + # corresponds to denominator of e_θ(x_t, t) in formula (9) + model_output_denom_coeff = alpha_prod_t * beta_prod_t_prev ** (0.5) + ( + alpha_prod_t * beta_prod_t * alpha_prod_t_prev + ) ** (0.5) + + # full formula (9) + prev_sample = ( + sample_coeff * sample - (alpha_prod_t_prev - alpha_prod_t) * model_output / model_output_denom_coeff + ) + + return prev_sample + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.Tensor: + # Make sure alphas_cumprod and timestep have same device and dtype as original_samples + self.alphas_cumprod = self.alphas_cumprod.to(device=original_samples.device, dtype=original_samples.dtype) + timesteps = timesteps.to(original_samples.device) + + sqrt_alpha_prod = self.alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(original_samples.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - self.alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(original_samples.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_pndm_flax.py b/diffusers/src/diffusers/schedulers/scheduling_pndm_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..572da534643b134b1759d4726368c3ac29d35b93 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_pndm_flax.py @@ -0,0 +1,511 @@ +# Copyright 2022 Zhejiang University Team and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/ermongroup/ddim + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax +import jax.numpy as jnp + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import ( + CommonSchedulerState, + FlaxKarrasDiffusionSchedulers, + FlaxSchedulerMixin, + FlaxSchedulerOutput, + add_noise_common, +) + + +@flax.struct.dataclass +class PNDMSchedulerState: + common: CommonSchedulerState + final_alpha_cumprod: jnp.ndarray + + # setable values + init_noise_sigma: jnp.ndarray + timesteps: jnp.ndarray + num_inference_steps: Optional[int] = None + prk_timesteps: Optional[jnp.ndarray] = None + plms_timesteps: Optional[jnp.ndarray] = None + + # running values + cur_model_output: Optional[jnp.ndarray] = None + counter: Optional[jnp.int32] = None + cur_sample: Optional[jnp.ndarray] = None + ets: Optional[jnp.ndarray] = None + + @classmethod + def create( + cls, + common: CommonSchedulerState, + final_alpha_cumprod: jnp.ndarray, + init_noise_sigma: jnp.ndarray, + timesteps: jnp.ndarray, + ): + return cls( + common=common, + final_alpha_cumprod=final_alpha_cumprod, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + +@dataclass +class FlaxPNDMSchedulerOutput(FlaxSchedulerOutput): + state: PNDMSchedulerState + + +class FlaxPNDMScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + Pseudo numerical methods for diffusion models (PNDM) proposes using more advanced ODE integration techniques, + namely Runge-Kutta method and a linear multi-step method. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2202.09778 + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + trained_betas (`jnp.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + skip_prk_steps (`bool`): + allows the scheduler to skip the Runge-Kutta steps that are defined in the original paper as being required + before plms steps; defaults to `False`. + set_alpha_to_one (`bool`, default `False`): + each diffusion step uses the value of alphas product at that step and at the previous one. For the final + step there is no previous alpha. When this option is `True` the previous alpha product is fixed to `1`, + otherwise it uses the value of alpha at step 0. + steps_offset (`int`, default `0`): + an offset added to the inference steps. You can use a combination of `offset=1` and + `set_alpha_to_one=False`, to make the last step use step 0 for the previous alpha product, as done in + stable diffusion. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion + process), `sample` (directly predicting the noisy sample`) or `v_prediction` (see section 2.4 + https://imagen.research.google/video/paper.pdf) + dtype (`jnp.dtype`, *optional*, defaults to `jnp.float32`): + the `dtype` used for params and computation. + """ + + _compatibles = [e.name for e in FlaxKarrasDiffusionSchedulers] + + dtype: jnp.dtype + pndm_order: int + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + trained_betas: Optional[jnp.ndarray] = None, + skip_prk_steps: bool = False, + set_alpha_to_one: bool = False, + steps_offset: int = 0, + prediction_type: str = "epsilon", + dtype: jnp.dtype = jnp.float32, + ): + self.dtype = dtype + + # For now we only support F-PNDM, i.e. the runge-kutta method + # For more information on the algorithm please take a look at the paper: https://arxiv.org/pdf/2202.09778.pdf + # mainly at formula (9), (12), (13) and the Algorithm 2. + self.pndm_order = 4 + + def create_state(self, common: Optional[CommonSchedulerState] = None) -> PNDMSchedulerState: + if common is None: + common = CommonSchedulerState.create(self) + + # At every step in ddim, we are looking into the previous alphas_cumprod + # For the final step, there is no previous alphas_cumprod because we are already at 0 + # `set_alpha_to_one` decides whether we set this parameter simply to one or + # whether we use the final alpha of the "non-previous" one. + final_alpha_cumprod = ( + jnp.array(1.0, dtype=self.dtype) if self.config.set_alpha_to_one else common.alphas_cumprod[0] + ) + + # standard deviation of the initial noise distribution + init_noise_sigma = jnp.array(1.0, dtype=self.dtype) + + timesteps = jnp.arange(0, self.config.num_train_timesteps).round()[::-1] + + return PNDMSchedulerState.create( + common=common, + final_alpha_cumprod=final_alpha_cumprod, + init_noise_sigma=init_noise_sigma, + timesteps=timesteps, + ) + + def set_timesteps(self, state: PNDMSchedulerState, num_inference_steps: int, shape: Tuple) -> PNDMSchedulerState: + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`PNDMSchedulerState`): + the `FlaxPNDMScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + shape (`Tuple`): + the shape of the samples to be generated. + """ + + step_ratio = self.config.num_train_timesteps // num_inference_steps + # creates integer timesteps by multiplying by ratio + # rounding to avoid issues when num_inference_step is power of 3 + _timesteps = (jnp.arange(0, num_inference_steps) * step_ratio).round() + self.config.steps_offset + + if self.config.skip_prk_steps: + # for some models like stable diffusion the prk steps can/should be skipped to + # produce better results. When using PNDM with `self.config.skip_prk_steps` the implementation + # is based on crowsonkb's PLMS sampler implementation: https://github.com/CompVis/latent-diffusion/pull/51 + + prk_timesteps = jnp.array([], dtype=jnp.int32) + plms_timesteps = jnp.concatenate([_timesteps[:-1], _timesteps[-2:-1], _timesteps[-1:]])[::-1] + + else: + prk_timesteps = _timesteps[-self.pndm_order :].repeat(2) + jnp.tile( + jnp.array([0, self.config.num_train_timesteps // num_inference_steps // 2], dtype=jnp.int32), + self.pndm_order, + ) + + prk_timesteps = (prk_timesteps[:-1].repeat(2)[1:-1])[::-1] + plms_timesteps = _timesteps[:-3][::-1] + + timesteps = jnp.concatenate([prk_timesteps, plms_timesteps]) + + # initial running values + + cur_model_output = jnp.zeros(shape, dtype=self.dtype) + counter = jnp.int32(0) + cur_sample = jnp.zeros(shape, dtype=self.dtype) + ets = jnp.zeros((4,) + shape, dtype=self.dtype) + + return state.replace( + timesteps=timesteps, + num_inference_steps=num_inference_steps, + prk_timesteps=prk_timesteps, + plms_timesteps=plms_timesteps, + cur_model_output=cur_model_output, + counter=counter, + cur_sample=cur_sample, + ets=ets, + ) + + def scale_model_input( + self, state: PNDMSchedulerState, sample: jnp.ndarray, timestep: Optional[int] = None + ) -> jnp.ndarray: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + sample (`jnp.ndarray`): input sample + timestep (`int`, optional): current timestep + + Returns: + `jnp.ndarray`: scaled input sample + """ + return sample + + def step( + self, + state: PNDMSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + return_dict: bool = True, + ) -> Union[FlaxPNDMSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + This function calls `step_prk()` or `step_plms()` depending on the internal variable `counter`. + + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than FlaxPNDMSchedulerOutput class + + Returns: + [`FlaxPNDMSchedulerOutput`] or `tuple`: [`FlaxPNDMSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if self.config.skip_prk_steps: + prev_sample, state = self.step_plms(state, model_output, timestep, sample) + else: + prk_prev_sample, prk_state = self.step_prk(state, model_output, timestep, sample) + plms_prev_sample, plms_state = self.step_plms(state, model_output, timestep, sample) + + cond = state.counter < len(state.prk_timesteps) + + prev_sample = jax.lax.select(cond, prk_prev_sample, plms_prev_sample) + + state = state.replace( + cur_model_output=jax.lax.select(cond, prk_state.cur_model_output, plms_state.cur_model_output), + ets=jax.lax.select(cond, prk_state.ets, plms_state.ets), + cur_sample=jax.lax.select(cond, prk_state.cur_sample, plms_state.cur_sample), + counter=jax.lax.select(cond, prk_state.counter, plms_state.counter), + ) + + if not return_dict: + return (prev_sample, state) + + return FlaxPNDMSchedulerOutput(prev_sample=prev_sample, state=state) + + def step_prk( + self, + state: PNDMSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + ) -> Union[FlaxPNDMSchedulerOutput, Tuple]: + """ + Step function propagating the sample with the Runge-Kutta method. RK takes 4 forward passes to approximate the + solution to the differential equation. + + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than FlaxPNDMSchedulerOutput class + + Returns: + [`FlaxPNDMSchedulerOutput`] or `tuple`: [`FlaxPNDMSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + diff_to_prev = jnp.where( + state.counter % 2, 0, self.config.num_train_timesteps // state.num_inference_steps // 2 + ) + prev_timestep = timestep - diff_to_prev + timestep = state.prk_timesteps[state.counter // 4 * 4] + + model_output = jax.lax.select( + (state.counter % 4) != 3, + model_output, # remainder 0, 1, 2 + state.cur_model_output + 1 / 6 * model_output, # remainder 3 + ) + + state = state.replace( + cur_model_output=jax.lax.select_n( + state.counter % 4, + state.cur_model_output + 1 / 6 * model_output, # remainder 0 + state.cur_model_output + 1 / 3 * model_output, # remainder 1 + state.cur_model_output + 1 / 3 * model_output, # remainder 2 + jnp.zeros_like(state.cur_model_output), # remainder 3 + ), + ets=jax.lax.select( + (state.counter % 4) == 0, + state.ets.at[0:3].set(state.ets[1:4]).at[3].set(model_output), # remainder 0 + state.ets, # remainder 1, 2, 3 + ), + cur_sample=jax.lax.select( + (state.counter % 4) == 0, + sample, # remainder 0 + state.cur_sample, # remainder 1, 2, 3 + ), + ) + + cur_sample = state.cur_sample + prev_sample = self._get_prev_sample(state, cur_sample, timestep, prev_timestep, model_output) + state = state.replace(counter=state.counter + 1) + + return (prev_sample, state) + + def step_plms( + self, + state: PNDMSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + ) -> Union[FlaxPNDMSchedulerOutput, Tuple]: + """ + Step function propagating the sample with the linear multi-step method. This has one forward pass with multiple + times to approximate the solution. + + Args: + state (`PNDMSchedulerState`): the `FlaxPNDMScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + return_dict (`bool`): option for returning tuple rather than FlaxPNDMSchedulerOutput class + + Returns: + [`FlaxPNDMSchedulerOutput`] or `tuple`: [`FlaxPNDMSchedulerOutput`] if `return_dict` is True, otherwise a + `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + + if state.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + # NOTE: There is no way to check in the jitted runtime if the prk mode was ran before + + prev_timestep = timestep - self.config.num_train_timesteps // state.num_inference_steps + prev_timestep = jnp.where(prev_timestep > 0, prev_timestep, 0) + + # Reference: + # if state.counter != 1: + # state.ets.append(model_output) + # else: + # prev_timestep = timestep + # timestep = timestep + self.config.num_train_timesteps // state.num_inference_steps + + prev_timestep = jnp.where(state.counter == 1, timestep, prev_timestep) + timestep = jnp.where( + state.counter == 1, timestep + self.config.num_train_timesteps // state.num_inference_steps, timestep + ) + + # Reference: + # if len(state.ets) == 1 and state.counter == 0: + # model_output = model_output + # state.cur_sample = sample + # elif len(state.ets) == 1 and state.counter == 1: + # model_output = (model_output + state.ets[-1]) / 2 + # sample = state.cur_sample + # state.cur_sample = None + # elif len(state.ets) == 2: + # model_output = (3 * state.ets[-1] - state.ets[-2]) / 2 + # elif len(state.ets) == 3: + # model_output = (23 * state.ets[-1] - 16 * state.ets[-2] + 5 * state.ets[-3]) / 12 + # else: + # model_output = (1 / 24) * (55 * state.ets[-1] - 59 * state.ets[-2] + 37 * state.ets[-3] - 9 * state.ets[-4]) + + state = state.replace( + ets=jax.lax.select( + state.counter != 1, + state.ets.at[0:3].set(state.ets[1:4]).at[3].set(model_output), # counter != 1 + state.ets, # counter 1 + ), + cur_sample=jax.lax.select( + state.counter != 1, + sample, # counter != 1 + state.cur_sample, # counter 1 + ), + ) + + state = state.replace( + cur_model_output=jax.lax.select_n( + jnp.clip(state.counter, 0, 4), + model_output, # counter 0 + (model_output + state.ets[-1]) / 2, # counter 1 + (3 * state.ets[-1] - state.ets[-2]) / 2, # counter 2 + (23 * state.ets[-1] - 16 * state.ets[-2] + 5 * state.ets[-3]) / 12, # counter 3 + (1 / 24) + * (55 * state.ets[-1] - 59 * state.ets[-2] + 37 * state.ets[-3] - 9 * state.ets[-4]), # counter >= 4 + ), + ) + + sample = state.cur_sample + model_output = state.cur_model_output + prev_sample = self._get_prev_sample(state, sample, timestep, prev_timestep, model_output) + state = state.replace(counter=state.counter + 1) + + return (prev_sample, state) + + def _get_prev_sample(self, state: PNDMSchedulerState, sample, timestep, prev_timestep, model_output): + # See formula (9) of PNDM paper https://arxiv.org/pdf/2202.09778.pdf + # this function computes x_(t−δ) using the formula of (9) + # Note that x_t needs to be added to both sides of the equation + + # Notation ( -> + # alpha_prod_t -> α_t + # alpha_prod_t_prev -> α_(t−δ) + # beta_prod_t -> (1 - α_t) + # beta_prod_t_prev -> (1 - α_(t−δ)) + # sample -> x_t + # model_output -> e_θ(x_t, t) + # prev_sample -> x_(t−δ) + alpha_prod_t = state.common.alphas_cumprod[timestep] + alpha_prod_t_prev = jnp.where( + prev_timestep >= 0, state.common.alphas_cumprod[prev_timestep], state.final_alpha_cumprod + ) + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + if self.config.prediction_type == "v_prediction": + model_output = (alpha_prod_t**0.5) * model_output + (beta_prod_t**0.5) * sample + elif self.config.prediction_type != "epsilon": + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon` or `v_prediction`" + ) + + # corresponds to (α_(t−δ) - α_t) divided by + # denominator of x_t in formula (9) and plus 1 + # Note: (α_(t−δ) - α_t) / (sqrt(α_t) * (sqrt(α_(t−δ)) + sqr(α_t))) = + # sqrt(α_(t−δ)) / sqrt(α_t)) + sample_coeff = (alpha_prod_t_prev / alpha_prod_t) ** (0.5) + + # corresponds to denominator of e_θ(x_t, t) in formula (9) + model_output_denom_coeff = alpha_prod_t * beta_prod_t_prev ** (0.5) + ( + alpha_prod_t * beta_prod_t * alpha_prod_t_prev + ) ** (0.5) + + # full formula (9) + prev_sample = ( + sample_coeff * sample - (alpha_prod_t_prev - alpha_prod_t) * model_output / model_output_denom_coeff + ) + + return prev_sample + + def add_noise( + self, + state: PNDMSchedulerState, + original_samples: jnp.ndarray, + noise: jnp.ndarray, + timesteps: jnp.ndarray, + ) -> jnp.ndarray: + return add_noise_common(state.common, original_samples, noise, timesteps) + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_repaint.py b/diffusers/src/diffusers/schedulers/scheduling_repaint.py new file mode 100644 index 0000000000000000000000000000000000000000..d72072356f31bff1e49acf8a11d2f80a95b2e25f --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_repaint.py @@ -0,0 +1,328 @@ +# Copyright 2022 ETH Zurich Computer Vision Lab and The HuggingFace Team. All rights reserved. +# +# 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. + +import math +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import SchedulerMixin + + +@dataclass +class RePaintSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from + the current timestep. `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: torch.FloatTensor + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class RePaintScheduler(SchedulerMixin, ConfigMixin): + """ + RePaint is a schedule for DDPM inpainting inside a given mask. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/pdf/2201.09865.pdf + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + beta_start (`float`): the starting `beta` value of inference. + beta_end (`float`): the final `beta` value. + beta_schedule (`str`): + the beta schedule, a mapping from a beta range to a sequence of betas for stepping the model. Choose from + `linear`, `scaled_linear`, or `squaredcos_cap_v2`. + eta (`float`): + The weight of noise for added noise in a diffusion step. Its value is between 0.0 and 1.0 -0.0 is DDIM and + 1.0 is DDPM scheduler respectively. + trained_betas (`np.ndarray`, optional): + option to pass an array of betas directly to the constructor to bypass `beta_start`, `beta_end` etc. + variance_type (`str`): + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small`, + `fixed_small_log`, `fixed_large`, `fixed_large_log`, `learned` or `learned_range`. + clip_sample (`bool`, default `True`): + option to clip predicted sample between -1 and 1 for numerical stability. + + """ + + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + beta_start: float = 0.0001, + beta_end: float = 0.02, + beta_schedule: str = "linear", + eta: float = 0.0, + trained_betas: Optional[np.ndarray] = None, + clip_sample: bool = True, + ): + if trained_betas is not None: + self.betas = torch.from_numpy(trained_betas) + elif beta_schedule == "linear": + self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32) + elif beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + self.betas = ( + torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2 + ) + elif beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + self.betas = betas_for_alpha_bar(num_train_timesteps) + elif beta_schedule == "sigmoid": + # GeoDiff sigmoid schedule + betas = torch.linspace(-6, 6, num_train_timesteps) + self.betas = torch.sigmoid(betas) * (beta_end - beta_start) + beta_start + else: + raise NotImplementedError(f"{beta_schedule} does is not implemented for {self.__class__}") + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.one = torch.tensor(1.0) + + self.final_alpha_cumprod = torch.tensor(1.0) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # setable values + self.num_inference_steps = None + self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy()) + + self.eta = eta + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def set_timesteps( + self, + num_inference_steps: int, + jump_length: int = 10, + jump_n_sample: int = 10, + device: Union[str, torch.device] = None, + ): + num_inference_steps = min(self.config.num_train_timesteps, num_inference_steps) + self.num_inference_steps = num_inference_steps + + timesteps = [] + + jumps = {} + for j in range(0, num_inference_steps - jump_length, jump_length): + jumps[j] = jump_n_sample - 1 + + t = num_inference_steps + while t >= 1: + t = t - 1 + timesteps.append(t) + + if jumps.get(t, 0) > 0: + jumps[t] = jumps[t] - 1 + for _ in range(jump_length): + t = t + 1 + timesteps.append(t) + + timesteps = np.array(timesteps) * (self.config.num_train_timesteps // self.num_inference_steps) + self.timesteps = torch.from_numpy(timesteps).to(device) + + def _get_variance(self, t): + prev_timestep = t - self.config.num_train_timesteps // self.num_inference_steps + + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + # For t > 0, compute predicted variance βt (see formula (6) and (7) from + # https://arxiv.org/pdf/2006.11239.pdf) and sample from it to get + # previous sample x_{t-1} ~ N(pred_prev_sample, variance) == add + # variance to pred_sample + # Is equivalent to formula (16) in https://arxiv.org/pdf/2010.02502.pdf + # without eta. + # variance = (1 - alpha_prod_t_prev) / (1 - alpha_prod_t) * self.betas[t] + variance = (beta_prod_t_prev / beta_prod_t) * (1 - alpha_prod_t / alpha_prod_t_prev) + + return variance + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + original_image: torch.FloatTensor, + mask: torch.FloatTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[RePaintSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned + diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + original_image (`torch.FloatTensor`): + the original image to inpaint on. + mask (`torch.FloatTensor`): + the mask where 0.0 values define which part of the original image to inpaint (change). + generator (`torch.Generator`, *optional*): random number generator. + return_dict (`bool`): option for returning tuple rather than + DDPMSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.RePaintSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.RePaintSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + t = timestep + prev_timestep = timestep - self.config.num_train_timesteps // self.num_inference_steps + + # 1. compute alphas, betas + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + beta_prod_t = 1 - alpha_prod_t + + # 2. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf + pred_original_sample = (sample - beta_prod_t**0.5 * model_output) / alpha_prod_t**0.5 + + # 3. Clip "predicted x_0" + if self.config.clip_sample: + pred_original_sample = torch.clamp(pred_original_sample, -1, 1) + + # We choose to follow RePaint Algorithm 1 to get x_{t-1}, however we + # substitute formula (7) in the algorithm coming from DDPM paper + # (formula (4) Algorithm 2 - Sampling) with formula (12) from DDIM paper. + # DDIM schedule gives the same results as DDPM with eta = 1.0 + # Noise is being reused in 7. and 8., but no impact on quality has + # been observed. + + # 5. Add noise + device = model_output.device + noise = randn_tensor(model_output.shape, generator=generator, device=device, dtype=model_output.dtype) + std_dev_t = self.eta * self._get_variance(timestep) ** 0.5 + + variance = 0 + if t > 0 and self.eta > 0: + variance = std_dev_t * noise + + # 6. compute "direction pointing to x_t" of formula (12) + # from https://arxiv.org/pdf/2010.02502.pdf + pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** 0.5 * model_output + + # 7. compute x_{t-1} of formula (12) from https://arxiv.org/pdf/2010.02502.pdf + prev_unknown_part = alpha_prod_t_prev**0.5 * pred_original_sample + pred_sample_direction + variance + + # 8. Algorithm 1 Line 5 https://arxiv.org/pdf/2201.09865.pdf + prev_known_part = (alpha_prod_t_prev**0.5) * original_image + ((1 - alpha_prod_t_prev) ** 0.5) * noise + + # 9. Algorithm 1 Line 8 https://arxiv.org/pdf/2201.09865.pdf + pred_prev_sample = mask * prev_known_part + (1.0 - mask) * prev_unknown_part + + if not return_dict: + return ( + pred_prev_sample, + pred_original_sample, + ) + + return RePaintSchedulerOutput(prev_sample=pred_prev_sample, pred_original_sample=pred_original_sample) + + def undo_step(self, sample, timestep, generator=None): + n = self.config.num_train_timesteps // self.num_inference_steps + + for i in range(n): + beta = self.betas[timestep + i] + if sample.device.type == "mps": + # randn does not work reproducibly on mps + noise = randn_tensor(sample.shape, dtype=sample.dtype, generator=generator) + noise = noise.to(sample.device) + else: + noise = randn_tensor(sample.shape, generator=generator, device=sample.device, dtype=sample.dtype) + + # 10. Algorithm 1 Line 10 https://arxiv.org/pdf/2201.09865.pdf + sample = (1 - beta) ** 0.5 * sample + beta**0.5 * noise + + return sample + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.IntTensor, + ) -> torch.FloatTensor: + raise NotImplementedError("Use `DDPMScheduler.add_noise()` to train for sampling with RePaint.") + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_sde_ve.py b/diffusers/src/diffusers/schedulers/scheduling_sde_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..7a190370ee817c6c2565bc1715a252cb1a472764 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_sde_ve.py @@ -0,0 +1,281 @@ +# Copyright 2022 Google Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/yang-song/score_sde_pytorch + +import math +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import SchedulerMixin, SchedulerOutput + + +@dataclass +class SdeVeOutput(BaseOutput): + """ + Output class for the ScoreSdeVeScheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + prev_sample_mean (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Mean averaged `prev_sample`. Same as `prev_sample`, only mean-averaged over previous timesteps. + """ + + prev_sample: torch.FloatTensor + prev_sample_mean: torch.FloatTensor + + +class ScoreSdeVeScheduler(SchedulerMixin, ConfigMixin): + """ + The variance exploding stochastic differential equation (SDE) scheduler. + + For more information, see the original paper: https://arxiv.org/abs/2011.13456 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + snr (`float`): + coefficient weighting the step from the model_output sample (from the network) to the random noise. + sigma_min (`float`): + initial noise scale for sigma sequence in sampling procedure. The minimum sigma should mirror the + distribution of the data. + sigma_max (`float`): maximum value used for the range of continuous timesteps passed into the model. + sampling_eps (`float`): the end value of sampling, where timesteps decrease progressively from 1 to + epsilon. + correct_steps (`int`): number of correction steps performed on a produced sample. + """ + + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 2000, + snr: float = 0.15, + sigma_min: float = 0.01, + sigma_max: float = 1348.0, + sampling_eps: float = 1e-5, + correct_steps: int = 1, + ): + # standard deviation of the initial noise distribution + self.init_noise_sigma = sigma_max + + # setable values + self.timesteps = None + + self.set_sigmas(num_train_timesteps, sigma_min, sigma_max, sampling_eps) + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def set_timesteps( + self, num_inference_steps: int, sampling_eps: float = None, device: Union[str, torch.device] = None + ): + """ + Sets the continuous timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + sampling_eps (`float`, optional): final timestep value (overrides value given at Scheduler instantiation). + + """ + sampling_eps = sampling_eps if sampling_eps is not None else self.config.sampling_eps + + self.timesteps = torch.linspace(1, sampling_eps, num_inference_steps, device=device) + + def set_sigmas( + self, num_inference_steps: int, sigma_min: float = None, sigma_max: float = None, sampling_eps: float = None + ): + """ + Sets the noise scales used for the diffusion chain. Supporting function to be run before inference. + + The sigmas control the weight of the `drift` and `diffusion` components of sample update. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + sigma_min (`float`, optional): + initial noise scale value (overrides value given at Scheduler instantiation). + sigma_max (`float`, optional): final noise scale value (overrides value given at Scheduler instantiation). + sampling_eps (`float`, optional): final timestep value (overrides value given at Scheduler instantiation). + + """ + sigma_min = sigma_min if sigma_min is not None else self.config.sigma_min + sigma_max = sigma_max if sigma_max is not None else self.config.sigma_max + sampling_eps = sampling_eps if sampling_eps is not None else self.config.sampling_eps + if self.timesteps is None: + self.set_timesteps(num_inference_steps, sampling_eps) + + self.sigmas = sigma_min * (sigma_max / sigma_min) ** (self.timesteps / sampling_eps) + self.discrete_sigmas = torch.exp(torch.linspace(math.log(sigma_min), math.log(sigma_max), num_inference_steps)) + self.sigmas = torch.tensor([sigma_min * (sigma_max / sigma_min) ** t for t in self.timesteps]) + + def get_adjacent_sigma(self, timesteps, t): + return torch.where( + timesteps == 0, + torch.zeros_like(t.to(timesteps.device)), + self.discrete_sigmas[timesteps - 1].to(timesteps.device), + ) + + def step_pred( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SdeVeOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~schedulers.scheduling_sde_ve.SdeVeOutput`] or `tuple`: [`~schedulers.scheduling_sde_ve.SdeVeOutput`] if + `return_dict` is True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.timesteps is None: + raise ValueError( + "`self.timesteps` is not set, you need to run 'set_timesteps' after creating the scheduler" + ) + + timestep = timestep * torch.ones( + sample.shape[0], device=sample.device + ) # torch.repeat_interleave(timestep, sample.shape[0]) + timesteps = (timestep * (len(self.timesteps) - 1)).long() + + # mps requires indices to be in the same device, so we use cpu as is the default with cuda + timesteps = timesteps.to(self.discrete_sigmas.device) + + sigma = self.discrete_sigmas[timesteps].to(sample.device) + adjacent_sigma = self.get_adjacent_sigma(timesteps, timestep).to(sample.device) + drift = torch.zeros_like(sample) + diffusion = (sigma**2 - adjacent_sigma**2) ** 0.5 + + # equation 6 in the paper: the model_output modeled by the network is grad_x log pt(x) + # also equation 47 shows the analog from SDE models to ancestral sampling methods + diffusion = diffusion.flatten() + while len(diffusion.shape) < len(sample.shape): + diffusion = diffusion.unsqueeze(-1) + drift = drift - diffusion**2 * model_output + + # equation 6: sample noise for the diffusion term of + noise = randn_tensor( + sample.shape, layout=sample.layout, generator=generator, device=sample.device, dtype=sample.dtype + ) + prev_sample_mean = sample - drift # subtract because `dt` is a small negative timestep + # TODO is the variable diffusion the correct scaling term for the noise? + prev_sample = prev_sample_mean + diffusion * noise # add impact of diffusion field g + + if not return_dict: + return (prev_sample, prev_sample_mean) + + return SdeVeOutput(prev_sample=prev_sample, prev_sample_mean=prev_sample_mean) + + def step_correct( + self, + model_output: torch.FloatTensor, + sample: torch.FloatTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[SchedulerOutput, Tuple]: + """ + Correct the predicted sample based on the output model_output of the network. This is often run repeatedly + after making the prediction for the previous timestep. + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than SchedulerOutput class + + Returns: + [`~schedulers.scheduling_sde_ve.SdeVeOutput`] or `tuple`: [`~schedulers.scheduling_sde_ve.SdeVeOutput`] if + `return_dict` is True, otherwise a `tuple`. When returning a tuple, the first element is the sample tensor. + + """ + if self.timesteps is None: + raise ValueError( + "`self.timesteps` is not set, you need to run 'set_timesteps' after creating the scheduler" + ) + + # For small batch sizes, the paper "suggest replacing norm(z) with sqrt(d), where d is the dim. of z" + # sample noise for correction + noise = randn_tensor(sample.shape, layout=sample.layout, generator=generator).to(sample.device) + + # compute step size from the model_output, the noise, and the snr + grad_norm = torch.norm(model_output.reshape(model_output.shape[0], -1), dim=-1).mean() + noise_norm = torch.norm(noise.reshape(noise.shape[0], -1), dim=-1).mean() + step_size = (self.config.snr * noise_norm / grad_norm) ** 2 * 2 + step_size = step_size * torch.ones(sample.shape[0]).to(sample.device) + # self.repeat_scalar(step_size, sample.shape[0]) + + # compute corrected sample: model_output term and noise term + step_size = step_size.flatten() + while len(step_size.shape) < len(sample.shape): + step_size = step_size.unsqueeze(-1) + prev_sample_mean = sample + step_size * model_output + prev_sample = prev_sample_mean + ((step_size * 2) ** 0.5) * noise + + if not return_dict: + return (prev_sample,) + + return SchedulerOutput(prev_sample=prev_sample) + + def add_noise( + self, + original_samples: torch.FloatTensor, + noise: torch.FloatTensor, + timesteps: torch.FloatTensor, + ) -> torch.FloatTensor: + # Make sure sigmas and timesteps have the same device and dtype as original_samples + timesteps = timesteps.to(original_samples.device) + sigmas = self.discrete_sigmas.to(original_samples.device)[timesteps] + noise = torch.randn_like(original_samples) * sigmas[:, None, None, None] + noisy_samples = noise + original_samples + return noisy_samples + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_sde_ve_flax.py b/diffusers/src/diffusers/schedulers/scheduling_sde_ve_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..d1f762bc90c471d6bbc7f33e5854d014b1e25667 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_sde_ve_flax.py @@ -0,0 +1,276 @@ +# Copyright 2022 Google Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/yang-song/score_sde_pytorch + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import flax +import jax.numpy as jnp +from jax import random + +from ..configuration_utils import ConfigMixin, register_to_config +from .scheduling_utils_flax import FlaxSchedulerMixin, FlaxSchedulerOutput, broadcast_to_shape_from_left + + +@flax.struct.dataclass +class ScoreSdeVeSchedulerState: + # setable values + timesteps: Optional[jnp.ndarray] = None + discrete_sigmas: Optional[jnp.ndarray] = None + sigmas: Optional[jnp.ndarray] = None + + @classmethod + def create(cls): + return cls() + + +@dataclass +class FlaxSdeVeOutput(FlaxSchedulerOutput): + """ + Output class for the ScoreSdeVeScheduler's step function output. + + Args: + state (`ScoreSdeVeSchedulerState`): + prev_sample (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + prev_sample_mean (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)` for images): + Mean averaged `prev_sample`. Same as `prev_sample`, only mean-averaged over previous timesteps. + """ + + state: ScoreSdeVeSchedulerState + prev_sample: jnp.ndarray + prev_sample_mean: Optional[jnp.ndarray] = None + + +class FlaxScoreSdeVeScheduler(FlaxSchedulerMixin, ConfigMixin): + """ + The variance exploding stochastic differential equation (SDE) scheduler. + + For more information, see the original paper: https://arxiv.org/abs/2011.13456 + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + snr (`float`): + coefficient weighting the step from the model_output sample (from the network) to the random noise. + sigma_min (`float`): + initial noise scale for sigma sequence in sampling procedure. The minimum sigma should mirror the + distribution of the data. + sigma_max (`float`): maximum value used for the range of continuous timesteps passed into the model. + sampling_eps (`float`): the end value of sampling, where timesteps decrease progressively from 1 to + epsilon. + correct_steps (`int`): number of correction steps performed on a produced sample. + """ + + @property + def has_state(self): + return True + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 2000, + snr: float = 0.15, + sigma_min: float = 0.01, + sigma_max: float = 1348.0, + sampling_eps: float = 1e-5, + correct_steps: int = 1, + ): + pass + + def create_state(self): + state = ScoreSdeVeSchedulerState.create() + return self.set_sigmas( + state, + self.config.num_train_timesteps, + self.config.sigma_min, + self.config.sigma_max, + self.config.sampling_eps, + ) + + def set_timesteps( + self, state: ScoreSdeVeSchedulerState, num_inference_steps: int, shape: Tuple = (), sampling_eps: float = None + ) -> ScoreSdeVeSchedulerState: + """ + Sets the continuous timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + state (`ScoreSdeVeSchedulerState`): the `FlaxScoreSdeVeScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + sampling_eps (`float`, optional): final timestep value (overrides value given at Scheduler instantiation). + + """ + sampling_eps = sampling_eps if sampling_eps is not None else self.config.sampling_eps + + timesteps = jnp.linspace(1, sampling_eps, num_inference_steps) + return state.replace(timesteps=timesteps) + + def set_sigmas( + self, + state: ScoreSdeVeSchedulerState, + num_inference_steps: int, + sigma_min: float = None, + sigma_max: float = None, + sampling_eps: float = None, + ) -> ScoreSdeVeSchedulerState: + """ + Sets the noise scales used for the diffusion chain. Supporting function to be run before inference. + + The sigmas control the weight of the `drift` and `diffusion` components of sample update. + + Args: + state (`ScoreSdeVeSchedulerState`): the `FlaxScoreSdeVeScheduler` state data class instance. + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + sigma_min (`float`, optional): + initial noise scale value (overrides value given at Scheduler instantiation). + sigma_max (`float`, optional): final noise scale value (overrides value given at Scheduler instantiation). + sampling_eps (`float`, optional): final timestep value (overrides value given at Scheduler instantiation). + """ + sigma_min = sigma_min if sigma_min is not None else self.config.sigma_min + sigma_max = sigma_max if sigma_max is not None else self.config.sigma_max + sampling_eps = sampling_eps if sampling_eps is not None else self.config.sampling_eps + if state.timesteps is None: + state = self.set_timesteps(state, num_inference_steps, sampling_eps) + + discrete_sigmas = jnp.exp(jnp.linspace(jnp.log(sigma_min), jnp.log(sigma_max), num_inference_steps)) + sigmas = jnp.array([sigma_min * (sigma_max / sigma_min) ** t for t in state.timesteps]) + + return state.replace(discrete_sigmas=discrete_sigmas, sigmas=sigmas) + + def get_adjacent_sigma(self, state, timesteps, t): + return jnp.where(timesteps == 0, jnp.zeros_like(t), state.discrete_sigmas[timesteps - 1]) + + def step_pred( + self, + state: ScoreSdeVeSchedulerState, + model_output: jnp.ndarray, + timestep: int, + sample: jnp.ndarray, + key: random.KeyArray, + return_dict: bool = True, + ) -> Union[FlaxSdeVeOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + state (`ScoreSdeVeSchedulerState`): the `FlaxScoreSdeVeScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than FlaxSdeVeOutput class + + Returns: + [`FlaxSdeVeOutput`] or `tuple`: [`FlaxSdeVeOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + if state.timesteps is None: + raise ValueError( + "`state.timesteps` is not set, you need to run 'set_timesteps' after creating the scheduler" + ) + + timestep = timestep * jnp.ones( + sample.shape[0], + ) + timesteps = (timestep * (len(state.timesteps) - 1)).long() + + sigma = state.discrete_sigmas[timesteps] + adjacent_sigma = self.get_adjacent_sigma(state, timesteps, timestep) + drift = jnp.zeros_like(sample) + diffusion = (sigma**2 - adjacent_sigma**2) ** 0.5 + + # equation 6 in the paper: the model_output modeled by the network is grad_x log pt(x) + # also equation 47 shows the analog from SDE models to ancestral sampling methods + diffusion = diffusion.flatten() + diffusion = broadcast_to_shape_from_left(diffusion, sample.shape) + drift = drift - diffusion**2 * model_output + + # equation 6: sample noise for the diffusion term of + key = random.split(key, num=1) + noise = random.normal(key=key, shape=sample.shape) + prev_sample_mean = sample - drift # subtract because `dt` is a small negative timestep + # TODO is the variable diffusion the correct scaling term for the noise? + prev_sample = prev_sample_mean + diffusion * noise # add impact of diffusion field g + + if not return_dict: + return (prev_sample, prev_sample_mean, state) + + return FlaxSdeVeOutput(prev_sample=prev_sample, prev_sample_mean=prev_sample_mean, state=state) + + def step_correct( + self, + state: ScoreSdeVeSchedulerState, + model_output: jnp.ndarray, + sample: jnp.ndarray, + key: random.KeyArray, + return_dict: bool = True, + ) -> Union[FlaxSdeVeOutput, Tuple]: + """ + Correct the predicted sample based on the output model_output of the network. This is often run repeatedly + after making the prediction for the previous timestep. + + Args: + state (`ScoreSdeVeSchedulerState`): the `FlaxScoreSdeVeScheduler` state data class instance. + model_output (`jnp.ndarray`): direct output from learned diffusion model. + sample (`jnp.ndarray`): + current instance of sample being created by diffusion process. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than FlaxSdeVeOutput class + + Returns: + [`FlaxSdeVeOutput`] or `tuple`: [`FlaxSdeVeOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + if state.timesteps is None: + raise ValueError( + "`state.timesteps` is not set, you need to run 'set_timesteps' after creating the scheduler" + ) + + # For small batch sizes, the paper "suggest replacing norm(z) with sqrt(d), where d is the dim. of z" + # sample noise for correction + key = random.split(key, num=1) + noise = random.normal(key=key, shape=sample.shape) + + # compute step size from the model_output, the noise, and the snr + grad_norm = jnp.linalg.norm(model_output) + noise_norm = jnp.linalg.norm(noise) + step_size = (self.config.snr * noise_norm / grad_norm) ** 2 * 2 + step_size = step_size * jnp.ones(sample.shape[0]) + + # compute corrected sample: model_output term and noise term + step_size = step_size.flatten() + step_size = broadcast_to_shape_from_left(step_size, sample.shape) + prev_sample_mean = sample + step_size * model_output + prev_sample = prev_sample_mean + ((step_size * 2) ** 0.5) * noise + + if not return_dict: + return (prev_sample, state) + + return FlaxSdeVeOutput(prev_sample=prev_sample, state=state) + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_sde_vp.py b/diffusers/src/diffusers/schedulers/scheduling_sde_vp.py new file mode 100644 index 0000000000000000000000000000000000000000..293df40847692681866c941d533d482879c7262f --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_sde_vp.py @@ -0,0 +1,90 @@ +# Copyright 2022 Google Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +# DISCLAIMER: This file is strongly influenced by https://github.com/yang-song/score_sde_pytorch + +import math +from typing import Union + +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import randn_tensor +from .scheduling_utils import SchedulerMixin + + +class ScoreSdeVpScheduler(SchedulerMixin, ConfigMixin): + """ + The variance preserving stochastic differential equation (SDE) scheduler. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more information, see the original paper: https://arxiv.org/abs/2011.13456 + + UNDER CONSTRUCTION + + """ + + order = 1 + + @register_to_config + def __init__(self, num_train_timesteps=2000, beta_min=0.1, beta_max=20, sampling_eps=1e-3): + self.sigmas = None + self.discrete_sigmas = None + self.timesteps = None + + def set_timesteps(self, num_inference_steps, device: Union[str, torch.device] = None): + self.timesteps = torch.linspace(1, self.config.sampling_eps, num_inference_steps, device=device) + + def step_pred(self, score, x, t, generator=None): + if self.timesteps is None: + raise ValueError( + "`self.timesteps` is not set, you need to run 'set_timesteps' after creating the scheduler" + ) + + # TODO(Patrick) better comments + non-PyTorch + # postprocess model score + log_mean_coeff = ( + -0.25 * t**2 * (self.config.beta_max - self.config.beta_min) - 0.5 * t * self.config.beta_min + ) + std = torch.sqrt(1.0 - torch.exp(2.0 * log_mean_coeff)) + std = std.flatten() + while len(std.shape) < len(score.shape): + std = std.unsqueeze(-1) + score = -score / std + + # compute + dt = -1.0 / len(self.timesteps) + + beta_t = self.config.beta_min + t * (self.config.beta_max - self.config.beta_min) + beta_t = beta_t.flatten() + while len(beta_t.shape) < len(x.shape): + beta_t = beta_t.unsqueeze(-1) + drift = -0.5 * beta_t * x + + diffusion = torch.sqrt(beta_t) + drift = drift - diffusion**2 * score + x_mean = x + drift * dt + + # add noise + noise = randn_tensor(x.shape, layout=x.layout, generator=generator, device=x.device, dtype=x.dtype) + x = x_mean + diffusion * math.sqrt(-dt) * noise + + return x, x_mean + + def __len__(self): + return self.config.num_train_timesteps diff --git a/diffusers/src/diffusers/schedulers/scheduling_unclip.py b/diffusers/src/diffusers/schedulers/scheduling_unclip.py new file mode 100644 index 0000000000000000000000000000000000000000..da074ec61fa4bf7c460659aac9c68a7b923ba9e8 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_unclip.py @@ -0,0 +1,305 @@ +# Copyright 2022 Kakao Brain and The HuggingFace Team. All rights reserved. +# +# 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. + +import math +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import numpy as np +import torch + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput, randn_tensor +from .scheduling_utils import SchedulerMixin + + +@dataclass +# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->UnCLIP +class UnCLIPSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + """ + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None + + +def betas_for_alpha_bar(num_diffusion_timesteps, max_beta=0.999): + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`np.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return torch.tensor(betas, dtype=torch.float32) + + +class UnCLIPScheduler(SchedulerMixin, ConfigMixin): + """ + This is a modified DDPM Scheduler specifically for the karlo unCLIP model. + + This scheduler has some minor variations in how it calculates the learned range variance and dynamically + re-calculates betas based off the timesteps it is skipping. + + The scheduler also uses a slightly different step ratio when computing timesteps to use for inference. + + See [`~DDPMScheduler`] for more information on DDPM scheduling + + Args: + num_train_timesteps (`int`): number of diffusion steps used to train the model. + variance_type (`str`): + options to clip the variance used when adding noise to the denoised sample. Choose from `fixed_small_log` + or `learned_range`. + clip_sample (`bool`, default `True`): + option to clip predicted sample between `-clip_sample_range` and `clip_sample_range` for numerical + stability. + clip_sample_range (`float`, default `1.0`): + The range to clip the sample between. See `clip_sample`. + prediction_type (`str`, default `epsilon`, optional): + prediction type of the scheduler function, one of `epsilon` (predicting the noise of the diffusion process) + or `sample` (directly predicting the noisy sample`) + """ + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + variance_type: str = "fixed_small_log", + clip_sample: bool = True, + clip_sample_range: Optional[float] = 1.0, + prediction_type: str = "epsilon", + beta_schedule: str = "squaredcos_cap_v2", + ): + if beta_schedule != "squaredcos_cap_v2": + raise ValueError("UnCLIPScheduler only supports `beta_schedule`: 'squaredcos_cap_v2'") + + self.betas = betas_for_alpha_bar(num_train_timesteps) + + self.alphas = 1.0 - self.betas + self.alphas_cumprod = torch.cumprod(self.alphas, dim=0) + self.one = torch.tensor(1.0) + + # standard deviation of the initial noise distribution + self.init_noise_sigma = 1.0 + + # setable values + self.num_inference_steps = None + self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy()) + + self.variance_type = variance_type + + def scale_model_input(self, sample: torch.FloatTensor, timestep: Optional[int] = None) -> torch.FloatTensor: + """ + Ensures interchangeability with schedulers that need to scale the denoising model input depending on the + current timestep. + + Args: + sample (`torch.FloatTensor`): input sample + timestep (`int`, optional): current timestep + + Returns: + `torch.FloatTensor`: scaled input sample + """ + return sample + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Note that this scheduler uses a slightly different step ratio than the other diffusers schedulers. The + different step ratio is to mimic the original karlo implementation and does not affect the quality or accuracy + of the results. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + """ + self.num_inference_steps = num_inference_steps + step_ratio = (self.config.num_train_timesteps - 1) / (self.num_inference_steps - 1) + timesteps = (np.arange(0, num_inference_steps) * step_ratio).round()[::-1].copy().astype(np.int64) + self.timesteps = torch.from_numpy(timesteps).to(device) + + def _get_variance(self, t, prev_timestep=None, predicted_variance=None, variance_type=None): + if prev_timestep is None: + prev_timestep = t - 1 + + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.one + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + if prev_timestep == t - 1: + beta = self.betas[t] + else: + beta = 1 - alpha_prod_t / alpha_prod_t_prev + + # For t > 0, compute predicted variance βt (see formula (6) and (7) from https://arxiv.org/pdf/2006.11239.pdf) + # and sample from it to get previous sample + # x_{t-1} ~ N(pred_prev_sample, variance) == add variance to pred_sample + variance = beta_prod_t_prev / beta_prod_t * beta + + if variance_type is None: + variance_type = self.config.variance_type + + # hacks - were probably added for training stability + if variance_type == "fixed_small_log": + variance = torch.log(torch.clamp(variance, min=1e-20)) + variance = torch.exp(0.5 * variance) + elif variance_type == "learned_range": + # NOTE difference with DDPM scheduler + min_log = variance.log() + max_log = beta.log() + + frac = (predicted_variance + 1) / 2 + variance = frac * max_log + (1 - frac) * min_log + + return variance + + def step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + prev_timestep: Optional[int] = None, + generator=None, + return_dict: bool = True, + ) -> Union[UnCLIPSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep by reversing the SDE. Core function to propagate the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): direct output from learned diffusion model. + timestep (`int`): current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + current instance of sample being created by diffusion process. + prev_timestep (`int`, *optional*): The previous timestep to predict the previous sample at. + Used to dynamically compute beta. If not given, `t-1` is used and the pre-computed beta is used. + generator: random number generator. + return_dict (`bool`): option for returning tuple rather than UnCLIPSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.UnCLIPSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.UnCLIPSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. When + returning a tuple, the first element is the sample tensor. + + """ + t = timestep + + if model_output.shape[1] == sample.shape[1] * 2 and self.variance_type == "learned_range": + model_output, predicted_variance = torch.split(model_output, sample.shape[1], dim=1) + else: + predicted_variance = None + + # 1. compute alphas, betas + if prev_timestep is None: + prev_timestep = t - 1 + + alpha_prod_t = self.alphas_cumprod[t] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.one + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + if prev_timestep == t - 1: + beta = self.betas[t] + alpha = self.alphas[t] + else: + beta = 1 - alpha_prod_t / alpha_prod_t_prev + alpha = 1 - beta + + # 2. compute predicted original sample from predicted noise also called + # "predicted x_0" of formula (15) from https://arxiv.org/pdf/2006.11239.pdf + if self.config.prediction_type == "epsilon": + pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5) + elif self.config.prediction_type == "sample": + pred_original_sample = model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon` or `sample`" + " for the UnCLIPScheduler." + ) + + # 3. Clip "predicted x_0" + if self.config.clip_sample: + pred_original_sample = torch.clamp( + pred_original_sample, -self.config.clip_sample_range, self.config.clip_sample_range + ) + + # 4. Compute coefficients for pred_original_sample x_0 and current sample x_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_original_sample_coeff = (alpha_prod_t_prev ** (0.5) * beta) / beta_prod_t + current_sample_coeff = alpha ** (0.5) * beta_prod_t_prev / beta_prod_t + + # 5. Compute predicted previous sample µ_t + # See formula (7) from https://arxiv.org/pdf/2006.11239.pdf + pred_prev_sample = pred_original_sample_coeff * pred_original_sample + current_sample_coeff * sample + + # 6. Add noise + variance = 0 + if t > 0: + variance_noise = randn_tensor( + model_output.shape, dtype=model_output.dtype, generator=generator, device=model_output.device + ) + + variance = self._get_variance( + t, + predicted_variance=predicted_variance, + prev_timestep=prev_timestep, + ) + + if self.variance_type == "fixed_small_log": + variance = variance + elif self.variance_type == "learned_range": + variance = (0.5 * variance).exp() + else: + raise ValueError( + f"variance_type given as {self.variance_type} must be one of `fixed_small_log` or `learned_range`" + " for the UnCLIPScheduler." + ) + + variance = variance * variance_noise + + pred_prev_sample = pred_prev_sample + variance + + if not return_dict: + return (pred_prev_sample,) + + return UnCLIPSchedulerOutput(prev_sample=pred_prev_sample, pred_original_sample=pred_original_sample) diff --git a/diffusers/src/diffusers/schedulers/scheduling_utils.py b/diffusers/src/diffusers/schedulers/scheduling_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f4103d4d62cc745d7ca025e44d4bf1d27093dcca --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_utils.py @@ -0,0 +1,170 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import importlib +import os +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, Optional, Union + +import torch + +from ..utils import BaseOutput + + +SCHEDULER_CONFIG_NAME = "scheduler_config.json" + + +class KarrasDiffusionSchedulers(Enum): + DDIMScheduler = 1 + DDPMScheduler = 2 + PNDMScheduler = 3 + LMSDiscreteScheduler = 4 + EulerDiscreteScheduler = 5 + HeunDiscreteScheduler = 6 + EulerAncestralDiscreteScheduler = 7 + DPMSolverMultistepScheduler = 8 + DPMSolverSinglestepScheduler = 9 + KDPM2DiscreteScheduler = 10 + KDPM2AncestralDiscreteScheduler = 11 + DEISMultistepScheduler = 12 + + +@dataclass +class SchedulerOutput(BaseOutput): + """ + Base class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + """ + + prev_sample: torch.FloatTensor + + +class SchedulerMixin: + """ + Mixin containing common functions for the schedulers. + + Class attributes: + - **_compatibles** (`List[str]`) -- A list of classes that are compatible with the parent class, so that + `from_config` can be used from a class different than the one used to save the config (should be overridden + by parent class). + """ + + config_name = SCHEDULER_CONFIG_NAME + _compatibles = [] + has_compatibles = True + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: Dict[str, Any] = None, + subfolder: Optional[str] = None, + return_unused_kwargs=False, + **kwargs, + ): + r""" + Instantiate a Scheduler class from a pre-defined JSON configuration file inside a directory or Hub repo. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *model id* of a model repo on huggingface.co. Valid model ids should have an + organization name, like `google/ddpm-celebahq-256`. + - A path to a *directory* containing the schedluer configurations saved using + [`~SchedulerMixin.save_pretrained`], e.g., `./my_model_directory/`. + subfolder (`str`, *optional*): + In case the relevant files are located inside a subfolder of the model repo (either remote in + huggingface.co or downloaded locally), you can specify the folder name here. + return_unused_kwargs (`bool`, *optional*, defaults to `False`): + Whether kwargs that are not consumed by the Python class should be returned or not. + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `transformers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + + + Activate the special ["offline-mode"](https://huggingface.co/transformers/installation.html#offline-mode) to + use this method in a firewalled environment. + + + + """ + config, kwargs = cls.load_config( + pretrained_model_name_or_path=pretrained_model_name_or_path, + subfolder=subfolder, + return_unused_kwargs=True, + **kwargs, + ) + return cls.from_config(config, return_unused_kwargs=return_unused_kwargs, **kwargs) + + def save_pretrained(self, save_directory: Union[str, os.PathLike], push_to_hub: bool = False, **kwargs): + """ + Save a scheduler configuration object to the directory `save_directory`, so that it can be re-loaded using the + [`~SchedulerMixin.from_pretrained`] class method. + + Args: + save_directory (`str` or `os.PathLike`): + Directory where the configuration JSON file will be saved (will be created if it does not exist). + """ + self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs) + + @property + def compatibles(self): + """ + Returns all schedulers that are compatible with this scheduler + + Returns: + `List[SchedulerMixin]`: List of compatible schedulers + """ + return self._get_compatibles() + + @classmethod + def _get_compatibles(cls): + compatible_classes_str = list(set([cls.__name__] + cls._compatibles)) + diffusers_library = importlib.import_module(__name__.split(".")[0]) + compatible_classes = [ + getattr(diffusers_library, c) for c in compatible_classes_str if hasattr(diffusers_library, c) + ] + return compatible_classes diff --git a/diffusers/src/diffusers/schedulers/scheduling_utils_flax.py b/diffusers/src/diffusers/schedulers/scheduling_utils_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..9708c0883760037a4129c94a4d40686926d8ae30 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_utils_flax.py @@ -0,0 +1,280 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +import importlib +import math +import os +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, Optional, Tuple, Union + +import flax +import jax.numpy as jnp + +from ..utils import BaseOutput + + +SCHEDULER_CONFIG_NAME = "scheduler_config.json" + + +class FlaxKarrasDiffusionSchedulers(Enum): + FlaxDDIMScheduler = 1 + FlaxDDPMScheduler = 2 + FlaxPNDMScheduler = 3 + FlaxLMSDiscreteScheduler = 4 + FlaxDPMSolverMultistepScheduler = 5 + + +@dataclass +class FlaxSchedulerOutput(BaseOutput): + """ + Base class for the scheduler's step function output. + + Args: + prev_sample (`jnp.ndarray` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + """ + + prev_sample: jnp.ndarray + + +class FlaxSchedulerMixin: + """ + Mixin containing common functions for the schedulers. + + Class attributes: + - **_compatibles** (`List[str]`) -- A list of classes that are compatible with the parent class, so that + `from_config` can be used from a class different than the one used to save the config (should be overridden + by parent class). + """ + + config_name = SCHEDULER_CONFIG_NAME + ignore_for_config = ["dtype"] + _compatibles = [] + has_compatibles = True + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: Dict[str, Any] = None, + subfolder: Optional[str] = None, + return_unused_kwargs=False, + **kwargs, + ): + r""" + Instantiate a Scheduler class from a pre-defined JSON-file. + + Parameters: + pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*): + Can be either: + + - A string, the *model id* of a model repo on huggingface.co. Valid model ids should have an + organization name, like `google/ddpm-celebahq-256`. + - A path to a *directory* containing model weights saved using [`~SchedulerMixin.save_pretrained`], + e.g., `./my_model_directory/`. + subfolder (`str`, *optional*): + In case the relevant files are located inside a subfolder of the model repo (either remote in + huggingface.co or downloaded locally), you can specify the folder name here. + return_unused_kwargs (`bool`, *optional*, defaults to `False`): + Whether kwargs that are not consumed by the Python class should be returned or not. + + cache_dir (`Union[str, os.PathLike]`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the + standard cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force the (re-)download of the model weights and configuration files, overriding the + cached versions if they exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received files. Will attempt to resume the download if such a + file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request. + output_loading_info(`bool`, *optional*, defaults to `False`): + Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages. + local_files_only(`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `transformers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + + + + It is required to be logged in (`huggingface-cli login`) when you want to use private or [gated + models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + + + Activate the special ["offline-mode"](https://huggingface.co/transformers/installation.html#offline-mode) to + use this method in a firewalled environment. + + + + """ + config, kwargs = cls.load_config( + pretrained_model_name_or_path=pretrained_model_name_or_path, + subfolder=subfolder, + return_unused_kwargs=True, + **kwargs, + ) + scheduler, unused_kwargs = cls.from_config(config, return_unused_kwargs=True, **kwargs) + + if hasattr(scheduler, "create_state") and getattr(scheduler, "has_state", False): + state = scheduler.create_state() + + if return_unused_kwargs: + return scheduler, state, unused_kwargs + + return scheduler, state + + def save_pretrained(self, save_directory: Union[str, os.PathLike], push_to_hub: bool = False, **kwargs): + """ + Save a scheduler configuration object to the directory `save_directory`, so that it can be re-loaded using the + [`~FlaxSchedulerMixin.from_pretrained`] class method. + + Args: + save_directory (`str` or `os.PathLike`): + Directory where the configuration JSON file will be saved (will be created if it does not exist). + """ + self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs) + + @property + def compatibles(self): + """ + Returns all schedulers that are compatible with this scheduler + + Returns: + `List[SchedulerMixin]`: List of compatible schedulers + """ + return self._get_compatibles() + + @classmethod + def _get_compatibles(cls): + compatible_classes_str = list(set([cls.__name__] + cls._compatibles)) + diffusers_library = importlib.import_module(__name__.split(".")[0]) + compatible_classes = [ + getattr(diffusers_library, c) for c in compatible_classes_str if hasattr(diffusers_library, c) + ] + return compatible_classes + + +def broadcast_to_shape_from_left(x: jnp.ndarray, shape: Tuple[int]) -> jnp.ndarray: + assert len(shape) >= x.ndim + return jnp.broadcast_to(x.reshape(x.shape + (1,) * (len(shape) - x.ndim)), shape) + + +def betas_for_alpha_bar(num_diffusion_timesteps: int, max_beta=0.999, dtype=jnp.float32) -> jnp.ndarray: + """ + Create a beta schedule that discretizes the given alpha_t_bar function, which defines the cumulative product of + (1-beta) over time from t = [0,1]. + + Contains a function alpha_bar that takes an argument t and transforms it to the cumulative product of (1-beta) up + to that part of the diffusion process. + + + Args: + num_diffusion_timesteps (`int`): the number of betas to produce. + max_beta (`float`): the maximum beta to use; use values lower than 1 to + prevent singularities. + + Returns: + betas (`jnp.ndarray`): the betas used by the scheduler to step the model outputs + """ + + def alpha_bar(time_step): + return math.cos((time_step + 0.008) / 1.008 * math.pi / 2) ** 2 + + betas = [] + for i in range(num_diffusion_timesteps): + t1 = i / num_diffusion_timesteps + t2 = (i + 1) / num_diffusion_timesteps + betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta)) + return jnp.array(betas, dtype=dtype) + + +@flax.struct.dataclass +class CommonSchedulerState: + alphas: jnp.ndarray + betas: jnp.ndarray + alphas_cumprod: jnp.ndarray + + @classmethod + def create(cls, scheduler): + config = scheduler.config + + if config.trained_betas is not None: + betas = jnp.asarray(config.trained_betas, dtype=scheduler.dtype) + elif config.beta_schedule == "linear": + betas = jnp.linspace(config.beta_start, config.beta_end, config.num_train_timesteps, dtype=scheduler.dtype) + elif config.beta_schedule == "scaled_linear": + # this schedule is very specific to the latent diffusion model. + betas = ( + jnp.linspace( + config.beta_start**0.5, config.beta_end**0.5, config.num_train_timesteps, dtype=scheduler.dtype + ) + ** 2 + ) + elif config.beta_schedule == "squaredcos_cap_v2": + # Glide cosine schedule + betas = betas_for_alpha_bar(config.num_train_timesteps, dtype=scheduler.dtype) + else: + raise NotImplementedError( + f"beta_schedule {config.beta_schedule} is not implemented for scheduler {scheduler.__class__.__name__}" + ) + + alphas = 1.0 - betas + + alphas_cumprod = jnp.cumprod(alphas, axis=0) + + return cls( + alphas=alphas, + betas=betas, + alphas_cumprod=alphas_cumprod, + ) + + +def get_sqrt_alpha_prod( + state: CommonSchedulerState, original_samples: jnp.ndarray, noise: jnp.ndarray, timesteps: jnp.ndarray +): + alphas_cumprod = state.alphas_cumprod + + sqrt_alpha_prod = alphas_cumprod[timesteps] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + sqrt_alpha_prod = broadcast_to_shape_from_left(sqrt_alpha_prod, original_samples.shape) + + sqrt_one_minus_alpha_prod = (1 - alphas_cumprod[timesteps]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + sqrt_one_minus_alpha_prod = broadcast_to_shape_from_left(sqrt_one_minus_alpha_prod, original_samples.shape) + + return sqrt_alpha_prod, sqrt_one_minus_alpha_prod + + +def add_noise_common( + state: CommonSchedulerState, original_samples: jnp.ndarray, noise: jnp.ndarray, timesteps: jnp.ndarray +): + sqrt_alpha_prod, sqrt_one_minus_alpha_prod = get_sqrt_alpha_prod(state, original_samples, noise, timesteps) + noisy_samples = sqrt_alpha_prod * original_samples + sqrt_one_minus_alpha_prod * noise + return noisy_samples + + +def get_velocity_common(state: CommonSchedulerState, sample: jnp.ndarray, noise: jnp.ndarray, timesteps: jnp.ndarray): + sqrt_alpha_prod, sqrt_one_minus_alpha_prod = get_sqrt_alpha_prod(state, sample, noise, timesteps) + velocity = sqrt_alpha_prod * noise - sqrt_one_minus_alpha_prod * sample + return velocity diff --git a/diffusers/src/diffusers/schedulers/scheduling_vq_diffusion.py b/diffusers/src/diffusers/schedulers/scheduling_vq_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..89ba722a1852cbbac3bbd053effedbe97d370993 --- /dev/null +++ b/diffusers/src/diffusers/schedulers/scheduling_vq_diffusion.py @@ -0,0 +1,496 @@ +# Copyright 2022 Microsoft and The HuggingFace Team. All rights reserved. +# +# 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. + +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn.functional as F + +from ..configuration_utils import ConfigMixin, register_to_config +from ..utils import BaseOutput +from .scheduling_utils import SchedulerMixin + + +@dataclass +class VQDiffusionSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.LongTensor` of shape `(batch size, num latent pixels)`): + Computed sample x_{t-1} of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + """ + + prev_sample: torch.LongTensor + + +def index_to_log_onehot(x: torch.LongTensor, num_classes: int) -> torch.FloatTensor: + """ + Convert batch of vector of class indices into batch of log onehot vectors + + Args: + x (`torch.LongTensor` of shape `(batch size, vector length)`): + Batch of class indices + + num_classes (`int`): + number of classes to be used for the onehot vectors + + Returns: + `torch.FloatTensor` of shape `(batch size, num classes, vector length)`: + Log onehot vectors + """ + x_onehot = F.one_hot(x, num_classes) + x_onehot = x_onehot.permute(0, 2, 1) + log_x = torch.log(x_onehot.float().clamp(min=1e-30)) + return log_x + + +def gumbel_noised(logits: torch.FloatTensor, generator: Optional[torch.Generator]) -> torch.FloatTensor: + """ + Apply gumbel noise to `logits` + """ + uniform = torch.rand(logits.shape, device=logits.device, generator=generator) + gumbel_noise = -torch.log(-torch.log(uniform + 1e-30) + 1e-30) + noised = gumbel_noise + logits + return noised + + +def alpha_schedules(num_diffusion_timesteps: int, alpha_cum_start=0.99999, alpha_cum_end=0.000009): + """ + Cumulative and non-cumulative alpha schedules. + + See section 4.1. + """ + att = ( + np.arange(0, num_diffusion_timesteps) / (num_diffusion_timesteps - 1) * (alpha_cum_end - alpha_cum_start) + + alpha_cum_start + ) + att = np.concatenate(([1], att)) + at = att[1:] / att[:-1] + att = np.concatenate((att[1:], [1])) + return at, att + + +def gamma_schedules(num_diffusion_timesteps: int, gamma_cum_start=0.000009, gamma_cum_end=0.99999): + """ + Cumulative and non-cumulative gamma schedules. + + See section 4.1. + """ + ctt = ( + np.arange(0, num_diffusion_timesteps) / (num_diffusion_timesteps - 1) * (gamma_cum_end - gamma_cum_start) + + gamma_cum_start + ) + ctt = np.concatenate(([0], ctt)) + one_minus_ctt = 1 - ctt + one_minus_ct = one_minus_ctt[1:] / one_minus_ctt[:-1] + ct = 1 - one_minus_ct + ctt = np.concatenate((ctt[1:], [0])) + return ct, ctt + + +class VQDiffusionScheduler(SchedulerMixin, ConfigMixin): + """ + The VQ-diffusion transformer outputs predicted probabilities of the initial unnoised image. + + The VQ-diffusion scheduler converts the transformer's output into a sample for the unnoised image at the previous + diffusion timestep. + + [`~ConfigMixin`] takes care of storing all config attributes that are passed in the scheduler's `__init__` + function, such as `num_train_timesteps`. They can be accessed via `scheduler.config.num_train_timesteps`. + [`SchedulerMixin`] provides general loading and saving functionality via the [`SchedulerMixin.save_pretrained`] and + [`~SchedulerMixin.from_pretrained`] functions. + + For more details, see the original paper: https://arxiv.org/abs/2111.14822 + + Args: + num_vec_classes (`int`): + The number of classes of the vector embeddings of the latent pixels. Includes the class for the masked + latent pixel. + + num_train_timesteps (`int`): + Number of diffusion steps used to train the model. + + alpha_cum_start (`float`): + The starting cumulative alpha value. + + alpha_cum_end (`float`): + The ending cumulative alpha value. + + gamma_cum_start (`float`): + The starting cumulative gamma value. + + gamma_cum_end (`float`): + The ending cumulative gamma value. + """ + + order = 1 + + @register_to_config + def __init__( + self, + num_vec_classes: int, + num_train_timesteps: int = 100, + alpha_cum_start: float = 0.99999, + alpha_cum_end: float = 0.000009, + gamma_cum_start: float = 0.000009, + gamma_cum_end: float = 0.99999, + ): + self.num_embed = num_vec_classes + + # By convention, the index for the mask class is the last class index + self.mask_class = self.num_embed - 1 + + at, att = alpha_schedules(num_train_timesteps, alpha_cum_start=alpha_cum_start, alpha_cum_end=alpha_cum_end) + ct, ctt = gamma_schedules(num_train_timesteps, gamma_cum_start=gamma_cum_start, gamma_cum_end=gamma_cum_end) + + num_non_mask_classes = self.num_embed - 1 + bt = (1 - at - ct) / num_non_mask_classes + btt = (1 - att - ctt) / num_non_mask_classes + + at = torch.tensor(at.astype("float64")) + bt = torch.tensor(bt.astype("float64")) + ct = torch.tensor(ct.astype("float64")) + log_at = torch.log(at) + log_bt = torch.log(bt) + log_ct = torch.log(ct) + + att = torch.tensor(att.astype("float64")) + btt = torch.tensor(btt.astype("float64")) + ctt = torch.tensor(ctt.astype("float64")) + log_cumprod_at = torch.log(att) + log_cumprod_bt = torch.log(btt) + log_cumprod_ct = torch.log(ctt) + + self.log_at = log_at.float() + self.log_bt = log_bt.float() + self.log_ct = log_ct.float() + self.log_cumprod_at = log_cumprod_at.float() + self.log_cumprod_bt = log_cumprod_bt.float() + self.log_cumprod_ct = log_cumprod_ct.float() + + # setable values + self.num_inference_steps = None + self.timesteps = torch.from_numpy(np.arange(0, num_train_timesteps)[::-1].copy()) + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None): + """ + Sets the discrete timesteps used for the diffusion chain. Supporting function to be run before inference. + + Args: + num_inference_steps (`int`): + the number of diffusion steps used when generating samples with a pre-trained model. + + device (`str` or `torch.device`): + device to place the timesteps and the diffusion process parameters (alpha, beta, gamma) on. + """ + self.num_inference_steps = num_inference_steps + timesteps = np.arange(0, self.num_inference_steps)[::-1].copy() + self.timesteps = torch.from_numpy(timesteps).to(device) + + self.log_at = self.log_at.to(device) + self.log_bt = self.log_bt.to(device) + self.log_ct = self.log_ct.to(device) + self.log_cumprod_at = self.log_cumprod_at.to(device) + self.log_cumprod_bt = self.log_cumprod_bt.to(device) + self.log_cumprod_ct = self.log_cumprod_ct.to(device) + + def step( + self, + model_output: torch.FloatTensor, + timestep: torch.long, + sample: torch.LongTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[VQDiffusionSchedulerOutput, Tuple]: + """ + Predict the sample at the previous timestep via the reverse transition distribution i.e. Equation (11). See the + docstring for `self.q_posterior` for more in depth docs on how Equation (11) is computed. + + Args: + log_p_x_0: (`torch.FloatTensor` of shape `(batch size, num classes - 1, num latent pixels)`): + The log probabilities for the predicted classes of the initial latent pixels. Does not include a + prediction for the masked class as the initial unnoised image cannot be masked. + + t (`torch.long`): + The timestep that determines which transition matrices are used. + + x_t: (`torch.LongTensor` of shape `(batch size, num latent pixels)`): + The classes of each latent pixel at time `t` + + generator: (`torch.Generator` or None): + RNG for the noise applied to p(x_{t-1} | x_t) before it is sampled from. + + return_dict (`bool`): + option for returning tuple rather than VQDiffusionSchedulerOutput class + + Returns: + [`~schedulers.scheduling_utils.VQDiffusionSchedulerOutput`] or `tuple`: + [`~schedulers.scheduling_utils.VQDiffusionSchedulerOutput`] if `return_dict` is True, otherwise a `tuple`. + When returning a tuple, the first element is the sample tensor. + """ + if timestep == 0: + log_p_x_t_min_1 = model_output + else: + log_p_x_t_min_1 = self.q_posterior(model_output, sample, timestep) + + log_p_x_t_min_1 = gumbel_noised(log_p_x_t_min_1, generator) + + x_t_min_1 = log_p_x_t_min_1.argmax(dim=1) + + if not return_dict: + return (x_t_min_1,) + + return VQDiffusionSchedulerOutput(prev_sample=x_t_min_1) + + def q_posterior(self, log_p_x_0, x_t, t): + """ + Calculates the log probabilities for the predicted classes of the image at timestep `t-1`. I.e. Equation (11). + + Instead of directly computing equation (11), we use Equation (5) to restate Equation (11) in terms of only + forward probabilities. + + Equation (11) stated in terms of forward probabilities via Equation (5): + + Where: + - the sum is over x_0 = {C_0 ... C_{k-1}} (classes for x_0) + + p(x_{t-1} | x_t) = sum( q(x_t | x_{t-1}) * q(x_{t-1} | x_0) * p(x_0) / q(x_t | x_0) ) + + Args: + log_p_x_0: (`torch.FloatTensor` of shape `(batch size, num classes - 1, num latent pixels)`): + The log probabilities for the predicted classes of the initial latent pixels. Does not include a + prediction for the masked class as the initial unnoised image cannot be masked. + + x_t: (`torch.LongTensor` of shape `(batch size, num latent pixels)`): + The classes of each latent pixel at time `t` + + t (torch.Long): + The timestep that determines which transition matrix is used. + + Returns: + `torch.FloatTensor` of shape `(batch size, num classes, num latent pixels)`: + The log probabilities for the predicted classes of the image at timestep `t-1`. I.e. Equation (11). + """ + log_onehot_x_t = index_to_log_onehot(x_t, self.num_embed) + + log_q_x_t_given_x_0 = self.log_Q_t_transitioning_to_known_class( + t=t, x_t=x_t, log_onehot_x_t=log_onehot_x_t, cumulative=True + ) + + log_q_t_given_x_t_min_1 = self.log_Q_t_transitioning_to_known_class( + t=t, x_t=x_t, log_onehot_x_t=log_onehot_x_t, cumulative=False + ) + + # p_0(x_0=C_0 | x_t) / q(x_t | x_0=C_0) ... p_n(x_0=C_0 | x_t) / q(x_t | x_0=C_0) + # . . . + # . . . + # . . . + # p_0(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) ... p_n(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) + q = log_p_x_0 - log_q_x_t_given_x_0 + + # sum_0 = p_0(x_0=C_0 | x_t) / q(x_t | x_0=C_0) + ... + p_0(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}), ... , + # sum_n = p_n(x_0=C_0 | x_t) / q(x_t | x_0=C_0) + ... + p_n(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) + q_log_sum_exp = torch.logsumexp(q, dim=1, keepdim=True) + + # p_0(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_0 ... p_n(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_n + # . . . + # . . . + # . . . + # p_0(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_0 ... p_n(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_n + q = q - q_log_sum_exp + + # (p_0(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_0) * a_cumulative_{t-1} + b_cumulative_{t-1} ... (p_n(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_n) * a_cumulative_{t-1} + b_cumulative_{t-1} + # . . . + # . . . + # . . . + # (p_0(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_0) * a_cumulative_{t-1} + b_cumulative_{t-1} ... (p_n(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_n) * a_cumulative_{t-1} + b_cumulative_{t-1} + # c_cumulative_{t-1} ... c_cumulative_{t-1} + q = self.apply_cumulative_transitions(q, t - 1) + + # ((p_0(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_0) * a_cumulative_{t-1} + b_cumulative_{t-1}) * q(x_t | x_{t-1}=C_0) * sum_0 ... ((p_n(x_0=C_0 | x_t) / q(x_t | x_0=C_0) / sum_n) * a_cumulative_{t-1} + b_cumulative_{t-1}) * q(x_t | x_{t-1}=C_0) * sum_n + # . . . + # . . . + # . . . + # ((p_0(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_0) * a_cumulative_{t-1} + b_cumulative_{t-1}) * q(x_t | x_{t-1}=C_{k-1}) * sum_0 ... ((p_n(x_0=C_{k-1} | x_t) / q(x_t | x_0=C_{k-1}) / sum_n) * a_cumulative_{t-1} + b_cumulative_{t-1}) * q(x_t | x_{t-1}=C_{k-1}) * sum_n + # c_cumulative_{t-1} * q(x_t | x_{t-1}=C_k) * sum_0 ... c_cumulative_{t-1} * q(x_t | x_{t-1}=C_k) * sum_0 + log_p_x_t_min_1 = q + log_q_t_given_x_t_min_1 + q_log_sum_exp + + # For each column, there are two possible cases. + # + # Where: + # - sum(p_n(x_0))) is summing over all classes for x_0 + # - C_i is the class transitioning from (not to be confused with c_t and c_cumulative_t being used for gamma's) + # - C_j is the class transitioning to + # + # 1. x_t is masked i.e. x_t = c_k + # + # Simplifying the expression, the column vector is: + # . + # . + # . + # (c_t / c_cumulative_t) * (a_cumulative_{t-1} * p_n(x_0 = C_i | x_t) + b_cumulative_{t-1} * sum(p_n(x_0))) + # . + # . + # . + # (c_cumulative_{t-1} / c_cumulative_t) * sum(p_n(x_0)) + # + # From equation (11) stated in terms of forward probabilities, the last row is trivially verified. + # + # For the other rows, we can state the equation as ... + # + # (c_t / c_cumulative_t) * [b_cumulative_{t-1} * p(x_0=c_0) + ... + (a_cumulative_{t-1} + b_cumulative_{t-1}) * p(x_0=C_i) + ... + b_cumulative_{k-1} * p(x_0=c_{k-1})] + # + # This verifies the other rows. + # + # 2. x_t is not masked + # + # Simplifying the expression, there are two cases for the rows of the column vector, where C_j = C_i and where C_j != C_i: + # . + # . + # . + # C_j != C_i: b_t * ((b_cumulative_{t-1} / b_cumulative_t) * p_n(x_0 = c_0) + ... + ((a_cumulative_{t-1} + b_cumulative_{t-1}) / b_cumulative_t) * p_n(x_0 = C_i) + ... + (b_cumulative_{t-1} / (a_cumulative_t + b_cumulative_t)) * p_n(c_0=C_j) + ... + (b_cumulative_{t-1} / b_cumulative_t) * p_n(x_0 = c_{k-1})) + # . + # . + # . + # C_j = C_i: (a_t + b_t) * ((b_cumulative_{t-1} / b_cumulative_t) * p_n(x_0 = c_0) + ... + ((a_cumulative_{t-1} + b_cumulative_{t-1}) / (a_cumulative_t + b_cumulative_t)) * p_n(x_0 = C_i = C_j) + ... + (b_cumulative_{t-1} / b_cumulative_t) * p_n(x_0 = c_{k-1})) + # . + # . + # . + # 0 + # + # The last row is trivially verified. The other rows can be verified by directly expanding equation (11) stated in terms of forward probabilities. + return log_p_x_t_min_1 + + def log_Q_t_transitioning_to_known_class( + self, *, t: torch.int, x_t: torch.LongTensor, log_onehot_x_t: torch.FloatTensor, cumulative: bool + ): + """ + Returns the log probabilities of the rows from the (cumulative or non-cumulative) transition matrix for each + latent pixel in `x_t`. + + See equation (7) for the complete non-cumulative transition matrix. The complete cumulative transition matrix + is the same structure except the parameters (alpha, beta, gamma) are the cumulative analogs. + + Args: + t (torch.Long): + The timestep that determines which transition matrix is used. + + x_t (`torch.LongTensor` of shape `(batch size, num latent pixels)`): + The classes of each latent pixel at time `t`. + + log_onehot_x_t (`torch.FloatTensor` of shape `(batch size, num classes, num latent pixels)`): + The log one-hot vectors of `x_t` + + cumulative (`bool`): + If cumulative is `False`, we use the single step transition matrix `t-1`->`t`. If cumulative is `True`, + we use the cumulative transition matrix `0`->`t`. + + Returns: + `torch.FloatTensor` of shape `(batch size, num classes - 1, num latent pixels)`: + Each _column_ of the returned matrix is a _row_ of log probabilities of the complete probability + transition matrix. + + When non cumulative, returns `self.num_classes - 1` rows because the initial latent pixel cannot be + masked. + + Where: + - `q_n` is the probability distribution for the forward process of the `n`th latent pixel. + - C_0 is a class of a latent pixel embedding + - C_k is the class of the masked latent pixel + + non-cumulative result (omitting logarithms): + ``` + q_0(x_t | x_{t-1} = C_0) ... q_n(x_t | x_{t-1} = C_0) + . . . + . . . + . . . + q_0(x_t | x_{t-1} = C_k) ... q_n(x_t | x_{t-1} = C_k) + ``` + + cumulative result (omitting logarithms): + ``` + q_0_cumulative(x_t | x_0 = C_0) ... q_n_cumulative(x_t | x_0 = C_0) + . . . + . . . + . . . + q_0_cumulative(x_t | x_0 = C_{k-1}) ... q_n_cumulative(x_t | x_0 = C_{k-1}) + ``` + """ + if cumulative: + a = self.log_cumprod_at[t] + b = self.log_cumprod_bt[t] + c = self.log_cumprod_ct[t] + else: + a = self.log_at[t] + b = self.log_bt[t] + c = self.log_ct[t] + + if not cumulative: + # The values in the onehot vector can also be used as the logprobs for transitioning + # from masked latent pixels. If we are not calculating the cumulative transitions, + # we need to save these vectors to be re-appended to the final matrix so the values + # aren't overwritten. + # + # `P(x_t!=mask|x_{t-1=mask}) = 0` and 0 will be the value of the last row of the onehot vector + # if x_t is not masked + # + # `P(x_t=mask|x_{t-1=mask}) = 1` and 1 will be the value of the last row of the onehot vector + # if x_t is masked + log_onehot_x_t_transitioning_from_masked = log_onehot_x_t[:, -1, :].unsqueeze(1) + + # `index_to_log_onehot` will add onehot vectors for masked pixels, + # so the default one hot matrix has one too many rows. See the doc string + # for an explanation of the dimensionality of the returned matrix. + log_onehot_x_t = log_onehot_x_t[:, :-1, :] + + # this is a cheeky trick to produce the transition probabilities using log one-hot vectors. + # + # Don't worry about what values this sets in the columns that mark transitions + # to masked latent pixels. They are overwrote later with the `mask_class_mask`. + # + # Looking at the below logspace formula in non-logspace, each value will evaluate to either + # `1 * a + b = a + b` where `log_Q_t` has the one hot value in the column + # or + # `0 * a + b = b` where `log_Q_t` has the 0 values in the column. + # + # See equation 7 for more details. + log_Q_t = (log_onehot_x_t + a).logaddexp(b) + + # The whole column of each masked pixel is `c` + mask_class_mask = x_t == self.mask_class + mask_class_mask = mask_class_mask.unsqueeze(1).expand(-1, self.num_embed - 1, -1) + log_Q_t[mask_class_mask] = c + + if not cumulative: + log_Q_t = torch.cat((log_Q_t, log_onehot_x_t_transitioning_from_masked), dim=1) + + return log_Q_t + + def apply_cumulative_transitions(self, q, t): + bsz = q.shape[0] + a = self.log_cumprod_at[t] + b = self.log_cumprod_bt[t] + c = self.log_cumprod_ct[t] + + num_latent_pixels = q.shape[2] + c = c.expand(bsz, 1, num_latent_pixels) + + q = (q + a).logaddexp(b) + q = torch.cat((q, c), dim=1) + + return q diff --git a/diffusers/src/diffusers/training_utils.py b/diffusers/src/diffusers/training_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c77ea03adf3e920977295e5f4b5dd1bb6b482e9c --- /dev/null +++ b/diffusers/src/diffusers/training_utils.py @@ -0,0 +1,308 @@ +import copy +import os +import random +from typing import Any, Dict, Iterable, Optional, Union + +import numpy as np +import torch + +from .utils import deprecate + + +def enable_full_determinism(seed: int): + """ + Helper function for reproducible behavior during distributed training. See + - https://pytorch.org/docs/stable/notes/randomness.html for pytorch + """ + # set seed first + set_seed(seed) + + # Enable PyTorch deterministic mode. This potentially requires either the environment + # variable 'CUDA_LAUNCH_BLOCKING' or 'CUBLAS_WORKSPACE_CONFIG' to be set, + # depending on the CUDA version, so we set them both here + os.environ["CUDA_LAUNCH_BLOCKING"] = "1" + os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8" + torch.use_deterministic_algorithms(True) + + # Enable CUDNN deterministic mode + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + +def set_seed(seed: int): + """ + Args: + Helper function for reproducible behavior to set the seed in `random`, `numpy`, `torch`. + seed (`int`): The seed to set. + """ + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + # ^^ safe to call this function even if cuda is not available + + +# Adapted from torch-ema https://github.com/fadel/pytorch_ema/blob/master/torch_ema/ema.py#L14 +class EMAModel: + """ + Exponential Moving Average of models weights + """ + + def __init__( + self, + parameters: Iterable[torch.nn.Parameter], + decay: float = 0.9999, + min_decay: float = 0.0, + update_after_step: int = 0, + use_ema_warmup: bool = False, + inv_gamma: Union[float, int] = 1.0, + power: Union[float, int] = 2 / 3, + model_cls: Optional[Any] = None, + model_config: Dict[str, Any] = None, + **kwargs, + ): + """ + Args: + parameters (Iterable[torch.nn.Parameter]): The parameters to track. + decay (float): The decay factor for the exponential moving average. + min_decay (float): The minimum decay factor for the exponential moving average. + update_after_step (int): The number of steps to wait before starting to update the EMA weights. + use_ema_warmup (bool): Whether to use EMA warmup. + inv_gamma (float): + Inverse multiplicative factor of EMA warmup. Default: 1. Only used if `use_ema_warmup` is True. + power (float): Exponential factor of EMA warmup. Default: 2/3. Only used if `use_ema_warmup` is True. + device (Optional[Union[str, torch.device]]): The device to store the EMA weights on. If None, the EMA + weights will be stored on CPU. + + @crowsonkb's notes on EMA Warmup: + If gamma=1 and power=1, implements a simple average. gamma=1, power=2/3 are good values for models you plan + to train for a million or more steps (reaches decay factor 0.999 at 31.6K steps, 0.9999 at 1M steps), + gamma=1, power=3/4 for models you plan to train for less (reaches decay factor 0.999 at 10K steps, 0.9999 + at 215.4k steps). + """ + + if isinstance(parameters, torch.nn.Module): + deprecation_message = ( + "Passing a `torch.nn.Module` to `ExponentialMovingAverage` is deprecated. " + "Please pass the parameters of the module instead." + ) + deprecate( + "passing a `torch.nn.Module` to `ExponentialMovingAverage`", + "1.0.0", + deprecation_message, + standard_warn=False, + ) + parameters = parameters.parameters() + + # set use_ema_warmup to True if a torch.nn.Module is passed for backwards compatibility + use_ema_warmup = True + + if kwargs.get("max_value", None) is not None: + deprecation_message = "The `max_value` argument is deprecated. Please use `decay` instead." + deprecate("max_value", "1.0.0", deprecation_message, standard_warn=False) + decay = kwargs["max_value"] + + if kwargs.get("min_value", None) is not None: + deprecation_message = "The `min_value` argument is deprecated. Please use `min_decay` instead." + deprecate("min_value", "1.0.0", deprecation_message, standard_warn=False) + min_decay = kwargs["min_value"] + + parameters = list(parameters) + self.shadow_params = [p.clone().detach() for p in parameters] + + if kwargs.get("device", None) is not None: + deprecation_message = "The `device` argument is deprecated. Please use `to` instead." + deprecate("device", "1.0.0", deprecation_message, standard_warn=False) + self.to(device=kwargs["device"]) + + self.collected_params = None + + self.decay = decay + self.min_decay = min_decay + self.update_after_step = update_after_step + self.use_ema_warmup = use_ema_warmup + self.inv_gamma = inv_gamma + self.power = power + self.optimization_step = 0 + self.cur_decay_value = None # set in `step()` + + self.model_cls = model_cls + self.model_config = model_config + + @classmethod + def from_pretrained(cls, path, model_cls) -> "EMAModel": + _, ema_kwargs = model_cls.load_config(path, return_unused_kwargs=True) + model = model_cls.from_pretrained(path) + + ema_model = cls(model.parameters(), model_cls=model_cls, model_config=model.config) + + ema_model.load_state_dict(ema_kwargs) + return ema_model + + def save_pretrained(self, path): + if self.model_cls is None: + raise ValueError("`save_pretrained` can only be used if `model_cls` was defined at __init__.") + + if self.model_config is None: + raise ValueError("`save_pretrained` can only be used if `model_config` was defined at __init__.") + + model = self.model_cls.from_config(self.model_config) + state_dict = self.state_dict() + state_dict.pop("shadow_params", None) + state_dict.pop("collected_params", None) + + model.register_to_config(**state_dict) + self.copy_to(model.parameters()) + model.save_pretrained(path) + + def get_decay(self, optimization_step: int) -> float: + """ + Compute the decay factor for the exponential moving average. + """ + step = max(0, optimization_step - self.update_after_step - 1) + + if step <= 0: + return 0.0 + + if self.use_ema_warmup: + cur_decay_value = 1 - (1 + step / self.inv_gamma) ** -self.power + else: + cur_decay_value = (1 + step) / (10 + step) + + cur_decay_value = min(cur_decay_value, self.decay) + # make sure decay is not smaller than min_decay + cur_decay_value = max(cur_decay_value, self.min_decay) + return cur_decay_value + + @torch.no_grad() + def step(self, parameters: Iterable[torch.nn.Parameter]): + if isinstance(parameters, torch.nn.Module): + deprecation_message = ( + "Passing a `torch.nn.Module` to `ExponentialMovingAverage.step` is deprecated. " + "Please pass the parameters of the module instead." + ) + deprecate( + "passing a `torch.nn.Module` to `ExponentialMovingAverage.step`", + "1.0.0", + deprecation_message, + standard_warn=False, + ) + parameters = parameters.parameters() + + parameters = list(parameters) + + self.optimization_step += 1 + + # Compute the decay factor for the exponential moving average. + decay = self.get_decay(self.optimization_step) + self.cur_decay_value = decay + one_minus_decay = 1 - decay + + for s_param, param in zip(self.shadow_params, parameters): + if param.requires_grad: + s_param.sub_(one_minus_decay * (s_param - param)) + else: + s_param.copy_(param) + + torch.cuda.empty_cache() + + def copy_to(self, parameters: Iterable[torch.nn.Parameter]) -> None: + """ + Copy current averaged parameters into given collection of parameters. + + Args: + parameters: Iterable of `torch.nn.Parameter`; the parameters to be + updated with the stored moving averages. If `None`, the parameters with which this + `ExponentialMovingAverage` was initialized will be used. + """ + parameters = list(parameters) + for s_param, param in zip(self.shadow_params, parameters): + param.data.copy_(s_param.to(param.device).data) + + def to(self, device=None, dtype=None) -> None: + r"""Move internal buffers of the ExponentialMovingAverage to `device`. + + Args: + device: like `device` argument to `torch.Tensor.to` + """ + # .to() on the tensors handles None correctly + self.shadow_params = [ + p.to(device=device, dtype=dtype) if p.is_floating_point() else p.to(device=device) + for p in self.shadow_params + ] + + def state_dict(self) -> dict: + r""" + Returns the state of the ExponentialMovingAverage as a dict. This method is used by accelerate during + checkpointing to save the ema state dict. + """ + # Following PyTorch conventions, references to tensors are returned: + # "returns a reference to the state and not its copy!" - + # https://pytorch.org/tutorials/beginner/saving_loading_models.html#what-is-a-state-dict + return { + "decay": self.decay, + "min_decay": self.min_decay, + "optimization_step": self.optimization_step, + "update_after_step": self.update_after_step, + "use_ema_warmup": self.use_ema_warmup, + "inv_gamma": self.inv_gamma, + "power": self.power, + "shadow_params": self.shadow_params, + "collected_params": self.collected_params, + } + + def load_state_dict(self, state_dict: dict) -> None: + r""" + Args: + Loads the ExponentialMovingAverage state. This method is used by accelerate during checkpointing to save the + ema state dict. + state_dict (dict): EMA state. Should be an object returned + from a call to :meth:`state_dict`. + """ + # deepcopy, to be consistent with module API + state_dict = copy.deepcopy(state_dict) + + self.decay = state_dict.get("decay", self.decay) + if self.decay < 0.0 or self.decay > 1.0: + raise ValueError("Decay must be between 0 and 1") + + self.min_decay = state_dict.get("min_decay", self.min_decay) + if not isinstance(self.min_decay, float): + raise ValueError("Invalid min_decay") + + self.optimization_step = state_dict.get("optimization_step", self.optimization_step) + if not isinstance(self.optimization_step, int): + raise ValueError("Invalid optimization_step") + + self.update_after_step = state_dict.get("update_after_step", self.update_after_step) + if not isinstance(self.update_after_step, int): + raise ValueError("Invalid update_after_step") + + self.use_ema_warmup = state_dict.get("use_ema_warmup", self.use_ema_warmup) + if not isinstance(self.use_ema_warmup, bool): + raise ValueError("Invalid use_ema_warmup") + + self.inv_gamma = state_dict.get("inv_gamma", self.inv_gamma) + if not isinstance(self.inv_gamma, (float, int)): + raise ValueError("Invalid inv_gamma") + + self.power = state_dict.get("power", self.power) + if not isinstance(self.power, (float, int)): + raise ValueError("Invalid power") + + shadow_params = state_dict.get("shadow_params", None) + if shadow_params is not None: + self.shadow_params = shadow_params + if not isinstance(self.shadow_params, list): + raise ValueError("shadow_params must be a list") + if not all(isinstance(p, torch.Tensor) for p in self.shadow_params): + raise ValueError("shadow_params must all be Tensors") + + self.collected_params = state_dict.get("collected_params", None) + if self.collected_params is not None: + if not isinstance(self.collected_params, list): + raise ValueError("collected_params must be a list") + if not all(isinstance(p, torch.Tensor) for p in self.collected_params): + raise ValueError("collected_params must all be Tensors") + if len(self.collected_params) != len(self.shadow_params): + raise ValueError("collected_params and shadow_params must have the same length") diff --git a/diffusers/src/diffusers/utils/__init__.py b/diffusers/src/diffusers/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8e61b5757eb582e1547074a00e1cd899db3cd619 --- /dev/null +++ b/diffusers/src/diffusers/utils/__init__.py @@ -0,0 +1,101 @@ +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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. + + +import os + +from packaging import version + +from .. import __version__ +from .constants import ( + CONFIG_NAME, + DIFFUSERS_CACHE, + DIFFUSERS_DYNAMIC_MODULE_NAME, + FLAX_WEIGHTS_NAME, + HF_MODULES_CACHE, + HUGGINGFACE_CO_RESOLVE_ENDPOINT, + ONNX_EXTERNAL_WEIGHTS_NAME, + ONNX_WEIGHTS_NAME, + SAFETENSORS_WEIGHTS_NAME, + WEIGHTS_NAME, +) +from .deprecation_utils import deprecate +from .doc_utils import replace_example_docstring +from .dynamic_modules_utils import get_class_from_dynamic_module +from .hub_utils import HF_HUB_OFFLINE, http_user_agent +from .import_utils import ( + ENV_VARS_TRUE_AND_AUTO_VALUES, + ENV_VARS_TRUE_VALUES, + USE_JAX, + USE_TF, + USE_TORCH, + DummyObject, + OptionalDependencyNotAvailable, + is_accelerate_available, + is_flax_available, + is_inflect_available, + is_k_diffusion_available, + is_k_diffusion_version, + is_librosa_available, + is_omegaconf_available, + is_onnx_available, + is_safetensors_available, + is_scipy_available, + is_tensorboard_available, + is_tf_available, + is_torch_available, + is_torch_version, + is_transformers_available, + is_transformers_version, + is_unidecode_available, + is_wandb_available, + is_xformers_available, + requires_backends, +) +from .logging import get_logger +from .outputs import BaseOutput +from .pil_utils import PIL_INTERPOLATION +from .torch_utils import randn_tensor + + +if is_torch_available(): + from .testing_utils import ( + floats_tensor, + load_hf_numpy, + load_image, + load_numpy, + nightly, + parse_flag_from_env, + print_tensor_test, + require_torch_gpu, + slow, + torch_all_close, + torch_device, + ) + + +logger = get_logger(__name__) + + +def check_min_version(min_version): + if version.parse(__version__) < version.parse(min_version): + if "dev" in min_version: + error_message = ( + "This example requires a source install from HuggingFace diffusers (see " + "`https://huggingface.co/docs/diffusers/installation#install-from-source`)," + ) + else: + error_message = f"This example requires a minimum version of {min_version}," + error_message += f" but the version found is {__version__}.\n" + raise ImportError(error_message) diff --git a/diffusers/src/diffusers/utils/constants.py b/diffusers/src/diffusers/utils/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..0edb4c57f0769eccd1a1e1296170f5a6af63a04c --- /dev/null +++ b/diffusers/src/diffusers/utils/constants.py @@ -0,0 +1,32 @@ +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# 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. +import os + + +hf_cache_home = os.path.expanduser( + os.getenv("HF_HOME", os.path.join(os.getenv("XDG_CACHE_HOME", "~/.cache"), "huggingface")) +) +default_cache_path = os.path.join(hf_cache_home, "diffusers") + + +CONFIG_NAME = "config.json" +WEIGHTS_NAME = "diffusion_pytorch_model.bin" +FLAX_WEIGHTS_NAME = "diffusion_flax_model.msgpack" +ONNX_WEIGHTS_NAME = "model.onnx" +SAFETENSORS_WEIGHTS_NAME = "diffusion_pytorch_model.safetensors" +ONNX_EXTERNAL_WEIGHTS_NAME = "weights.pb" +HUGGINGFACE_CO_RESOLVE_ENDPOINT = "https://huggingface.co" +DIFFUSERS_CACHE = default_cache_path +DIFFUSERS_DYNAMIC_MODULE_NAME = "diffusers_modules" +HF_MODULES_CACHE = os.getenv("HF_MODULES_CACHE", os.path.join(hf_cache_home, "modules")) diff --git a/diffusers/src/diffusers/utils/deprecation_utils.py b/diffusers/src/diffusers/utils/deprecation_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6bdda664e102ea9913503b9e169fa97225d52c78 --- /dev/null +++ b/diffusers/src/diffusers/utils/deprecation_utils.py @@ -0,0 +1,49 @@ +import inspect +import warnings +from typing import Any, Dict, Optional, Union + +from packaging import version + + +def deprecate(*args, take_from: Optional[Union[Dict, Any]] = None, standard_warn=True): + from .. import __version__ + + deprecated_kwargs = take_from + values = () + if not isinstance(args[0], tuple): + args = (args,) + + for attribute, version_name, message in args: + if version.parse(version.parse(__version__).base_version) >= version.parse(version_name): + raise ValueError( + f"The deprecation tuple {(attribute, version_name, message)} should be removed since diffusers'" + f" version {__version__} is >= {version_name}" + ) + + warning = None + if isinstance(deprecated_kwargs, dict) and attribute in deprecated_kwargs: + values += (deprecated_kwargs.pop(attribute),) + warning = f"The `{attribute}` argument is deprecated and will be removed in version {version_name}." + elif hasattr(deprecated_kwargs, attribute): + values += (getattr(deprecated_kwargs, attribute),) + warning = f"The `{attribute}` attribute is deprecated and will be removed in version {version_name}." + elif deprecated_kwargs is None: + warning = f"`{attribute}` is deprecated and will be removed in version {version_name}." + + if warning is not None: + warning = warning + " " if standard_warn else "" + warnings.warn(warning + message, FutureWarning, stacklevel=2) + + if isinstance(deprecated_kwargs, dict) and len(deprecated_kwargs) > 0: + call_frame = inspect.getouterframes(inspect.currentframe())[1] + filename = call_frame.filename + line_number = call_frame.lineno + function = call_frame.function + key, value = next(iter(deprecated_kwargs.items())) + raise TypeError(f"{function} in {filename} line {line_number-1} got an unexpected keyword argument `{key}`") + + if len(values) == 0: + return + elif len(values) == 1: + return values[0] + return values diff --git a/diffusers/src/diffusers/utils/doc_utils.py b/diffusers/src/diffusers/utils/doc_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ba79175c14bccf17dcc19102a4b923b6695d77b8 --- /dev/null +++ b/diffusers/src/diffusers/utils/doc_utils.py @@ -0,0 +1,38 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +""" +Doc utilities: Utilities related to documentation +""" +import re + + +def replace_example_docstring(example_docstring): + def docstring_decorator(fn): + func_doc = fn.__doc__ + lines = func_doc.split("\n") + i = 0 + while i < len(lines) and re.search(r"^\s*Examples?:\s*$", lines[i]) is None: + i += 1 + if i < len(lines): + lines[i] = example_docstring + func_doc = "\n".join(lines) + else: + raise ValueError( + f"The function {fn} should have an empty 'Examples:' in its docstring as placeholder, " + f"current docstring is:\n{func_doc}" + ) + fn.__doc__ = func_doc + return fn + + return docstring_decorator diff --git a/diffusers/src/diffusers/utils/dummy_flax_and_transformers_objects.py b/diffusers/src/diffusers/utils/dummy_flax_and_transformers_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..5db4c7d58d1e9c17c8824c1d24edf88e44799eba --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_flax_and_transformers_objects.py @@ -0,0 +1,47 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class FlaxStableDiffusionImg2ImgPipeline(metaclass=DummyObject): + _backends = ["flax", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) + + +class FlaxStableDiffusionInpaintPipeline(metaclass=DummyObject): + _backends = ["flax", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) + + +class FlaxStableDiffusionPipeline(metaclass=DummyObject): + _backends = ["flax", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax", "transformers"]) diff --git a/diffusers/src/diffusers/utils/dummy_flax_objects.py b/diffusers/src/diffusers/utils/dummy_flax_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..7772c1a06b49dc970a82243295106c6c01595d72 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_flax_objects.py @@ -0,0 +1,182 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class FlaxModelMixin(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxUNet2DConditionModel(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxAutoencoderKL(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxDiffusionPipeline(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxDDIMScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxDDPMScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxDPMSolverMultistepScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxKarrasVeScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxLMSDiscreteScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxPNDMScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxSchedulerMixin(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + +class FlaxScoreSdeVeScheduler(metaclass=DummyObject): + _backends = ["flax"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["flax"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["flax"]) diff --git a/diffusers/src/diffusers/utils/dummy_onnx_objects.py b/diffusers/src/diffusers/utils/dummy_onnx_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..bde5f6ad0793e2d81bc638600b46ff81748d09ee --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_onnx_objects.py @@ -0,0 +1,17 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class OnnxRuntimeModel(metaclass=DummyObject): + _backends = ["onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["onnx"]) diff --git a/diffusers/src/diffusers/utils/dummy_pt_objects.py b/diffusers/src/diffusers/utils/dummy_pt_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..546992bc436e402d07dde741eecf0df5f63f79b0 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_pt_objects.py @@ -0,0 +1,630 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class AutoencoderKL(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class ModelMixin(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class PriorTransformer(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class Transformer2DModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class UNet1DModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class UNet2DConditionModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class UNet2DModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class VQModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +def get_constant_schedule(*args, **kwargs): + requires_backends(get_constant_schedule, ["torch"]) + + +def get_constant_schedule_with_warmup(*args, **kwargs): + requires_backends(get_constant_schedule_with_warmup, ["torch"]) + + +def get_cosine_schedule_with_warmup(*args, **kwargs): + requires_backends(get_cosine_schedule_with_warmup, ["torch"]) + + +def get_cosine_with_hard_restarts_schedule_with_warmup(*args, **kwargs): + requires_backends(get_cosine_with_hard_restarts_schedule_with_warmup, ["torch"]) + + +def get_linear_schedule_with_warmup(*args, **kwargs): + requires_backends(get_linear_schedule_with_warmup, ["torch"]) + + +def get_polynomial_decay_schedule_with_warmup(*args, **kwargs): + requires_backends(get_polynomial_decay_schedule_with_warmup, ["torch"]) + + +def get_scheduler(*args, **kwargs): + requires_backends(get_scheduler, ["torch"]) + + +class AudioPipelineOutput(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DanceDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DDIMPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DDPMPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DiffusionPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DiTPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class ImagePipelineOutput(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class KarrasVePipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class LDMPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class LDMSuperResolutionPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class PNDMPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class RePaintPipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class ScoreSdeVePipeline(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DDIMScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DDPMScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DEISMultistepScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DPMSolverMultistepScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class DPMSolverSinglestepScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class EulerAncestralDiscreteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class EulerDiscreteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class HeunDiscreteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class IPNDMScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class KarrasVeScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class KDPM2AncestralDiscreteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class KDPM2DiscreteScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class PNDMScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class RePaintScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class SchedulerMixin(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class ScoreSdeVeScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class UnCLIPScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class VQDiffusionScheduler(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + +class EMAModel(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) diff --git a/diffusers/src/diffusers/utils/dummy_torch_and_librosa_objects.py b/diffusers/src/diffusers/utils/dummy_torch_and_librosa_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..2088bc4a744198284f22fe54e6f1055cf3568566 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_torch_and_librosa_objects.py @@ -0,0 +1,32 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class AudioDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "librosa"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "librosa"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "librosa"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "librosa"]) + + +class Mel(metaclass=DummyObject): + _backends = ["torch", "librosa"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "librosa"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "librosa"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "librosa"]) diff --git a/diffusers/src/diffusers/utils/dummy_torch_and_scipy_objects.py b/diffusers/src/diffusers/utils/dummy_torch_and_scipy_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..a1ff25863822b04971d2c6dfdc17f5b28774cf05 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_torch_and_scipy_objects.py @@ -0,0 +1,17 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class LMSDiscreteScheduler(metaclass=DummyObject): + _backends = ["torch", "scipy"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "scipy"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "scipy"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "scipy"]) diff --git a/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_k_diffusion_objects.py b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_k_diffusion_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..56836f0b6d77b8daa25e956101694863e418339f --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_k_diffusion_objects.py @@ -0,0 +1,17 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class StableDiffusionKDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers", "k_diffusion"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "k_diffusion"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "k_diffusion"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "k_diffusion"]) diff --git a/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_onnx_objects.py b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_onnx_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..204500a1f195790aabf4a0136de0f0900faec5c9 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_and_onnx_objects.py @@ -0,0 +1,77 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class OnnxStableDiffusionImg2ImgPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers", "onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + +class OnnxStableDiffusionInpaintPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers", "onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + +class OnnxStableDiffusionInpaintPipelineLegacy(metaclass=DummyObject): + _backends = ["torch", "transformers", "onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + +class OnnxStableDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers", "onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + +class StableDiffusionOnnxPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers", "onnx"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers", "onnx"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers", "onnx"]) diff --git a/diffusers/src/diffusers/utils/dummy_torch_and_transformers_objects.py b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_objects.py new file mode 100644 index 0000000000000000000000000000000000000000..79755c27e6fe0f62305327146d155f22de905d04 --- /dev/null +++ b/diffusers/src/diffusers/utils/dummy_torch_and_transformers_objects.py @@ -0,0 +1,332 @@ +# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +class AltDiffusionImg2ImgPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class AltDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class CycleDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class LDMTextToImagePipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class PaintByExamplePipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionDepth2ImgPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionImageVariationPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionImg2ImgPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionInpaintPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionInpaintPipelineLegacy(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionInstructPix2PixPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionLatentUpscalePipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionPipelineSafe(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class StableDiffusionUpscalePipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class UnCLIPImageVariationPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class UnCLIPPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class VersatileDiffusionDualGuidedPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class VersatileDiffusionImageVariationPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class VersatileDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class VersatileDiffusionTextToImagePipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + +class VQDiffusionPipeline(metaclass=DummyObject): + _backends = ["torch", "transformers"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch", "transformers"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch", "transformers"]) diff --git a/diffusers/src/diffusers/utils/dynamic_modules_utils.py b/diffusers/src/diffusers/utils/dynamic_modules_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..464257bd7b352fd00003c3496547663dd5d79e3b --- /dev/null +++ b/diffusers/src/diffusers/utils/dynamic_modules_utils.py @@ -0,0 +1,456 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +"""Utilities to dynamically load objects from the Hub.""" + +import importlib +import inspect +import json +import os +import re +import shutil +import sys +from distutils.version import StrictVersion +from pathlib import Path +from typing import Dict, Optional, Union +from urllib import request + +from huggingface_hub import HfFolder, cached_download, hf_hub_download, model_info + +from .. import __version__ +from . import DIFFUSERS_DYNAMIC_MODULE_NAME, HF_MODULES_CACHE, logging + + +COMMUNITY_PIPELINES_URL = ( + "https://raw.githubusercontent.com/huggingface/diffusers/{revision}/examples/community/{pipeline}.py" +) + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def get_diffusers_versions(): + url = "https://pypi.org/pypi/diffusers/json" + releases = json.loads(request.urlopen(url).read())["releases"].keys() + return sorted(releases, key=StrictVersion) + + +def init_hf_modules(): + """ + Creates the cache directory for modules with an init, and adds it to the Python path. + """ + # This function has already been executed if HF_MODULES_CACHE already is in the Python path. + if HF_MODULES_CACHE in sys.path: + return + + sys.path.append(HF_MODULES_CACHE) + os.makedirs(HF_MODULES_CACHE, exist_ok=True) + init_path = Path(HF_MODULES_CACHE) / "__init__.py" + if not init_path.exists(): + init_path.touch() + + +def create_dynamic_module(name: Union[str, os.PathLike]): + """ + Creates a dynamic module in the cache directory for modules. + """ + init_hf_modules() + dynamic_module_path = Path(HF_MODULES_CACHE) / name + # If the parent module does not exist yet, recursively create it. + if not dynamic_module_path.parent.exists(): + create_dynamic_module(dynamic_module_path.parent) + os.makedirs(dynamic_module_path, exist_ok=True) + init_path = dynamic_module_path / "__init__.py" + if not init_path.exists(): + init_path.touch() + + +def get_relative_imports(module_file): + """ + Get the list of modules that are relatively imported in a module file. + + Args: + module_file (`str` or `os.PathLike`): The module file to inspect. + """ + with open(module_file, "r", encoding="utf-8") as f: + content = f.read() + + # Imports of the form `import .xxx` + relative_imports = re.findall("^\s*import\s+\.(\S+)\s*$", content, flags=re.MULTILINE) + # Imports of the form `from .xxx import yyy` + relative_imports += re.findall("^\s*from\s+\.(\S+)\s+import", content, flags=re.MULTILINE) + # Unique-ify + return list(set(relative_imports)) + + +def get_relative_import_files(module_file): + """ + Get the list of all files that are needed for a given module. Note that this function recurses through the relative + imports (if a imports b and b imports c, it will return module files for b and c). + + Args: + module_file (`str` or `os.PathLike`): The module file to inspect. + """ + no_change = False + files_to_check = [module_file] + all_relative_imports = [] + + # Let's recurse through all relative imports + while not no_change: + new_imports = [] + for f in files_to_check: + new_imports.extend(get_relative_imports(f)) + + module_path = Path(module_file).parent + new_import_files = [str(module_path / m) for m in new_imports] + new_import_files = [f for f in new_import_files if f not in all_relative_imports] + files_to_check = [f"{f}.py" for f in new_import_files] + + no_change = len(new_import_files) == 0 + all_relative_imports.extend(files_to_check) + + return all_relative_imports + + +def check_imports(filename): + """ + Check if the current Python environment contains all the libraries that are imported in a file. + """ + with open(filename, "r", encoding="utf-8") as f: + content = f.read() + + # Imports of the form `import xxx` + imports = re.findall("^\s*import\s+(\S+)\s*$", content, flags=re.MULTILINE) + # Imports of the form `from xxx import yyy` + imports += re.findall("^\s*from\s+(\S+)\s+import", content, flags=re.MULTILINE) + # Only keep the top-level module + imports = [imp.split(".")[0] for imp in imports if not imp.startswith(".")] + + # Unique-ify and test we got them all + imports = list(set(imports)) + missing_packages = [] + for imp in imports: + try: + importlib.import_module(imp) + except ImportError: + missing_packages.append(imp) + + if len(missing_packages) > 0: + raise ImportError( + "This modeling file requires the following packages that were not found in your environment: " + f"{', '.join(missing_packages)}. Run `pip install {' '.join(missing_packages)}`" + ) + + return get_relative_imports(filename) + + +def get_class_in_module(class_name, module_path): + """ + Import a module on the cache directory for modules and extract a class from it. + """ + module_path = module_path.replace(os.path.sep, ".") + module = importlib.import_module(module_path) + + if class_name is None: + return find_pipeline_class(module) + return getattr(module, class_name) + + +def find_pipeline_class(loaded_module): + """ + Retrieve pipeline class that inherits from `DiffusionPipeline`. Note that there has to be exactly one class + inheriting from `DiffusionPipeline`. + """ + from ..pipelines import DiffusionPipeline + + cls_members = dict(inspect.getmembers(loaded_module, inspect.isclass)) + + pipeline_class = None + for cls_name, cls in cls_members.items(): + if ( + cls_name != DiffusionPipeline.__name__ + and issubclass(cls, DiffusionPipeline) + and cls.__module__.split(".")[0] != "diffusers" + ): + if pipeline_class is not None: + raise ValueError( + f"Multiple classes that inherit from {DiffusionPipeline.__name__} have been found:" + f" {pipeline_class.__name__}, and {cls_name}. Please make sure to define only one in" + f" {loaded_module}." + ) + pipeline_class = cls + + return pipeline_class + + +def get_cached_module_file( + pretrained_model_name_or_path: Union[str, os.PathLike], + module_file: str, + cache_dir: Optional[Union[str, os.PathLike]] = None, + force_download: bool = False, + resume_download: bool = False, + proxies: Optional[Dict[str, str]] = None, + use_auth_token: Optional[Union[bool, str]] = None, + revision: Optional[str] = None, + local_files_only: bool = False, +): + """ + Prepares Downloads a module from a local folder or a distant repo and returns its path inside the cached + Transformers module. + + Args: + pretrained_model_name_or_path (`str` or `os.PathLike`): + This can be either: + + - a string, the *model id* of a pretrained model configuration hosted inside a model repo on + huggingface.co. Valid model ids can be located at the root-level, like `bert-base-uncased`, or namespaced + under a user or organization name, like `dbmdz/bert-base-german-cased`. + - a path to a *directory* containing a configuration file saved using the + [`~PreTrainedTokenizer.save_pretrained`] method, e.g., `./my_model_directory/`. + + module_file (`str`): + The name of the module file containing the class to look for. + cache_dir (`str` or `os.PathLike`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the standard + cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force to (re-)download the configuration files and override the cached versions if they + exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received file. Attempts to resume the download if such a file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}.` The proxies are used on each request. + use_auth_token (`str` or *bool*, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `transformers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + local_files_only (`bool`, *optional*, defaults to `False`): + If `True`, will only try to load the tokenizer configuration from local files. + + + + You may pass a token in `use_auth_token` if you are not logged in (`huggingface-cli long`) and want to use private + or [gated models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + Returns: + `str`: The path to the module inside the cache. + """ + # Download and cache module_file from the repo `pretrained_model_name_or_path` of grab it if it's a local file. + pretrained_model_name_or_path = str(pretrained_model_name_or_path) + + module_file_or_url = os.path.join(pretrained_model_name_or_path, module_file) + + if os.path.isfile(module_file_or_url): + resolved_module_file = module_file_or_url + submodule = "local" + elif pretrained_model_name_or_path.count("/") == 0: + available_versions = get_diffusers_versions() + # cut ".dev0" + latest_version = "v" + ".".join(__version__.split(".")[:3]) + + # retrieve github version that matches + if revision is None: + revision = latest_version if latest_version in available_versions else "main" + logger.info(f"Defaulting to latest_version: {revision}.") + elif revision in available_versions: + revision = f"v{revision}" + elif revision == "main": + revision = revision + else: + raise ValueError( + f"`custom_revision`: {revision} does not exist. Please make sure to choose one of" + f" {', '.join(available_versions + ['main'])}." + ) + + # community pipeline on GitHub + github_url = COMMUNITY_PIPELINES_URL.format(revision=revision, pipeline=pretrained_model_name_or_path) + try: + resolved_module_file = cached_download( + github_url, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + local_files_only=local_files_only, + use_auth_token=False, + ) + submodule = "git" + module_file = pretrained_model_name_or_path + ".py" + except EnvironmentError: + logger.error(f"Could not locate the {module_file} inside {pretrained_model_name_or_path}.") + raise + else: + try: + # Load from URL or cache if already cached + resolved_module_file = hf_hub_download( + pretrained_model_name_or_path, + module_file, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + local_files_only=local_files_only, + use_auth_token=use_auth_token, + ) + submodule = os.path.join("local", "--".join(pretrained_model_name_or_path.split("/"))) + except EnvironmentError: + logger.error(f"Could not locate the {module_file} inside {pretrained_model_name_or_path}.") + raise + + # Check we have all the requirements in our environment + modules_needed = check_imports(resolved_module_file) + + # Now we move the module inside our cached dynamic modules. + full_submodule = DIFFUSERS_DYNAMIC_MODULE_NAME + os.path.sep + submodule + create_dynamic_module(full_submodule) + submodule_path = Path(HF_MODULES_CACHE) / full_submodule + if submodule == "local" or submodule == "git": + # We always copy local files (we could hash the file to see if there was a change, and give them the name of + # that hash, to only copy when there is a modification but it seems overkill for now). + # The only reason we do the copy is to avoid putting too many folders in sys.path. + shutil.copy(resolved_module_file, submodule_path / module_file) + for module_needed in modules_needed: + module_needed = f"{module_needed}.py" + shutil.copy(os.path.join(pretrained_model_name_or_path, module_needed), submodule_path / module_needed) + else: + # Get the commit hash + # TODO: we will get this info in the etag soon, so retrieve it from there and not here. + if isinstance(use_auth_token, str): + token = use_auth_token + elif use_auth_token is True: + token = HfFolder.get_token() + else: + token = None + + commit_hash = model_info(pretrained_model_name_or_path, revision=revision, token=token).sha + + # The module file will end up being placed in a subfolder with the git hash of the repo. This way we get the + # benefit of versioning. + submodule_path = submodule_path / commit_hash + full_submodule = full_submodule + os.path.sep + commit_hash + create_dynamic_module(full_submodule) + + if not (submodule_path / module_file).exists(): + shutil.copy(resolved_module_file, submodule_path / module_file) + # Make sure we also have every file with relative + for module_needed in modules_needed: + if not (submodule_path / module_needed).exists(): + get_cached_module_file( + pretrained_model_name_or_path, + f"{module_needed}.py", + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + use_auth_token=use_auth_token, + revision=revision, + local_files_only=local_files_only, + ) + return os.path.join(full_submodule, module_file) + + +def get_class_from_dynamic_module( + pretrained_model_name_or_path: Union[str, os.PathLike], + module_file: str, + class_name: Optional[str] = None, + cache_dir: Optional[Union[str, os.PathLike]] = None, + force_download: bool = False, + resume_download: bool = False, + proxies: Optional[Dict[str, str]] = None, + use_auth_token: Optional[Union[bool, str]] = None, + revision: Optional[str] = None, + local_files_only: bool = False, + **kwargs, +): + """ + Extracts a class from a module file, present in the local folder or repository of a model. + + + + Calling this function will execute the code in the module file found locally or downloaded from the Hub. It should + therefore only be called on trusted repos. + + + + Args: + pretrained_model_name_or_path (`str` or `os.PathLike`): + This can be either: + + - a string, the *model id* of a pretrained model configuration hosted inside a model repo on + huggingface.co. Valid model ids can be located at the root-level, like `bert-base-uncased`, or namespaced + under a user or organization name, like `dbmdz/bert-base-german-cased`. + - a path to a *directory* containing a configuration file saved using the + [`~PreTrainedTokenizer.save_pretrained`] method, e.g., `./my_model_directory/`. + + module_file (`str`): + The name of the module file containing the class to look for. + class_name (`str`): + The name of the class to import in the module. + cache_dir (`str` or `os.PathLike`, *optional*): + Path to a directory in which a downloaded pretrained model configuration should be cached if the standard + cache should not be used. + force_download (`bool`, *optional*, defaults to `False`): + Whether or not to force to (re-)download the configuration files and override the cached versions if they + exist. + resume_download (`bool`, *optional*, defaults to `False`): + Whether or not to delete incompletely received file. Attempts to resume the download if such a file exists. + proxies (`Dict[str, str]`, *optional*): + A dictionary of proxy servers to use by protocol or endpoint, e.g., `{'http': 'foo.bar:3128', + 'http://hostname': 'foo.bar:4012'}.` The proxies are used on each request. + use_auth_token (`str` or `bool`, *optional*): + The token to use as HTTP bearer authorization for remote files. If `True`, will use the token generated + when running `transformers-cli login` (stored in `~/.huggingface`). + revision (`str`, *optional*, defaults to `"main"`): + The specific model version to use. It can be a branch name, a tag name, or a commit id, since we use a + git-based system for storing models and other artifacts on huggingface.co, so `revision` can be any + identifier allowed by git. + local_files_only (`bool`, *optional*, defaults to `False`): + If `True`, will only try to load the tokenizer configuration from local files. + + + + You may pass a token in `use_auth_token` if you are not logged in (`huggingface-cli long`) and want to use private + or [gated models](https://huggingface.co/docs/hub/models-gated#gated-models). + + + + Returns: + `type`: The class, dynamically imported from the module. + + Examples: + + ```python + # Download module `modeling.py` from huggingface.co and cache then extract the class `MyBertModel` from this + # module. + cls = get_class_from_dynamic_module("sgugger/my-bert-model", "modeling.py", "MyBertModel") + ```""" + # And lastly we get the class inside our newly created module + final_module = get_cached_module_file( + pretrained_model_name_or_path, + module_file, + cache_dir=cache_dir, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + use_auth_token=use_auth_token, + revision=revision, + local_files_only=local_files_only, + ) + return get_class_in_module(class_name, final_module.replace(".py", "")) diff --git a/diffusers/src/diffusers/utils/hub_utils.py b/diffusers/src/diffusers/utils/hub_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7e6bd7870de770ba69f5bacac8ea8c2a9488d416 --- /dev/null +++ b/diffusers/src/diffusers/utils/hub_utils.py @@ -0,0 +1,131 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + + +import os +import sys +from pathlib import Path +from typing import Dict, Optional, Union +from uuid import uuid4 + +from huggingface_hub import HfFolder, ModelCard, ModelCardData, whoami +from huggingface_hub.utils import is_jinja_available + +from .. import __version__ +from .constants import HUGGINGFACE_CO_RESOLVE_ENDPOINT +from .import_utils import ( + ENV_VARS_TRUE_VALUES, + _flax_version, + _jax_version, + _onnxruntime_version, + _torch_version, + is_flax_available, + is_onnx_available, + is_torch_available, +) +from .logging import get_logger + + +logger = get_logger(__name__) + + +MODEL_CARD_TEMPLATE_PATH = Path(__file__).parent / "model_card_template.md" +SESSION_ID = uuid4().hex +HF_HUB_OFFLINE = os.getenv("HF_HUB_OFFLINE", "").upper() in ENV_VARS_TRUE_VALUES +DISABLE_TELEMETRY = os.getenv("DISABLE_TELEMETRY", "").upper() in ENV_VARS_TRUE_VALUES +HUGGINGFACE_CO_TELEMETRY = HUGGINGFACE_CO_RESOLVE_ENDPOINT + "/api/telemetry/" + + +def http_user_agent(user_agent: Union[Dict, str, None] = None) -> str: + """ + Formats a user-agent string with basic info about a request. + """ + ua = f"diffusers/{__version__}; python/{sys.version.split()[0]}; session_id/{SESSION_ID}" + if DISABLE_TELEMETRY or HF_HUB_OFFLINE: + return ua + "; telemetry/off" + if is_torch_available(): + ua += f"; torch/{_torch_version}" + if is_flax_available(): + ua += f"; jax/{_jax_version}" + ua += f"; flax/{_flax_version}" + if is_onnx_available(): + ua += f"; onnxruntime/{_onnxruntime_version}" + # CI will set this value to True + if os.environ.get("DIFFUSERS_IS_CI", "").upper() in ENV_VARS_TRUE_VALUES: + ua += "; is_ci/true" + if isinstance(user_agent, dict): + ua += "; " + "; ".join(f"{k}/{v}" for k, v in user_agent.items()) + elif isinstance(user_agent, str): + ua += "; " + user_agent + return ua + + +def get_full_repo_name(model_id: str, organization: Optional[str] = None, token: Optional[str] = None): + if token is None: + token = HfFolder.get_token() + if organization is None: + username = whoami(token)["name"] + return f"{username}/{model_id}" + else: + return f"{organization}/{model_id}" + + +def create_model_card(args, model_name): + if not is_jinja_available(): + raise ValueError( + "Modelcard rendering is based on Jinja templates." + " Please make sure to have `jinja` installed before using `create_model_card`." + " To install it, please run `pip install Jinja2`." + ) + + if hasattr(args, "local_rank") and args.local_rank not in [-1, 0]: + return + + hub_token = args.hub_token if hasattr(args, "hub_token") else None + repo_name = get_full_repo_name(model_name, token=hub_token) + + model_card = ModelCard.from_template( + card_data=ModelCardData( # Card metadata object that will be converted to YAML block + language="en", + license="apache-2.0", + library_name="diffusers", + tags=[], + datasets=args.dataset_name, + metrics=[], + ), + template_path=MODEL_CARD_TEMPLATE_PATH, + model_name=model_name, + repo_name=repo_name, + dataset_name=args.dataset_name if hasattr(args, "dataset_name") else None, + learning_rate=args.learning_rate, + train_batch_size=args.train_batch_size, + eval_batch_size=args.eval_batch_size, + gradient_accumulation_steps=( + args.gradient_accumulation_steps if hasattr(args, "gradient_accumulation_steps") else None + ), + adam_beta1=args.adam_beta1 if hasattr(args, "adam_beta1") else None, + adam_beta2=args.adam_beta2 if hasattr(args, "adam_beta2") else None, + adam_weight_decay=args.adam_weight_decay if hasattr(args, "adam_weight_decay") else None, + adam_epsilon=args.adam_epsilon if hasattr(args, "adam_epsilon") else None, + lr_scheduler=args.lr_scheduler if hasattr(args, "lr_scheduler") else None, + lr_warmup_steps=args.lr_warmup_steps if hasattr(args, "lr_warmup_steps") else None, + ema_inv_gamma=args.ema_inv_gamma if hasattr(args, "ema_inv_gamma") else None, + ema_power=args.ema_power if hasattr(args, "ema_power") else None, + ema_max_decay=args.ema_max_decay if hasattr(args, "ema_max_decay") else None, + mixed_precision=args.mixed_precision, + ) + + card_path = os.path.join(args.output_dir, "README.md") + model_card.save(card_path) diff --git a/diffusers/src/diffusers/utils/import_utils.py b/diffusers/src/diffusers/utils/import_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..cc607138758f7540fa6f0bfe0c15586a4a0458f9 --- /dev/null +++ b/diffusers/src/diffusers/utils/import_utils.py @@ -0,0 +1,494 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +""" +Import utilities: Utilities related to imports and our lazy inits. +""" +import importlib.util +import operator as op +import os +import sys +from collections import OrderedDict +from typing import Union + +from huggingface_hub.utils import is_jinja_available # noqa: F401 +from packaging import version +from packaging.version import Version, parse + +from . import logging + + +# The package importlib_metadata is in a different place, depending on the python version. +if sys.version_info < (3, 8): + import importlib_metadata +else: + import importlib.metadata as importlib_metadata + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + +ENV_VARS_TRUE_VALUES = {"1", "ON", "YES", "TRUE"} +ENV_VARS_TRUE_AND_AUTO_VALUES = ENV_VARS_TRUE_VALUES.union({"AUTO"}) + +USE_TF = os.environ.get("USE_TF", "AUTO").upper() +USE_TORCH = os.environ.get("USE_TORCH", "AUTO").upper() +USE_JAX = os.environ.get("USE_FLAX", "AUTO").upper() +USE_SAFETENSORS = os.environ.get("USE_SAFETENSORS", "AUTO").upper() + +STR_OPERATION_TO_FUNC = {">": op.gt, ">=": op.ge, "==": op.eq, "!=": op.ne, "<=": op.le, "<": op.lt} + +_torch_version = "N/A" +if USE_TORCH in ENV_VARS_TRUE_AND_AUTO_VALUES and USE_TF not in ENV_VARS_TRUE_VALUES: + _torch_available = importlib.util.find_spec("torch") is not None + if _torch_available: + try: + _torch_version = importlib_metadata.version("torch") + logger.info(f"PyTorch version {_torch_version} available.") + except importlib_metadata.PackageNotFoundError: + _torch_available = False +else: + logger.info("Disabling PyTorch because USE_TORCH is set") + _torch_available = False + + +_tf_version = "N/A" +if USE_TF in ENV_VARS_TRUE_AND_AUTO_VALUES and USE_TORCH not in ENV_VARS_TRUE_VALUES: + _tf_available = importlib.util.find_spec("tensorflow") is not None + if _tf_available: + candidates = ( + "tensorflow", + "tensorflow-cpu", + "tensorflow-gpu", + "tf-nightly", + "tf-nightly-cpu", + "tf-nightly-gpu", + "intel-tensorflow", + "intel-tensorflow-avx512", + "tensorflow-rocm", + "tensorflow-macos", + "tensorflow-aarch64", + ) + _tf_version = None + # For the metadata, we have to look for both tensorflow and tensorflow-cpu + for pkg in candidates: + try: + _tf_version = importlib_metadata.version(pkg) + break + except importlib_metadata.PackageNotFoundError: + pass + _tf_available = _tf_version is not None + if _tf_available: + if version.parse(_tf_version) < version.parse("2"): + logger.info(f"TensorFlow found but with version {_tf_version}. Diffusers requires version 2 minimum.") + _tf_available = False + else: + logger.info(f"TensorFlow version {_tf_version} available.") +else: + logger.info("Disabling Tensorflow because USE_TORCH is set") + _tf_available = False + +_jax_version = "N/A" +_flax_version = "N/A" +if USE_JAX in ENV_VARS_TRUE_AND_AUTO_VALUES: + _flax_available = importlib.util.find_spec("jax") is not None and importlib.util.find_spec("flax") is not None + if _flax_available: + try: + _jax_version = importlib_metadata.version("jax") + _flax_version = importlib_metadata.version("flax") + logger.info(f"JAX version {_jax_version}, Flax version {_flax_version} available.") + except importlib_metadata.PackageNotFoundError: + _flax_available = False +else: + _flax_available = False + +if USE_SAFETENSORS in ENV_VARS_TRUE_AND_AUTO_VALUES: + _safetensors_available = importlib.util.find_spec("safetensors") is not None + if _safetensors_available: + try: + _safetensors_version = importlib_metadata.version("safetensors") + logger.info(f"Safetensors version {_safetensors_version} available.") + except importlib_metadata.PackageNotFoundError: + _safetensors_available = False +else: + logger.info("Disabling Safetensors because USE_TF is set") + _safetensors_available = False + +_transformers_available = importlib.util.find_spec("transformers") is not None +try: + _transformers_version = importlib_metadata.version("transformers") + logger.debug(f"Successfully imported transformers version {_transformers_version}") +except importlib_metadata.PackageNotFoundError: + _transformers_available = False + + +_inflect_available = importlib.util.find_spec("inflect") is not None +try: + _inflect_version = importlib_metadata.version("inflect") + logger.debug(f"Successfully imported inflect version {_inflect_version}") +except importlib_metadata.PackageNotFoundError: + _inflect_available = False + + +_unidecode_available = importlib.util.find_spec("unidecode") is not None +try: + _unidecode_version = importlib_metadata.version("unidecode") + logger.debug(f"Successfully imported unidecode version {_unidecode_version}") +except importlib_metadata.PackageNotFoundError: + _unidecode_available = False + + +_onnxruntime_version = "N/A" +_onnx_available = importlib.util.find_spec("onnxruntime") is not None +if _onnx_available: + candidates = ( + "onnxruntime", + "onnxruntime-gpu", + "onnxruntime-directml", + "onnxruntime-openvino", + "ort_nightly_directml", + ) + _onnxruntime_version = None + # For the metadata, we have to look for both onnxruntime and onnxruntime-gpu + for pkg in candidates: + try: + _onnxruntime_version = importlib_metadata.version(pkg) + break + except importlib_metadata.PackageNotFoundError: + pass + _onnx_available = _onnxruntime_version is not None + if _onnx_available: + logger.debug(f"Successfully imported onnxruntime version {_onnxruntime_version}") + + +_scipy_available = importlib.util.find_spec("scipy") is not None +try: + _scipy_version = importlib_metadata.version("scipy") + logger.debug(f"Successfully imported scipy version {_scipy_version}") +except importlib_metadata.PackageNotFoundError: + _scipy_available = False + +_librosa_available = importlib.util.find_spec("librosa") is not None +try: + _librosa_version = importlib_metadata.version("librosa") + logger.debug(f"Successfully imported librosa version {_librosa_version}") +except importlib_metadata.PackageNotFoundError: + _librosa_available = False + +_accelerate_available = importlib.util.find_spec("accelerate") is not None +try: + _accelerate_version = importlib_metadata.version("accelerate") + logger.debug(f"Successfully imported accelerate version {_accelerate_version}") +except importlib_metadata.PackageNotFoundError: + _accelerate_available = False + +_xformers_available = importlib.util.find_spec("xformers") is not None +try: + _xformers_version = importlib_metadata.version("xformers") + if _torch_available: + import torch + + if version.Version(torch.__version__) < version.Version("1.12"): + raise ValueError("PyTorch should be >= 1.12") + logger.debug(f"Successfully imported xformers version {_xformers_version}") +except importlib_metadata.PackageNotFoundError: + _xformers_available = False + +_k_diffusion_available = importlib.util.find_spec("k_diffusion") is not None +try: + _k_diffusion_version = importlib_metadata.version("k_diffusion") + logger.debug(f"Successfully imported k-diffusion version {_k_diffusion_version}") +except importlib_metadata.PackageNotFoundError: + _k_diffusion_available = False + +_wandb_available = importlib.util.find_spec("wandb") is not None +try: + _wandb_version = importlib_metadata.version("wandb") + logger.debug(f"Successfully imported wandb version {_wandb_version }") +except importlib_metadata.PackageNotFoundError: + _wandb_available = False + +_omegaconf_available = importlib.util.find_spec("omegaconf") is not None +try: + _omegaconf_version = importlib_metadata.version("omegaconf") + logger.debug(f"Successfully imported omegaconf version {_omegaconf_version}") +except importlib_metadata.PackageNotFoundError: + _omegaconf_available = False + +_tensorboard_available = importlib.util.find_spec("tensorboard") +try: + _tensorboard_version = importlib_metadata.version("tensorboard") + logger.debug(f"Successfully imported tensorboard version {_tensorboard_version}") +except importlib_metadata.PackageNotFoundError: + _tensorboard_available = False + + +def is_torch_available(): + return _torch_available + + +def is_safetensors_available(): + return _safetensors_available + + +def is_tf_available(): + return _tf_available + + +def is_flax_available(): + return _flax_available + + +def is_transformers_available(): + return _transformers_available + + +def is_inflect_available(): + return _inflect_available + + +def is_unidecode_available(): + return _unidecode_available + + +def is_onnx_available(): + return _onnx_available + + +def is_scipy_available(): + return _scipy_available + + +def is_librosa_available(): + return _librosa_available + + +def is_xformers_available(): + return _xformers_available + + +def is_accelerate_available(): + return _accelerate_available + + +def is_k_diffusion_available(): + return _k_diffusion_available + + +def is_wandb_available(): + return _wandb_available + + +def is_omegaconf_available(): + return _omegaconf_available + + +def is_tensorboard_available(): + return _tensorboard_available + + +# docstyle-ignore +FLAX_IMPORT_ERROR = """ +{0} requires the FLAX library but it was not found in your environment. Checkout the instructions on the +installation page: https://github.com/google/flax and follow the ones that match your environment. +""" + +# docstyle-ignore +INFLECT_IMPORT_ERROR = """ +{0} requires the inflect library but it was not found in your environment. You can install it with pip: `pip install +inflect` +""" + +# docstyle-ignore +PYTORCH_IMPORT_ERROR = """ +{0} requires the PyTorch library but it was not found in your environment. Checkout the instructions on the +installation page: https://pytorch.org/get-started/locally/ and follow the ones that match your environment. +""" + +# docstyle-ignore +ONNX_IMPORT_ERROR = """ +{0} requires the onnxruntime library but it was not found in your environment. You can install it with pip: `pip +install onnxruntime` +""" + +# docstyle-ignore +SCIPY_IMPORT_ERROR = """ +{0} requires the scipy library but it was not found in your environment. You can install it with pip: `pip install +scipy` +""" + +# docstyle-ignore +LIBROSA_IMPORT_ERROR = """ +{0} requires the librosa library but it was not found in your environment. Checkout the instructions on the +installation page: https://librosa.org/doc/latest/install.html and follow the ones that match your environment. +""" + +# docstyle-ignore +TRANSFORMERS_IMPORT_ERROR = """ +{0} requires the transformers library but it was not found in your environment. You can install it with pip: `pip +install transformers` +""" + +# docstyle-ignore +UNIDECODE_IMPORT_ERROR = """ +{0} requires the unidecode library but it was not found in your environment. You can install it with pip: `pip install +Unidecode` +""" + +# docstyle-ignore +K_DIFFUSION_IMPORT_ERROR = """ +{0} requires the k-diffusion library but it was not found in your environment. You can install it with pip: `pip +install k-diffusion` +""" + +# docstyle-ignore +WANDB_IMPORT_ERROR = """ +{0} requires the wandb library but it was not found in your environment. You can install it with pip: `pip +install wandb` +""" + +# docstyle-ignore +OMEGACONF_IMPORT_ERROR = """ +{0} requires the omegaconf library but it was not found in your environment. You can install it with pip: `pip +install omegaconf` +""" + +# docstyle-ignore +TENSORBOARD_IMPORT_ERROR = """ +{0} requires the tensorboard library but it was not found in your environment. You can install it with pip: `pip +install tensorboard` +""" + +BACKENDS_MAPPING = OrderedDict( + [ + ("flax", (is_flax_available, FLAX_IMPORT_ERROR)), + ("inflect", (is_inflect_available, INFLECT_IMPORT_ERROR)), + ("onnx", (is_onnx_available, ONNX_IMPORT_ERROR)), + ("scipy", (is_scipy_available, SCIPY_IMPORT_ERROR)), + ("torch", (is_torch_available, PYTORCH_IMPORT_ERROR)), + ("transformers", (is_transformers_available, TRANSFORMERS_IMPORT_ERROR)), + ("unidecode", (is_unidecode_available, UNIDECODE_IMPORT_ERROR)), + ("librosa", (is_librosa_available, LIBROSA_IMPORT_ERROR)), + ("k_diffusion", (is_k_diffusion_available, K_DIFFUSION_IMPORT_ERROR)), + ("wandb", (is_wandb_available, WANDB_IMPORT_ERROR)), + ("omegaconf", (is_omegaconf_available, OMEGACONF_IMPORT_ERROR)), + ("tensorboard", (_tensorboard_available, TENSORBOARD_IMPORT_ERROR)), + ] +) + + +def requires_backends(obj, backends): + if not isinstance(backends, (list, tuple)): + backends = [backends] + + name = obj.__name__ if hasattr(obj, "__name__") else obj.__class__.__name__ + checks = (BACKENDS_MAPPING[backend] for backend in backends) + failed = [msg.format(name) for available, msg in checks if not available()] + if failed: + raise ImportError("".join(failed)) + + if name in [ + "VersatileDiffusionTextToImagePipeline", + "VersatileDiffusionPipeline", + "VersatileDiffusionDualGuidedPipeline", + "StableDiffusionImageVariationPipeline", + "UnCLIPPipeline", + ] and is_transformers_version("<", "4.25.0"): + raise ImportError( + f"You need to install `transformers>=4.25` in order to use {name}: \n```\n pip install" + " --upgrade transformers \n```" + ) + + if name in [ + "StableDiffusionDepth2ImgPipeline", + ] and is_transformers_version("<", "4.26.0"): + raise ImportError( + f"You need to install `transformers>=4.26` in order to use {name}: \n```\n pip install" + " --upgrade transformers \n```" + ) + + +class DummyObject(type): + """ + Metaclass for the dummy objects. Any class inheriting from it will return the ImportError generated by + `requires_backend` each time a user tries to access any method of that class. + """ + + def __getattr__(cls, key): + if key.startswith("_"): + return super().__getattr__(cls, key) + requires_backends(cls, cls._backends) + + +# This function was copied from: https://github.com/huggingface/accelerate/blob/874c4967d94badd24f893064cc3bef45f57cadf7/src/accelerate/utils/versions.py#L319 +def compare_versions(library_or_version: Union[str, Version], operation: str, requirement_version: str): + """ + Args: + Compares a library version to some requirement using a given operation. + library_or_version (`str` or `packaging.version.Version`): + A library name or a version to check. + operation (`str`): + A string representation of an operator, such as `">"` or `"<="`. + requirement_version (`str`): + The version to compare the library version against + """ + if operation not in STR_OPERATION_TO_FUNC.keys(): + raise ValueError(f"`operation` must be one of {list(STR_OPERATION_TO_FUNC.keys())}, received {operation}") + operation = STR_OPERATION_TO_FUNC[operation] + if isinstance(library_or_version, str): + library_or_version = parse(importlib_metadata.version(library_or_version)) + return operation(library_or_version, parse(requirement_version)) + + +# This function was copied from: https://github.com/huggingface/accelerate/blob/874c4967d94badd24f893064cc3bef45f57cadf7/src/accelerate/utils/versions.py#L338 +def is_torch_version(operation: str, version: str): + """ + Args: + Compares the current PyTorch version to a given reference with an operation. + operation (`str`): + A string representation of an operator, such as `">"` or `"<="` + version (`str`): + A string version of PyTorch + """ + return compare_versions(parse(_torch_version), operation, version) + + +def is_transformers_version(operation: str, version: str): + """ + Args: + Compares the current Transformers version to a given reference with an operation. + operation (`str`): + A string representation of an operator, such as `">"` or `"<="` + version (`str`): + A version string + """ + if not _transformers_available: + return False + return compare_versions(parse(_transformers_version), operation, version) + + +def is_k_diffusion_version(operation: str, version: str): + """ + Args: + Compares the current k-diffusion version to a given reference with an operation. + operation (`str`): + A string representation of an operator, such as `">"` or `"<="` + version (`str`): + A version string + """ + if not _k_diffusion_available: + return False + return compare_versions(parse(_k_diffusion_version), operation, version) + + +class OptionalDependencyNotAvailable(BaseException): + """An error indicating that an optional dependency of Diffusers was not found in the environment.""" diff --git a/diffusers/src/diffusers/utils/logging.py b/diffusers/src/diffusers/utils/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..1c682c2b0454dfc09da90e1ade8a0a721881b801 --- /dev/null +++ b/diffusers/src/diffusers/utils/logging.py @@ -0,0 +1,342 @@ +# coding=utf-8 +# Copyright 2022 Optuna, Hugging Face +# +# 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 + DEBUG, # NOQA + ERROR, # NOQA + FATAL, # NOQA + INFO, # NOQA + NOTSET, # NOQA + WARN, # NOQA + WARNING, # NOQA +) +from typing import Optional + +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.WARNING + +_tqdm_active = True + + +def _get_default_logging_level(): + """ + If DIFFUSERS_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("DIFFUSERS_VERBOSITY", None) + if env_level_str: + if env_level_str in log_levels: + return log_levels[env_level_str] + else: + logging.getLogger().warning( + f"Unknown option DIFFUSERS_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 + + +def get_logger(name: Optional[str] = None) -> logging.Logger: + """ + Return a logger with the specified name. + + This function is not supposed to be directly accessed unless you are writing a custom diffusers module. + """ + + if name is None: + name = _get_library_name() + + _configure_library_root_logger() + return logging.getLogger(name) + + +def get_verbosity() -> int: + """ + Return the current level for the 🤗 Diffusers' root logger as an int. + + Returns: + `int`: The logging level. + + + + 🤗 Diffusers has following logging levels: + + - 50: `diffusers.logging.CRITICAL` or `diffusers.logging.FATAL` + - 40: `diffusers.logging.ERROR` + - 30: `diffusers.logging.WARNING` or `diffusers.logging.WARN` + - 20: `diffusers.logging.INFO` + - 10: `diffusers.logging.DEBUG` + + """ + + _configure_library_root_logger() + return _get_library_root_logger().getEffectiveLevel() + + +def set_verbosity(verbosity: int) -> None: + """ + Set the verbosity level for the 🤗 Diffusers' root logger. + + Args: + verbosity (`int`): + Logging level, e.g., one of: + + - `diffusers.logging.CRITICAL` or `diffusers.logging.FATAL` + - `diffusers.logging.ERROR` + - `diffusers.logging.WARNING` or `diffusers.logging.WARN` + - `diffusers.logging.INFO` + - `diffusers.logging.DEBUG` + """ + + _configure_library_root_logger() + _get_library_root_logger().setLevel(verbosity) + + +def set_verbosity_info(): + """Set the verbosity to the `INFO` level.""" + return set_verbosity(INFO) + + +def set_verbosity_warning(): + """Set the verbosity to the `WARNING` level.""" + return set_verbosity(WARNING) + + +def set_verbosity_debug(): + """Set the verbosity to the `DEBUG` level.""" + return set_verbosity(DEBUG) + + +def set_verbosity_error(): + """Set the verbosity to the `ERROR` level.""" + return set_verbosity(ERROR) + + +def disable_default_handler() -> None: + """Disable the default handler of the HuggingFace Diffusers' 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 the HuggingFace Diffusers' 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 the HuggingFace Diffusers' 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 HuggingFace Diffusers' 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 HuggingFace Diffusers' 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 HuggingFace Diffusers' logger. The explicit formatter is as follows: + ``` + [LEVELNAME|FILENAME|LINE NUMBER] TIME >> 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("[%(levelname)s|%(filename)s:%(lineno)s] %(asctime)s >> %(message)s") + handler.setFormatter(formatter) + + +def reset_format() -> None: + """ + Resets the formatting for HuggingFace Diffusers' 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 DIFFUSERS_NO_ADVISORY_WARNINGS=1 is set, this + warning will not be printed + """ + no_advisory_warnings = os.getenv("DIFFUSERS_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 + + +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 diff --git a/diffusers/src/diffusers/utils/model_card_template.md b/diffusers/src/diffusers/utils/model_card_template.md new file mode 100644 index 0000000000000000000000000000000000000000..f19c85b0fcf2f7b07e9c3f950a9657b3f2053f21 --- /dev/null +++ b/diffusers/src/diffusers/utils/model_card_template.md @@ -0,0 +1,50 @@ +--- +{{ card_data }} +--- + + + +# {{ model_name | default("Diffusion Model") }} + +## Model description + +This diffusion model is trained with the [🤗 Diffusers](https://github.com/huggingface/diffusers) library +on the `{{ dataset_name }}` dataset. + +## Intended uses & limitations + +#### How to use + +```python +# TODO: add an example code snippet for running this diffusion pipeline +``` + +#### Limitations and bias + +[TODO: provide examples of latent issues and potential remediations] + +## Training data + +[TODO: describe the data used to train the model] + +### Training hyperparameters + +The following hyperparameters were used during training: +- learning_rate: {{ learning_rate }} +- train_batch_size: {{ train_batch_size }} +- eval_batch_size: {{ eval_batch_size }} +- gradient_accumulation_steps: {{ gradient_accumulation_steps }} +- optimizer: AdamW with betas=({{ adam_beta1 }}, {{ adam_beta2 }}), weight_decay={{ adam_weight_decay }} and epsilon={{ adam_epsilon }} +- lr_scheduler: {{ lr_scheduler }} +- lr_warmup_steps: {{ lr_warmup_steps }} +- ema_inv_gamma: {{ ema_inv_gamma }} +- ema_inv_gamma: {{ ema_power }} +- ema_inv_gamma: {{ ema_max_decay }} +- mixed_precision: {{ mixed_precision }} + +### Training results + +📈 [TensorBoard logs](https://huggingface.co/{{ repo_name }}/tensorboard?#scalars) + + diff --git a/diffusers/src/diffusers/utils/outputs.py b/diffusers/src/diffusers/utils/outputs.py new file mode 100644 index 0000000000000000000000000000000000000000..5d902dd394ccddc408d85b48e4142facc7242550 --- /dev/null +++ b/diffusers/src/diffusers/utils/outputs.py @@ -0,0 +1,108 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +""" +Generic utilities +""" + +from collections import OrderedDict +from dataclasses import fields +from typing import Any, Tuple + +import numpy as np + +from .import_utils import is_torch_available + + +def is_tensor(x): + """ + Tests if `x` is a `torch.Tensor` or `np.ndarray`. + """ + if is_torch_available(): + import torch + + if isinstance(x, torch.Tensor): + return True + + return isinstance(x, np.ndarray) + + +class BaseOutput(OrderedDict): + """ + Base class for all model outputs as dataclass. Has a `__getitem__` that allows indexing by integer or slice (like a + tuple) or strings (like a dictionary) that will ignore the `None` attributes. Otherwise behaves like a regular + python dictionary. + + + + You can't unpack a `BaseOutput` directly. Use the [`~utils.BaseOutput.to_tuple`] method to convert it to a tuple + before. + + + """ + + def __post_init__(self): + class_fields = fields(self) + + # Safety and consistency checks + if not len(class_fields): + raise ValueError(f"{self.__class__.__name__} has no fields.") + + first_field = getattr(self, class_fields[0].name) + other_fields_are_none = all(getattr(self, field.name) is None for field in class_fields[1:]) + + if other_fields_are_none and isinstance(first_field, dict): + for key, value in first_field.items(): + self[key] = value + else: + for field in class_fields: + v = getattr(self, field.name) + if v is not None: + self[field.name] = v + + def __delitem__(self, *args, **kwargs): + raise Exception(f"You cannot use ``__delitem__`` on a {self.__class__.__name__} instance.") + + def setdefault(self, *args, **kwargs): + raise Exception(f"You cannot use ``setdefault`` on a {self.__class__.__name__} instance.") + + def pop(self, *args, **kwargs): + raise Exception(f"You cannot use ``pop`` on a {self.__class__.__name__} instance.") + + def update(self, *args, **kwargs): + raise Exception(f"You cannot use ``update`` on a {self.__class__.__name__} instance.") + + def __getitem__(self, k): + if isinstance(k, str): + inner_dict = {k: v for (k, v) in self.items()} + return inner_dict[k] + else: + return self.to_tuple()[k] + + def __setattr__(self, name, value): + if name in self.keys() and value is not None: + # Don't call self.__setitem__ to avoid recursion errors + super().__setitem__(name, value) + super().__setattr__(name, value) + + def __setitem__(self, key, value): + # Will raise a KeyException if needed + super().__setitem__(key, value) + # Don't call self.__setattr__ to avoid recursion errors + super().__setattr__(key, value) + + def to_tuple(self) -> Tuple[Any]: + """ + Convert self to a tuple containing all the attributes/keys that are not `None`. + """ + return tuple(self[k] for k in self.keys()) diff --git a/diffusers/src/diffusers/utils/pil_utils.py b/diffusers/src/diffusers/utils/pil_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..39d0a15a4e2fe39fecb01951b36c43368492f983 --- /dev/null +++ b/diffusers/src/diffusers/utils/pil_utils.py @@ -0,0 +1,21 @@ +import PIL.Image +import PIL.ImageOps +from packaging import version + + +if version.parse(version.parse(PIL.__version__).base_version) >= version.parse("9.1.0"): + PIL_INTERPOLATION = { + "linear": PIL.Image.Resampling.BILINEAR, + "bilinear": PIL.Image.Resampling.BILINEAR, + "bicubic": PIL.Image.Resampling.BICUBIC, + "lanczos": PIL.Image.Resampling.LANCZOS, + "nearest": PIL.Image.Resampling.NEAREST, + } +else: + PIL_INTERPOLATION = { + "linear": PIL.Image.LINEAR, + "bilinear": PIL.Image.BILINEAR, + "bicubic": PIL.Image.BICUBIC, + "lanczos": PIL.Image.LANCZOS, + "nearest": PIL.Image.NEAREST, + } diff --git a/diffusers/src/diffusers/utils/testing_utils.py b/diffusers/src/diffusers/utils/testing_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a36e5ccf27cdcf474ea808eb13628c2e1cbc9cbe --- /dev/null +++ b/diffusers/src/diffusers/utils/testing_utils.py @@ -0,0 +1,435 @@ +import inspect +import logging +import os +import random +import re +import unittest +import urllib.parse +from distutils.util import strtobool +from io import BytesIO, StringIO +from pathlib import Path +from typing import Optional, Union + +import numpy as np +import PIL.Image +import PIL.ImageOps +import requests +from packaging import version + +from .import_utils import is_flax_available, is_onnx_available, is_torch_available +from .logging import get_logger + + +global_rng = random.Random() + +logger = get_logger(__name__) + +if is_torch_available(): + import torch + + if "DIFFUSERS_TEST_DEVICE" in os.environ: + torch_device = os.environ["DIFFUSERS_TEST_DEVICE"] + + available_backends = ["cuda", "cpu", "mps"] + if torch_device not in available_backends: + raise ValueError( + f"unknown torch backend for diffusers tests: {torch_device}. Available backends are:" + f" {available_backends}" + ) + logger.info(f"torch_device overrode to {torch_device}") + else: + torch_device = "cuda" if torch.cuda.is_available() else "cpu" + is_torch_higher_equal_than_1_12 = version.parse( + version.parse(torch.__version__).base_version + ) >= version.parse("1.12") + + if is_torch_higher_equal_than_1_12: + # Some builds of torch 1.12 don't have the mps backend registered. See #892 for more details + mps_backend_registered = hasattr(torch.backends, "mps") + torch_device = "mps" if (mps_backend_registered and torch.backends.mps.is_available()) else torch_device + + +def torch_all_close(a, b, *args, **kwargs): + if not is_torch_available(): + raise ValueError("PyTorch needs to be installed to use this function.") + if not torch.allclose(a, b, *args, **kwargs): + assert False, f"Max diff is absolute {(a - b).abs().max()}. Diff tensor is {(a - b).abs()}." + return True + + +def print_tensor_test(tensor, filename="test_corrections.txt", expected_tensor_name="expected_slice"): + test_name = os.environ.get("PYTEST_CURRENT_TEST") + if not torch.is_tensor(tensor): + tensor = torch.from_numpy(tensor) + + tensor_str = str(tensor.detach().cpu().flatten().to(torch.float32)).replace("\n", "") + # format is usually: + # expected_slice = np.array([-0.5713, -0.3018, -0.9814, 0.04663, -0.879, 0.76, -1.734, 0.1044, 1.161]) + output_str = tensor_str.replace("tensor", f"{expected_tensor_name} = np.array") + test_file, test_class, test_fn = test_name.split("::") + test_fn = test_fn.split()[0] + with open(filename, "a") as f: + print(";".join([test_file, test_class, test_fn, output_str]), file=f) + + +def get_tests_dir(append_path=None): + """ + Args: + append_path: optional path to append to the tests dir path + Return: + The full path to the `tests` dir, so that the tests can be invoked from anywhere. Optionally `append_path` is + joined after the `tests` dir the former is provided. + """ + # this function caller's __file__ + caller__file__ = inspect.stack()[1][1] + tests_dir = os.path.abspath(os.path.dirname(caller__file__)) + + while not tests_dir.endswith("tests"): + tests_dir = os.path.dirname(tests_dir) + + if append_path: + return os.path.join(tests_dir, append_path) + else: + return tests_dir + + +def parse_flag_from_env(key, default=False): + try: + value = os.environ[key] + except KeyError: + # KEY isn't set, default to `default`. + _value = default + else: + # KEY is set, convert it to True or False. + try: + _value = strtobool(value) + except ValueError: + # More values are supported, but let's keep the message simple. + raise ValueError(f"If set, {key} must be yes or no.") + return _value + + +_run_slow_tests = parse_flag_from_env("RUN_SLOW", default=False) +_run_nightly_tests = parse_flag_from_env("RUN_NIGHTLY", default=False) + + +def floats_tensor(shape, scale=1.0, rng=None, name=None): + """Creates a random float32 tensor""" + if rng is None: + rng = global_rng + + total_dims = 1 + for dim in shape: + total_dims *= dim + + values = [] + for _ in range(total_dims): + values.append(rng.random() * scale) + + return torch.tensor(data=values, dtype=torch.float).view(shape).contiguous() + + +def slow(test_case): + """ + Decorator marking a test as slow. + + Slow tests are skipped by default. Set the RUN_SLOW environment variable to a truthy value to run them. + + """ + return unittest.skipUnless(_run_slow_tests, "test is slow")(test_case) + + +def nightly(test_case): + """ + Decorator marking a test that runs nightly in the diffusers CI. + + Slow tests are skipped by default. Set the RUN_NIGHTLY environment variable to a truthy value to run them. + + """ + return unittest.skipUnless(_run_nightly_tests, "test is nightly")(test_case) + + +def require_torch(test_case): + """ + Decorator marking a test that requires PyTorch. These tests are skipped when PyTorch isn't installed. + """ + return unittest.skipUnless(is_torch_available(), "test requires PyTorch")(test_case) + + +def require_torch_gpu(test_case): + """Decorator marking a test that requires CUDA and PyTorch.""" + return unittest.skipUnless(is_torch_available() and torch_device == "cuda", "test requires PyTorch+CUDA")( + test_case + ) + + +def require_flax(test_case): + """ + Decorator marking a test that requires JAX & Flax. These tests are skipped when one / both are not installed + """ + return unittest.skipUnless(is_flax_available(), "test requires JAX & Flax")(test_case) + + +def require_onnxruntime(test_case): + """ + Decorator marking a test that requires onnxruntime. These tests are skipped when onnxruntime isn't installed. + """ + return unittest.skipUnless(is_onnx_available(), "test requires onnxruntime")(test_case) + + +def load_numpy(arry: Union[str, np.ndarray], local_path: Optional[str] = None) -> np.ndarray: + if isinstance(arry, str): + # local_path = "/home/patrick_huggingface_co/" + if local_path is not None: + # local_path can be passed to correct images of tests + return os.path.join(local_path, "/".join([arry.split("/")[-5], arry.split("/")[-2], arry.split("/")[-1]])) + elif arry.startswith("http://") or arry.startswith("https://"): + response = requests.get(arry) + response.raise_for_status() + arry = np.load(BytesIO(response.content)) + elif os.path.isfile(arry): + arry = np.load(arry) + else: + raise ValueError( + f"Incorrect path or url, URLs must start with `http://` or `https://`, and {arry} is not a valid path" + ) + elif isinstance(arry, np.ndarray): + pass + else: + raise ValueError( + "Incorrect format used for numpy ndarray. Should be an url linking to an image, a local path, or a" + " ndarray." + ) + + return arry + + +def load_image(image: Union[str, PIL.Image.Image]) -> PIL.Image.Image: + """ + Args: + Loads `image` to a PIL Image. + image (`str` or `PIL.Image.Image`): + The image to convert to the PIL Image format. + Returns: + `PIL.Image.Image`: A PIL Image. + """ + if isinstance(image, str): + if image.startswith("http://") or image.startswith("https://"): + image = PIL.Image.open(requests.get(image, stream=True).raw) + elif os.path.isfile(image): + image = PIL.Image.open(image) + else: + raise ValueError( + f"Incorrect path or url, URLs must start with `http://` or `https://`, and {image} is not a valid path" + ) + elif isinstance(image, PIL.Image.Image): + image = image + else: + raise ValueError( + "Incorrect format used for image. Should be an url linking to an image, a local path, or a PIL image." + ) + image = PIL.ImageOps.exif_transpose(image) + image = image.convert("RGB") + return image + + +def load_hf_numpy(path) -> np.ndarray: + if not path.startswith("http://") or path.startswith("https://"): + path = os.path.join( + "https://huggingface.co/datasets/fusing/diffusers-testing/resolve/main", urllib.parse.quote(path) + ) + + return load_numpy(path) + + +# --- pytest conf functions --- # + +# to avoid multiple invocation from tests/conftest.py and examples/conftest.py - make sure it's called only once +pytest_opt_registered = {} + + +def pytest_addoption_shared(parser): + """ + This function is to be called from `conftest.py` via `pytest_addoption` wrapper that has to be defined there. + + It allows loading both `conftest.py` files at once without causing a failure due to adding the same `pytest` + option. + + """ + option = "--make-reports" + if option not in pytest_opt_registered: + parser.addoption( + option, + action="store", + default=False, + help="generate report files. The value of this option is used as a prefix to report names", + ) + pytest_opt_registered[option] = 1 + + +def pytest_terminal_summary_main(tr, id): + """ + Generate multiple reports at the end of test suite run - each report goes into a dedicated file in the current + directory. The report files are prefixed with the test suite name. + + This function emulates --duration and -rA pytest arguments. + + This function is to be called from `conftest.py` via `pytest_terminal_summary` wrapper that has to be defined + there. + + Args: + - tr: `terminalreporter` passed from `conftest.py` + - id: unique id like `tests` or `examples` that will be incorporated into the final reports filenames - this is + needed as some jobs have multiple runs of pytest, so we can't have them overwrite each other. + + NB: this functions taps into a private _pytest API and while unlikely, it could break should + pytest do internal changes - also it calls default internal methods of terminalreporter which + can be hijacked by various `pytest-` plugins and interfere. + + """ + from _pytest.config import create_terminal_writer + + if not len(id): + id = "tests" + + config = tr.config + orig_writer = config.get_terminal_writer() + orig_tbstyle = config.option.tbstyle + orig_reportchars = tr.reportchars + + dir = "reports" + Path(dir).mkdir(parents=True, exist_ok=True) + report_files = { + k: f"{dir}/{id}_{k}.txt" + for k in [ + "durations", + "errors", + "failures_long", + "failures_short", + "failures_line", + "passes", + "stats", + "summary_short", + "warnings", + ] + } + + # custom durations report + # note: there is no need to call pytest --durations=XX to get this separate report + # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/runner.py#L66 + dlist = [] + for replist in tr.stats.values(): + for rep in replist: + if hasattr(rep, "duration"): + dlist.append(rep) + if dlist: + dlist.sort(key=lambda x: x.duration, reverse=True) + with open(report_files["durations"], "w") as f: + durations_min = 0.05 # sec + f.write("slowest durations\n") + for i, rep in enumerate(dlist): + if rep.duration < durations_min: + f.write(f"{len(dlist)-i} durations < {durations_min} secs were omitted") + break + f.write(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}\n") + + def summary_failures_short(tr): + # expecting that the reports were --tb=long (default) so we chop them off here to the last frame + reports = tr.getreports("failed") + if not reports: + return + tr.write_sep("=", "FAILURES SHORT STACK") + for rep in reports: + msg = tr._getfailureheadline(rep) + tr.write_sep("_", msg, red=True, bold=True) + # chop off the optional leading extra frames, leaving only the last one + longrepr = re.sub(r".*_ _ _ (_ ){10,}_ _ ", "", rep.longreprtext, 0, re.M | re.S) + tr._tw.line(longrepr) + # note: not printing out any rep.sections to keep the report short + + # use ready-made report funcs, we are just hijacking the filehandle to log to a dedicated file each + # adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/terminal.py#L814 + # note: some pytest plugins may interfere by hijacking the default `terminalreporter` (e.g. + # pytest-instafail does that) + + # report failures with line/short/long styles + config.option.tbstyle = "auto" # full tb + with open(report_files["failures_long"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_failures() + + # config.option.tbstyle = "short" # short tb + with open(report_files["failures_short"], "w") as f: + tr._tw = create_terminal_writer(config, f) + summary_failures_short(tr) + + config.option.tbstyle = "line" # one line per error + with open(report_files["failures_line"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_failures() + + with open(report_files["errors"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_errors() + + with open(report_files["warnings"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_warnings() # normal warnings + tr.summary_warnings() # final warnings + + tr.reportchars = "wPpsxXEf" # emulate -rA (used in summary_passes() and short_test_summary()) + with open(report_files["passes"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_passes() + + with open(report_files["summary_short"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.short_test_summary() + + with open(report_files["stats"], "w") as f: + tr._tw = create_terminal_writer(config, f) + tr.summary_stats() + + # restore: + tr._tw = orig_writer + tr.reportchars = orig_reportchars + config.option.tbstyle = orig_tbstyle + + +class CaptureLogger: + """ + Args: + Context manager to capture `logging` streams + logger: 'logging` logger object + Returns: + The captured output is available via `self.out` + Example: + ```python + >>> from diffusers import logging + >>> from diffusers.testing_utils import CaptureLogger + + >>> msg = "Testing 1, 2, 3" + >>> logging.set_verbosity_info() + >>> logger = logging.get_logger("diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.py") + >>> with CaptureLogger(logger) as cl: + ... logger.info(msg) + >>> assert cl.out, msg + "\n" + ``` + """ + + def __init__(self, logger): + self.logger = logger + self.io = StringIO() + self.sh = logging.StreamHandler(self.io) + self.out = "" + + def __enter__(self): + self.logger.addHandler(self.sh) + return self + + def __exit__(self, *exc): + self.logger.removeHandler(self.sh) + self.out = self.io.getvalue() + + def __repr__(self): + return f"captured: {self.out}\n" diff --git a/diffusers/src/diffusers/utils/torch_utils.py b/diffusers/src/diffusers/utils/torch_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..42d50c4b5ba76056beb36a5fa935f806676f0137 --- /dev/null +++ b/diffusers/src/diffusers/utils/torch_utils.py @@ -0,0 +1,70 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. +""" +PyTorch utilities: Utilities related to PyTorch +""" +from typing import List, Optional, Tuple, Union + +from . import logging +from .import_utils import is_torch_available + + +if is_torch_available(): + import torch + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def randn_tensor( + shape: Union[Tuple, List], + generator: Optional[Union[List["torch.Generator"], "torch.Generator"]] = None, + device: Optional["torch.device"] = None, + dtype: Optional["torch.dtype"] = None, + layout: Optional["torch.layout"] = None, +): + """This is a helper function that allows to create random tensors on the desired `device` with the desired `dtype`. When + passing a list of generators one can seed each batched size individually. If CPU generators are passed the tensor + will always be created on CPU. + """ + # device on which tensor is created defaults to device + rand_device = device + batch_size = shape[0] + + layout = layout or torch.strided + device = device or torch.device("cpu") + + if generator is not None: + gen_device_type = generator.device.type if not isinstance(generator, list) else generator[0].device.type + if gen_device_type != device.type and gen_device_type == "cpu": + rand_device = "cpu" + if device != "mps": + logger.info( + f"The passed generator was created on 'cpu' even though a tensor on {device} was expected." + f" Tensors will be created on 'cpu' and then moved to {device}. Note that one can probably" + f" slighly speed up this function by passing a generator that was created on the {device} device." + ) + elif gen_device_type != device.type and gen_device_type == "cuda": + raise ValueError(f"Cannot generate a {device} tensor from a generator of type {gen_device_type}.") + + if isinstance(generator, list): + shape = (1,) + shape[1:] + latents = [ + torch.randn(shape, generator=generator[i], device=rand_device, dtype=dtype, layout=layout) + for i in range(batch_size) + ] + latents = torch.cat(latents, dim=0).to(device) + else: + latents = torch.randn(shape, generator=generator, device=rand_device, dtype=dtype, layout=layout).to(device) + + return latents diff --git a/diffusers/tests/__init__.py b/diffusers/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/conftest.py b/diffusers/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..3cfab533e43c9eee59af831c8e5023cea6bee3ce --- /dev/null +++ b/diffusers/tests/conftest.py @@ -0,0 +1,44 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +# tests directory-specific settings - this file is run automatically +# by pytest before any tests are run + +import sys +import warnings +from os.path import abspath, dirname, join + + +# allow having multiple repository checkouts and not needing to remember to rerun +# 'pip install -e .[dev]' when switching between checkouts and running tests. +git_repo_path = abspath(join(dirname(dirname(__file__)), "src")) +sys.path.insert(1, git_repo_path) + +# silence FutureWarning warnings in tests since often we can't act on them until +# they become normal warnings - i.e. the tests still need to test the current functionality +warnings.simplefilter(action="ignore", category=FutureWarning) + + +def pytest_addoption(parser): + from diffusers.utils.testing_utils import pytest_addoption_shared + + pytest_addoption_shared(parser) + + +def pytest_terminal_summary(terminalreporter): + from diffusers.utils.testing_utils import pytest_terminal_summary_main + + make_reports = terminalreporter.config.getoption("--make-reports") + if make_reports: + pytest_terminal_summary_main(terminalreporter, id=make_reports) diff --git a/diffusers/tests/fixtures/custom_pipeline/pipeline.py b/diffusers/tests/fixtures/custom_pipeline/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..0667edcfc62a2085295244805163a5311440e455 --- /dev/null +++ b/diffusers/tests/fixtures/custom_pipeline/pipeline.py @@ -0,0 +1,101 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import Optional, Tuple, Union + +import torch + +from diffusers import DiffusionPipeline, ImagePipelineOutput + + +class CustomLocalPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image. Can be one of + [`DDPMScheduler`], or [`DDIMScheduler`]. + """ + + def __init__(self, unet, scheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + generator: Optional[torch.Generator] = None, + num_inference_steps: int = 50, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + eta (`float`, *optional*, defaults to 0.0): + The eta parameter which controls the scale of the variance (0 is DDIM and 1 is one type of DDPM). + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipelines.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipelines.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if + `return_dict` is True, otherwise a `tuple. When returning a tuple, the first element is a list with the + generated images. + """ + + # Sample gaussian noise to begin loop + image = torch.randn( + (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + generator=generator, + ) + image = image.to(self.device) + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(image, t).sample + + # 2. predict previous mean of image x_t-1 and add variance depending on eta + # eta corresponds to η in paper and should be between [0, 1] + # do x_t -> x_t-1 + image = self.scheduler.step(model_output, t, image).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,), "This is a local test" + + return ImagePipelineOutput(images=image), "This is a local test" diff --git a/diffusers/tests/fixtures/custom_pipeline/what_ever.py b/diffusers/tests/fixtures/custom_pipeline/what_ever.py new file mode 100644 index 0000000000000000000000000000000000000000..e7429d0a1945467c90bb703cd014f8c4a9312b2e --- /dev/null +++ b/diffusers/tests/fixtures/custom_pipeline/what_ever.py @@ -0,0 +1,101 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + + +from typing import Optional, Tuple, Union + +import torch + +from diffusers.pipeline_utils import DiffusionPipeline, ImagePipelineOutput + + +class CustomLocalPipeline(DiffusionPipeline): + r""" + This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the + library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.) + + Parameters: + unet ([`UNet2DModel`]): U-Net architecture to denoise the encoded image. + scheduler ([`SchedulerMixin`]): + A scheduler to be used in combination with `unet` to denoise the encoded image. Can be one of + [`DDPMScheduler`], or [`DDIMScheduler`]. + """ + + def __init__(self, unet, scheduler): + super().__init__() + self.register_modules(unet=unet, scheduler=scheduler) + + @torch.no_grad() + def __call__( + self, + batch_size: int = 1, + generator: Optional[torch.Generator] = None, + num_inference_steps: int = 50, + output_type: Optional[str] = "pil", + return_dict: bool = True, + **kwargs, + ) -> Union[ImagePipelineOutput, Tuple]: + r""" + Args: + batch_size (`int`, *optional*, defaults to 1): + The number of images to generate. + generator (`torch.Generator`, *optional*): + A [torch generator](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make generation + deterministic. + eta (`float`, *optional*, defaults to 0.0): + The eta parameter which controls the scale of the variance (0 is DDIM and 1 is one type of DDPM). + num_inference_steps (`int`, *optional*, defaults to 50): + The number of denoising steps. More denoising steps usually lead to a higher quality image at the + expense of slower inference. + output_type (`str`, *optional*, defaults to `"pil"`): + The output format of the generate image. Choose between + [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~pipeline_utils.ImagePipelineOutput`] instead of a plain tuple. + + Returns: + [`~pipeline_utils.ImagePipelineOutput`] or `tuple`: [`~pipelines.utils.ImagePipelineOutput`] if + `return_dict` is True, otherwise a `tuple. When returning a tuple, the first element is a list with the + generated images. + """ + + # Sample gaussian noise to begin loop + image = torch.randn( + (batch_size, self.unet.in_channels, self.unet.sample_size, self.unet.sample_size), + generator=generator, + ) + image = image.to(self.device) + + # set step values + self.scheduler.set_timesteps(num_inference_steps) + + for t in self.progress_bar(self.scheduler.timesteps): + # 1. predict noise model_output + model_output = self.unet(image, t).sample + + # 2. predict previous mean of image x_t-1 and add variance depending on eta + # eta corresponds to η in paper and should be between [0, 1] + # do x_t -> x_t-1 + image = self.scheduler.step(model_output, t, image).prev_sample + + image = (image / 2 + 0.5).clamp(0, 1) + image = image.cpu().permute(0, 2, 3, 1).numpy() + if output_type == "pil": + image = self.numpy_to_pil(image) + + if not return_dict: + return (image,), "This is a local test" + + return ImagePipelineOutput(images=image), "This is a local test" diff --git a/diffusers/tests/models/__init__.py b/diffusers/tests/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/models/test_models_unet_1d.py b/diffusers/tests/models/test_models_unet_1d.py new file mode 100644 index 0000000000000000000000000000000000000000..b494c231b5fe8f6e51e644528faef46fefbfca7e --- /dev/null +++ b/diffusers/tests/models/test_models_unet_1d.py @@ -0,0 +1,276 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import torch + +from diffusers import UNet1DModel +from diffusers.utils import floats_tensor, slow, torch_device + +from ..test_modeling_common import ModelTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class UNet1DModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet1DModel + + @property + def dummy_input(self): + batch_size = 4 + num_features = 14 + seq_len = 16 + + noise = floats_tensor((batch_size, num_features, seq_len)).to(torch_device) + time_step = torch.tensor([10] * batch_size).to(torch_device) + + return {"sample": noise, "timestep": time_step} + + @property + def input_shape(self): + return (4, 14, 16) + + @property + def output_shape(self): + return (4, 14, 16) + + def test_ema_training(self): + pass + + def test_training(self): + pass + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_determinism(self): + super().test_determinism() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_outputs_equivalence(self): + super().test_outputs_equivalence() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_from_save_pretrained(self): + super().test_from_save_pretrained() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_model_from_pretrained(self): + super().test_model_from_pretrained() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_output(self): + super().test_output() + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": (32, 64, 128, 256), + "in_channels": 14, + "out_channels": 14, + "time_embedding_type": "positional", + "use_timestep_embedding": True, + "flip_sin_to_cos": False, + "freq_shift": 1.0, + "out_block_type": "OutConv1DBlock", + "mid_block_type": "MidResTemporalBlock1D", + "down_block_types": ("DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D"), + "up_block_types": ("UpResnetBlock1D", "UpResnetBlock1D", "UpResnetBlock1D"), + "act_fn": "mish", + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_from_pretrained_hub(self): + model, loading_info = UNet1DModel.from_pretrained( + "bglick13/hopper-medium-v2-value-function-hor32", output_loading_info=True, subfolder="unet" + ) + self.assertIsNotNone(model) + self.assertEqual(len(loading_info["missing_keys"]), 0) + + model.to(torch_device) + image = model(**self.dummy_input) + + assert image is not None, "Make sure output is not None" + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_output_pretrained(self): + model = UNet1DModel.from_pretrained("bglick13/hopper-medium-v2-value-function-hor32", subfolder="unet") + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + num_features = model.in_channels + seq_len = 16 + noise = torch.randn((1, seq_len, num_features)).permute( + 0, 2, 1 + ) # match original, we can update values and remove + time_step = torch.full((num_features,), 0) + + with torch.no_grad(): + output = model(noise, time_step).sample.permute(0, 2, 1) + + output_slice = output[0, -3:, -3:].flatten() + # fmt: off + expected_output_slice = torch.tensor([-2.137172, 1.1426016, 0.3688687, -0.766922, 0.7303146, 0.11038864, -0.4760633, 0.13270172, 0.02591348]) + # fmt: on + self.assertTrue(torch.allclose(output_slice, expected_output_slice, rtol=1e-3)) + + def test_forward_with_norm_groups(self): + # Not implemented yet for this UNet + pass + + @slow + def test_unet_1d_maestro(self): + model_id = "harmonai/maestro-150k" + model = UNet1DModel.from_pretrained(model_id, subfolder="unet") + model.to(torch_device) + + sample_size = 65536 + noise = torch.sin(torch.arange(sample_size)[None, None, :].repeat(1, 2, 1)).to(torch_device) + timestep = torch.tensor([1]).to(torch_device) + + with torch.no_grad(): + output = model(noise, timestep).sample + + output_sum = output.abs().sum() + output_max = output.abs().max() + + assert (output_sum - 224.0896).abs() < 4e-2 + assert (output_max - 0.0607).abs() < 4e-4 + + +class UNetRLModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet1DModel + + @property + def dummy_input(self): + batch_size = 4 + num_features = 14 + seq_len = 16 + + noise = floats_tensor((batch_size, num_features, seq_len)).to(torch_device) + time_step = torch.tensor([10] * batch_size).to(torch_device) + + return {"sample": noise, "timestep": time_step} + + @property + def input_shape(self): + return (4, 14, 16) + + @property + def output_shape(self): + return (4, 14, 1) + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_determinism(self): + super().test_determinism() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_outputs_equivalence(self): + super().test_outputs_equivalence() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_from_save_pretrained(self): + super().test_from_save_pretrained() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_model_from_pretrained(self): + super().test_model_from_pretrained() + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_output(self): + # UNetRL is a value-function is different output shape + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = torch.Size((inputs_dict["sample"].shape[0], 1)) + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_ema_training(self): + pass + + def test_training(self): + pass + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "in_channels": 14, + "out_channels": 14, + "down_block_types": ["DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D", "DownResnetBlock1D"], + "up_block_types": [], + "out_block_type": "ValueFunction", + "mid_block_type": "ValueFunctionMidBlock1D", + "block_out_channels": [32, 64, 128, 256], + "layers_per_block": 1, + "downsample_each_block": True, + "use_timestep_embedding": True, + "freq_shift": 1.0, + "flip_sin_to_cos": False, + "time_embedding_type": "positional", + "act_fn": "mish", + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_from_pretrained_hub(self): + value_function, vf_loading_info = UNet1DModel.from_pretrained( + "bglick13/hopper-medium-v2-value-function-hor32", output_loading_info=True, subfolder="value_function" + ) + self.assertIsNotNone(value_function) + self.assertEqual(len(vf_loading_info["missing_keys"]), 0) + + value_function.to(torch_device) + image = value_function(**self.dummy_input) + + assert image is not None, "Make sure output is not None" + + @unittest.skipIf(torch_device == "mps", "mish op not supported in MPS") + def test_output_pretrained(self): + value_function, vf_loading_info = UNet1DModel.from_pretrained( + "bglick13/hopper-medium-v2-value-function-hor32", output_loading_info=True, subfolder="value_function" + ) + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + num_features = value_function.in_channels + seq_len = 14 + noise = torch.randn((1, seq_len, num_features)).permute( + 0, 2, 1 + ) # match original, we can update values and remove + time_step = torch.full((num_features,), 0) + + with torch.no_grad(): + output = value_function(noise, time_step).sample + + # fmt: off + expected_output_slice = torch.tensor([165.25] * seq_len) + # fmt: on + self.assertTrue(torch.allclose(output, expected_output_slice, rtol=1e-3)) + + def test_forward_with_norm_groups(self): + # Not implemented yet for this UNet + pass diff --git a/diffusers/tests/models/test_models_unet_2d.py b/diffusers/tests/models/test_models_unet_2d.py new file mode 100644 index 0000000000000000000000000000000000000000..39cd98a147260d89dfccdf3439ec6f9d996d9cb2 --- /dev/null +++ b/diffusers/tests/models/test_models_unet_2d.py @@ -0,0 +1,325 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import math +import tracemalloc +import unittest + +import torch + +from diffusers import UNet2DModel +from diffusers.utils import floats_tensor, logging, slow, torch_all_close, torch_device + +from ..test_modeling_common import ModelTesterMixin + + +logger = logging.get_logger(__name__) +torch.backends.cuda.matmul.allow_tf32 = False + + +class Unet2DModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet2DModel + + @property + def dummy_input(self): + batch_size = 4 + num_channels = 3 + sizes = (32, 32) + + noise = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor([10]).to(torch_device) + + return {"sample": noise, "timestep": time_step} + + @property + def input_shape(self): + return (3, 32, 32) + + @property + def output_shape(self): + return (3, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": (32, 64), + "down_block_types": ("DownBlock2D", "AttnDownBlock2D"), + "up_block_types": ("AttnUpBlock2D", "UpBlock2D"), + "attention_head_dim": None, + "out_channels": 3, + "in_channels": 3, + "layers_per_block": 2, + "sample_size": 32, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + +class UNetLDMModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet2DModel + + @property + def dummy_input(self): + batch_size = 4 + num_channels = 4 + sizes = (32, 32) + + noise = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor([10]).to(torch_device) + + return {"sample": noise, "timestep": time_step} + + @property + def input_shape(self): + return (4, 32, 32) + + @property + def output_shape(self): + return (4, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "sample_size": 32, + "in_channels": 4, + "out_channels": 4, + "layers_per_block": 2, + "block_out_channels": (32, 64), + "attention_head_dim": 32, + "down_block_types": ("DownBlock2D", "DownBlock2D"), + "up_block_types": ("UpBlock2D", "UpBlock2D"), + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_from_pretrained_hub(self): + model, loading_info = UNet2DModel.from_pretrained("fusing/unet-ldm-dummy-update", output_loading_info=True) + + self.assertIsNotNone(model) + self.assertEqual(len(loading_info["missing_keys"]), 0) + + model.to(torch_device) + image = model(**self.dummy_input).sample + + assert image is not None, "Make sure output is not None" + + @unittest.skipIf(torch_device != "cuda", "This test is supposed to run on GPU") + def test_from_pretrained_accelerate(self): + model, _ = UNet2DModel.from_pretrained("fusing/unet-ldm-dummy-update", output_loading_info=True) + model.to(torch_device) + image = model(**self.dummy_input).sample + + assert image is not None, "Make sure output is not None" + + @unittest.skipIf(torch_device != "cuda", "This test is supposed to run on GPU") + def test_from_pretrained_accelerate_wont_change_results(self): + # by defautl model loading will use accelerate as `low_cpu_mem_usage=True` + model_accelerate, _ = UNet2DModel.from_pretrained("fusing/unet-ldm-dummy-update", output_loading_info=True) + model_accelerate.to(torch_device) + model_accelerate.eval() + + noise = torch.randn( + 1, + model_accelerate.config.in_channels, + model_accelerate.config.sample_size, + model_accelerate.config.sample_size, + generator=torch.manual_seed(0), + ) + noise = noise.to(torch_device) + time_step = torch.tensor([10] * noise.shape[0]).to(torch_device) + + arr_accelerate = model_accelerate(noise, time_step)["sample"] + + # two models don't need to stay in the device at the same time + del model_accelerate + torch.cuda.empty_cache() + gc.collect() + + model_normal_load, _ = UNet2DModel.from_pretrained( + "fusing/unet-ldm-dummy-update", output_loading_info=True, low_cpu_mem_usage=False + ) + model_normal_load.to(torch_device) + model_normal_load.eval() + arr_normal_load = model_normal_load(noise, time_step)["sample"] + + assert torch_all_close(arr_accelerate, arr_normal_load, rtol=1e-3) + + @unittest.skipIf(torch_device != "cuda", "This test is supposed to run on GPU") + def test_memory_footprint_gets_reduced(self): + torch.cuda.empty_cache() + gc.collect() + + tracemalloc.start() + # by defautl model loading will use accelerate as `low_cpu_mem_usage=True` + model_accelerate, _ = UNet2DModel.from_pretrained("fusing/unet-ldm-dummy-update", output_loading_info=True) + model_accelerate.to(torch_device) + model_accelerate.eval() + _, peak_accelerate = tracemalloc.get_traced_memory() + + del model_accelerate + torch.cuda.empty_cache() + gc.collect() + + model_normal_load, _ = UNet2DModel.from_pretrained( + "fusing/unet-ldm-dummy-update", output_loading_info=True, low_cpu_mem_usage=False + ) + model_normal_load.to(torch_device) + model_normal_load.eval() + _, peak_normal = tracemalloc.get_traced_memory() + + tracemalloc.stop() + + assert peak_accelerate < peak_normal + + def test_output_pretrained(self): + model = UNet2DModel.from_pretrained("fusing/unet-ldm-dummy-update") + model.eval() + model.to(torch_device) + + noise = torch.randn( + 1, + model.config.in_channels, + model.config.sample_size, + model.config.sample_size, + generator=torch.manual_seed(0), + ) + noise = noise.to(torch_device) + time_step = torch.tensor([10] * noise.shape[0]).to(torch_device) + + with torch.no_grad(): + output = model(noise, time_step).sample + + output_slice = output[0, -1, -3:, -3:].flatten().cpu() + # fmt: off + expected_output_slice = torch.tensor([-13.3258, -20.1100, -15.9873, -17.6617, -23.0596, -17.9419, -13.3675, -16.1889, -12.3800]) + # fmt: on + + self.assertTrue(torch_all_close(output_slice, expected_output_slice, rtol=1e-3)) + + +class NCSNppModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet2DModel + + @property + def dummy_input(self, sizes=(32, 32)): + batch_size = 4 + num_channels = 3 + + noise = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor(batch_size * [10]).to(dtype=torch.int32, device=torch_device) + + return {"sample": noise, "timestep": time_step} + + @property + def input_shape(self): + return (3, 32, 32) + + @property + def output_shape(self): + return (3, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": [32, 64, 64, 64], + "in_channels": 3, + "layers_per_block": 1, + "out_channels": 3, + "time_embedding_type": "fourier", + "norm_eps": 1e-6, + "mid_block_scale_factor": math.sqrt(2.0), + "norm_num_groups": None, + "down_block_types": [ + "SkipDownBlock2D", + "AttnSkipDownBlock2D", + "SkipDownBlock2D", + "SkipDownBlock2D", + ], + "up_block_types": [ + "SkipUpBlock2D", + "SkipUpBlock2D", + "AttnSkipUpBlock2D", + "SkipUpBlock2D", + ], + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + @slow + def test_from_pretrained_hub(self): + model, loading_info = UNet2DModel.from_pretrained("google/ncsnpp-celebahq-256", output_loading_info=True) + self.assertIsNotNone(model) + self.assertEqual(len(loading_info["missing_keys"]), 0) + + model.to(torch_device) + inputs = self.dummy_input + noise = floats_tensor((4, 3) + (256, 256)).to(torch_device) + inputs["sample"] = noise + image = model(**inputs) + + assert image is not None, "Make sure output is not None" + + @slow + def test_output_pretrained_ve_mid(self): + model = UNet2DModel.from_pretrained("google/ncsnpp-celebahq-256") + model.to(torch_device) + + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + batch_size = 4 + num_channels = 3 + sizes = (256, 256) + + noise = torch.ones((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor(batch_size * [1e-4]).to(torch_device) + + with torch.no_grad(): + output = model(noise, time_step).sample + + output_slice = output[0, -3:, -3:, -1].flatten().cpu() + # fmt: off + expected_output_slice = torch.tensor([-4836.2231, -6487.1387, -3816.7969, -7964.9253, -10966.2842, -20043.6016, 8137.0571, 2340.3499, 544.6114]) + # fmt: on + + self.assertTrue(torch_all_close(output_slice, expected_output_slice, rtol=1e-2)) + + def test_output_pretrained_ve_large(self): + model = UNet2DModel.from_pretrained("fusing/ncsnpp-ffhq-ve-dummy-update") + model.to(torch_device) + + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + batch_size = 4 + num_channels = 3 + sizes = (32, 32) + + noise = torch.ones((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor(batch_size * [1e-4]).to(torch_device) + + with torch.no_grad(): + output = model(noise, time_step).sample + + output_slice = output[0, -3:, -3:, -1].flatten().cpu() + # fmt: off + expected_output_slice = torch.tensor([-0.0325, -0.0900, -0.0869, -0.0332, -0.0725, -0.0270, -0.0101, 0.0227, 0.0256]) + # fmt: on + + self.assertTrue(torch_all_close(output_slice, expected_output_slice, rtol=1e-2)) + + def test_forward_with_norm_groups(self): + # not required for this model + pass diff --git a/diffusers/tests/models/test_models_unet_2d_condition.py b/diffusers/tests/models/test_models_unet_2d_condition.py new file mode 100644 index 0000000000000000000000000000000000000000..6ee8c2ffc00236ffc225c892d99ebee5ccfb960d --- /dev/null +++ b/diffusers/tests/models/test_models_unet_2d_condition.py @@ -0,0 +1,749 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import tempfile +import unittest + +import torch +from parameterized import parameterized + +from diffusers import UNet2DConditionModel +from diffusers.models.cross_attention import CrossAttnProcessor, LoRACrossAttnProcessor +from diffusers.utils import ( + floats_tensor, + load_hf_numpy, + logging, + require_torch_gpu, + slow, + torch_all_close, + torch_device, +) +from diffusers.utils.import_utils import is_xformers_available + +from ..test_modeling_common import ModelTesterMixin + + +logger = logging.get_logger(__name__) +torch.backends.cuda.matmul.allow_tf32 = False + + +def create_lora_layers(model): + lora_attn_procs = {} + for name in model.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else model.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = model.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(model.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = model.config.block_out_channels[block_id] + + lora_attn_procs[name] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim + ) + lora_attn_procs[name] = lora_attn_procs[name].to(model.device) + + # add 1 to weights to mock trained weights + with torch.no_grad(): + lora_attn_procs[name].to_q_lora.up.weight += 1 + lora_attn_procs[name].to_k_lora.up.weight += 1 + lora_attn_procs[name].to_v_lora.up.weight += 1 + lora_attn_procs[name].to_out_lora.up.weight += 1 + + return lora_attn_procs + + +class UNet2DConditionModelTests(ModelTesterMixin, unittest.TestCase): + model_class = UNet2DConditionModel + + @property + def dummy_input(self): + batch_size = 4 + num_channels = 4 + sizes = (32, 32) + + noise = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + time_step = torch.tensor([10]).to(torch_device) + encoder_hidden_states = floats_tensor((batch_size, 4, 32)).to(torch_device) + + return {"sample": noise, "timestep": time_step, "encoder_hidden_states": encoder_hidden_states} + + @property + def input_shape(self): + return (4, 32, 32) + + @property + def output_shape(self): + return (4, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": (32, 64), + "down_block_types": ("CrossAttnDownBlock2D", "DownBlock2D"), + "up_block_types": ("UpBlock2D", "CrossAttnUpBlock2D"), + "cross_attention_dim": 32, + "attention_head_dim": 8, + "out_channels": 4, + "in_channels": 4, + "layers_per_block": 2, + "sample_size": 32, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + @unittest.skipIf( + torch_device != "cuda" or not is_xformers_available(), + reason="XFormers attention is only available with CUDA and `xformers` installed", + ) + def test_xformers_enable_works(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.model_class(**init_dict) + + model.enable_xformers_memory_efficient_attention() + + assert ( + model.mid_block.attentions[0].transformer_blocks[0].attn1._use_memory_efficient_attention_xformers + ), "xformers is not enabled" + + @unittest.skipIf(torch_device == "mps", "Gradient checkpointing skipped on MPS") + def test_gradient_checkpointing(self): + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.model_class(**init_dict) + model.to(torch_device) + + assert not model.is_gradient_checkpointing and model.training + + out = model(**inputs_dict).sample + # run the backwards pass on the model. For backwards pass, for simplicity purpose, + # we won't calculate the loss and rather backprop on out.sum() + model.zero_grad() + + labels = torch.randn_like(out) + loss = (out - labels).mean() + loss.backward() + + # re-instantiate the model now enabling gradient checkpointing + model_2 = self.model_class(**init_dict) + # clone model + model_2.load_state_dict(model.state_dict()) + model_2.to(torch_device) + model_2.enable_gradient_checkpointing() + + assert model_2.is_gradient_checkpointing and model_2.training + + out_2 = model_2(**inputs_dict).sample + # run the backwards pass on the model. For backwards pass, for simplicity purpose, + # we won't calculate the loss and rather backprop on out.sum() + model_2.zero_grad() + loss_2 = (out_2 - labels).mean() + loss_2.backward() + + # compare the output and parameters gradients + self.assertTrue((loss - loss_2).abs() < 1e-5) + named_params = dict(model.named_parameters()) + named_params_2 = dict(model_2.named_parameters()) + for name, param in named_params.items(): + self.assertTrue(torch_all_close(param.grad.data, named_params_2[name].grad.data, atol=5e-5)) + + def test_model_with_attention_head_dim_tuple(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_model_with_use_linear_projection(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["use_linear_projection"] = True + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_model_attention_slicing(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + model.set_attention_slice("auto") + with torch.no_grad(): + output = model(**inputs_dict) + assert output is not None + + model.set_attention_slice("max") + with torch.no_grad(): + output = model(**inputs_dict) + assert output is not None + + model.set_attention_slice(2) + with torch.no_grad(): + output = model(**inputs_dict) + assert output is not None + + def test_model_slicable_head_dim(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + model = self.model_class(**init_dict) + + def check_slicable_dim_attr(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + assert isinstance(module.sliceable_head_dim, int) + + for child in module.children(): + check_slicable_dim_attr(child) + + # retrieve number of attention layers + for module in model.children(): + check_slicable_dim_attr(module) + + def test_special_attn_proc(self): + class AttnEasyProc(torch.nn.Module): + def __init__(self, num): + super().__init__() + self.weight = torch.nn.Parameter(torch.tensor(num)) + self.is_run = False + self.number = 0 + self.counter = 0 + + def __call__(self, attn, hidden_states, encoder_hidden_states=None, attention_mask=None, number=None): + batch_size, sequence_length, _ = hidden_states.shape + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + + query = attn.to_q(hidden_states) + + encoder_hidden_states = encoder_hidden_states if encoder_hidden_states is not None else hidden_states + key = attn.to_k(encoder_hidden_states) + value = attn.to_v(encoder_hidden_states) + + query = attn.head_to_batch_dim(query) + key = attn.head_to_batch_dim(key) + value = attn.head_to_batch_dim(value) + + attention_probs = attn.get_attention_scores(query, key, attention_mask) + hidden_states = torch.bmm(attention_probs, value) + hidden_states = attn.batch_to_head_dim(hidden_states) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + hidden_states += self.weight + + self.is_run = True + self.counter += 1 + self.number = number + + return hidden_states + + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + model = self.model_class(**init_dict) + model.to(torch_device) + + processor = AttnEasyProc(5.0) + + model.set_attn_processor(processor) + model(**inputs_dict, cross_attention_kwargs={"number": 123}).sample + + assert processor.counter == 12 + assert processor.is_run + assert processor.number == 123 + + def test_lora_processors(self): + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + model = self.model_class(**init_dict) + model.to(torch_device) + + with torch.no_grad(): + sample1 = model(**inputs_dict).sample + + lora_attn_procs = {} + for name in model.attn_processors.keys(): + cross_attention_dim = None if name.endswith("attn1.processor") else model.config.cross_attention_dim + if name.startswith("mid_block"): + hidden_size = model.config.block_out_channels[-1] + elif name.startswith("up_blocks"): + block_id = int(name[len("up_blocks.")]) + hidden_size = list(reversed(model.config.block_out_channels))[block_id] + elif name.startswith("down_blocks"): + block_id = int(name[len("down_blocks.")]) + hidden_size = model.config.block_out_channels[block_id] + + lora_attn_procs[name] = LoRACrossAttnProcessor( + hidden_size=hidden_size, cross_attention_dim=cross_attention_dim + ) + + # add 1 to weights to mock trained weights + with torch.no_grad(): + lora_attn_procs[name].to_q_lora.up.weight += 1 + lora_attn_procs[name].to_k_lora.up.weight += 1 + lora_attn_procs[name].to_v_lora.up.weight += 1 + lora_attn_procs[name].to_out_lora.up.weight += 1 + + # make sure we can set a list of attention processors + model.set_attn_processor(lora_attn_procs) + model.to(torch_device) + + # test that attn processors can be set to itself + model.set_attn_processor(model.attn_processors) + + with torch.no_grad(): + sample2 = model(**inputs_dict, cross_attention_kwargs={"scale": 0.0}).sample + sample3 = model(**inputs_dict, cross_attention_kwargs={"scale": 0.5}).sample + sample4 = model(**inputs_dict, cross_attention_kwargs={"scale": 0.5}).sample + + assert (sample1 - sample2).abs().max() < 1e-4 + assert (sample3 - sample4).abs().max() < 1e-4 + + # sample 2 and sample 3 should be different + assert (sample2 - sample3).abs().max() > 1e-4 + + def test_lora_save_load(self): + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + torch.manual_seed(0) + model = self.model_class(**init_dict) + model.to(torch_device) + + with torch.no_grad(): + old_sample = model(**inputs_dict).sample + + lora_attn_procs = create_lora_layers(model) + model.set_attn_processor(lora_attn_procs) + + with torch.no_grad(): + sample = model(**inputs_dict, cross_attention_kwargs={"scale": 0.5}).sample + + with tempfile.TemporaryDirectory() as tmpdirname: + model.save_attn_procs(tmpdirname) + torch.manual_seed(0) + new_model = self.model_class(**init_dict) + new_model.to(torch_device) + new_model.load_attn_procs(tmpdirname) + + with torch.no_grad(): + new_sample = new_model(**inputs_dict, cross_attention_kwargs={"scale": 0.5}).sample + + assert (sample - new_sample).abs().max() < 1e-4 + + # LoRA and no LoRA should NOT be the same + assert (sample - old_sample).abs().max() > 1e-4 + + def test_lora_on_off(self): + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + torch.manual_seed(0) + model = self.model_class(**init_dict) + model.to(torch_device) + + with torch.no_grad(): + old_sample = model(**inputs_dict).sample + + lora_attn_procs = create_lora_layers(model) + model.set_attn_processor(lora_attn_procs) + + with torch.no_grad(): + sample = model(**inputs_dict, cross_attention_kwargs={"scale": 0.0}).sample + + model.set_attn_processor(CrossAttnProcessor()) + + with torch.no_grad(): + new_sample = model(**inputs_dict).sample + + assert (sample - new_sample).abs().max() < 1e-4 + assert (sample - old_sample).abs().max() < 1e-4 + + @unittest.skipIf( + torch_device != "cuda" or not is_xformers_available(), + reason="XFormers attention is only available with CUDA and `xformers` installed", + ) + def test_lora_xformers_on_off(self): + # enable deterministic behavior for gradient checkpointing + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["attention_head_dim"] = (8, 16) + + torch.manual_seed(0) + model = self.model_class(**init_dict) + model.to(torch_device) + lora_attn_procs = create_lora_layers(model) + model.set_attn_processor(lora_attn_procs) + + # default + with torch.no_grad(): + sample = model(**inputs_dict).sample + + model.enable_xformers_memory_efficient_attention() + on_sample = model(**inputs_dict).sample + + model.disable_xformers_memory_efficient_attention() + off_sample = model(**inputs_dict).sample + + assert (sample - on_sample).abs().max() < 1e-4 + assert (sample - off_sample).abs().max() < 1e-4 + + +@slow +class UNet2DConditionModelIntegrationTests(unittest.TestCase): + def get_file_format(self, seed, shape): + return f"gaussian_noise_s={seed}_shape={'_'.join([str(s) for s in shape])}.npy" + + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_latents(self, seed=0, shape=(4, 4, 64, 64), fp16=False): + dtype = torch.float16 if fp16 else torch.float32 + image = torch.from_numpy(load_hf_numpy(self.get_file_format(seed, shape))).to(torch_device).to(dtype) + return image + + def get_unet_model(self, fp16=False, model_id="CompVis/stable-diffusion-v1-4"): + revision = "fp16" if fp16 else None + torch_dtype = torch.float16 if fp16 else torch.float32 + + model = UNet2DConditionModel.from_pretrained( + model_id, subfolder="unet", torch_dtype=torch_dtype, revision=revision + ) + model.to(torch_device).eval() + + return model + + def test_set_attention_slice_auto(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + unet = self.get_unet_model() + unet.set_attention_slice("auto") + + latents = self.get_latents(33) + encoder_hidden_states = self.get_encoder_hidden_states(33) + timestep = 1 + + with torch.no_grad(): + _ = unet(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + mem_bytes = torch.cuda.max_memory_allocated() + + assert mem_bytes < 5 * 10**9 + + def test_set_attention_slice_max(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + unet = self.get_unet_model() + unet.set_attention_slice("max") + + latents = self.get_latents(33) + encoder_hidden_states = self.get_encoder_hidden_states(33) + timestep = 1 + + with torch.no_grad(): + _ = unet(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + mem_bytes = torch.cuda.max_memory_allocated() + + assert mem_bytes < 5 * 10**9 + + def test_set_attention_slice_int(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + unet = self.get_unet_model() + unet.set_attention_slice(2) + + latents = self.get_latents(33) + encoder_hidden_states = self.get_encoder_hidden_states(33) + timestep = 1 + + with torch.no_grad(): + _ = unet(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + mem_bytes = torch.cuda.max_memory_allocated() + + assert mem_bytes < 5 * 10**9 + + def test_set_attention_slice_list(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + # there are 32 slicable layers + slice_list = 16 * [2, 3] + unet = self.get_unet_model() + unet.set_attention_slice(slice_list) + + latents = self.get_latents(33) + encoder_hidden_states = self.get_encoder_hidden_states(33) + timestep = 1 + + with torch.no_grad(): + _ = unet(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + mem_bytes = torch.cuda.max_memory_allocated() + + assert mem_bytes < 5 * 10**9 + + def get_encoder_hidden_states(self, seed=0, shape=(4, 77, 768), fp16=False): + dtype = torch.float16 if fp16 else torch.float32 + hidden_states = torch.from_numpy(load_hf_numpy(self.get_file_format(seed, shape))).to(torch_device).to(dtype) + return hidden_states + + @parameterized.expand( + [ + # fmt: off + [33, 4, [-0.4424, 0.1510, -0.1937, 0.2118, 0.3746, -0.3957, 0.0160, -0.0435]], + [47, 0.55, [-0.1508, 0.0379, -0.3075, 0.2540, 0.3633, -0.0821, 0.1719, -0.0207]], + [21, 0.89, [-0.6479, 0.6364, -0.3464, 0.8697, 0.4443, -0.6289, -0.0091, 0.1778]], + [9, 1000, [0.8888, -0.5659, 0.5834, -0.7469, 1.1912, -0.3923, 1.1241, -0.4424]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_v1_4(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="CompVis/stable-diffusion-v1-4") + latents = self.get_latents(seed) + encoder_hidden_states = self.get_encoder_hidden_states(seed) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == latents.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [83, 4, [-0.2323, -0.1304, 0.0813, -0.3093, -0.0919, -0.1571, -0.1125, -0.5806]], + [17, 0.55, [-0.0831, -0.2443, 0.0901, -0.0919, 0.3396, 0.0103, -0.3743, 0.0701]], + [8, 0.89, [-0.4863, 0.0859, 0.0875, -0.1658, 0.9199, -0.0114, 0.4839, 0.4639]], + [3, 1000, [-0.5649, 0.2402, -0.5518, 0.1248, 1.1328, -0.2443, -0.0325, -1.0078]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_v1_4_fp16(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="CompVis/stable-diffusion-v1-4", fp16=True) + latents = self.get_latents(seed, fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, fp16=True) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == latents.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=5e-3) + + @parameterized.expand( + [ + # fmt: off + [33, 4, [-0.4430, 0.1570, -0.1867, 0.2376, 0.3205, -0.3681, 0.0525, -0.0722]], + [47, 0.55, [-0.1415, 0.0129, -0.3136, 0.2257, 0.3430, -0.0536, 0.2114, -0.0436]], + [21, 0.89, [-0.7091, 0.6664, -0.3643, 0.9032, 0.4499, -0.6541, 0.0139, 0.1750]], + [9, 1000, [0.8878, -0.5659, 0.5844, -0.7442, 1.1883, -0.3927, 1.1192, -0.4423]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_v1_5(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="runwayml/stable-diffusion-v1-5") + latents = self.get_latents(seed) + encoder_hidden_states = self.get_encoder_hidden_states(seed) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == latents.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [83, 4, [-0.2695, -0.1669, 0.0073, -0.3181, -0.1187, -0.1676, -0.1395, -0.5972]], + [17, 0.55, [-0.1290, -0.2588, 0.0551, -0.0916, 0.3286, 0.0238, -0.3669, 0.0322]], + [8, 0.89, [-0.5283, 0.1198, 0.0870, -0.1141, 0.9189, -0.0150, 0.5474, 0.4319]], + [3, 1000, [-0.5601, 0.2411, -0.5435, 0.1268, 1.1338, -0.2427, -0.0280, -1.0020]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_v1_5_fp16(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="runwayml/stable-diffusion-v1-5", fp16=True) + latents = self.get_latents(seed, fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, fp16=True) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == latents.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=5e-3) + + @parameterized.expand( + [ + # fmt: off + [33, 4, [-0.7639, 0.0106, -0.1615, -0.3487, -0.0423, -0.7972, 0.0085, -0.4858]], + [47, 0.55, [-0.6564, 0.0795, -1.9026, -0.6258, 1.8235, 1.2056, 1.2169, 0.9073]], + [21, 0.89, [0.0327, 0.4399, -0.6358, 0.3417, 0.4120, -0.5621, -0.0397, -1.0430]], + [9, 1000, [0.1600, 0.7303, -1.0556, -0.3515, -0.7440, -1.2037, -1.8149, -1.8931]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_inpaint(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="runwayml/stable-diffusion-inpainting") + latents = self.get_latents(seed, shape=(4, 9, 64, 64)) + encoder_hidden_states = self.get_encoder_hidden_states(seed) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == (4, 4, 64, 64) + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [83, 4, [-0.1047, -1.7227, 0.1067, 0.0164, -0.5698, -0.4172, -0.1388, 1.1387]], + [17, 0.55, [0.0975, -0.2856, -0.3508, -0.4600, 0.3376, 0.2930, -0.2747, -0.7026]], + [8, 0.89, [-0.0952, 0.0183, -0.5825, -0.1981, 0.1131, 0.4668, -0.0395, -0.3486]], + [3, 1000, [0.4790, 0.4949, -1.0732, -0.7158, 0.7959, -0.9478, 0.1105, -0.9741]], + # fmt: on + ] + ) + @require_torch_gpu + def test_compvis_sd_inpaint_fp16(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="runwayml/stable-diffusion-inpainting", fp16=True) + latents = self.get_latents(seed, shape=(4, 9, 64, 64), fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, fp16=True) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == (4, 4, 64, 64) + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=5e-3) + + @parameterized.expand( + [ + # fmt: off + [83, 4, [0.1514, 0.0807, 0.1624, 0.1016, -0.1896, 0.0263, 0.0677, 0.2310]], + [17, 0.55, [0.1164, -0.0216, 0.0170, 0.1589, -0.3120, 0.1005, -0.0581, -0.1458]], + [8, 0.89, [-0.1758, -0.0169, 0.1004, -0.1411, 0.1312, 0.1103, -0.1996, 0.2139]], + [3, 1000, [0.1214, 0.0352, -0.0731, -0.1562, -0.0994, -0.0906, -0.2340, -0.0539]], + # fmt: on + ] + ) + @require_torch_gpu + def test_stabilityai_sd_v2_fp16(self, seed, timestep, expected_slice): + model = self.get_unet_model(model_id="stabilityai/stable-diffusion-2", fp16=True) + latents = self.get_latents(seed, shape=(4, 4, 96, 96), fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, shape=(4, 77, 1024), fp16=True) + + timestep = torch.tensor([timestep], dtype=torch.long, device=torch_device) + + with torch.no_grad(): + sample = model(latents, timestep=timestep, encoder_hidden_states=encoder_hidden_states).sample + + assert sample.shape == latents.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=5e-3) diff --git a/diffusers/tests/models/test_models_unet_2d_flax.py b/diffusers/tests/models/test_models_unet_2d_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..69a0704dca9dae32a7d612b82cbedc0454a0a1b5 --- /dev/null +++ b/diffusers/tests/models/test_models_unet_2d_flax.py @@ -0,0 +1,104 @@ +import gc +import unittest + +from parameterized import parameterized + +from diffusers import FlaxUNet2DConditionModel +from diffusers.utils import is_flax_available +from diffusers.utils.testing_utils import load_hf_numpy, require_flax, slow + + +if is_flax_available(): + import jax + import jax.numpy as jnp + + +@slow +@require_flax +class FlaxUNet2DConditionModelIntegrationTests(unittest.TestCase): + def get_file_format(self, seed, shape): + return f"gaussian_noise_s={seed}_shape={'_'.join([str(s) for s in shape])}.npy" + + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + + def get_latents(self, seed=0, shape=(4, 4, 64, 64), fp16=False): + dtype = jnp.bfloat16 if fp16 else jnp.float32 + image = jnp.array(load_hf_numpy(self.get_file_format(seed, shape)), dtype=dtype) + return image + + def get_unet_model(self, fp16=False, model_id="CompVis/stable-diffusion-v1-4"): + dtype = jnp.bfloat16 if fp16 else jnp.float32 + revision = "bf16" if fp16 else None + + model, params = FlaxUNet2DConditionModel.from_pretrained( + model_id, subfolder="unet", dtype=dtype, revision=revision + ) + return model, params + + def get_encoder_hidden_states(self, seed=0, shape=(4, 77, 768), fp16=False): + dtype = jnp.bfloat16 if fp16 else jnp.float32 + hidden_states = jnp.array(load_hf_numpy(self.get_file_format(seed, shape)), dtype=dtype) + return hidden_states + + @parameterized.expand( + [ + # fmt: off + [83, 4, [-0.2323, -0.1304, 0.0813, -0.3093, -0.0919, -0.1571, -0.1125, -0.5806]], + [17, 0.55, [-0.0831, -0.2443, 0.0901, -0.0919, 0.3396, 0.0103, -0.3743, 0.0701]], + [8, 0.89, [-0.4863, 0.0859, 0.0875, -0.1658, 0.9199, -0.0114, 0.4839, 0.4639]], + [3, 1000, [-0.5649, 0.2402, -0.5518, 0.1248, 1.1328, -0.2443, -0.0325, -1.0078]], + # fmt: on + ] + ) + def test_compvis_sd_v1_4_flax_vs_torch_fp16(self, seed, timestep, expected_slice): + model, params = self.get_unet_model(model_id="CompVis/stable-diffusion-v1-4", fp16=True) + latents = self.get_latents(seed, fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, fp16=True) + + sample = model.apply( + {"params": params}, + latents, + jnp.array(timestep, dtype=jnp.int32), + encoder_hidden_states=encoder_hidden_states, + ).sample + + assert sample.shape == latents.shape + + output_slice = jnp.asarray(jax.device_get((sample[-1, -2:, -2:, :2].flatten())), dtype=jnp.float32) + expected_output_slice = jnp.array(expected_slice, dtype=jnp.float32) + + # Found torch (float16) and flax (bfloat16) outputs to be within this tolerance, in the same hardware + assert jnp.allclose(output_slice, expected_output_slice, atol=1e-2) + + @parameterized.expand( + [ + # fmt: off + [83, 4, [0.1514, 0.0807, 0.1624, 0.1016, -0.1896, 0.0263, 0.0677, 0.2310]], + [17, 0.55, [0.1164, -0.0216, 0.0170, 0.1589, -0.3120, 0.1005, -0.0581, -0.1458]], + [8, 0.89, [-0.1758, -0.0169, 0.1004, -0.1411, 0.1312, 0.1103, -0.1996, 0.2139]], + [3, 1000, [0.1214, 0.0352, -0.0731, -0.1562, -0.0994, -0.0906, -0.2340, -0.0539]], + # fmt: on + ] + ) + def test_stabilityai_sd_v2_flax_vs_torch_fp16(self, seed, timestep, expected_slice): + model, params = self.get_unet_model(model_id="stabilityai/stable-diffusion-2", fp16=True) + latents = self.get_latents(seed, shape=(4, 4, 96, 96), fp16=True) + encoder_hidden_states = self.get_encoder_hidden_states(seed, shape=(4, 77, 1024), fp16=True) + + sample = model.apply( + {"params": params}, + latents, + jnp.array(timestep, dtype=jnp.int32), + encoder_hidden_states=encoder_hidden_states, + ).sample + + assert sample.shape == latents.shape + + output_slice = jnp.asarray(jax.device_get((sample[-1, -2:, -2:, :2].flatten())), dtype=jnp.float32) + expected_output_slice = jnp.array(expected_slice, dtype=jnp.float32) + + # Found torch (float16) and flax (bfloat16) outputs to be within this tolerance, on the same hardware + assert jnp.allclose(output_slice, expected_output_slice, atol=1e-2) diff --git a/diffusers/tests/models/test_models_vae.py b/diffusers/tests/models/test_models_vae.py new file mode 100644 index 0000000000000000000000000000000000000000..1cf4bc3c446cf40c0d4f0ba9f8cf359e56854f4d --- /dev/null +++ b/diffusers/tests/models/test_models_vae.py @@ -0,0 +1,310 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import torch +from parameterized import parameterized + +from diffusers import AutoencoderKL +from diffusers.models import ModelMixin +from diffusers.utils import floats_tensor, load_hf_numpy, require_torch_gpu, slow, torch_all_close, torch_device + +from ..test_modeling_common import ModelTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class AutoencoderKLTests(ModelTesterMixin, unittest.TestCase): + model_class = AutoencoderKL + + @property + def dummy_input(self): + batch_size = 4 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + + return {"sample": image} + + @property + def input_shape(self): + return (3, 32, 32) + + @property + def output_shape(self): + return (3, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": [32, 64], + "in_channels": 3, + "out_channels": 3, + "down_block_types": ["DownEncoderBlock2D", "DownEncoderBlock2D"], + "up_block_types": ["UpDecoderBlock2D", "UpDecoderBlock2D"], + "latent_channels": 4, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_forward_signature(self): + pass + + def test_training(self): + pass + + def test_from_pretrained_hub(self): + model, loading_info = AutoencoderKL.from_pretrained("fusing/autoencoder-kl-dummy", output_loading_info=True) + self.assertIsNotNone(model) + self.assertEqual(len(loading_info["missing_keys"]), 0) + + model.to(torch_device) + image = model(**self.dummy_input) + + assert image is not None, "Make sure output is not None" + + def test_output_pretrained(self): + model = AutoencoderKL.from_pretrained("fusing/autoencoder-kl-dummy") + model = model.to(torch_device) + model.eval() + + # One-time warmup pass (see #372) + if torch_device == "mps" and isinstance(model, ModelMixin): + image = torch.randn(1, model.config.in_channels, model.config.sample_size, model.config.sample_size) + image = image.to(torch_device) + with torch.no_grad(): + _ = model(image, sample_posterior=True).sample + generator = torch.manual_seed(0) + else: + generator = torch.Generator(device=torch_device).manual_seed(0) + + image = torch.randn( + 1, + model.config.in_channels, + model.config.sample_size, + model.config.sample_size, + generator=torch.manual_seed(0), + ) + image = image.to(torch_device) + with torch.no_grad(): + output = model(image, sample_posterior=True, generator=generator).sample + + output_slice = output[0, -1, -3:, -3:].flatten().cpu() + + # Since the VAE Gaussian prior's generator is seeded on the appropriate device, + # the expected output slices are not the same for CPU and GPU. + if torch_device == "mps": + expected_output_slice = torch.tensor( + [ + -4.0078e-01, + -3.8323e-04, + -1.2681e-01, + -1.1462e-01, + 2.0095e-01, + 1.0893e-01, + -8.8247e-02, + -3.0361e-01, + -9.8644e-03, + ] + ) + elif torch_device == "cpu": + expected_output_slice = torch.tensor( + [-0.1352, 0.0878, 0.0419, -0.0818, -0.1069, 0.0688, -0.1458, -0.4446, -0.0026] + ) + else: + expected_output_slice = torch.tensor( + [-0.2421, 0.4642, 0.2507, -0.0438, 0.0682, 0.3160, -0.2018, -0.0727, 0.2485] + ) + + self.assertTrue(torch_all_close(output_slice, expected_output_slice, rtol=1e-2)) + + +@slow +class AutoencoderKLIntegrationTests(unittest.TestCase): + def get_file_format(self, seed, shape): + return f"gaussian_noise_s={seed}_shape={'_'.join([str(s) for s in shape])}.npy" + + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_sd_image(self, seed=0, shape=(4, 3, 512, 512), fp16=False): + dtype = torch.float16 if fp16 else torch.float32 + image = torch.from_numpy(load_hf_numpy(self.get_file_format(seed, shape))).to(torch_device).to(dtype) + return image + + def get_sd_vae_model(self, model_id="CompVis/stable-diffusion-v1-4", fp16=False): + revision = "fp16" if fp16 else None + torch_dtype = torch.float16 if fp16 else torch.float32 + + model = AutoencoderKL.from_pretrained( + model_id, + subfolder="vae", + torch_dtype=torch_dtype, + revision=revision, + ) + model.to(torch_device).eval() + + return model + + def get_generator(self, seed=0): + if torch_device == "mps": + return torch.manual_seed(seed) + return torch.Generator(device=torch_device).manual_seed(seed) + + @parameterized.expand( + [ + # fmt: off + [33, [-0.1603, 0.9878, -0.0495, -0.0790, -0.2709, 0.8375, -0.2060, -0.0824], [-0.2395, 0.0098, 0.0102, -0.0709, -0.2840, -0.0274, -0.0718, -0.1824]], + [47, [-0.2376, 0.1168, 0.1332, -0.4840, -0.2508, -0.0791, -0.0493, -0.4089], [0.0350, 0.0847, 0.0467, 0.0344, -0.0842, -0.0547, -0.0633, -0.1131]], + # fmt: on + ] + ) + def test_stable_diffusion(self, seed, expected_slice, expected_slice_mps): + model = self.get_sd_vae_model() + image = self.get_sd_image(seed) + generator = self.get_generator(seed) + + with torch.no_grad(): + sample = model(image, generator=generator, sample_posterior=True).sample + + assert sample.shape == image.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice_mps if torch_device == "mps" else expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [33, [-0.0513, 0.0289, 1.3799, 0.2166, -0.2573, -0.0871, 0.5103, -0.0999]], + [47, [-0.4128, -0.1320, -0.3704, 0.1965, -0.4116, -0.2332, -0.3340, 0.2247]], + # fmt: on + ] + ) + @require_torch_gpu + def test_stable_diffusion_fp16(self, seed, expected_slice): + model = self.get_sd_vae_model(fp16=True) + image = self.get_sd_image(seed, fp16=True) + generator = self.get_generator(seed) + + with torch.no_grad(): + sample = model(image, generator=generator, sample_posterior=True).sample + + assert sample.shape == image.shape + + output_slice = sample[-1, -2:, :2, -2:].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-2) + + @parameterized.expand( + [ + # fmt: off + [33, [-0.1609, 0.9866, -0.0487, -0.0777, -0.2716, 0.8368, -0.2055, -0.0814], [-0.2395, 0.0098, 0.0102, -0.0709, -0.2840, -0.0274, -0.0718, -0.1824]], + [47, [-0.2377, 0.1147, 0.1333, -0.4841, -0.2506, -0.0805, -0.0491, -0.4085], [0.0350, 0.0847, 0.0467, 0.0344, -0.0842, -0.0547, -0.0633, -0.1131]], + # fmt: on + ] + ) + def test_stable_diffusion_mode(self, seed, expected_slice, expected_slice_mps): + model = self.get_sd_vae_model() + image = self.get_sd_image(seed) + + with torch.no_grad(): + sample = model(image).sample + + assert sample.shape == image.shape + + output_slice = sample[-1, -2:, -2:, :2].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice_mps if torch_device == "mps" else expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [13, [-0.2051, -0.1803, -0.2311, -0.2114, -0.3292, -0.3574, -0.2953, -0.3323]], + [37, [-0.2632, -0.2625, -0.2199, -0.2741, -0.4539, -0.4990, -0.3720, -0.4925]], + # fmt: on + ] + ) + @require_torch_gpu + def test_stable_diffusion_decode(self, seed, expected_slice): + model = self.get_sd_vae_model() + encoding = self.get_sd_image(seed, shape=(3, 4, 64, 64)) + + with torch.no_grad(): + sample = model.decode(encoding).sample + + assert list(sample.shape) == [3, 3, 512, 512] + + output_slice = sample[-1, -2:, :2, -2:].flatten().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=1e-3) + + @parameterized.expand( + [ + # fmt: off + [27, [-0.0369, 0.0207, -0.0776, -0.0682, -0.1747, -0.1930, -0.1465, -0.2039]], + [16, [-0.1628, -0.2134, -0.2747, -0.2642, -0.3774, -0.4404, -0.3687, -0.4277]], + # fmt: on + ] + ) + @require_torch_gpu + def test_stable_diffusion_decode_fp16(self, seed, expected_slice): + model = self.get_sd_vae_model(fp16=True) + encoding = self.get_sd_image(seed, shape=(3, 4, 64, 64), fp16=True) + + with torch.no_grad(): + sample = model.decode(encoding).sample + + assert list(sample.shape) == [3, 3, 512, 512] + + output_slice = sample[-1, -2:, :2, -2:].flatten().float().cpu() + expected_output_slice = torch.tensor(expected_slice) + + assert torch_all_close(output_slice, expected_output_slice, atol=5e-3) + + @parameterized.expand( + [ + # fmt: off + [33, [-0.3001, 0.0918, -2.6984, -3.9720, -3.2099, -5.0353, 1.7338, -0.2065, 3.4267]], + [47, [-1.5030, -4.3871, -6.0355, -9.1157, -1.6661, -2.7853, 2.1607, -5.0823, 2.5633]], + # fmt: on + ] + ) + def test_stable_diffusion_encode_sample(self, seed, expected_slice): + model = self.get_sd_vae_model() + image = self.get_sd_image(seed) + generator = self.get_generator(seed) + + with torch.no_grad(): + dist = model.encode(image).latent_dist + sample = dist.sample(generator=generator) + + assert list(sample.shape) == [image.shape[0], 4] + [i // 8 for i in image.shape[2:]] + + output_slice = sample[0, -1, -3:, -3:].flatten().cpu() + expected_output_slice = torch.tensor(expected_slice) + + tolerance = 1e-3 if torch_device != "mps" else 1e-2 + assert torch_all_close(output_slice, expected_output_slice, atol=tolerance) diff --git a/diffusers/tests/models/test_models_vae_flax.py b/diffusers/tests/models/test_models_vae_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..8fedb85eccfc73e9a0900f7bb947887da3ffe4e9 --- /dev/null +++ b/diffusers/tests/models/test_models_vae_flax.py @@ -0,0 +1,39 @@ +import unittest + +from diffusers import FlaxAutoencoderKL +from diffusers.utils import is_flax_available +from diffusers.utils.testing_utils import require_flax + +from ..test_modeling_common_flax import FlaxModelTesterMixin + + +if is_flax_available(): + import jax + + +@require_flax +class FlaxAutoencoderKLTests(FlaxModelTesterMixin, unittest.TestCase): + model_class = FlaxAutoencoderKL + + @property + def dummy_input(self): + batch_size = 4 + num_channels = 3 + sizes = (32, 32) + + prng_key = jax.random.PRNGKey(0) + image = jax.random.uniform(prng_key, ((batch_size, num_channels) + sizes)) + + return {"sample": image, "prng_key": prng_key} + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": [32, 64], + "in_channels": 3, + "out_channels": 3, + "down_block_types": ["DownEncoderBlock2D", "DownEncoderBlock2D"], + "up_block_types": ["UpDecoderBlock2D", "UpDecoderBlock2D"], + "latent_channels": 4, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict diff --git a/diffusers/tests/models/test_models_vq.py b/diffusers/tests/models/test_models_vq.py new file mode 100644 index 0000000000000000000000000000000000000000..f58e90469885f51786669063c6d2cdfa0d7d941e --- /dev/null +++ b/diffusers/tests/models/test_models_vq.py @@ -0,0 +1,97 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import torch + +from diffusers import VQModel +from diffusers.utils import floats_tensor, torch_device + +from ..test_modeling_common import ModelTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VQModelTests(ModelTesterMixin, unittest.TestCase): + model_class = VQModel + + @property + def dummy_input(self, sizes=(32, 32)): + batch_size = 4 + num_channels = 3 + + image = floats_tensor((batch_size, num_channels) + sizes).to(torch_device) + + return {"sample": image} + + @property + def input_shape(self): + return (3, 32, 32) + + @property + def output_shape(self): + return (3, 32, 32) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "block_out_channels": [32, 64], + "in_channels": 3, + "out_channels": 3, + "down_block_types": ["DownEncoderBlock2D", "DownEncoderBlock2D"], + "up_block_types": ["UpDecoderBlock2D", "UpDecoderBlock2D"], + "latent_channels": 3, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_forward_signature(self): + pass + + def test_training(self): + pass + + def test_from_pretrained_hub(self): + model, loading_info = VQModel.from_pretrained("fusing/vqgan-dummy", output_loading_info=True) + self.assertIsNotNone(model) + self.assertEqual(len(loading_info["missing_keys"]), 0) + + model.to(torch_device) + image = model(**self.dummy_input) + + assert image is not None, "Make sure output is not None" + + def test_output_pretrained(self): + model = VQModel.from_pretrained("fusing/vqgan-dummy") + model.to(torch_device).eval() + + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + image = torch.randn(1, model.config.in_channels, model.config.sample_size, model.config.sample_size) + image = image.to(torch_device) + with torch.no_grad(): + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = model(image) + output = model(image).sample + + output_slice = output[0, -1, -3:, -3:].flatten().cpu() + # fmt: off + expected_output_slice = torch.tensor([-0.0153, -0.4044, -0.1880, -0.5161, -0.2418, -0.4072, -0.1612, -0.0633, -0.0143]) + # fmt: on + self.assertTrue(torch.allclose(output_slice, expected_output_slice, atol=1e-3)) diff --git a/diffusers/tests/pipelines/__init__.py b/diffusers/tests/pipelines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/altdiffusion/__init__.py b/diffusers/tests/pipelines/altdiffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion.py b/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..1740e9af382f9f9bfec4f77e2ce5baa6bb9bc35c --- /dev/null +++ b/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion.py @@ -0,0 +1,241 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, XLMRobertaTokenizer + +from diffusers import AltDiffusionPipeline, AutoencoderKL, DDIMScheduler, PNDMScheduler, UNet2DConditionModel +from diffusers.pipelines.alt_diffusion.modeling_roberta_series import ( + RobertaSeriesConfig, + RobertaSeriesModelWithTransformation, +) +from diffusers.utils import slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class AltDiffusionPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = AltDiffusionPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + ) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + + # TODO: address the non-deterministic text encoder (fails for save-load tests) + # torch.manual_seed(0) + # text_encoder_config = RobertaSeriesConfig( + # hidden_size=32, + # project_dim=32, + # intermediate_size=37, + # layer_norm_eps=1e-05, + # num_attention_heads=4, + # num_hidden_layers=5, + # vocab_size=5002, + # ) + # text_encoder = RobertaSeriesModelWithTransformation(text_encoder_config) + + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + projection_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=5002, + ) + text_encoder = CLIPTextModel(text_encoder_config) + + tokenizer = XLMRobertaTokenizer.from_pretrained("hf-internal-testing/tiny-xlm-roberta") + tokenizer.model_max_length = 77 + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_alt_diffusion_ddim(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + torch.manual_seed(0) + text_encoder_config = RobertaSeriesConfig( + hidden_size=32, + project_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + vocab_size=5002, + ) + # TODO: remove after fixing the non-deterministic text encoder + text_encoder = RobertaSeriesModelWithTransformation(text_encoder_config) + components["text_encoder"] = text_encoder + + alt_pipe = AltDiffusionPipeline(**components) + alt_pipe = alt_pipe.to(device) + alt_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = "A photo of an astronaut" + output = alt_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [0.5748162, 0.60447145, 0.48821217, 0.50100636, 0.5431185, 0.45763683, 0.49657696, 0.48132733, 0.47573093] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_alt_diffusion_pndm(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + components["scheduler"] = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + text_encoder_config = RobertaSeriesConfig( + hidden_size=32, + project_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + vocab_size=5002, + ) + # TODO: remove after fixing the non-deterministic text encoder + text_encoder = RobertaSeriesModelWithTransformation(text_encoder_config) + components["text_encoder"] = text_encoder + alt_pipe = AltDiffusionPipeline(**components) + alt_pipe = alt_pipe.to(device) + alt_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = alt_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [0.51605093, 0.5707241, 0.47365507, 0.50578886, 0.5633877, 0.4642503, 0.5182081, 0.48763484, 0.49084237] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch_gpu +class AltDiffusionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_alt_diffusion(self): + # make sure here that pndm scheduler skips prk + alt_pipe = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion", safety_checker=None) + alt_pipe = alt_pipe.to(torch_device) + alt_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + output = alt_pipe([prompt], generator=generator, guidance_scale=6.0, num_inference_steps=20, output_type="np") + + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.1010, 0.0800, 0.0794, 0.0885, 0.0843, 0.0762, 0.0769, 0.0729, 0.0586]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_alt_diffusion_fast_ddim(self): + scheduler = DDIMScheduler.from_pretrained("BAAI/AltDiffusion", subfolder="scheduler") + + alt_pipe = AltDiffusionPipeline.from_pretrained("BAAI/AltDiffusion", scheduler=scheduler, safety_checker=None) + alt_pipe = alt_pipe.to(torch_device) + alt_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + + output = alt_pipe([prompt], generator=generator, num_inference_steps=2, output_type="numpy") + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.4019, 0.4052, 0.3810, 0.4119, 0.3916, 0.3982, 0.4651, 0.4195, 0.5323]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion_img2img.py b/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..d34f8e15cd6a255721bec5d1164f753f686ea26c --- /dev/null +++ b/diffusers/tests/pipelines/altdiffusion/test_alt_diffusion_img2img.py @@ -0,0 +1,291 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from transformers import XLMRobertaTokenizer + +from diffusers import AltDiffusionImg2ImgPipeline, AutoencoderKL, PNDMScheduler, UNet2DConditionModel +from diffusers.pipelines.alt_diffusion.modeling_roberta_series import ( + RobertaSeriesConfig, + RobertaSeriesModelWithTransformation, +) +from diffusers.utils import floats_tensor, load_image, load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class AltDiffusionImg2ImgPipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + @property + def dummy_cond_unet(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = RobertaSeriesConfig( + hidden_size=32, + project_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=5006, + ) + return RobertaSeriesModelWithTransformation(config) + + @property + def dummy_extractor(self): + def extract(*args, **kwargs): + class Out: + def __init__(self): + self.pixel_values = torch.ones([0]) + + def to(self, device): + self.pixel_values.to(device) + return self + + return Out() + + return extract + + def test_stable_diffusion_img2img_default_case(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = XLMRobertaTokenizer.from_pretrained("hf-internal-testing/tiny-xlm-roberta") + tokenizer.model_max_length = 77 + + init_image = self.dummy_image.to(device) + + # make sure here that pndm scheduler skips prk + alt_pipe = AltDiffusionImg2ImgPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + alt_pipe = alt_pipe.to(device) + alt_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.Generator(device=device).manual_seed(0) + output = alt_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + image=init_image, + ) + + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = alt_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + image=init_image, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4115, 0.3870, 0.4089, 0.4807, 0.4668, 0.4144, 0.4151, 0.4721, 0.4569]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-3 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 5e-3 + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_img2img_fp16(self): + """Test that stable diffusion img2img works with fp16""" + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = XLMRobertaTokenizer.from_pretrained("hf-internal-testing/tiny-xlm-roberta") + tokenizer.model_max_length = 77 + + init_image = self.dummy_image.to(torch_device) + + # put models in fp16 + unet = unet.half() + vae = vae.half() + bert = bert.half() + + # make sure here that pndm scheduler skips prk + alt_pipe = AltDiffusionImg2ImgPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + alt_pipe = alt_pipe.to(torch_device) + alt_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + image = alt_pipe( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + image=init_image, + ).images + + assert image.shape == (1, 32, 32, 3) + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_img2img_pipeline_multiple_of_8(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/img2img/sketch-mountains-input.jpg" + ) + # resize to resolution that is divisible by 8 but not 16 or 32 + init_image = init_image.resize((760, 504)) + + model_id = "BAAI/AltDiffusion" + pipe = AltDiffusionImg2ImgPipeline.from_pretrained( + model_id, + safety_checker=None, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "A fantasy landscape, trending on artstation" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=init_image, + strength=0.75, + guidance_scale=7.5, + generator=generator, + output_type="np", + ) + image = output.images[0] + + image_slice = image[255:258, 383:386, -1] + + assert image.shape == (504, 760, 3) + expected_slice = np.array([0.9358, 0.9397, 0.9599, 0.9901, 1.0000, 1.0000, 0.9882, 1.0000, 1.0000]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + +@slow +@require_torch_gpu +class AltDiffusionImg2ImgPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_stable_diffusion_img2img_pipeline_default(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/img2img/sketch-mountains-input.jpg" + ) + init_image = init_image.resize((768, 512)) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/img2img/fantasy_landscape_alt.npy" + ) + + model_id = "BAAI/AltDiffusion" + pipe = AltDiffusionImg2ImgPipeline.from_pretrained( + model_id, + safety_checker=None, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "A fantasy landscape, trending on artstation" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=init_image, + strength=0.75, + guidance_scale=7.5, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (512, 768, 3) + # img2img is flaky across GPUs even in fp32, so using MAE here + assert np.abs(expected_image - image).max() < 1e-3 diff --git a/diffusers/tests/pipelines/audio_diffusion/__init__.py b/diffusers/tests/pipelines/audio_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/audio_diffusion/test_audio_diffusion.py b/diffusers/tests/pipelines/audio_diffusion/test_audio_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..b68e940bdcdafda0a0fc5e5543aa1e97fadefce3 --- /dev/null +++ b/diffusers/tests/pipelines/audio_diffusion/test_audio_diffusion.py @@ -0,0 +1,190 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch + +from diffusers import ( + AudioDiffusionPipeline, + AutoencoderKL, + DDIMScheduler, + DDPMScheduler, + DiffusionPipeline, + Mel, + UNet2DConditionModel, + UNet2DModel, +) +from diffusers.utils import slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class PipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + sample_size=(32, 64), + in_channels=1, + out_channels=1, + layers_per_block=2, + block_out_channels=(128, 128), + down_block_types=("AttnDownBlock2D", "DownBlock2D"), + up_block_types=("UpBlock2D", "AttnUpBlock2D"), + ) + return model + + @property + def dummy_unet_condition(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + sample_size=(64, 32), + in_channels=1, + out_channels=1, + layers_per_block=2, + block_out_channels=(128, 128), + down_block_types=("CrossAttnDownBlock2D", "DownBlock2D"), + up_block_types=("UpBlock2D", "CrossAttnUpBlock2D"), + cross_attention_dim=10, + ) + return model + + @property + def dummy_vqvae_and_unet(self): + torch.manual_seed(0) + vqvae = AutoencoderKL( + sample_size=(128, 64), + in_channels=1, + out_channels=1, + latent_channels=1, + layers_per_block=2, + block_out_channels=(128, 128), + down_block_types=("DownEncoderBlock2D", "DownEncoderBlock2D"), + up_block_types=("UpDecoderBlock2D", "UpDecoderBlock2D"), + ) + unet = UNet2DModel( + sample_size=(64, 32), + in_channels=1, + out_channels=1, + layers_per_block=2, + block_out_channels=(128, 128), + down_block_types=("AttnDownBlock2D", "DownBlock2D"), + up_block_types=("UpBlock2D", "AttnUpBlock2D"), + ) + return vqvae, unet + + def test_audio_diffusion(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + mel = Mel() + + scheduler = DDPMScheduler() + pipe = AudioDiffusionPipeline(vqvae=None, unet=self.dummy_unet, mel=mel, scheduler=scheduler) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=device).manual_seed(42) + output = pipe(generator=generator, steps=4) + audio = output.audios[0] + image = output.images[0] + + generator = torch.Generator(device=device).manual_seed(42) + output = pipe(generator=generator, steps=4, return_dict=False) + image_from_tuple = output[0][0] + + assert audio.shape == (1, (self.dummy_unet.sample_size[1] - 1) * mel.hop_length) + assert image.height == self.dummy_unet.sample_size[0] and image.width == self.dummy_unet.sample_size[1] + image_slice = np.frombuffer(image.tobytes(), dtype="uint8")[:10] + image_from_tuple_slice = np.frombuffer(image_from_tuple.tobytes(), dtype="uint8")[:10] + expected_slice = np.array([69, 255, 255, 255, 0, 0, 77, 181, 12, 127]) + + assert np.abs(image_slice.flatten() - expected_slice).max() == 0 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() == 0 + + scheduler = DDIMScheduler() + dummy_vqvae_and_unet = self.dummy_vqvae_and_unet + pipe = AudioDiffusionPipeline( + vqvae=self.dummy_vqvae_and_unet[0], unet=dummy_vqvae_and_unet[1], mel=mel, scheduler=scheduler + ) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + np.random.seed(0) + raw_audio = np.random.uniform(-1, 1, ((dummy_vqvae_and_unet[0].sample_size[1] - 1) * mel.hop_length,)) + generator = torch.Generator(device=device).manual_seed(42) + output = pipe(raw_audio=raw_audio, generator=generator, start_step=5, steps=10) + image = output.images[0] + + assert ( + image.height == self.dummy_vqvae_and_unet[0].sample_size[0] + and image.width == self.dummy_vqvae_and_unet[0].sample_size[1] + ) + image_slice = np.frombuffer(image.tobytes(), dtype="uint8")[:10] + expected_slice = np.array([120, 117, 110, 109, 138, 167, 138, 148, 132, 121]) + + assert np.abs(image_slice.flatten() - expected_slice).max() == 0 + + dummy_unet_condition = self.dummy_unet_condition + pipe = AudioDiffusionPipeline( + vqvae=self.dummy_vqvae_and_unet[0], unet=dummy_unet_condition, mel=mel, scheduler=scheduler + ) + + np.random.seed(0) + encoding = torch.rand((1, 1, 10)) + output = pipe(generator=generator, encoding=encoding) + image = output.images[0] + image_slice = np.frombuffer(image.tobytes(), dtype="uint8")[:10] + expected_slice = np.array([120, 139, 147, 123, 124, 96, 115, 121, 126, 144]) + + assert np.abs(image_slice.flatten() - expected_slice).max() == 0 + + +@slow +@require_torch_gpu +class PipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_audio_diffusion(self): + device = torch_device + + pipe = DiffusionPipeline.from_pretrained("teticio/audio-diffusion-ddim-256") + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=device).manual_seed(42) + output = pipe(generator=generator) + audio = output.audios[0] + image = output.images[0] + + assert audio.shape == (1, (pipe.unet.sample_size[1] - 1) * pipe.mel.hop_length) + assert image.height == pipe.unet.sample_size[0] and image.width == pipe.unet.sample_size[1] + image_slice = np.frombuffer(image.tobytes(), dtype="uint8")[:10] + expected_slice = np.array([151, 167, 154, 144, 122, 134, 121, 105, 70, 26]) + + assert np.abs(image_slice.flatten() - expected_slice).max() == 0 diff --git a/diffusers/tests/pipelines/dance_diffusion/__init__.py b/diffusers/tests/pipelines/dance_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/dance_diffusion/test_dance_diffusion.py b/diffusers/tests/pipelines/dance_diffusion/test_dance_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..08ca2e0bee305c88baf0ce4062f3932615c23901 --- /dev/null +++ b/diffusers/tests/pipelines/dance_diffusion/test_dance_diffusion.py @@ -0,0 +1,134 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch + +from diffusers import DanceDiffusionPipeline, IPNDMScheduler, UNet1DModel +from diffusers.utils import slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class DanceDiffusionPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = DanceDiffusionPipeline + test_attention_slicing = False + test_cpu_offload = False + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet1DModel( + block_out_channels=(32, 32, 64), + extra_in_channels=16, + sample_size=512, + sample_rate=16_000, + in_channels=2, + out_channels=2, + flip_sin_to_cos=True, + use_timestep_embedding=False, + time_embedding_type="fourier", + mid_block_type="UNetMidBlock1D", + down_block_types=("DownBlock1DNoSkip", "DownBlock1D", "AttnDownBlock1D"), + up_block_types=("AttnUpBlock1D", "UpBlock1D", "UpBlock1DNoSkip"), + ) + scheduler = IPNDMScheduler() + + components = { + "unet": unet, + "scheduler": scheduler, + } + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "batch_size": 1, + "generator": generator, + "num_inference_steps": 4, + } + return inputs + + def test_dance_diffusion(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = DanceDiffusionPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = pipe(**inputs) + audio = output.audios + + audio_slice = audio[0, -3:, -3:] + + assert audio.shape == (1, 2, components["unet"].sample_size) + expected_slice = np.array([-0.7265, 1.0000, -0.8388, 0.1175, 0.9498, -1.0000]) + assert np.abs(audio_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch_gpu +class PipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_dance_diffusion(self): + device = torch_device + + pipe = DanceDiffusionPipeline.from_pretrained("harmonai/maestro-150k") + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + output = pipe(generator=generator, num_inference_steps=100, audio_length_in_s=4.096) + audio = output.audios + + audio_slice = audio[0, -3:, -3:] + + assert audio.shape == (1, 2, pipe.unet.sample_size) + expected_slice = np.array([-0.0192, -0.0231, -0.0318, -0.0059, 0.0002, -0.0020]) + + assert np.abs(audio_slice.flatten() - expected_slice).max() < 1e-2 + + def test_dance_diffusion_fp16(self): + device = torch_device + + pipe = DanceDiffusionPipeline.from_pretrained("harmonai/maestro-150k", torch_dtype=torch.float16) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + output = pipe(generator=generator, num_inference_steps=100, audio_length_in_s=4.096) + audio = output.audios + + audio_slice = audio[0, -3:, -3:] + + assert audio.shape == (1, 2, pipe.unet.sample_size) + expected_slice = np.array([-0.0367, -0.0488, -0.0771, -0.0525, -0.0444, -0.0341]) + + assert np.abs(audio_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/ddim/__init__.py b/diffusers/tests/pipelines/ddim/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/ddim/test_ddim.py b/diffusers/tests/pipelines/ddim/test_ddim.py new file mode 100644 index 0000000000000000000000000000000000000000..300e7217f8c30ea383adaca50e0e266a45903618 --- /dev/null +++ b/diffusers/tests/pipelines/ddim/test_ddim.py @@ -0,0 +1,123 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import DDIMPipeline, DDIMScheduler, UNet2DModel +from diffusers.utils.testing_utils import require_torch_gpu, slow, torch_device + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class DDIMPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = DDIMPipeline + test_cpu_offload = False + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + scheduler = DDIMScheduler() + components = {"unet": unet, "scheduler": scheduler} + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "batch_size": 1, + "generator": generator, + "num_inference_steps": 2, + "output_type": "numpy", + } + return inputs + + def test_inference(self): + device = "cpu" + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + self.assertEqual(image.shape, (1, 32, 32, 3)) + expected_slice = np.array( + [1.000e00, 5.717e-01, 4.717e-01, 1.000e00, 0.000e00, 1.000e00, 3.000e-04, 0.000e00, 9.000e-04] + ) + max_diff = np.abs(image_slice.flatten() - expected_slice).max() + self.assertLessEqual(max_diff, 1e-3) + + +@slow +@require_torch_gpu +class DDIMPipelineIntegrationTests(unittest.TestCase): + def test_inference_cifar10(self): + model_id = "google/ddpm-cifar10-32" + + unet = UNet2DModel.from_pretrained(model_id) + scheduler = DDIMScheduler() + + ddim = DDIMPipeline(unet=unet, scheduler=scheduler) + ddim.to(torch_device) + ddim.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = ddim(generator=generator, eta=0.0, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.1723, 0.1617, 0.1600, 0.1626, 0.1497, 0.1513, 0.1505, 0.1442, 0.1453]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_inference_ema_bedroom(self): + model_id = "google/ddpm-ema-bedroom-256" + + unet = UNet2DModel.from_pretrained(model_id) + scheduler = DDIMScheduler.from_pretrained(model_id) + + ddpm = DDIMPipeline(unet=unet, scheduler=scheduler) + ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = ddpm(generator=generator, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 256, 256, 3) + expected_slice = np.array([0.0060, 0.0201, 0.0344, 0.0024, 0.0018, 0.0002, 0.0022, 0.0000, 0.0069]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/ddpm/__init__.py b/diffusers/tests/pipelines/ddpm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/ddpm/test_ddpm.py b/diffusers/tests/pipelines/ddpm/test_ddpm.py new file mode 100644 index 0000000000000000000000000000000000000000..c3ea0045b4c15710f7b34034a3502d84fcb55e05 --- /dev/null +++ b/diffusers/tests/pipelines/ddpm/test_ddpm.py @@ -0,0 +1,115 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import DDPMPipeline, DDPMScheduler, UNet2DModel +from diffusers.utils.testing_utils import require_torch_gpu, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class DDPMPipelineFastTests(unittest.TestCase): + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + def test_fast_inference(self): + device = "cpu" + unet = self.dummy_uncond_unet + scheduler = DDPMScheduler() + + ddpm = DDPMPipeline(unet=unet, scheduler=scheduler) + ddpm.to(device) + ddpm.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=device).manual_seed(0) + image = ddpm(generator=generator, num_inference_steps=2, output_type="numpy").images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = ddpm(generator=generator, num_inference_steps=2, output_type="numpy", return_dict=False)[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array( + [9.956e-01, 5.785e-01, 4.675e-01, 9.930e-01, 0.0, 1.000, 1.199e-03, 2.648e-04, 5.101e-04] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_inference_predict_sample(self): + unet = self.dummy_uncond_unet + scheduler = DDPMScheduler(prediction_type="sample") + + ddpm = DDPMPipeline(unet=unet, scheduler=scheduler) + ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = ddpm(num_inference_steps=1) + + generator = torch.manual_seed(0) + image = ddpm(generator=generator, num_inference_steps=2, output_type="numpy").images + + generator = torch.manual_seed(0) + image_eps = ddpm(generator=generator, num_inference_steps=2, output_type="numpy")[0] + + image_slice = image[0, -3:, -3:, -1] + image_eps_slice = image_eps[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + tolerance = 1e-2 if torch_device != "mps" else 3e-2 + assert np.abs(image_slice.flatten() - image_eps_slice.flatten()).max() < tolerance + + +@slow +@require_torch_gpu +class DDPMPipelineIntegrationTests(unittest.TestCase): + def test_inference_cifar10(self): + model_id = "google/ddpm-cifar10-32" + + unet = UNet2DModel.from_pretrained(model_id) + scheduler = DDPMScheduler.from_pretrained(model_id) + + ddpm = DDPMPipeline(unet=unet, scheduler=scheduler) + ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = ddpm(generator=generator, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4200, 0.3588, 0.1939, 0.3847, 0.3382, 0.2647, 0.4155, 0.3582, 0.3385]) + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/dit/__init__.py b/diffusers/tests/pipelines/dit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/dit/test_dit.py b/diffusers/tests/pipelines/dit/test_dit.py new file mode 100644 index 0000000000000000000000000000000000000000..f1838ebef05d04c254cf56f29a7e92fdb7c00db2 --- /dev/null +++ b/diffusers/tests/pipelines/dit/test_dit.py @@ -0,0 +1,133 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch + +from diffusers import AutoencoderKL, DDIMScheduler, DiTPipeline, DPMSolverMultistepScheduler, Transformer2DModel +from diffusers.utils import load_numpy, slow +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class DiTPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = DiTPipeline + test_cpu_offload = False + + def get_dummy_components(self): + torch.manual_seed(0) + transformer = Transformer2DModel( + sample_size=16, + num_layers=2, + patch_size=4, + attention_head_dim=8, + num_attention_heads=2, + in_channels=4, + out_channels=8, + attention_bias=True, + activation_fn="gelu-approximate", + num_embeds_ada_norm=1000, + norm_type="ada_norm_zero", + norm_elementwise_affine=False, + ) + vae = AutoencoderKL() + scheduler = DDIMScheduler() + components = {"transformer": transformer.eval(), "vae": vae.eval(), "scheduler": scheduler} + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "class_labels": [1], + "generator": generator, + "num_inference_steps": 2, + "output_type": "numpy", + } + return inputs + + def test_inference(self): + device = "cpu" + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + self.assertEqual(image.shape, (1, 16, 16, 3)) + expected_slice = np.array([0.4380, 0.4141, 0.5159, 0.0000, 0.4282, 0.6680, 0.5485, 0.2545, 0.6719]) + max_diff = np.abs(image_slice.flatten() - expected_slice).max() + self.assertLessEqual(max_diff, 1e-3) + + def test_inference_batch_single_identical(self): + self._test_inference_batch_single_identical(relax_max_difference=True) + + +@require_torch_gpu +@slow +class DiTPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_dit_256(self): + generator = torch.manual_seed(0) + + pipe = DiTPipeline.from_pretrained("facebook/DiT-XL-2-256") + pipe.to("cuda") + + words = ["vase", "umbrella", "white shark", "white wolf"] + ids = pipe.get_label_ids(words) + + images = pipe(ids, generator=generator, num_inference_steps=40, output_type="np").images + + for word, image in zip(words, images): + expected_image = load_numpy( + f"https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/dit/{word}.npy" + ) + assert np.abs((expected_image - image).max()) < 1e-3 + + def test_dit_512_fp16(self): + pipe = DiTPipeline.from_pretrained("facebook/DiT-XL-2-512", torch_dtype=torch.float16) + pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) + pipe.to("cuda") + + words = ["vase", "umbrella"] + ids = pipe.get_label_ids(words) + + generator = torch.manual_seed(0) + images = pipe(ids, generator=generator, num_inference_steps=25, output_type="np").images + + for word, image in zip(words, images): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + f"/dit/{word}_fp16.npy" + ) + + assert np.abs((expected_image - image).max()) < 7.5e-1 diff --git a/diffusers/tests/pipelines/karras_ve/__init__.py b/diffusers/tests/pipelines/karras_ve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/karras_ve/test_karras_ve.py b/diffusers/tests/pipelines/karras_ve/test_karras_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..79fa503e6dcd8cc0d17e1d07ee5e49a5c328f8fc --- /dev/null +++ b/diffusers/tests/pipelines/karras_ve/test_karras_ve.py @@ -0,0 +1,86 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import KarrasVePipeline, KarrasVeScheduler, UNet2DModel +from diffusers.utils.testing_utils import require_torch, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class KarrasVePipelineFastTests(unittest.TestCase): + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + def test_inference(self): + unet = self.dummy_uncond_unet + scheduler = KarrasVeScheduler() + + pipe = KarrasVePipeline(unet=unet, scheduler=scheduler) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = pipe(num_inference_steps=2, generator=generator, output_type="numpy").images + + generator = torch.manual_seed(0) + image_from_tuple = pipe(num_inference_steps=2, generator=generator, output_type="numpy", return_dict=False)[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch +class KarrasVePipelineIntegrationTests(unittest.TestCase): + def test_inference(self): + model_id = "google/ncsnpp-celebahq-256" + model = UNet2DModel.from_pretrained(model_id) + scheduler = KarrasVeScheduler() + + pipe = KarrasVePipeline(unet=model, scheduler=scheduler) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = pipe(num_inference_steps=20, generator=generator, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + assert image.shape == (1, 256, 256, 3) + expected_slice = np.array([0.578, 0.5811, 0.5924, 0.5809, 0.587, 0.5886, 0.5861, 0.5802, 0.586]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/latent_diffusion/__init__.py b/diffusers/tests/pipelines/latent_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion.py b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..24d75068af29e363a677727924cd0a1090b2363b --- /dev/null +++ b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion.py @@ -0,0 +1,189 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, DDIMScheduler, LDMTextToImagePipeline, UNet2DConditionModel +from diffusers.utils.testing_utils import load_numpy, nightly, require_torch_gpu, slow, torch_device + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class LDMTextToImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = LDMTextToImagePipeline + test_cpu_offload = False + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + ) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=(32, 64), + in_channels=3, + out_channels=3, + down_block_types=("DownEncoderBlock2D", "DownEncoderBlock2D"), + up_block_types=("UpDecoderBlock2D", "UpDecoderBlock2D"), + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vqvae": vae, + "bert": text_encoder, + "tokenizer": tokenizer, + } + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_inference_text2img(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + pipe = LDMTextToImagePipeline(**components) + pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 16, 16, 3) + expected_slice = np.array([0.59450, 0.64078, 0.55509, 0.51229, 0.69640, 0.36960, 0.59296, 0.60801, 0.49332]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + +@slow +@require_torch_gpu +class LDMTextToImagePipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, dtype=torch.float32, seed=0): + generator = torch.manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 32, 32)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "latents": latents, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_ldm_default_ddim(self): + pipe = LDMTextToImagePipeline.from_pretrained("CompVis/ldm-text2im-large-256").to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 256, 256, 3) + expected_slice = np.array([0.51825, 0.52850, 0.52543, 0.54258, 0.52304, 0.52569, 0.54363, 0.55276, 0.56878]) + max_diff = np.abs(expected_slice - image_slice).max() + assert max_diff < 1e-3 + + +@nightly +@require_torch_gpu +class LDMTextToImagePipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, dtype=torch.float32, seed=0): + generator = torch.manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 32, 32)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "latents": latents, + "generator": generator, + "num_inference_steps": 50, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_ldm_default_ddim(self): + pipe = LDMTextToImagePipeline.from_pretrained("CompVis/ldm-text2im-large-256").to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main/ldm_text2img/ldm_large_256_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_superresolution.py b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_superresolution.py new file mode 100644 index 0000000000000000000000000000000000000000..b7c54f01923e56c76e318409c0161c3300699545 --- /dev/null +++ b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_superresolution.py @@ -0,0 +1,132 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import random +import unittest + +import numpy as np +import torch + +from diffusers import DDIMScheduler, LDMSuperResolutionPipeline, UNet2DModel, VQModel +from diffusers.utils import PIL_INTERPOLATION, floats_tensor, load_image, slow, torch_device +from diffusers.utils.testing_utils import require_torch + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class LDMSuperResolutionPipelineFastTests(unittest.TestCase): + @property + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=6, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + @property + def dummy_vq_model(self): + torch.manual_seed(0) + model = VQModel( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=3, + ) + return model + + def test_inference_superresolution(self): + device = "cpu" + unet = self.dummy_uncond_unet + scheduler = DDIMScheduler() + vqvae = self.dummy_vq_model + + ldm = LDMSuperResolutionPipeline(unet=unet, vqvae=vqvae, scheduler=scheduler) + ldm.to(device) + ldm.set_progress_bar_config(disable=None) + + init_image = self.dummy_image.to(device) + + generator = torch.Generator(device=device).manual_seed(0) + image = ldm(image=init_image, generator=generator, num_inference_steps=2, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.8678, 0.8245, 0.6381, 0.6830, 0.4385, 0.5599, 0.4641, 0.6201, 0.5150]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_inference_superresolution_fp16(self): + unet = self.dummy_uncond_unet + scheduler = DDIMScheduler() + vqvae = self.dummy_vq_model + + # put models in fp16 + unet = unet.half() + vqvae = vqvae.half() + + ldm = LDMSuperResolutionPipeline(unet=unet, vqvae=vqvae, scheduler=scheduler) + ldm.to(torch_device) + ldm.set_progress_bar_config(disable=None) + + init_image = self.dummy_image.to(torch_device) + + image = ldm(init_image, num_inference_steps=2, output_type="numpy").images + + assert image.shape == (1, 64, 64, 3) + + +@slow +@require_torch +class LDMSuperResolutionPipelineIntegrationTests(unittest.TestCase): + def test_inference_superresolution(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/vq_diffusion/teddy_bear_pool.png" + ) + init_image = init_image.resize((64, 64), resample=PIL_INTERPOLATION["lanczos"]) + + ldm = LDMSuperResolutionPipeline.from_pretrained("duongna/ldm-super-resolution", device_map="auto") + ldm.to(torch_device) + ldm.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = ldm(image=init_image, generator=generator, num_inference_steps=20, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 256, 256, 3) + expected_slice = np.array([0.7644, 0.7679, 0.7642, 0.7633, 0.7666, 0.7560, 0.7425, 0.7257, 0.6907]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_uncond.py b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_uncond.py new file mode 100644 index 0000000000000000000000000000000000000000..bc7d2d4cd78fcfaa6643033966b6be97410dd2c9 --- /dev/null +++ b/diffusers/tests/pipelines/latent_diffusion/test_latent_diffusion_uncond.py @@ -0,0 +1,121 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel + +from diffusers import DDIMScheduler, LDMPipeline, UNet2DModel, VQModel +from diffusers.utils.testing_utils import require_torch, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class LDMPipelineFastTests(unittest.TestCase): + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + @property + def dummy_vq_model(self): + torch.manual_seed(0) + model = VQModel( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=3, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModel(config) + + def test_inference_uncond(self): + unet = self.dummy_uncond_unet + scheduler = DDIMScheduler() + vae = self.dummy_vq_model + + ldm = LDMPipeline(unet=unet, vqvae=vae, scheduler=scheduler) + ldm.to(torch_device) + ldm.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + generator = torch.manual_seed(0) + _ = ldm(generator=generator, num_inference_steps=1, output_type="numpy").images + + generator = torch.manual_seed(0) + image = ldm(generator=generator, num_inference_steps=2, output_type="numpy").images + + generator = torch.manual_seed(0) + image_from_tuple = ldm(generator=generator, num_inference_steps=2, output_type="numpy", return_dict=False)[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.8512, 0.818, 0.6411, 0.6808, 0.4465, 0.5618, 0.46, 0.6231, 0.5172]) + tolerance = 1e-2 if torch_device != "mps" else 3e-2 + + assert np.abs(image_slice.flatten() - expected_slice).max() < tolerance + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < tolerance + + +@slow +@require_torch +class LDMPipelineIntegrationTests(unittest.TestCase): + def test_inference_uncond(self): + ldm = LDMPipeline.from_pretrained("CompVis/ldm-celebahq-256") + ldm.to(torch_device) + ldm.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = ldm(generator=generator, num_inference_steps=5, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 256, 256, 3) + expected_slice = np.array([0.4399, 0.44975, 0.46825, 0.474, 0.4359, 0.4581, 0.45095, 0.4341, 0.4447]) + tolerance = 1e-2 if torch_device != "mps" else 3e-2 + + assert np.abs(image_slice.flatten() - expected_slice).max() < tolerance diff --git a/diffusers/tests/pipelines/paint_by_example/__init__.py b/diffusers/tests/pipelines/paint_by_example/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/paint_by_example/test_paint_by_example.py b/diffusers/tests/pipelines/paint_by_example/test_paint_by_example.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e04d20a0671c63c5b901ec6acdd2f8edc34716 --- /dev/null +++ b/diffusers/tests/pipelines/paint_by_example/test_paint_by_example.py @@ -0,0 +1,226 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPImageProcessor, CLIPVisionConfig + +from diffusers import AutoencoderKL, PaintByExamplePipeline, PNDMScheduler, UNet2DConditionModel +from diffusers.pipelines.paint_by_example import PaintByExampleImageEncoder +from diffusers.utils import floats_tensor, load_image, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class PaintByExamplePipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = PaintByExamplePipeline + + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=9, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + config = CLIPVisionConfig( + hidden_size=32, + projection_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + image_size=32, + patch_size=4, + ) + image_encoder = PaintByExampleImageEncoder(config, proj_size=32) + feature_extractor = CLIPImageProcessor(crop_size=32, size=32) + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "image_encoder": image_encoder, + "safety_checker": None, + "feature_extractor": feature_extractor, + } + return components + + def convert_to_pt(self, image): + image = np.array(image.convert("RGB")) + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + return image + + def get_dummy_inputs(self, device="cpu", seed=0): + # TODO: use tensor inputs instead of PIL, this is here just to leave the old expected_slices untouched + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + image = image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((64, 64)) + example_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((32, 32)) + + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "example_image": example_image, + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_paint_by_example_inpaint(self): + components = self.get_dummy_components() + + # make sure here that pndm scheduler skips prk + pipe = PaintByExamplePipeline(**components) + pipe = pipe.to("cpu") + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + output = pipe(**inputs) + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4701, 0.5555, 0.3994, 0.5107, 0.5691, 0.4517, 0.5125, 0.4769, 0.4539]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_paint_by_example_image_tensor(self): + device = "cpu" + inputs = self.get_dummy_inputs() + inputs.pop("mask_image") + image = self.convert_to_pt(inputs.pop("image")) + mask_image = image.clamp(0, 1) / 2 + + # make sure here that pndm scheduler skips prk + pipe = PaintByExamplePipeline(**self.get_dummy_components()) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + output = pipe(image=image, mask_image=mask_image[:, 0], **inputs) + out_1 = output.images + + image = image.cpu().permute(0, 2, 3, 1)[0] + mask_image = mask_image.cpu().permute(0, 2, 3, 1)[0] + + image = Image.fromarray(np.uint8(image)).convert("RGB") + mask_image = Image.fromarray(np.uint8(mask_image)).convert("RGB") + + output = pipe(**self.get_dummy_inputs()) + out_2 = output.images + + assert out_1.shape == (1, 64, 64, 3) + assert np.abs(out_1.flatten() - out_2.flatten()).max() < 5e-2 + + def test_paint_by_example_inpaint_with_num_images_per_prompt(self): + device = "cpu" + pipe = PaintByExamplePipeline(**self.get_dummy_components()) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + + images = pipe(**inputs, num_images_per_prompt=2).images + + # check if the output is a list of 2 images + assert len(images) == 2 + + +@slow +@require_torch_gpu +class PaintByExamplePipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_paint_by_example(self): + # make sure here that pndm scheduler skips prk + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/paint_by_example/dog_in_bucket.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/paint_by_example/mask.png" + ) + example_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/paint_by_example/panda.jpg" + ) + + pipe = PaintByExamplePipeline.from_pretrained("Fantasy-Studio/Paint-by-Example") + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(321) + output = pipe( + image=init_image, + mask_image=mask_image, + example_image=example_image, + generator=generator, + guidance_scale=5.0, + num_inference_steps=50, + output_type="np", + ) + + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.4834, 0.4811, 0.4874, 0.5122, 0.5081, 0.5144, 0.5291, 0.5290, 0.5374]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/pndm/__init__.py b/diffusers/tests/pipelines/pndm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/pndm/test_pndm.py b/diffusers/tests/pipelines/pndm/test_pndm.py new file mode 100644 index 0000000000000000000000000000000000000000..46452c4eab90995c31db01413e23dde340576316 --- /dev/null +++ b/diffusers/tests/pipelines/pndm/test_pndm.py @@ -0,0 +1,87 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import PNDMPipeline, PNDMScheduler, UNet2DModel +from diffusers.utils.testing_utils import require_torch, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class PNDMPipelineFastTests(unittest.TestCase): + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + def test_inference(self): + unet = self.dummy_uncond_unet + scheduler = PNDMScheduler() + + pndm = PNDMPipeline(unet=unet, scheduler=scheduler) + pndm.to(torch_device) + pndm.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = pndm(generator=generator, num_inference_steps=20, output_type="numpy").images + + generator = torch.manual_seed(0) + image_from_tuple = pndm(generator=generator, num_inference_steps=20, output_type="numpy", return_dict=False)[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch +class PNDMPipelineIntegrationTests(unittest.TestCase): + def test_inference_cifar10(self): + model_id = "google/ddpm-cifar10-32" + + unet = UNet2DModel.from_pretrained(model_id) + scheduler = PNDMScheduler() + + pndm = PNDMPipeline(unet=unet, scheduler=scheduler) + pndm.to(torch_device) + pndm.set_progress_bar_config(disable=None) + generator = torch.manual_seed(0) + image = pndm(generator=generator, output_type="numpy").images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.1564, 0.14645, 0.1406, 0.14715, 0.12425, 0.14045, 0.13115, 0.12175, 0.125]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/repaint/__init__.py b/diffusers/tests/pipelines/repaint/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/repaint/test_repaint.py b/diffusers/tests/pipelines/repaint/test_repaint.py new file mode 100644 index 0000000000000000000000000000000000000000..f1beea00a01dc15737cc220ba6041ccbdb9856d9 --- /dev/null +++ b/diffusers/tests/pipelines/repaint/test_repaint.py @@ -0,0 +1,131 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch + +from diffusers import RePaintPipeline, RePaintScheduler, UNet2DModel +from diffusers.utils.testing_utils import load_image, load_numpy, nightly, require_torch_gpu, torch_device + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class RepaintPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = RePaintPipeline + test_cpu_offload = False + + def get_dummy_components(self): + torch.manual_seed(0) + torch.manual_seed(0) + unet = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + scheduler = RePaintScheduler() + components = {"unet": unet, "scheduler": scheduler} + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + image = np.random.RandomState(seed).standard_normal((1, 3, 32, 32)) + image = torch.from_numpy(image).to(device=device, dtype=torch.float32) + mask = (image > 0).to(device=device, dtype=torch.float32) + inputs = { + "image": image, + "mask_image": mask, + "generator": generator, + "num_inference_steps": 5, + "eta": 0.0, + "jump_length": 2, + "jump_n_sample": 2, + "output_type": "numpy", + } + return inputs + + def test_repaint(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = RePaintPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([1.0000, 0.5426, 0.5497, 0.2200, 1.0000, 1.0000, 0.5623, 1.0000, 0.6274]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + +@nightly +@require_torch_gpu +class RepaintPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_celebahq(self): + original_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/" + "repaint/celeba_hq_256.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/repaint/mask_256.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/" + "repaint/celeba_hq_256_result.npy" + ) + + model_id = "google/ddpm-ema-celebahq-256" + unet = UNet2DModel.from_pretrained(model_id) + scheduler = RePaintScheduler.from_pretrained(model_id) + + repaint = RePaintPipeline(unet=unet, scheduler=scheduler).to(torch_device) + repaint.set_progress_bar_config(disable=None) + repaint.enable_attention_slicing() + + generator = torch.manual_seed(0) + output = repaint( + original_image, + mask_image, + num_inference_steps=250, + eta=0.0, + jump_length=10, + jump_n_sample=10, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (256, 256, 3) + assert np.abs(expected_image - image).mean() < 1e-2 diff --git a/diffusers/tests/pipelines/score_sde_ve/__init__.py b/diffusers/tests/pipelines/score_sde_ve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/score_sde_ve/test_score_sde_ve.py b/diffusers/tests/pipelines/score_sde_ve/test_score_sde_ve.py new file mode 100644 index 0000000000000000000000000000000000000000..5e30fae2ec8d1baff10bf7bb086c0ccb5743315c --- /dev/null +++ b/diffusers/tests/pipelines/score_sde_ve/test_score_sde_ve.py @@ -0,0 +1,91 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import ScoreSdeVePipeline, ScoreSdeVeScheduler, UNet2DModel +from diffusers.utils.testing_utils import require_torch, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class ScoreSdeVeipelineFastTests(unittest.TestCase): + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + def test_inference(self): + unet = self.dummy_uncond_unet + scheduler = ScoreSdeVeScheduler() + + sde_ve = ScoreSdeVePipeline(unet=unet, scheduler=scheduler) + sde_ve.to(torch_device) + sde_ve.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = sde_ve(num_inference_steps=2, output_type="numpy", generator=generator).images + + generator = torch.manual_seed(0) + image_from_tuple = sde_ve(num_inference_steps=2, output_type="numpy", generator=generator, return_dict=False)[ + 0 + ] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch +class ScoreSdeVePipelineIntegrationTests(unittest.TestCase): + def test_inference(self): + model_id = "google/ncsnpp-church-256" + model = UNet2DModel.from_pretrained(model_id) + + scheduler = ScoreSdeVeScheduler.from_pretrained(model_id) + + sde_ve = ScoreSdeVePipeline(unet=model, scheduler=scheduler) + sde_ve.to(torch_device) + sde_ve.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + image = sde_ve(num_inference_steps=10, output_type="numpy", generator=generator).images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 256, 256, 3) + + expected_slice = np.array([0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion/__init__.py b/diffusers/tests/pipelines/stable_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_cycle_diffusion.py b/diffusers/tests/pipelines/stable_diffusion/test_cycle_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..948a39786dcb0adfb914bcc9d8a5dcbfba8aa795 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_cycle_diffusion.py @@ -0,0 +1,239 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, CycleDiffusionPipeline, DDIMScheduler, UNet2DConditionModel +from diffusers.utils import floats_tensor, load_image, load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class CycleDiffusionPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = CycleDiffusionPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + num_train_timesteps=1000, + clip_sample=False, + set_alpha_to_one=False, + ) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "An astronaut riding an elephant", + "source_prompt": "An astronaut riding a horse", + "image": image, + "generator": generator, + "num_inference_steps": 2, + "eta": 0.1, + "strength": 0.8, + "guidance_scale": 3, + "source_guidance_scale": 1, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_cycle(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + pipe = CycleDiffusionPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = pipe(**inputs) + images = output.images + + image_slice = images[0, -3:, -3:, -1] + + assert images.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4459, 0.4943, 0.4544, 0.6643, 0.5474, 0.4327, 0.5701, 0.5959, 0.5179]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_cycle_fp16(self): + components = self.get_dummy_components() + for name, module in components.items(): + if hasattr(module, "half"): + components[name] = module.half() + pipe = CycleDiffusionPipeline(**components) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs) + images = output.images + + image_slice = images[0, -3:, -3:, -1] + + assert images.shape == (1, 32, 32, 3) + expected_slice = np.array([0.3506, 0.4543, 0.446, 0.4575, 0.5195, 0.4155, 0.5273, 0.518, 0.4116]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch_gpu +class CycleDiffusionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_cycle_diffusion_pipeline_fp16(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/cycle-diffusion/black_colored_car.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/cycle-diffusion/blue_colored_car_fp16.npy" + ) + init_image = init_image.resize((512, 512)) + + model_id = "CompVis/stable-diffusion-v1-4" + scheduler = DDIMScheduler.from_pretrained(model_id, subfolder="scheduler") + pipe = CycleDiffusionPipeline.from_pretrained( + model_id, scheduler=scheduler, safety_checker=None, torch_dtype=torch.float16, revision="fp16" + ) + + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + source_prompt = "A black colored car" + prompt = "A blue colored car" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.85, + guidance_scale=3, + source_guidance_scale=1, + generator=generator, + output_type="np", + ) + image = output.images + + # the values aren't exactly equal, but the images look the same visually + assert np.abs(image - expected_image).max() < 5e-1 + + def test_cycle_diffusion_pipeline(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/cycle-diffusion/black_colored_car.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/cycle-diffusion/blue_colored_car.npy" + ) + init_image = init_image.resize((512, 512)) + + model_id = "CompVis/stable-diffusion-v1-4" + scheduler = DDIMScheduler.from_pretrained(model_id, subfolder="scheduler") + pipe = CycleDiffusionPipeline.from_pretrained(model_id, scheduler=scheduler, safety_checker=None) + + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + source_prompt = "A black colored car" + prompt = "A blue colored car" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + source_prompt=source_prompt, + image=init_image, + num_inference_steps=100, + eta=0.1, + strength=0.85, + guidance_scale=3, + source_guidance_scale=1, + generator=generator, + output_type="np", + ) + image = output.images + + assert np.abs(image - expected_image).max() < 1e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion.py b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..00f6fd02a77eb51a0b2c0eccdb7f319813d3ea72 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion.py @@ -0,0 +1,306 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import tempfile +import unittest + +import numpy as np + +from diffusers import ( + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + OnnxStableDiffusionPipeline, + PNDMScheduler, +) +from diffusers.utils.testing_utils import is_onnx_available, nightly, require_onnxruntime, require_torch_gpu + +from ...test_pipelines_onnx_common import OnnxPipelineTesterMixin + + +if is_onnx_available(): + import onnxruntime as ort + + +class OnnxStableDiffusionPipelineFastTests(OnnxPipelineTesterMixin, unittest.TestCase): + hub_checkpoint = "hf-internal-testing/tiny-random-OnnxStableDiffusionPipeline" + + def get_dummy_inputs(self, seed=0): + generator = np.random.RandomState(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_pipeline_default_ddim(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.65072, 0.58492, 0.48219, 0.55521, 0.53180, 0.55939, 0.50697, 0.39800, 0.46455]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_pipeline_pndm(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = PNDMScheduler.from_config(pipe.scheduler.config, skip_prk_steps=True) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.65863, 0.59425, 0.49326, 0.56313, 0.53875, 0.56627, 0.51065, 0.39777, 0.46330]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_pipeline_lms(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.53755, 0.60786, 0.47402, 0.49488, 0.51869, 0.49819, 0.47985, 0.38957, 0.44279]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_pipeline_euler(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.53755, 0.60786, 0.47402, 0.49488, 0.51869, 0.49819, 0.47985, 0.38957, 0.44279]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_pipeline_euler_ancestral(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.53817, 0.60812, 0.47384, 0.49530, 0.51894, 0.49814, 0.47984, 0.38958, 0.44271]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_pipeline_dpm_multistep(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.53895, 0.60808, 0.47933, 0.49608, 0.51886, 0.49950, 0.48053, 0.38957, 0.44200]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + +@nightly +@require_onnxruntime +@require_torch_gpu +class OnnxStableDiffusionPipelineIntegrationTests(unittest.TestCase): + @property + def gpu_provider(self): + return ( + "CUDAExecutionProvider", + { + "gpu_mem_limit": "15000000000", # 15GB + "arena_extend_strategy": "kSameAsRequested", + }, + ) + + @property + def gpu_options(self): + options = ort.SessionOptions() + options.enable_mem_pattern = False + return options + + def test_inference_default_pndm(self): + # using the PNDM scheduler by default + sd_pipe = OnnxStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + np.random.seed(0) + output = sd_pipe([prompt], guidance_scale=6.0, num_inference_steps=10, output_type="np") + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0452, 0.0390, 0.0087, 0.0350, 0.0617, 0.0364, 0.0544, 0.0523, 0.0720]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_inference_ddim(self): + ddim_scheduler = DDIMScheduler.from_pretrained( + "runwayml/stable-diffusion-v1-5", subfolder="scheduler", revision="onnx" + ) + sd_pipe = OnnxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + scheduler=ddim_scheduler, + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "open neural network exchange" + generator = np.random.RandomState(0) + output = sd_pipe([prompt], guidance_scale=7.5, num_inference_steps=10, generator=generator, output_type="np") + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.2867, 0.1974, 0.1481, 0.7294, 0.7251, 0.6667, 0.4194, 0.5642, 0.6486]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_inference_k_lms(self): + lms_scheduler = LMSDiscreteScheduler.from_pretrained( + "runwayml/stable-diffusion-v1-5", subfolder="scheduler", revision="onnx" + ) + sd_pipe = OnnxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + scheduler=lms_scheduler, + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "open neural network exchange" + generator = np.random.RandomState(0) + output = sd_pipe([prompt], guidance_scale=7.5, num_inference_steps=10, generator=generator, output_type="np") + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.2306, 0.1959, 0.1593, 0.6549, 0.6394, 0.5408, 0.5065, 0.6010, 0.6161]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_intermediate_state(self): + number_of_steps = 0 + + def test_callback_fn(step: int, timestep: int, latents: np.ndarray) -> None: + test_callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 0: + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.6772, -0.3835, -1.2456, 0.1905, -1.0974, 0.6967, -1.9353, 0.0178, 1.0167] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 1e-3 + elif step == 5: + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.3351, 0.2241, -0.1837, -0.2325, -0.6577, 0.3393, -0.0241, 0.5899, 1.3875] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 1e-3 + + test_callback_fn.has_been_called = False + + pipe = OnnxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "Andromeda galaxy in a bottle" + + generator = np.random.RandomState(0) + pipe( + prompt=prompt, + num_inference_steps=5, + guidance_scale=7.5, + generator=generator, + callback=test_callback_fn, + callback_steps=1, + ) + assert test_callback_fn.has_been_called + assert number_of_steps == 6 + + def test_stable_diffusion_no_safety_checker(self): + pipe = OnnxStableDiffusionPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + assert isinstance(pipe, OnnxStableDiffusionPipeline) + assert pipe.safety_checker is None + + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None + + # check that there's no error when saving a pipeline with one of the models being None + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = OnnxStableDiffusionPipeline.from_pretrained(tmpdirname) + + # sanity check that the pipeline still works + assert pipe.safety_checker is None + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None diff --git a/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_img2img.py b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..80bebf81ae3928c3f5b8d13db933d0fa113b16a1 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_img2img.py @@ -0,0 +1,245 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import random +import unittest + +import numpy as np + +from diffusers import ( + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + OnnxStableDiffusionImg2ImgPipeline, + PNDMScheduler, +) +from diffusers.utils import floats_tensor +from diffusers.utils.testing_utils import ( + is_onnx_available, + load_image, + nightly, + require_onnxruntime, + require_torch_gpu, +) + +from ...test_pipelines_onnx_common import OnnxPipelineTesterMixin + + +if is_onnx_available(): + import onnxruntime as ort + + +class OnnxStableDiffusionImg2ImgPipelineFastTests(OnnxPipelineTesterMixin, unittest.TestCase): + hub_checkpoint = "hf-internal-testing/tiny-random-OnnxStableDiffusionPipeline" + + def get_dummy_inputs(self, seed=0): + image = floats_tensor((1, 3, 128, 128), rng=random.Random(seed)) + generator = np.random.RandomState(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": image, + "generator": generator, + "num_inference_steps": 3, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_pipeline_default_ddim(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.69643, 0.58484, 0.50314, 0.58760, 0.55368, 0.59643, 0.51529, 0.41217, 0.49087]) + assert np.abs(image_slice - expected_slice).max() < 1e-1 + + def test_pipeline_pndm(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = PNDMScheduler.from_config(pipe.scheduler.config, skip_prk_steps=True) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.61710, 0.53390, 0.49310, 0.55622, 0.50982, 0.58240, 0.50716, 0.38629, 0.46856]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + def test_pipeline_lms(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + # warmup pass to apply optimizations + _ = pipe(**self.get_dummy_inputs()) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.52761, 0.59977, 0.49033, 0.49619, 0.54282, 0.50311, 0.47600, 0.40918, 0.45203]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + def test_pipeline_euler(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.52911, 0.60004, 0.49229, 0.49805, 0.54502, 0.50680, 0.47777, 0.41028, 0.45304]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + def test_pipeline_euler_ancestral(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.52911, 0.60004, 0.49229, 0.49805, 0.54502, 0.50680, 0.47777, 0.41028, 0.45304]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + def test_pipeline_dpm_multistep(self): + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained(self.hub_checkpoint, provider="CPUExecutionProvider") + pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 128, 128, 3) + expected_slice = np.array([0.65331, 0.58277, 0.48204, 0.56059, 0.53665, 0.56235, 0.50969, 0.40009, 0.46552]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + +@nightly +@require_onnxruntime +@require_torch_gpu +class OnnxStableDiffusionImg2ImgPipelineIntegrationTests(unittest.TestCase): + @property + def gpu_provider(self): + return ( + "CUDAExecutionProvider", + { + "gpu_mem_limit": "15000000000", # 15GB + "arena_extend_strategy": "kSameAsRequested", + }, + ) + + @property + def gpu_options(self): + options = ort.SessionOptions() + options.enable_mem_pattern = False + return options + + def test_inference_default_pndm(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/img2img/sketch-mountains-input.jpg" + ) + init_image = init_image.resize((768, 512)) + # using the PNDM scheduler by default + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "A fantasy landscape, trending on artstation" + + generator = np.random.RandomState(0) + output = pipe( + prompt=prompt, + image=init_image, + strength=0.75, + guidance_scale=7.5, + num_inference_steps=10, + generator=generator, + output_type="np", + ) + images = output.images + image_slice = images[0, 255:258, 383:386, -1] + + assert images.shape == (1, 512, 768, 3) + expected_slice = np.array([0.4909, 0.5059, 0.5372, 0.4623, 0.4876, 0.5049, 0.4820, 0.4956, 0.5019]) + # TODO: lower the tolerance after finding the cause of onnxruntime reproducibility issues + + assert np.abs(image_slice.flatten() - expected_slice).max() < 2e-2 + + def test_inference_k_lms(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/img2img/sketch-mountains-input.jpg" + ) + init_image = init_image.resize((768, 512)) + lms_scheduler = LMSDiscreteScheduler.from_pretrained( + "runwayml/stable-diffusion-v1-5", subfolder="scheduler", revision="onnx" + ) + pipe = OnnxStableDiffusionImg2ImgPipeline.from_pretrained( + "runwayml/stable-diffusion-v1-5", + revision="onnx", + scheduler=lms_scheduler, + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "A fantasy landscape, trending on artstation" + + generator = np.random.RandomState(0) + output = pipe( + prompt=prompt, + image=init_image, + strength=0.75, + guidance_scale=7.5, + num_inference_steps=20, + generator=generator, + output_type="np", + ) + images = output.images + image_slice = images[0, 255:258, 383:386, -1] + + assert images.shape == (1, 512, 768, 3) + expected_slice = np.array([0.8043, 0.926, 0.9581, 0.8119, 0.8954, 0.913, 0.7209, 0.7463, 0.7431]) + # TODO: lower the tolerance after finding the cause of onnxruntime reproducibility issues + + assert np.abs(image_slice.flatten() - expected_slice).max() < 2e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint.py b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..df8724c0faa04602ca9a450d2e975f3811012a8c --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint.py @@ -0,0 +1,141 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np + +from diffusers import LMSDiscreteScheduler, OnnxStableDiffusionInpaintPipeline +from diffusers.utils.testing_utils import ( + is_onnx_available, + load_image, + nightly, + require_onnxruntime, + require_torch_gpu, +) + +from ...test_pipelines_onnx_common import OnnxPipelineTesterMixin + + +if is_onnx_available(): + import onnxruntime as ort + + +class OnnxStableDiffusionPipelineFastTests(OnnxPipelineTesterMixin, unittest.TestCase): + # FIXME: add fast tests + pass + + +@nightly +@require_onnxruntime +@require_torch_gpu +class OnnxStableDiffusionInpaintPipelineIntegrationTests(unittest.TestCase): + @property + def gpu_provider(self): + return ( + "CUDAExecutionProvider", + { + "gpu_mem_limit": "15000000000", # 15GB + "arena_extend_strategy": "kSameAsRequested", + }, + ) + + @property + def gpu_options(self): + options = ort.SessionOptions() + options.enable_mem_pattern = False + return options + + def test_inference_default_pndm(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo_mask.png" + ) + pipe = OnnxStableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "A red cat sitting on a park bench" + + generator = np.random.RandomState(0) + output = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + guidance_scale=7.5, + num_inference_steps=10, + generator=generator, + output_type="np", + ) + images = output.images + image_slice = images[0, 255:258, 255:258, -1] + + assert images.shape == (1, 512, 512, 3) + expected_slice = np.array([0.2514, 0.3007, 0.3517, 0.1790, 0.2382, 0.3167, 0.1944, 0.2273, 0.2464]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_inference_k_lms(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo_mask.png" + ) + lms_scheduler = LMSDiscreteScheduler.from_pretrained( + "runwayml/stable-diffusion-inpainting", subfolder="scheduler", revision="onnx" + ) + pipe = OnnxStableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", + revision="onnx", + scheduler=lms_scheduler, + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "A red cat sitting on a park bench" + + generator = np.random.RandomState(0) + output = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + guidance_scale=7.5, + num_inference_steps=20, + generator=generator, + output_type="np", + ) + images = output.images + image_slice = images[0, 255:258, 255:258, -1] + + assert images.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0086, 0.0077, 0.0083, 0.0093, 0.0107, 0.0139, 0.0094, 0.0097, 0.0125]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint_legacy.py b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint_legacy.py new file mode 100644 index 0000000000000000000000000000000000000000..0237970ad2f33203720ed21493b13daef81bfa5d --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_onnx_stable_diffusion_inpaint_legacy.py @@ -0,0 +1,97 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np + +from diffusers import OnnxStableDiffusionInpaintPipelineLegacy +from diffusers.utils.testing_utils import ( + is_onnx_available, + load_image, + load_numpy, + nightly, + require_onnxruntime, + require_torch_gpu, +) + + +if is_onnx_available(): + import onnxruntime as ort + + +@nightly +@require_onnxruntime +@require_torch_gpu +class StableDiffusionOnnxInpaintLegacyPipelineIntegrationTests(unittest.TestCase): + @property + def gpu_provider(self): + return ( + "CUDAExecutionProvider", + { + "gpu_mem_limit": "15000000000", # 15GB + "arena_extend_strategy": "kSameAsRequested", + }, + ) + + @property + def gpu_options(self): + options = ort.SessionOptions() + options.enable_mem_pattern = False + return options + + def test_inference(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/overture-creations-5sI6fQgYIuo_mask.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/in_paint/red_cat_sitting_on_a_park_bench_onnx.npy" + ) + + # using the PNDM scheduler by default + pipe = OnnxStableDiffusionInpaintPipelineLegacy.from_pretrained( + "CompVis/stable-diffusion-v1-4", + revision="onnx", + safety_checker=None, + feature_extractor=None, + provider=self.gpu_provider, + sess_options=self.gpu_options, + ) + pipe.set_progress_bar_config(disable=None) + + prompt = "A red cat sitting on a park bench" + + generator = np.random.RandomState(0) + output = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + strength=0.75, + guidance_scale=7.5, + num_inference_steps=15, + generator=generator, + output_type="np", + ) + + image = output.images[0] + + assert image.shape == (512, 512, 3) + assert np.abs(expected_image - image).max() < 1e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..02774d69dc292be6d2d40a990b5975d5aa556c34 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion.py @@ -0,0 +1,902 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + + +import gc +import tempfile +import time +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, + logging, +) +from diffusers.utils import load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import CaptureLogger, require_torch_gpu + +from ...models.test_models_unet_2d_condition import create_lora_layers +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + ) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_ddim(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5643, 0.6017, 0.4799, 0.5267, 0.5584, 0.4641, 0.5159, 0.4963, 0.4791]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_lora(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + # forward 1 + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + # set lora layers + lora_attn_procs = create_lora_layers(sd_pipe.unet) + sd_pipe.unet.set_attn_processor(lora_attn_procs) + sd_pipe = sd_pipe.to(torch_device) + + # forward 2 + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs, cross_attention_kwargs={"scale": 0.0}) + image = output.images + image_slice_1 = image[0, -3:, -3:, -1] + + # forward 3 + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs, cross_attention_kwargs={"scale": 0.5}) + image = output.images + image_slice_2 = image[0, -3:, -3:, -1] + + assert np.abs(image_slice - image_slice_1).max() < 1e-2 + assert np.abs(image_slice - image_slice_2).max() > 1e-2 + + def test_stable_diffusion_prompt_embeds(self): + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + inputs["prompt"] = 3 * [inputs["prompt"]] + + # forward + output = sd_pipe(**inputs) + image_slice_1 = output.images[0, -3:, -3:, -1] + + inputs = self.get_dummy_inputs(torch_device) + prompt = 3 * [inputs.pop("prompt")] + + text_inputs = sd_pipe.tokenizer( + prompt, + padding="max_length", + max_length=sd_pipe.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_inputs = text_inputs["input_ids"].to(torch_device) + + prompt_embeds = sd_pipe.text_encoder(text_inputs)[0] + + inputs["prompt_embeds"] = prompt_embeds + + # forward + output = sd_pipe(**inputs) + image_slice_2 = output.images[0, -3:, -3:, -1] + + assert np.abs(image_slice_1.flatten() - image_slice_2.flatten()).max() < 1e-4 + + def test_stable_diffusion_negative_prompt_embeds(self): + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + negative_prompt = 3 * ["this is a negative prompt"] + inputs["negative_prompt"] = negative_prompt + inputs["prompt"] = 3 * [inputs["prompt"]] + + # forward + output = sd_pipe(**inputs) + image_slice_1 = output.images[0, -3:, -3:, -1] + + inputs = self.get_dummy_inputs(torch_device) + prompt = 3 * [inputs.pop("prompt")] + + embeds = [] + for p in [prompt, negative_prompt]: + text_inputs = sd_pipe.tokenizer( + p, + padding="max_length", + max_length=sd_pipe.tokenizer.model_max_length, + truncation=True, + return_tensors="pt", + ) + text_inputs = text_inputs["input_ids"].to(torch_device) + + embeds.append(sd_pipe.text_encoder(text_inputs)[0]) + + inputs["prompt_embeds"], inputs["negative_prompt_embeds"] = embeds + + # forward + output = sd_pipe(**inputs) + image_slice_2 = output.images[0, -3:, -3:, -1] + + assert np.abs(image_slice_1.flatten() - image_slice_2.flatten()).max() < 1e-4 + + def test_stable_diffusion_ddim_factor_8(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs, height=136, width=136) + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 136, 136, 3) + expected_slice = np.array([0.5524, 0.5626, 0.6069, 0.4727, 0.386, 0.3995, 0.4613, 0.4328, 0.4269]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_pndm(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe.scheduler = PNDMScheduler(skip_prk_steps=True) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5094, 0.5674, 0.4667, 0.5125, 0.5696, 0.4674, 0.5277, 0.4964, 0.4945]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_no_safety_checker(self): + pipe = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-lms-pipe", safety_checker=None + ) + assert isinstance(pipe, StableDiffusionPipeline) + assert isinstance(pipe.scheduler, LMSDiscreteScheduler) + assert pipe.safety_checker is None + + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None + + # check that there's no error when saving a pipeline with one of the models being None + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = StableDiffusionPipeline.from_pretrained(tmpdirname) + + # sanity check that the pipeline still works + assert pipe.safety_checker is None + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None + + def test_stable_diffusion_k_lms(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [ + 0.47082293033599854, + 0.5371589064598083, + 0.4562119245529175, + 0.5220914483070374, + 0.5733777284622192, + 0.4795039892196655, + 0.5465868711471558, + 0.5074326395988464, + 0.5042197108268738, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_k_euler_ancestral(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [ + 0.4707113206386566, + 0.5372191071510315, + 0.4563021957874298, + 0.5220003724098206, + 0.5734264850616455, + 0.4794946610927582, + 0.5463782548904419, + 0.5074145197868347, + 0.504422664642334, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_k_euler(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe.scheduler = EulerDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [ + 0.47082313895225525, + 0.5371587872505188, + 0.4562119245529175, + 0.5220913887023926, + 0.5733776688575745, + 0.47950395941734314, + 0.546586811542511, + 0.5074326992034912, + 0.5042197108268738, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_vae_slicing(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + image_count = 4 + + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * image_count + output_1 = sd_pipe(**inputs) + + # make sure sliced vae decode yields the same result + sd_pipe.enable_vae_slicing() + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * image_count + output_2 = sd_pipe(**inputs) + + # there is a small discrepancy at image borders vs. full batch decode + assert np.abs(output_2.images.flatten() - output_1.images.flatten()).max() < 3e-3 + + def test_stable_diffusion_negative_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = PNDMScheduler(skip_prk_steps=True) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + negative_prompt = "french fries" + output = sd_pipe(**inputs, negative_prompt=negative_prompt) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array( + [ + 0.5108221173286438, + 0.5688379406929016, + 0.4685141146183014, + 0.5098261833190918, + 0.5657756328582764, + 0.4631010890007019, + 0.5226285457611084, + 0.49129390716552734, + 0.4899061322212219, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_num_images_per_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = PNDMScheduler(skip_prk_steps=True) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + + # test num_images_per_prompt=1 (default) + images = sd_pipe(prompt, num_inference_steps=2, output_type="np").images + + assert images.shape == (1, 64, 64, 3) + + # test num_images_per_prompt=1 (default) for batch of prompts + batch_size = 2 + images = sd_pipe([prompt] * batch_size, num_inference_steps=2, output_type="np").images + + assert images.shape == (batch_size, 64, 64, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + images = sd_pipe( + prompt, num_inference_steps=2, output_type="np", num_images_per_prompt=num_images_per_prompt + ).images + + assert images.shape == (num_images_per_prompt, 64, 64, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + images = sd_pipe( + [prompt] * batch_size, num_inference_steps=2, output_type="np", num_images_per_prompt=num_images_per_prompt + ).images + + assert images.shape == (batch_size * num_images_per_prompt, 64, 64, 3) + + def test_stable_diffusion_long_prompt(self): + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + do_classifier_free_guidance = True + negative_prompt = None + num_images_per_prompt = 1 + logger = logging.get_logger("diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion") + + prompt = 25 * "@" + with CaptureLogger(logger) as cap_logger_3: + text_embeddings_3 = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + prompt = 100 * "@" + with CaptureLogger(logger) as cap_logger: + text_embeddings = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + negative_prompt = "Hello" + with CaptureLogger(logger) as cap_logger_2: + text_embeddings_2 = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + assert text_embeddings_3.shape == text_embeddings_2.shape == text_embeddings.shape + assert text_embeddings.shape[1] == 77 + + assert cap_logger.out == cap_logger_2.out + # 100 - 77 + 1 (BOS token) + 1 (EOS token) = 25 + assert cap_logger.out.count("@") == 25 + assert cap_logger_3.out == "" + + def test_stable_diffusion_height_width_opt(self): + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "hey" + + output = sd_pipe(prompt, num_inference_steps=1, output_type="np") + image_shape = output.images[0].shape[:2] + assert image_shape == (64, 64) + + output = sd_pipe(prompt, num_inference_steps=1, height=96, width=96, output_type="np") + image_shape = output.images[0].shape[:2] + assert image_shape == (96, 96) + + config = dict(sd_pipe.unet.config) + config["sample_size"] = 96 + sd_pipe.unet = UNet2DConditionModel.from_config(config).to(torch_device) + output = sd_pipe(prompt, num_inference_steps=1, output_type="np") + image_shape = output.images[0].shape[:2] + assert image_shape == (192, 192) + + +@slow +@require_torch_gpu +class StableDiffusionPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "a photograph of an astronaut riding a horse", + "latents": latents, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_1_1_pndm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-1") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.43625, 0.43554, 0.36670, 0.40660, 0.39703, 0.38658, 0.43936, 0.43557, 0.40592]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_1_4_pndm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.57400, 0.47841, 0.31625, 0.63583, 0.58306, 0.55056, 0.50825, 0.56306, 0.55748]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_ddim(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + sd_pipe.scheduler = DDIMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.38019, 0.28647, 0.27321, 0.40377, 0.38290, 0.35446, 0.39218, 0.38165, 0.42239]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_lms(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.10542, 0.09620, 0.07332, 0.09015, 0.09382, 0.07597, 0.08496, 0.07806, 0.06455]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_dpm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.03503, 0.03494, 0.01087, 0.03128, 0.02552, 0.00803, 0.00742, 0.00372, 0.00000]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_attention_slicing(self): + torch.cuda.reset_peak_memory_stats() + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # enable attention slicing + pipe.enable_attention_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image_sliced = pipe(**inputs).images + + mem_bytes = torch.cuda.max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + # make sure that less than 3.75 GB is allocated + assert mem_bytes < 3.75 * 10**9 + + # disable slicing + pipe.disable_attention_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image = pipe(**inputs).images + + # make sure that more than 3.75 GB is allocated + mem_bytes = torch.cuda.max_memory_allocated() + assert mem_bytes > 3.75 * 10**9 + assert np.abs(image_sliced - image).max() < 1e-3 + + def test_stable_diffusion_vae_slicing(self): + torch.cuda.reset_peak_memory_stats() + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + # enable vae slicing + pipe.enable_vae_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + inputs["prompt"] = [inputs["prompt"]] * 4 + inputs["latents"] = torch.cat([inputs["latents"]] * 4) + image_sliced = pipe(**inputs).images + + mem_bytes = torch.cuda.max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + # make sure that less than 4 GB is allocated + assert mem_bytes < 4e9 + + # disable vae slicing + pipe.disable_vae_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + inputs["prompt"] = [inputs["prompt"]] * 4 + inputs["latents"] = torch.cat([inputs["latents"]] * 4) + image = pipe(**inputs).images + + # make sure that more than 4 GB is allocated + mem_bytes = torch.cuda.max_memory_allocated() + assert mem_bytes > 4e9 + # There is a small discrepancy at the image borders vs. a fully batched version. + assert np.abs(image_sliced - image).max() < 1e-2 + + def test_stable_diffusion_fp16_vs_autocast(self): + # this test makes sure that the original model with autocast + # and the new model with fp16 yield the same result + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image_fp16 = pipe(**inputs).images + + with torch.autocast(torch_device): + inputs = self.get_inputs(torch_device) + image_autocast = pipe(**inputs).images + + # Make sure results are close enough + diff = np.abs(image_fp16.flatten() - image_autocast.flatten()) + # They ARE different since ops are not run always at the same precision + # however, they should be extremely close. + assert diff.mean() < 2e-2 + + def test_stable_diffusion_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.5693, -0.3018, -0.9746, 0.0518, -0.8770, 0.7559, -1.7402, 0.1022, 1.1582] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.1958, -0.2993, -1.0166, -0.5005, -0.4810, 0.6162, -0.9492, 0.6621, 1.4492] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == inputs["num_inference_steps"] + + def test_stable_diffusion_low_cpu_mem_usage(self): + pipeline_id = "CompVis/stable-diffusion-v1-4" + + start_time = time.time() + pipeline_low_cpu_mem_usage = StableDiffusionPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16) + pipeline_low_cpu_mem_usage.to(torch_device) + low_cpu_mem_usage_time = time.time() - start_time + + start_time = time.time() + _ = StableDiffusionPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16, low_cpu_mem_usage=False) + normal_load_time = time.time() - start_time + + assert 2 * low_cpu_mem_usage_time < normal_load_time + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.8 GB is allocated + assert mem_bytes < 2.8 * 10**9 + + +@nightly +@require_torch_gpu +class StableDiffusionPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "a photograph of an astronaut riding a horse", + "latents": latents, + "generator": generator, + "num_inference_steps": 50, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_1_4_pndm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4").to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_4_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_1_5_pndm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5").to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_5_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_ddim(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4").to(torch_device) + sd_pipe.scheduler = DDIMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_4_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_lms(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4").to(torch_device) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_4_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_euler(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4").to(torch_device) + sd_pipe.scheduler = EulerDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_4_euler.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_dpm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4").to(torch_device) + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 25 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_text2img/stable_diffusion_1_4_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_image_variation.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..a7aa4051774ddc5455f461401140a76a7a24c4b1 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_image_variation.py @@ -0,0 +1,339 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPImageProcessor, CLIPVisionConfig, CLIPVisionModelWithProjection + +from diffusers import ( + AutoencoderKL, + DPMSolverMultistepScheduler, + PNDMScheduler, + StableDiffusionImageVariationPipeline, + UNet2DConditionModel, +) +from diffusers.utils import floats_tensor, load_image, load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionImageVariationPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionImageVariationPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + image_encoder_config = CLIPVisionConfig( + hidden_size=32, + projection_dim=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + image_size=32, + patch_size=4, + ) + image_encoder = CLIPVisionModelWithProjection(image_encoder_config) + feature_extractor = CLIPImageProcessor(crop_size=32, size=32) + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "image_encoder": image_encoder, + "feature_extractor": feature_extractor, + "safety_checker": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)) + image = image.cpu().permute(0, 2, 3, 1)[0] + image = Image.fromarray(np.uint8(image)).convert("RGB").resize((32, 32)) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "image": image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_img_variation_default_case(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImageVariationPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5167, 0.5746, 0.4835, 0.4914, 0.5605, 0.4691, 0.5201, 0.4898, 0.4958]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img_variation_multiple_images(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImageVariationPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + inputs["image"] = 2 * [inputs["image"]] + output = sd_pipe(**inputs) + + image = output.images + + image_slice = image[-1, -3:, -3:, -1] + + assert image.shape == (2, 64, 64, 3) + expected_slice = np.array([0.6568, 0.5470, 0.5684, 0.5444, 0.5945, 0.6221, 0.5508, 0.5531, 0.5263]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img_variation_num_images_per_prompt(self): + device = "cpu" + components = self.get_dummy_components() + sd_pipe = StableDiffusionImageVariationPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + # test num_images_per_prompt=1 (default) + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs).images + + assert images.shape == (1, 64, 64, 3) + + # test num_images_per_prompt=1 (default) for batch of images + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["image"] = batch_size * [inputs["image"]] + images = sd_pipe(**inputs).images + + assert images.shape == (batch_size, 64, 64, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (num_images_per_prompt, 64, 64, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["image"] = batch_size * [inputs["image"]] + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (batch_size * num_images_per_prompt, 64, 64, 3) + + +@slow +@require_torch_gpu +class StableDiffusionImageVariationPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_imgvar/input_image_vermeer.png" + ) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "image": init_image, + "latents": latents, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_img_variation_pipeline_default(self): + sd_pipe = StableDiffusionImageVariationPipeline.from_pretrained( + "lambdalabs/sd-image-variations-diffusers", safety_checker=None + ) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.84491, 0.90789, 0.75708, 0.78734, 0.83485, 0.70099, 0.66938, 0.68727, 0.61379]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_img_variation_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.1621, 0.2837, -0.7979, -0.1221, -1.3057, 0.7681, -2.1191, 0.0464, 1.6309] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([0.6299, 1.7500, 1.1992, -2.1582, -1.8994, 0.7334, -0.7090, 1.0137, 1.5273]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionImageVariationPipeline.from_pretrained( + "fusing/sd-image-variations-diffusers", + safety_checker=None, + torch_dtype=torch.float16, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == inputs["num_inference_steps"] + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + model_id = "fusing/sd-image-variations-diffusers" + pipe = StableDiffusionImageVariationPipeline.from_pretrained( + model_id, safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.6 GB is allocated + assert mem_bytes < 2.6 * 10**9 + + +@nightly +@require_torch_gpu +class StableDiffusionImageVariationPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_imgvar/input_image_vermeer.png" + ) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "image": init_image, + "latents": latents, + "generator": generator, + "num_inference_steps": 50, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_img_variation_pndm(self): + sd_pipe = StableDiffusionImageVariationPipeline.from_pretrained("fusing/sd-image-variations-diffusers") + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_imgvar/lambdalabs_variations_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img_variation_dpm(self): + sd_pipe = StableDiffusionImageVariationPipeline.from_pretrained("fusing/sd-image-variations-diffusers") + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 25 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_imgvar/lambdalabs_variations_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_img2img.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..b162fe3ac610e9023f5d66f0303673ce7e560cf0 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_img2img.py @@ -0,0 +1,470 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionImg2ImgPipeline, + UNet2DConditionModel, +) +from diffusers.utils import floats_tensor, load_image, load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionImg2ImgPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionImg2ImgPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_img2img_default_case(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImg2ImgPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4492, 0.3865, 0.4222, 0.5854, 0.5139, 0.4379, 0.4193, 0.48, 0.4218]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_negative_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImg2ImgPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + negative_prompt = "french fries" + output = sd_pipe(**inputs, negative_prompt=negative_prompt) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4065, 0.3783, 0.4050, 0.5266, 0.4781, 0.4252, 0.4203, 0.4692, 0.4365]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_multiple_init_images(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImg2ImgPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * 2 + inputs["image"] = inputs["image"].repeat(2, 1, 1, 1) + image = sd_pipe(**inputs).images + image_slice = image[-1, -3:, -3:, -1] + + assert image.shape == (2, 32, 32, 3) + expected_slice = np.array([0.5144, 0.4447, 0.4735, 0.6676, 0.5526, 0.5454, 0.645, 0.5149, 0.4689]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_k_lms(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear" + ) + sd_pipe = StableDiffusionImg2ImgPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4367, 0.4986, 0.4372, 0.6706, 0.5665, 0.444, 0.5864, 0.6019, 0.5203]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_num_images_per_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionImg2ImgPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + # test num_images_per_prompt=1 (default) + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs).images + + assert images.shape == (1, 32, 32, 3) + + # test num_images_per_prompt=1 (default) for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = sd_pipe(**inputs).images + + assert images.shape == (batch_size, 32, 32, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (num_images_per_prompt, 32, 32, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (batch_size * num_images_per_prompt, 32, 32, 3) + + +@slow +@require_torch_gpu +class StableDiffusionImg2ImgPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/sketch-mountains-input.png" + ) + inputs = { + "prompt": "a fantasy landscape, concept art, high resolution", + "image": init_image, + "generator": generator, + "num_inference_steps": 3, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_img2img_default(self): + pipe = StableDiffusionImg2ImgPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 768, 3) + expected_slice = np.array([0.4300, 0.4662, 0.4930, 0.3990, 0.4307, 0.4525, 0.3719, 0.4064, 0.3923]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_k_lms(self): + pipe = StableDiffusionImg2ImgPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 768, 3) + expected_slice = np.array([0.0389, 0.0346, 0.0415, 0.0290, 0.0218, 0.0210, 0.0408, 0.0567, 0.0271]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_ddim(self): + pipe = StableDiffusionImg2ImgPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", safety_checker=None) + pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 768, 3) + expected_slice = np.array([0.0593, 0.0607, 0.0851, 0.0582, 0.0636, 0.0721, 0.0751, 0.0981, 0.0781]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_img2img_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 96) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([-0.4958, 0.5107, 1.1045, 2.7539, 4.6680, 3.8320, 1.5049, 1.8633, 2.6523]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 96) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([-0.4956, 0.5078, 1.0918, 2.7520, 4.6484, 3.8125, 1.5146, 1.8633, 2.6367]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionImg2ImgPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == 2 + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionImg2ImgPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.2 GB is allocated + assert mem_bytes < 2.2 * 10**9 + + def test_stable_diffusion_img2img_pipeline_multiple_of_8(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/img2img/sketch-mountains-input.jpg" + ) + # resize to resolution that is divisible by 8 but not 16 or 32 + init_image = init_image.resize((760, 504)) + + model_id = "CompVis/stable-diffusion-v1-4" + pipe = StableDiffusionImg2ImgPipeline.from_pretrained( + model_id, + safety_checker=None, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "A fantasy landscape, trending on artstation" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=init_image, + strength=0.75, + guidance_scale=7.5, + generator=generator, + output_type="np", + ) + image = output.images[0] + + image_slice = image[255:258, 383:386, -1] + + assert image.shape == (504, 760, 3) + expected_slice = np.array([0.9393, 0.9500, 0.9399, 0.9438, 0.9458, 0.9400, 0.9455, 0.9414, 0.9423]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-3 + + +@nightly +@require_torch_gpu +class StableDiffusionImg2ImgPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/sketch-mountains-input.png" + ) + inputs = { + "prompt": "a fantasy landscape, concept art, high resolution", + "image": init_image, + "generator": generator, + "num_inference_steps": 50, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_img2img_pndm(self): + sd_pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/stable_diffusion_1_5_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img2img_ddim(self): + sd_pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = DDIMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/stable_diffusion_1_5_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img2img_lms(self): + sd_pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/stable_diffusion_1_5_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img2img_dpm(self): + sd_pipe = StableDiffusionImg2ImgPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 30 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_img2img/stable_diffusion_1_5_dpm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..c44101d13c5aadd87a9eaba687e46deb0c100cb9 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint.py @@ -0,0 +1,548 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DPMSolverMultistepScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionInpaintPipeline, + UNet2DConditionModel, +) +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_inpaint import prepare_mask_and_masked_image +from diffusers.utils import floats_tensor, load_image, load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionInpaintPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionInpaintPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=9, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + # TODO: use tensor inputs instead of PIL, this is here just to leave the old expected_slices untouched + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + image = image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((64, 64)) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_inpaint(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInpaintPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4723, 0.5731, 0.3939, 0.5441, 0.5922, 0.4392, 0.5059, 0.4651, 0.4474]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_inpaint_image_tensor(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInpaintPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + output = sd_pipe(**inputs) + out_pil = output.images + + inputs = self.get_dummy_inputs(device) + inputs["image"] = torch.tensor(np.array(inputs["image"]) / 127.5 - 1).permute(2, 0, 1).unsqueeze(0) + inputs["mask_image"] = torch.tensor(np.array(inputs["mask_image"]) / 255).permute(2, 0, 1)[:1].unsqueeze(0) + output = sd_pipe(**inputs) + out_tensor = output.images + + assert out_pil.shape == (1, 64, 64, 3) + assert np.abs(out_pil.flatten() - out_tensor.flatten()).max() < 5e-2 + + def test_stable_diffusion_inpaint_with_num_images_per_prompt(self): + device = "cpu" + components = self.get_dummy_components() + sd_pipe = StableDiffusionInpaintPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs, num_images_per_prompt=2).images + + # check if the output is a list of 2 images + assert len(images) == 2 + + +@slow +@require_torch_gpu +class StableDiffusionInpaintPipelineSlowTests(unittest.TestCase): + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_mask.png" + ) + inputs = { + "prompt": "Face of a yellow cat, high resolution, sitting on a park bench", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_inpaint_ddim(self): + pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", safety_checker=None + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0427, 0.0460, 0.0483, 0.0460, 0.0584, 0.0521, 0.1549, 0.1695, 0.1794]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_inpaint_fp16(self): + pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16, safety_checker=None + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.1443, 0.1218, 0.1587, 0.1594, 0.1411, 0.1284, 0.1370, 0.1506, 0.2339]) + + assert np.abs(expected_slice - image_slice).max() < 5e-2 + + def test_stable_diffusion_inpaint_pndm(self): + pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", safety_checker=None + ) + pipe.scheduler = PNDMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0425, 0.0273, 0.0344, 0.1694, 0.1727, 0.1812, 0.3256, 0.3311, 0.3272]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_inpaint_k_lms(self): + pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", safety_checker=None + ) + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.9314, 0.7575, 0.9432, 0.8885, 0.9028, 0.7298, 0.9811, 0.9667, 0.7633]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_inpaint_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionInpaintPipeline.from_pretrained( + "runwayml/stable-diffusion-inpainting", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.2 GB is allocated + assert mem_bytes < 2.2 * 10**9 + + +@nightly +@require_torch_gpu +class StableDiffusionInpaintPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_mask.png" + ) + inputs = { + "prompt": "Face of a yellow cat, high resolution, sitting on a park bench", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 50, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_inpaint_ddim(self): + sd_pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting") + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/stable_diffusion_inpaint_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_pndm(self): + sd_pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting") + sd_pipe.scheduler = PNDMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/stable_diffusion_inpaint_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_lms(self): + sd_pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting") + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/stable_diffusion_inpaint_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_dpm(self): + sd_pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting") + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 30 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/stable_diffusion_inpaint_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + +class StableDiffusionInpaintingPrepareMaskAndMaskedImageTests(unittest.TestCase): + def test_pil_inputs(self): + im = np.random.randint(0, 255, (32, 32, 3), dtype=np.uint8) + im = Image.fromarray(im) + mask = np.random.randint(0, 255, (32, 32), dtype=np.uint8) > 127.5 + mask = Image.fromarray((mask * 255).astype(np.uint8)) + + t_mask, t_masked = prepare_mask_and_masked_image(im, mask) + + self.assertTrue(isinstance(t_mask, torch.Tensor)) + self.assertTrue(isinstance(t_masked, torch.Tensor)) + + self.assertEqual(t_mask.ndim, 4) + self.assertEqual(t_masked.ndim, 4) + + self.assertEqual(t_mask.shape, (1, 1, 32, 32)) + self.assertEqual(t_masked.shape, (1, 3, 32, 32)) + + self.assertTrue(t_mask.dtype == torch.float32) + self.assertTrue(t_masked.dtype == torch.float32) + + self.assertTrue(t_mask.min() >= 0.0) + self.assertTrue(t_mask.max() <= 1.0) + self.assertTrue(t_masked.min() >= -1.0) + self.assertTrue(t_masked.min() <= 1.0) + + self.assertTrue(t_mask.sum() > 0.0) + + def test_np_inputs(self): + im_np = np.random.randint(0, 255, (32, 32, 3), dtype=np.uint8) + im_pil = Image.fromarray(im_np) + mask_np = np.random.randint(0, 255, (32, 32), dtype=np.uint8) > 127.5 + mask_pil = Image.fromarray((mask_np * 255).astype(np.uint8)) + + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + t_mask_pil, t_masked_pil = prepare_mask_and_masked_image(im_pil, mask_pil) + + self.assertTrue((t_mask_np == t_mask_pil).all()) + self.assertTrue((t_masked_np == t_masked_pil).all()) + + def test_torch_3D_2D_inputs(self): + im_tensor = torch.randint(0, 255, (3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (32, 32), dtype=torch.uint8) > 127.5 + im_np = im_tensor.numpy().transpose(1, 2, 0) + mask_np = mask_tensor.numpy() + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_3D_3D_inputs(self): + im_tensor = torch.randint(0, 255, (3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (1, 32, 32), dtype=torch.uint8) > 127.5 + im_np = im_tensor.numpy().transpose(1, 2, 0) + mask_np = mask_tensor.numpy()[0] + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_4D_2D_inputs(self): + im_tensor = torch.randint(0, 255, (1, 3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (32, 32), dtype=torch.uint8) > 127.5 + im_np = im_tensor.numpy()[0].transpose(1, 2, 0) + mask_np = mask_tensor.numpy() + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_4D_3D_inputs(self): + im_tensor = torch.randint(0, 255, (1, 3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (1, 32, 32), dtype=torch.uint8) > 127.5 + im_np = im_tensor.numpy()[0].transpose(1, 2, 0) + mask_np = mask_tensor.numpy()[0] + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_4D_4D_inputs(self): + im_tensor = torch.randint(0, 255, (1, 3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (1, 1, 32, 32), dtype=torch.uint8) > 127.5 + im_np = im_tensor.numpy()[0].transpose(1, 2, 0) + mask_np = mask_tensor.numpy()[0][0] + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + t_mask_np, t_masked_np = prepare_mask_and_masked_image(im_np, mask_np) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_batch_4D_3D(self): + im_tensor = torch.randint(0, 255, (2, 3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (2, 32, 32), dtype=torch.uint8) > 127.5 + + im_nps = [im.numpy().transpose(1, 2, 0) for im in im_tensor] + mask_nps = [mask.numpy() for mask in mask_tensor] + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + nps = [prepare_mask_and_masked_image(i, m) for i, m in zip(im_nps, mask_nps)] + t_mask_np = torch.cat([n[0] for n in nps]) + t_masked_np = torch.cat([n[1] for n in nps]) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_torch_batch_4D_4D(self): + im_tensor = torch.randint(0, 255, (2, 3, 32, 32), dtype=torch.uint8) + mask_tensor = torch.randint(0, 255, (2, 1, 32, 32), dtype=torch.uint8) > 127.5 + + im_nps = [im.numpy().transpose(1, 2, 0) for im in im_tensor] + mask_nps = [mask.numpy()[0] for mask in mask_tensor] + + t_mask_tensor, t_masked_tensor = prepare_mask_and_masked_image(im_tensor / 127.5 - 1, mask_tensor) + nps = [prepare_mask_and_masked_image(i, m) for i, m in zip(im_nps, mask_nps)] + t_mask_np = torch.cat([n[0] for n in nps]) + t_masked_np = torch.cat([n[1] for n in nps]) + + self.assertTrue((t_mask_tensor == t_mask_np).all()) + self.assertTrue((t_masked_tensor == t_masked_np).all()) + + def test_shape_mismatch(self): + # test height and width + with self.assertRaises(AssertionError): + prepare_mask_and_masked_image(torch.randn(3, 32, 32), torch.randn(64, 64)) + # test batch dim + with self.assertRaises(AssertionError): + prepare_mask_and_masked_image(torch.randn(2, 3, 32, 32), torch.randn(4, 64, 64)) + # test batch dim + with self.assertRaises(AssertionError): + prepare_mask_and_masked_image(torch.randn(2, 3, 32, 32), torch.randn(4, 1, 64, 64)) + + def test_type_mismatch(self): + # test tensors-only + with self.assertRaises(TypeError): + prepare_mask_and_masked_image(torch.rand(3, 32, 32), torch.rand(3, 32, 32).numpy()) + # test tensors-only + with self.assertRaises(TypeError): + prepare_mask_and_masked_image(torch.rand(3, 32, 32).numpy(), torch.rand(3, 32, 32)) + + def test_channels_first(self): + # test channels first for 3D tensors + with self.assertRaises(AssertionError): + prepare_mask_and_masked_image(torch.rand(32, 32, 3), torch.rand(3, 32, 32)) + + def test_tensor_range(self): + # test im <= 1 + with self.assertRaises(ValueError): + prepare_mask_and_masked_image(torch.ones(3, 32, 32) * 2, torch.rand(32, 32)) + # test im >= -1 + with self.assertRaises(ValueError): + prepare_mask_and_masked_image(torch.ones(3, 32, 32) * (-2), torch.rand(32, 32)) + # test mask <= 1 + with self.assertRaises(ValueError): + prepare_mask_and_masked_image(torch.rand(3, 32, 32), torch.ones(32, 32) * 2) + # test mask >= 0 + with self.assertRaises(ValueError): + prepare_mask_and_masked_image(torch.rand(3, 32, 32), torch.ones(32, 32) * -1) diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint_legacy.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint_legacy.py new file mode 100644 index 0000000000000000000000000000000000000000..d330e0f7eded8f9cd9e803dbed93e65bfbed6c66 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_inpaint_legacy.py @@ -0,0 +1,538 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionInpaintPipelineLegacy, + UNet2DConditionModel, + UNet2DModel, + VQModel, +) +from diffusers.utils import floats_tensor, load_image, nightly, slow, torch_device +from diffusers.utils.testing_utils import load_numpy, require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionInpaintLegacyPipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + @property + def dummy_uncond_unet(self): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + @property + def dummy_cond_unet(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + return model + + @property + def dummy_cond_unet_inpaint(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=9, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + return model + + @property + def dummy_vq_model(self): + torch.manual_seed(0) + model = VQModel( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=3, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModel(config) + + @property + def dummy_extractor(self): + def extract(*args, **kwargs): + class Out: + def __init__(self): + self.pixel_values = torch.ones([0]) + + def to(self, device): + self.pixel_values.to(device) + return self + + return Out() + + return extract + + def test_stable_diffusion_inpaint_legacy(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB") + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((32, 32)) + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionInpaintPipelineLegacy( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + ) + + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4941, 0.5396, 0.4689, 0.6338, 0.5392, 0.4094, 0.5477, 0.5904, 0.5165]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_inpaint_legacy_negative_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB") + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((32, 32)) + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionInpaintPipelineLegacy( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + negative_prompt = "french fries" + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe( + prompt, + negative_prompt=negative_prompt, + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.4941, 0.5396, 0.4689, 0.6338, 0.5392, 0.4094, 0.5477, 0.5904, 0.5165]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_inpaint_legacy_num_images_per_prompt(self): + device = "cpu" + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB") + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((32, 32)) + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionInpaintPipelineLegacy( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + + # test num_images_per_prompt=1 (default) + images = sd_pipe( + prompt, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + ).images + + assert images.shape == (1, 32, 32, 3) + + # test num_images_per_prompt=1 (default) for batch of prompts + batch_size = 2 + images = sd_pipe( + [prompt] * batch_size, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + ).images + + assert images.shape == (batch_size, 32, 32, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + images = sd_pipe( + prompt, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + num_images_per_prompt=num_images_per_prompt, + ).images + + assert images.shape == (num_images_per_prompt, 32, 32, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + images = sd_pipe( + [prompt] * batch_size, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + num_images_per_prompt=num_images_per_prompt, + ).images + + assert images.shape == (batch_size * num_images_per_prompt, 32, 32, 3) + + +@slow +@require_torch_gpu +class StableDiffusionInpaintLegacyPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_mask.png" + ) + inputs = { + "prompt": "A red cat sitting on a park bench", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 3, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_inpaint_legacy_pndm(self): + pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained( + "CompVis/stable-diffusion-v1-4", safety_checker=None + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.5665, 0.6117, 0.6430, 0.4057, 0.4594, 0.5658, 0.1596, 0.3106, 0.4305]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_inpaint_legacy_k_lms(self): + pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained( + "CompVis/stable-diffusion-v1-4", safety_checker=None + ) + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.4534, 0.4467, 0.4329, 0.4329, 0.4339, 0.4220, 0.4244, 0.4332, 0.4426]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_inpaint_legacy_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([0.5977, 1.5449, 1.0586, -0.3250, 0.7383, -0.0862, 0.4631, -0.2571, -1.1289]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 1e-3 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([0.5190, 1.1621, 0.6885, 0.2424, 0.3337, -0.1617, 0.6914, -0.1957, -0.5474]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 1e-3 + + callback_fn.has_been_called = False + + pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained( + "CompVis/stable-diffusion-v1-4", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == 2 + + +@nightly +@require_torch_gpu +class StableDiffusionInpaintLegacyPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint/input_bench_mask.png" + ) + inputs = { + "prompt": "A red cat sitting on a park bench", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 50, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_inpaint_pndm(self): + sd_pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint_legacy/stable_diffusion_1_5_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_ddim(self): + sd_pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = DDIMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint_legacy/stable_diffusion_1_5_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_lms(self): + sd_pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint_legacy/stable_diffusion_1_5_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_inpaint_dpm(self): + sd_pipe = StableDiffusionInpaintPipelineLegacy.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 30 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_inpaint_legacy/stable_diffusion_1_5_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_instruction_pix2pix.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_instruction_pix2pix.py new file mode 100644 index 0000000000000000000000000000000000000000..4c232b573b4f49cf5d432c8a158c0a31c1eb6f3a --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_instruction_pix2pix.py @@ -0,0 +1,383 @@ +# coding=utf-8 +# Copyright 2023 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + EulerAncestralDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionInstructPix2PixPipeline, + UNet2DConditionModel, +) +from diffusers.utils import floats_tensor, load_image, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionInstructPix2PixPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionInstructPix2PixPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=8, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + image = image.cpu().permute(0, 2, 3, 1)[0] + image = Image.fromarray(np.uint8(image)).convert("RGB") + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "image_guidance_scale": 1, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_pix2pix_default_case(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInstructPix2PixPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.7318, 0.3723, 0.4662, 0.623, 0.5770, 0.5014, 0.4281, 0.5550, 0.4813]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_negative_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInstructPix2PixPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + negative_prompt = "french fries" + output = sd_pipe(**inputs, negative_prompt=negative_prompt) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.7323, 0.3688, 0.4611, 0.6255, 0.5746, 0.5017, 0.433, 0.5553, 0.4827]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_multiple_init_images(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInstructPix2PixPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * 2 + + image = np.array(inputs["image"]).astype(np.float32) / 255.0 + image = torch.from_numpy(image).unsqueeze(0).to(device) + image = image.permute(0, 3, 1, 2) + inputs["image"] = image.repeat(2, 1, 1, 1) + + image = sd_pipe(**inputs).images + image_slice = image[-1, -3:, -3:, -1] + + assert image.shape == (2, 32, 32, 3) + expected_slice = np.array([0.606, 0.5712, 0.5099, 0.598, 0.5805, 0.7205, 0.6793, 0.554, 0.5607]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_euler(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = EulerAncestralDiscreteScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear" + ) + sd_pipe = StableDiffusionInstructPix2PixPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + slice = [round(x, 4) for x in image_slice.flatten().tolist()] + print(",".join([str(x) for x in slice])) + + assert image.shape == (1, 32, 32, 3) + expected_slice = np.array([0.726, 0.3902, 0.4868, 0.585, 0.5672, 0.511, 0.3906, 0.551, 0.4846]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_num_images_per_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInstructPix2PixPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + # test num_images_per_prompt=1 (default) + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs).images + + assert images.shape == (1, 32, 32, 3) + + # test num_images_per_prompt=1 (default) for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = sd_pipe(**inputs).images + + assert images.shape == (batch_size, 32, 32, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + inputs = self.get_dummy_inputs(device) + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (num_images_per_prompt, 32, 32, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = sd_pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (batch_size * num_images_per_prompt, 32, 32, 3) + + +@slow +@require_torch_gpu +class StableDiffusionInstructPix2PixPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, seed=0): + generator = torch.manual_seed(seed) + image = load_image( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main/stable_diffusion_pix2pix/example.jpg" + ) + inputs = { + "prompt": "turn him into a cyborg", + "image": image, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 7.5, + "image_guidance_scale": 1.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_pix2pix_default(self): + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + "timbrooks/instruct-pix2pix", safety_checker=None + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.5902, 0.6015, 0.6027, 0.5983, 0.6092, 0.6061, 0.5765, 0.5785, 0.5555]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_k_lms(self): + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + "timbrooks/instruct-pix2pix", safety_checker=None + ) + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.6578, 0.6817, 0.6972, 0.6761, 0.6856, 0.6916, 0.6428, 0.6516, 0.6301]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_ddim(self): + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + "timbrooks/instruct-pix2pix", safety_checker=None + ) + pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.3828, 0.3834, 0.3818, 0.3792, 0.3865, 0.3752, 0.3792, 0.3847, 0.3753]) + + assert np.abs(expected_slice - image_slice).max() < 1e-3 + + def test_stable_diffusion_pix2pix_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([-0.2463, -0.4644, -0.9756, 1.5176, 1.4414, 0.7866, 0.9897, 0.8521, 0.7983]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([-0.2644, -0.4626, -0.9653, 1.5176, 1.4551, 0.7686, 0.9805, 0.8452, 0.8115]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + "timbrooks/instruct-pix2pix", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == 3 + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + "timbrooks/instruct-pix2pix", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs() + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.2 GB is allocated + assert mem_bytes < 2.2 * 10**9 + + def test_stable_diffusion_pix2pix_pipeline_multiple_of_8(self): + inputs = self.get_inputs() + # resize to resolution that is divisible by 8 but not 16 or 32 + inputs["image"] = inputs["image"].resize((504, 504)) + + model_id = "timbrooks/instruct-pix2pix" + pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained( + model_id, + safety_checker=None, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + output = pipe(**inputs) + image = output.images[0] + + image_slice = image[255:258, 383:386, -1] + + assert image.shape == (504, 504, 3) + expected_slice = np.array([0.2726, 0.2529, 0.2664, 0.2655, 0.2641, 0.2642, 0.2591, 0.2649, 0.2590]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_k_diffusion.py b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_k_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..4a56615ef5b94cdf2f7f96da2df26c0f93dc7986 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion/test_stable_diffusion_k_diffusion.py @@ -0,0 +1,77 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch + +from diffusers import StableDiffusionKDiffusionPipeline +from diffusers.utils import slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +@slow +@require_torch_gpu +class StableDiffusionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_stable_diffusion_1(self): + sd_pipe = StableDiffusionKDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + sd_pipe.set_scheduler("sample_euler") + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=9.0, num_inference_steps=20, output_type="np") + + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0447, 0.0492, 0.0468, 0.0408, 0.0383, 0.0408, 0.0354, 0.0380, 0.0339]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_2(self): + sd_pipe = StableDiffusionKDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + sd_pipe.set_scheduler("sample_euler") + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=9.0, num_inference_steps=20, output_type="np") + + image = output.images + + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.1237, 0.1320, 0.1438, 0.1359, 0.1390, 0.1132, 0.1277, 0.1175, 0.1112]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-1 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/__init__.py b/diffusers/tests/pipelines/stable_diffusion_2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..9c9a0a0186296bb316f8f8d848a305e39e3cc461 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion.py @@ -0,0 +1,506 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, + logging, +) +from diffusers.utils import load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import CaptureLogger, require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusion2PipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + # SD2-specific config below + attention_head_dim=(2, 4), + use_linear_projection=True, + ) + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + ) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + sample_size=128, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + # SD2-specific config below + hidden_act="gelu", + projection_dim=512, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_ddim(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5649, 0.6022, 0.4804, 0.5270, 0.5585, 0.4643, 0.5159, 0.4963, 0.4793]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_pndm(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = PNDMScheduler(skip_prk_steps=True) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5099, 0.5677, 0.4671, 0.5128, 0.5697, 0.4676, 0.5277, 0.4964, 0.4946]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_k_lms(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4717, 0.5376, 0.4568, 0.5225, 0.5734, 0.4797, 0.5467, 0.5074, 0.5043]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_k_euler_ancestral(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = EulerAncestralDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4715, 0.5376, 0.4569, 0.5224, 0.5734, 0.4797, 0.5465, 0.5074, 0.5046]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_k_euler(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + components["scheduler"] = EulerDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4717, 0.5376, 0.4568, 0.5225, 0.5734, 0.4797, 0.5467, 0.5074, 0.5043]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_long_prompt(self): + components = self.get_dummy_components() + components["scheduler"] = LMSDiscreteScheduler.from_config(components["scheduler"].config) + sd_pipe = StableDiffusionPipeline(**components) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + do_classifier_free_guidance = True + negative_prompt = None + num_images_per_prompt = 1 + logger = logging.get_logger("diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion") + + prompt = 25 * "@" + with CaptureLogger(logger) as cap_logger_3: + text_embeddings_3 = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + prompt = 100 * "@" + with CaptureLogger(logger) as cap_logger: + text_embeddings = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + negative_prompt = "Hello" + with CaptureLogger(logger) as cap_logger_2: + text_embeddings_2 = sd_pipe._encode_prompt( + prompt, torch_device, num_images_per_prompt, do_classifier_free_guidance, negative_prompt + ) + + assert text_embeddings_3.shape == text_embeddings_2.shape == text_embeddings.shape + assert text_embeddings.shape[1] == 77 + + assert cap_logger.out == cap_logger_2.out + # 100 - 77 + 1 (BOS token) + 1 (EOS token) = 25 + assert cap_logger.out.count("@") == 25 + assert cap_logger_3.out == "" + + +@slow +@require_torch_gpu +class StableDiffusion2PipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "a photograph of an astronaut riding a horse", + "latents": latents, + "generator": generator, + "num_inference_steps": 3, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_default_ddim(self): + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-base") + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.49493, 0.47896, 0.40798, 0.54214, 0.53212, 0.48202, 0.47656, 0.46329, 0.48506]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_pndm(self): + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-base") + pipe.scheduler = PNDMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.49493, 0.47896, 0.40798, 0.54214, 0.53212, 0.48202, 0.47656, 0.46329, 0.48506]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_k_lms(self): + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-base") + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1].flatten() + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.10440, 0.13115, 0.11100, 0.10141, 0.11440, 0.07215, 0.11332, 0.09693, 0.10006]) + assert np.abs(image_slice - expected_slice).max() < 1e-4 + + def test_stable_diffusion_attention_slicing(self): + torch.cuda.reset_peak_memory_stats() + pipe = StableDiffusionPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-base", torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # enable attention slicing + pipe.enable_attention_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image_sliced = pipe(**inputs).images + + mem_bytes = torch.cuda.max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + # make sure that less than 3.3 GB is allocated + assert mem_bytes < 3.3 * 10**9 + + # disable slicing + pipe.disable_attention_slicing() + inputs = self.get_inputs(torch_device, dtype=torch.float16) + image = pipe(**inputs).images + + # make sure that more than 3.3 GB is allocated + mem_bytes = torch.cuda.max_memory_allocated() + assert mem_bytes > 3.3 * 10**9 + assert np.abs(image_sliced - image).max() < 1e-3 + + def test_stable_diffusion_text2img_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.3862, -0.4507, -1.1729, 0.0686, -1.1045, 0.7124, -1.8301, 0.1903, 1.2773] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 64, 64) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [0.2720, -0.1863, -0.7383, -0.5029, -0.7534, 0.3970, -0.7646, 0.4468, 1.2686] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-base", torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == inputs["num_inference_steps"] + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-base", torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(torch_device, dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.8 GB is allocated + assert mem_bytes < 2.8 * 10**9 + + +@nightly +@require_torch_gpu +class StableDiffusion2PipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device, generator_device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=generator_device).manual_seed(seed) + latents = np.random.RandomState(seed).standard_normal((1, 4, 64, 64)) + latents = torch.from_numpy(latents).to(device=device, dtype=dtype) + inputs = { + "prompt": "a photograph of an astronaut riding a horse", + "latents": latents, + "generator": generator, + "num_inference_steps": 50, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_2_0_default_ddim(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-base").to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_0_base_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_2_1_default_pndm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base").to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_1_base_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_ddim(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base").to(torch_device) + sd_pipe.scheduler = DDIMScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_1_base_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_lms(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base").to(torch_device) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_1_base_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_euler(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base").to(torch_device) + sd_pipe.scheduler = EulerDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_1_base_euler.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_stable_diffusion_dpm(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1-base").to(torch_device) + sd_pipe.scheduler = DPMSolverMultistepScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs(torch_device) + inputs["num_inference_steps"] = 25 + image = sd_pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_2_text2img/stable_diffusion_2_1_base_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_depth.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_depth.py new file mode 100644 index 0000000000000000000000000000000000000000..af8a99fc8a07bfdced14726ef79420ec72298406 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_depth.py @@ -0,0 +1,614 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import tempfile +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import ( + CLIPTextConfig, + CLIPTextModel, + CLIPTokenizer, + DPTConfig, + DPTFeatureExtractor, + DPTForDepthEstimation, +) + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionDepth2ImgPipeline, + UNet2DConditionModel, +) +from diffusers.utils import floats_tensor, load_image, load_numpy, nightly, slow, torch_device +from diffusers.utils.import_utils import is_accelerate_available +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +@unittest.skipIf(torch_device == "mps", reason="The depth model does not support MPS yet") +class StableDiffusionDepth2ImgPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionDepth2ImgPipeline + test_save_load_optional_components = False + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=5, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + attention_head_dim=(2, 4), + use_linear_projection=True, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + backbone_config = { + "global_padding": "same", + "layer_type": "bottleneck", + "depths": [3, 4, 9], + "out_features": ["stage1", "stage2", "stage3"], + "embedding_dynamic_padding": True, + "hidden_sizes": [96, 192, 384, 768], + "num_groups": 2, + } + depth_estimator_config = DPTConfig( + image_size=32, + patch_size=16, + num_channels=3, + hidden_size=32, + num_hidden_layers=4, + backbone_out_indices=(0, 1, 2, 3), + num_attention_heads=4, + intermediate_size=37, + hidden_act="gelu", + hidden_dropout_prob=0.1, + attention_probs_dropout_prob=0.1, + is_decoder=False, + initializer_range=0.02, + is_hybrid=True, + backbone_config=backbone_config, + backbone_featmap_shape=[1, 384, 24, 24], + ) + depth_estimator = DPTForDepthEstimation(depth_estimator_config) + feature_extractor = DPTFeatureExtractor.from_pretrained( + "hf-internal-testing/tiny-random-DPTForDepthEstimation" + ) + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "depth_estimator": depth_estimator, + "feature_extractor": feature_extractor, + } + return components + + def get_dummy_inputs(self, device, seed=0): + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)) + image = image.cpu().permute(0, 2, 3, 1)[0] + image = Image.fromarray(np.uint8(image)).convert("RGB").resize((32, 32)) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + @unittest.skipIf(torch_device == "mps", reason="The depth model does not support MPS yet") + def test_save_load_local(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs)[0] + + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + pipe_loaded = self.pipeline_class.from_pretrained(tmpdir) + pipe_loaded.to(torch_device) + pipe_loaded.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output_loaded = pipe_loaded(**inputs)[0] + + max_diff = np.abs(output - output_loaded).max() + self.assertLess(max_diff, 1e-4) + + @unittest.skipIf(torch_device != "cuda", reason="float16 requires CUDA") + def test_save_load_float16(self): + components = self.get_dummy_components() + for name, module in components.items(): + if hasattr(module, "half"): + components[name] = module.to(torch_device).half() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs)[0] + + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + pipe_loaded = self.pipeline_class.from_pretrained(tmpdir, torch_dtype=torch.float16) + pipe_loaded.to(torch_device) + pipe_loaded.set_progress_bar_config(disable=None) + + for name, component in pipe_loaded.components.items(): + if hasattr(component, "dtype"): + self.assertTrue( + component.dtype == torch.float16, + f"`{name}.dtype` switched from `float16` to {component.dtype} after loading.", + ) + + inputs = self.get_dummy_inputs(torch_device) + output_loaded = pipe_loaded(**inputs)[0] + + max_diff = np.abs(output - output_loaded).max() + self.assertLess(max_diff, 2e-2, "The output of the fp16 pipeline changed after saving and loading.") + + @unittest.skipIf(torch_device != "cuda", reason="float16 requires CUDA") + def test_float16_inference(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + for name, module in components.items(): + if hasattr(module, "half"): + components[name] = module.half() + pipe_fp16 = self.pipeline_class(**components) + pipe_fp16.to(torch_device) + pipe_fp16.set_progress_bar_config(disable=None) + + output = pipe(**self.get_dummy_inputs(torch_device))[0] + output_fp16 = pipe_fp16(**self.get_dummy_inputs(torch_device))[0] + + max_diff = np.abs(output - output_fp16).max() + self.assertLess(max_diff, 1.3e-2, "The outputs of the fp16 and fp32 pipelines are too different.") + + @unittest.skipIf( + torch_device != "cuda" or not is_accelerate_available(), + reason="CPU offload is only available with CUDA and `accelerate` installed", + ) + def test_cpu_offload_forward_pass(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output_without_offload = pipe(**inputs)[0] + + pipe.enable_sequential_cpu_offload() + inputs = self.get_dummy_inputs(torch_device) + output_with_offload = pipe(**inputs)[0] + + max_diff = np.abs(output_with_offload - output_without_offload).max() + self.assertLess(max_diff, 1e-4, "CPU offloading should not affect the inference results") + + @unittest.skipIf(torch_device == "mps", reason="The depth model does not support MPS yet") + def test_dict_tuple_outputs_equivalent(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = pipe(**self.get_dummy_inputs(torch_device)) + + output = pipe(**self.get_dummy_inputs(torch_device))[0] + output_tuple = pipe(**self.get_dummy_inputs(torch_device), return_dict=False)[0] + + max_diff = np.abs(output - output_tuple).max() + self.assertLess(max_diff, 1e-4) + + @unittest.skipIf(torch_device == "mps", reason="The depth model does not support MPS yet") + def test_progress_bar(self): + super().test_progress_bar() + + def test_stable_diffusion_depth2img_default_case(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = StableDiffusionDepth2ImgPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + if torch_device == "mps": + expected_slice = np.array([0.6071, 0.5035, 0.4378, 0.5776, 0.5753, 0.4316, 0.4513, 0.5263, 0.4546]) + else: + expected_slice = np.array([0.6312, 0.4984, 0.4154, 0.4788, 0.5535, 0.4599, 0.4017, 0.5359, 0.4716]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_depth2img_negative_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = StableDiffusionDepth2ImgPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + negative_prompt = "french fries" + output = pipe(**inputs, negative_prompt=negative_prompt) + image = output.images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 32, 32, 3) + if torch_device == "mps": + expected_slice = np.array([0.5825, 0.5135, 0.4095, 0.5452, 0.6059, 0.4211, 0.3994, 0.5177, 0.4335]) + else: + expected_slice = np.array([0.6296, 0.5125, 0.3890, 0.4456, 0.5955, 0.4621, 0.3810, 0.5310, 0.4626]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_depth2img_multiple_init_images(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = StableDiffusionDepth2ImgPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * 2 + inputs["image"] = 2 * [inputs["image"]] + image = pipe(**inputs).images + image_slice = image[-1, -3:, -3:, -1] + + assert image.shape == (2, 32, 32, 3) + + if torch_device == "mps": + expected_slice = np.array([0.6501, 0.5150, 0.4939, 0.6688, 0.5437, 0.5758, 0.5115, 0.4406, 0.4551]) + else: + expected_slice = np.array([0.6267, 0.5232, 0.6001, 0.6738, 0.5029, 0.6429, 0.5364, 0.4159, 0.4674]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + def test_stable_diffusion_depth2img_num_images_per_prompt(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = StableDiffusionDepth2ImgPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + # test num_images_per_prompt=1 (default) + inputs = self.get_dummy_inputs(device) + images = pipe(**inputs).images + + assert images.shape == (1, 32, 32, 3) + + # test num_images_per_prompt=1 (default) for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = pipe(**inputs).images + + assert images.shape == (batch_size, 32, 32, 3) + + # test num_images_per_prompt for single prompt + num_images_per_prompt = 2 + inputs = self.get_dummy_inputs(device) + images = pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (num_images_per_prompt, 32, 32, 3) + + # test num_images_per_prompt for batch of prompts + batch_size = 2 + inputs = self.get_dummy_inputs(device) + inputs["prompt"] = [inputs["prompt"]] * batch_size + images = pipe(**inputs, num_images_per_prompt=num_images_per_prompt).images + + assert images.shape == (batch_size * num_images_per_prompt, 32, 32, 3) + + def test_stable_diffusion_depth2img_pil(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + pipe = StableDiffusionDepth2ImgPipeline(**components) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + if torch_device == "mps": + expected_slice = np.array([0.53232, 0.47015, 0.40868, 0.45651, 0.4891, 0.4668, 0.4287, 0.48822, 0.47439]) + else: + expected_slice = np.array([0.6312, 0.4984, 0.4154, 0.4788, 0.5535, 0.4599, 0.4017, 0.5359, 0.4716]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-3 + + +@slow +@require_torch_gpu +class StableDiffusionDepth2ImgPipelineSlowTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/depth2img/two_cats.png" + ) + inputs = { + "prompt": "two tigers", + "image": init_image, + "generator": generator, + "num_inference_steps": 3, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_depth2img_pipeline_default(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", safety_checker=None + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 480, 640, 3) + expected_slice = np.array([0.9057, 0.9365, 0.9258, 0.8937, 0.8555, 0.8541, 0.8260, 0.7747, 0.7421]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_depth2img_pipeline_k_lms(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", safety_checker=None + ) + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 480, 640, 3) + expected_slice = np.array([0.6363, 0.6274, 0.6309, 0.6370, 0.6226, 0.6286, 0.6213, 0.6453, 0.6306]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_depth2img_pipeline_ddim(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", safety_checker=None + ) + pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs() + image = pipe(**inputs).images + image_slice = image[0, 253:256, 253:256, -1].flatten() + + assert image.shape == (1, 480, 640, 3) + expected_slice = np.array([0.6424, 0.6524, 0.6249, 0.6041, 0.6634, 0.6420, 0.6522, 0.6555, 0.6436]) + + assert np.abs(expected_slice - image_slice).max() < 1e-4 + + def test_stable_diffusion_depth2img_intermediate_state(self): + number_of_steps = 0 + + def callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 1: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 60, 80) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.7168, -1.5137, -0.1418, -2.9219, -2.7266, -2.4414, -2.1035, -3.0078, -1.7051] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 2: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 60, 80) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array( + [-0.7109, -1.5068, -0.1403, -2.9160, -2.7207, -2.4414, -2.1035, -3.0059, -1.7090] + ) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + callback_fn.has_been_called = False + + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + inputs = self.get_inputs(dtype=torch.float16) + pipe(**inputs, callback=callback_fn, callback_steps=1) + assert callback_fn.has_been_called + assert number_of_steps == 2 + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-depth", safety_checker=None, torch_dtype=torch.float16 + ) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + inputs = self.get_inputs(dtype=torch.float16) + _ = pipe(**inputs) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.9 GB is allocated + assert mem_bytes < 2.9 * 10**9 + + +@nightly +@require_torch_gpu +class StableDiffusionImg2ImgPipelineNightlyTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def get_inputs(self, device="cpu", dtype=torch.float32, seed=0): + generator = torch.Generator(device=device).manual_seed(seed) + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/depth2img/two_cats.png" + ) + inputs = { + "prompt": "two tigers", + "image": init_image, + "generator": generator, + "num_inference_steps": 3, + "strength": 0.75, + "guidance_scale": 7.5, + "output_type": "numpy", + } + return inputs + + def test_depth2img_pndm(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth") + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs() + image = pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_depth2img/stable_diffusion_2_0_pndm.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_depth2img_ddim(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth") + pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs() + image = pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_depth2img/stable_diffusion_2_0_ddim.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img2img_lms(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth") + pipe.scheduler = LMSDiscreteScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs() + image = pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_depth2img/stable_diffusion_2_0_lms.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 + + def test_img2img_dpm(self): + pipe = StableDiffusionDepth2ImgPipeline.from_pretrained("stabilityai/stable-diffusion-2-depth") + pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_inputs() + inputs["num_inference_steps"] = 30 + image = pipe(**inputs).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/diffusers/test-arrays/resolve/main" + "/stable_diffusion_depth2img/stable_diffusion_2_0_dpm_multi.npy" + ) + max_diff = np.abs(expected_image - image).max() + assert max_diff < 1e-3 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..f10f0e1798273d61a04bd1f388716412f5edf7fe --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax.py @@ -0,0 +1,99 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +from diffusers import FlaxDPMSolverMultistepScheduler, FlaxStableDiffusionPipeline +from diffusers.utils import is_flax_available, slow +from diffusers.utils.testing_utils import require_flax + + +if is_flax_available(): + import jax + import jax.numpy as jnp + from flax.jax_utils import replicate + from flax.training.common_utils import shard + + +@slow +@require_flax +class FlaxStableDiffusion2PipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + + def test_stable_diffusion_flax(self): + sd_pipe, params = FlaxStableDiffusionPipeline.from_pretrained( + "stabilityai/stable-diffusion-2", + revision="bf16", + dtype=jnp.bfloat16, + ) + + prompt = "A painting of a squirrel eating a burger" + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = sd_pipe.prepare_inputs(prompt) + + params = replicate(params) + prompt_ids = shard(prompt_ids) + + prng_seed = jax.random.PRNGKey(0) + prng_seed = jax.random.split(prng_seed, jax.device_count()) + + images = sd_pipe(prompt_ids, params, prng_seed, num_inference_steps=25, jit=True)[0] + assert images.shape == (jax.device_count(), 1, 768, 768, 3) + + images = images.reshape((images.shape[0] * images.shape[1],) + images.shape[-3:]) + image_slice = images[0, 253:256, 253:256, -1] + + output_slice = jnp.asarray(jax.device_get(image_slice.flatten())) + expected_slice = jnp.array([0.4238, 0.4414, 0.4395, 0.4453, 0.4629, 0.4590, 0.4531, 0.45508, 0.4512]) + print(f"output_slice: {output_slice}") + assert jnp.abs(output_slice - expected_slice).max() < 1e-2 + + def test_stable_diffusion_dpm_flax(self): + model_id = "stabilityai/stable-diffusion-2" + scheduler, scheduler_params = FlaxDPMSolverMultistepScheduler.from_pretrained(model_id, subfolder="scheduler") + sd_pipe, params = FlaxStableDiffusionPipeline.from_pretrained( + model_id, + scheduler=scheduler, + revision="bf16", + dtype=jnp.bfloat16, + ) + params["scheduler"] = scheduler_params + + prompt = "A painting of a squirrel eating a burger" + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = sd_pipe.prepare_inputs(prompt) + + params = replicate(params) + prompt_ids = shard(prompt_ids) + + prng_seed = jax.random.PRNGKey(0) + prng_seed = jax.random.split(prng_seed, jax.device_count()) + + images = sd_pipe(prompt_ids, params, prng_seed, num_inference_steps=25, jit=True)[0] + assert images.shape == (jax.device_count(), 1, 768, 768, 3) + + images = images.reshape((images.shape[0] * images.shape[1],) + images.shape[-3:]) + image_slice = images[0, 253:256, 253:256, -1] + + output_slice = jnp.asarray(jax.device_get(image_slice.flatten())) + expected_slice = jnp.array([0.4336, 0.42969, 0.4453, 0.4199, 0.4297, 0.4531, 0.4434, 0.4434, 0.4297]) + print(f"output_slice: {output_slice}") + assert jnp.abs(output_slice - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax_inpaint.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..ddd29d98f2ea962417f92e53c1f0de9f7188db7e --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_flax_inpaint.py @@ -0,0 +1,82 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +from diffusers import FlaxStableDiffusionInpaintPipeline +from diffusers.utils import is_flax_available, load_image, slow +from diffusers.utils.testing_utils import require_flax + + +if is_flax_available(): + import jax + import jax.numpy as jnp + from flax.jax_utils import replicate + from flax.training.common_utils import shard + + +@slow +@require_flax +class FlaxStableDiffusionInpaintPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + + def test_stable_diffusion_inpaint_pipeline(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-inpaint/init_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint/mask.png" + ) + + model_id = "xvjiarui/stable-diffusion-2-inpainting" + pipeline, params = FlaxStableDiffusionInpaintPipeline.from_pretrained(model_id, safety_checker=None) + + prompt = "Face of a yellow cat, high resolution, sitting on a park bench" + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 50 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + init_image = num_samples * [init_image] + mask_image = num_samples * [mask_image] + prompt_ids, processed_masked_images, processed_masks = pipeline.prepare_inputs(prompt, init_image, mask_image) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, jax.device_count()) + prompt_ids = shard(prompt_ids) + processed_masked_images = shard(processed_masked_images) + processed_masks = shard(processed_masks) + + output = pipeline( + prompt_ids, processed_masks, processed_masked_images, params, prng_seed, num_inference_steps, jit=True + ) + + images = output.images.reshape(num_samples, 512, 512, 3) + + image_slice = images[0, 253:256, 253:256, -1] + + output_slice = jnp.asarray(jax.device_get(image_slice.flatten())) + expected_slice = jnp.array( + [0.3611307, 0.37649736, 0.3757408, 0.38213953, 0.39295167, 0.3841631, 0.41554978, 0.4137475, 0.4217084] + ) + print(f"output_slice: {output_slice}") + assert jnp.abs(output_slice - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_inpaint.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..58bdd465e422fb4f602d431cbaaeca3f3e890a30 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_inpaint.py @@ -0,0 +1,252 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, PNDMScheduler, StableDiffusionInpaintPipeline, UNet2DConditionModel +from diffusers.utils import floats_tensor, load_image, load_numpy, torch_device +from diffusers.utils.testing_utils import require_torch_gpu, slow + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusion2InpaintPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionInpaintPipeline + + def get_dummy_components(self): + torch.manual_seed(0) + unet = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=9, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + # SD2-specific config below + attention_head_dim=(2, 4), + use_linear_projection=True, + ) + scheduler = PNDMScheduler(skip_prk_steps=True) + torch.manual_seed(0) + vae = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + sample_size=128, + ) + torch.manual_seed(0) + text_encoder_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + # SD2-specific config below + hidden_act="gelu", + projection_dim=512, + ) + text_encoder = CLIPTextModel(text_encoder_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": unet, + "scheduler": scheduler, + "vae": vae, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "safety_checker": None, + "feature_extractor": None, + } + return components + + def get_dummy_inputs(self, device, seed=0): + # TODO: use tensor inputs instead of PIL, this is here just to leave the old expected_slices untouched + image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + image = image.cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((64, 64)) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": init_image, + "mask_image": mask_image, + "generator": generator, + "num_inference_steps": 2, + "guidance_scale": 6.0, + "output_type": "numpy", + } + return inputs + + def test_stable_diffusion_inpaint(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + components = self.get_dummy_components() + sd_pipe = StableDiffusionInpaintPipeline(**components) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = sd_pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4727, 0.5735, 0.3941, 0.5446, 0.5926, 0.4394, 0.5062, 0.4654, 0.4476]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch_gpu +class StableDiffusionInpaintPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_stable_diffusion_inpaint_pipeline(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-inpaint/init_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint/mask.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint" + "/yellow_cat_sitting_on_a_park_bench.npy" + ) + + model_id = "stabilityai/stable-diffusion-2-inpainting" + pipe = StableDiffusionInpaintPipeline.from_pretrained(model_id, safety_checker=None) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "Face of a yellow cat, high resolution, sitting on a park bench" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (512, 512, 3) + assert np.abs(expected_image - image).max() < 1e-3 + + def test_stable_diffusion_inpaint_pipeline_fp16(self): + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-inpaint/init_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint/mask.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint" + "/yellow_cat_sitting_on_a_park_bench_fp16.npy" + ) + + model_id = "stabilityai/stable-diffusion-2-inpainting" + pipe = StableDiffusionInpaintPipeline.from_pretrained( + model_id, + torch_dtype=torch.float16, + safety_checker=None, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "Face of a yellow cat, high resolution, sitting on a park bench" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (512, 512, 3) + assert np.abs(expected_image - image).max() < 5e-1 + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-inpaint/init_image.png" + ) + mask_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-inpaint/mask.png" + ) + + model_id = "stabilityai/stable-diffusion-2-inpainting" + pndm = PNDMScheduler.from_pretrained(model_id, subfolder="scheduler") + pipe = StableDiffusionInpaintPipeline.from_pretrained( + model_id, + safety_checker=None, + scheduler=pndm, + torch_dtype=torch.float16, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + prompt = "Face of a yellow cat, high resolution, sitting on a park bench" + + generator = torch.manual_seed(0) + _ = pipe( + prompt=prompt, + image=init_image, + mask_image=mask_image, + generator=generator, + num_inference_steps=2, + output_type="np", + ) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.65 GB is allocated + assert mem_bytes < 2.65 * 10**9 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_latent_upscale.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_latent_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..9709977623c79d973ed9421b850400fec73d2d2b --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_latent_upscale.py @@ -0,0 +1,219 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + EulerDiscreteScheduler, + StableDiffusionLatentUpscalePipeline, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.utils import floats_tensor, load_image, load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionLatentUpscalePipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = StableDiffusionLatentUpscalePipeline + test_cpu_offload = True + + @property + def dummy_image(self): + batch_size = 1 + num_channels = 4 + sizes = (16, 16) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + def get_dummy_components(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + act_fn="gelu", + attention_head_dim=8, + norm_num_groups=None, + block_out_channels=[32, 32, 64, 64], + time_cond_proj_dim=160, + conv_in_kernel=1, + conv_out_kernel=1, + cross_attention_dim=32, + down_block_types=( + "KDownBlock2D", + "KCrossAttnDownBlock2D", + "KCrossAttnDownBlock2D", + "KCrossAttnDownBlock2D", + ), + in_channels=8, + mid_block_type=None, + only_cross_attention=False, + out_channels=5, + resnet_time_scale_shift="scale_shift", + time_embedding_type="fourier", + timestep_post_act="gelu", + up_block_types=("KCrossAttnUpBlock2D", "KCrossAttnUpBlock2D", "KCrossAttnUpBlock2D", "KUpBlock2D"), + ) + vae = AutoencoderKL( + block_out_channels=[32, 32, 64, 64], + in_channels=3, + out_channels=3, + down_block_types=[ + "DownEncoderBlock2D", + "DownEncoderBlock2D", + "DownEncoderBlock2D", + "DownEncoderBlock2D", + ], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D", "UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + scheduler = EulerDiscreteScheduler(prediction_type="original_sample") + text_config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + hidden_act="quick_gelu", + projection_dim=512, + ) + text_encoder = CLIPTextModel(text_config) + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + components = { + "unet": model.eval(), + "vae": vae.eval(), + "scheduler": scheduler, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + } + + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "A painting of a squirrel eating a burger", + "image": self.dummy_image.cpu(), + "generator": generator, + "num_inference_steps": 2, + "output_type": "numpy", + } + return inputs + + def test_inference(self): + device = "cpu" + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(device) + image = pipe(**inputs).images + image_slice = image[0, -3:, -3:, -1] + + self.assertEqual(image.shape, (1, 256, 256, 3)) + expected_slice = np.array( + [0.47222412, 0.41921633, 0.44717434, 0.46874192, 0.42588258, 0.46150726, 0.4677534, 0.45583832, 0.48579055] + ) + max_diff = np.abs(image_slice.flatten() - expected_slice).max() + self.assertLessEqual(max_diff, 1e-3) + + def test_inference_batch_single_identical(self): + self._test_inference_batch_single_identical(relax_max_difference=False) + + +@require_torch_gpu +@slow +class StableDiffusionLatentUpscalePipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_latent_upscaler_fp16(self): + generator = torch.manual_seed(33) + + pipe = StableDiffusionPipeline.from_pretrained("CompVis/stable-diffusion-v1-4", torch_dtype=torch.float16) + pipe.to("cuda") + + upscaler = StableDiffusionLatentUpscalePipeline.from_pretrained( + "stabilityai/sd-x2-latent-upscaler", torch_dtype=torch.float16 + ) + upscaler.to("cuda") + + prompt = "a photo of an astronaut high resolution, unreal engine, ultra realistic" + + low_res_latents = pipe(prompt, generator=generator, output_type="latent").images + + image = upscaler( + prompt=prompt, + image=low_res_latents, + num_inference_steps=20, + guidance_scale=0, + generator=generator, + output_type="np", + ).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/latent-upscaler/astronaut_1024.npy" + ) + assert np.abs((expected_image - image).max()) < 5e-1 + + def test_latent_upscaler_fp16_image(self): + generator = torch.manual_seed(33) + + upscaler = StableDiffusionLatentUpscalePipeline.from_pretrained( + "stabilityai/sd-x2-latent-upscaler", torch_dtype=torch.float16 + ) + upscaler.to("cuda") + + prompt = "the temple of fire by Ross Tran and Gerardo Dottori, oil on canvas" + + low_res_img = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/latent-upscaler/fire_temple_512.png" + ) + + image = upscaler( + prompt=prompt, + image=low_res_img, + num_inference_steps=20, + guidance_scale=0, + generator=generator, + output_type="np", + ).images[0] + + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/latent-upscaler/fire_temple_1024.npy" + ) + assert np.abs((expected_image - image).max()) < 5e-2 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_upscale.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..ff0112b3263bf0b606175bbd490962ee661c15d9 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_upscale.py @@ -0,0 +1,362 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from PIL import Image +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, DDIMScheduler, DDPMScheduler, StableDiffusionUpscalePipeline, UNet2DConditionModel +from diffusers.utils import floats_tensor, load_image, load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusionUpscalePipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + @property + def dummy_cond_unet_upscale(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 32, 64), + layers_per_block=2, + sample_size=32, + in_channels=7, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + # SD2-specific config below + attention_head_dim=8, + use_linear_projection=True, + only_cross_attention=(True, True, False), + num_class_embeds=100, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + # SD2-specific config below + hidden_act="gelu", + projection_dim=512, + ) + return CLIPTextModel(config) + + def test_stable_diffusion_upscale(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet_upscale + low_res_scheduler = DDPMScheduler() + scheduler = DDIMScheduler(prediction_type="v_prediction") + vae = self.dummy_vae + text_encoder = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + low_res_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionUpscalePipeline( + unet=unet, + low_res_scheduler=low_res_scheduler, + scheduler=scheduler, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + max_noise_level=350, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe( + [prompt], + image=low_res_image, + generator=generator, + guidance_scale=6.0, + noise_level=20, + num_inference_steps=2, + output_type="np", + ) + + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + image=low_res_image, + generator=generator, + guidance_scale=6.0, + noise_level=20, + num_inference_steps=2, + output_type="np", + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + expected_height_width = low_res_image.size[0] * 4 + assert image.shape == (1, expected_height_width, expected_height_width, 3) + expected_slice = np.array([0.2562, 0.3606, 0.4204, 0.4469, 0.4822, 0.4647, 0.5315, 0.5748, 0.5606]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_upscale_batch(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet_upscale + low_res_scheduler = DDPMScheduler() + scheduler = DDIMScheduler(prediction_type="v_prediction") + vae = self.dummy_vae + text_encoder = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + low_res_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionUpscalePipeline( + unet=unet, + low_res_scheduler=low_res_scheduler, + scheduler=scheduler, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + max_noise_level=350, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + output = sd_pipe( + 2 * [prompt], + image=2 * [low_res_image], + guidance_scale=6.0, + noise_level=20, + num_inference_steps=2, + output_type="np", + ) + image = output.images + assert image.shape[0] == 2 + + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe( + [prompt], + image=low_res_image, + generator=generator, + num_images_per_prompt=2, + guidance_scale=6.0, + noise_level=20, + num_inference_steps=2, + output_type="np", + ) + image = output.images + assert image.shape[0] == 2 + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_upscale_fp16(self): + """Test that stable diffusion upscale works with fp16""" + unet = self.dummy_cond_unet_upscale + low_res_scheduler = DDPMScheduler() + scheduler = DDIMScheduler(prediction_type="v_prediction") + vae = self.dummy_vae + text_encoder = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image.cpu().permute(0, 2, 3, 1)[0] + low_res_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64)) + + # put models in fp16, except vae as it overflows in fp16 + unet = unet.half() + text_encoder = text_encoder.half() + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionUpscalePipeline( + unet=unet, + low_res_scheduler=low_res_scheduler, + scheduler=scheduler, + vae=vae, + text_encoder=text_encoder, + tokenizer=tokenizer, + max_noise_level=350, + ) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + image = sd_pipe( + [prompt], + image=low_res_image, + generator=generator, + num_inference_steps=2, + output_type="np", + ).images + + expected_height_width = low_res_image.size[0] * 4 + assert image.shape == (1, expected_height_width, expected_height_width, 3) + + +@slow +@require_torch_gpu +class StableDiffusionUpscalePipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_stable_diffusion_upscale_pipeline(self): + image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-upscale/low_res_cat.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-upscale" + "/upsampled_cat.npy" + ) + + model_id = "stabilityai/stable-diffusion-x4-upscaler" + pipe = StableDiffusionUpscalePipeline.from_pretrained(model_id) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "a cat sitting on a park bench" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=image, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (512, 512, 3) + assert np.abs(expected_image - image).max() < 1e-3 + + def test_stable_diffusion_upscale_pipeline_fp16(self): + image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-upscale/low_res_cat.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/sd2-upscale" + "/upsampled_cat_fp16.npy" + ) + + model_id = "stabilityai/stable-diffusion-x4-upscaler" + pipe = StableDiffusionUpscalePipeline.from_pretrained( + model_id, + torch_dtype=torch.float16, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "a cat sitting on a park bench" + + generator = torch.manual_seed(0) + output = pipe( + prompt=prompt, + image=image, + generator=generator, + output_type="np", + ) + image = output.images[0] + + assert image.shape == (512, 512, 3) + assert np.abs(expected_image - image).max() < 5e-1 + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/sd2-upscale/low_res_cat.png" + ) + + model_id = "stabilityai/stable-diffusion-x4-upscaler" + pipe = StableDiffusionUpscalePipeline.from_pretrained( + model_id, + torch_dtype=torch.float16, + ) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing(1) + pipe.enable_sequential_cpu_offload() + + prompt = "a cat sitting on a park bench" + + generator = torch.manual_seed(0) + _ = pipe( + prompt=prompt, + image=image, + generator=generator, + num_inference_steps=5, + output_type="np", + ) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.65 GB is allocated + assert mem_bytes < 2.65 * 10**9 diff --git a/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_v_pred.py b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_v_pred.py new file mode 100644 index 0000000000000000000000000000000000000000..39cc546f677467913d34b6f97b95274f49c70ace --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_2/test_stable_diffusion_v_pred.py @@ -0,0 +1,481 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import time +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMScheduler, + DPMSolverMultistepScheduler, + EulerDiscreteScheduler, + StableDiffusionPipeline, + UNet2DConditionModel, +) +from diffusers.utils import load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class StableDiffusion2VPredictionPipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_cond_unet(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + # SD2-specific config below + attention_head_dim=(2, 4), + use_linear_projection=True, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + sample_size=128, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + # SD2-specific config below + hidden_act="gelu", + projection_dim=64, + ) + return CLIPTextModel(config) + + def test_stable_diffusion_v_pred_ddim(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + prediction_type="v_prediction", + ) + + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=6.0, num_inference_steps=2, output_type="np") + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.6424, 0.6109, 0.494, 0.5088, 0.4984, 0.4525, 0.5059, 0.5068, 0.4474]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_v_pred_k_euler(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = EulerDiscreteScheduler( + beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", prediction_type="v_prediction" + ) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=6.0, num_inference_steps=2, output_type="np") + + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.4616, 0.5184, 0.4887, 0.5111, 0.4839, 0.48, 0.5119, 0.5263, 0.4776]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_v_pred_fp16(self): + """Test that stable diffusion v-prediction works with fp16""" + unet = self.dummy_cond_unet + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + prediction_type="v_prediction", + ) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # put models in fp16 + unet = unet.half() + vae = vae.half() + bert = bert.half() + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + image = sd_pipe([prompt], generator=generator, num_inference_steps=2, output_type="np").images + + assert image.shape == (1, 64, 64, 3) + + +@slow +@require_torch_gpu +class StableDiffusion2VPredictionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_stable_diffusion_v_pred_default(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.enable_attention_slicing() + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=7.5, num_inference_steps=20, output_type="np") + + image = output.images + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 768, 768, 3) + expected_slice = np.array([0.1868, 0.1922, 0.1527, 0.1921, 0.1908, 0.1624, 0.1779, 0.1652, 0.1734]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_v_pred_upcast_attention(self): + sd_pipe = StableDiffusionPipeline.from_pretrained( + "stabilityai/stable-diffusion-2-1", torch_dtype=torch.float16 + ) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.enable_attention_slicing() + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=7.5, num_inference_steps=20, output_type="np") + + image = output.images + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 768, 768, 3) + expected_slice = np.array([0.4209, 0.4087, 0.4097, 0.4209, 0.3860, 0.4329, 0.4280, 0.4324, 0.4187]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-2 + + def test_stable_diffusion_v_pred_euler(self): + scheduler = EulerDiscreteScheduler.from_pretrained("stabilityai/stable-diffusion-2", subfolder="scheduler") + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2", scheduler=scheduler) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.enable_attention_slicing() + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.manual_seed(0) + + output = sd_pipe([prompt], generator=generator, num_inference_steps=5, output_type="numpy") + image = output.images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 768, 768, 3) + expected_slice = np.array([0.1781, 0.1695, 0.1661, 0.1705, 0.1588, 0.1699, 0.2005, 0.1589, 0.1677]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_v_pred_dpm(self): + """ + TODO: update this test after making DPM compatible with V-prediction! + """ + scheduler = DPMSolverMultistepScheduler.from_pretrained( + "stabilityai/stable-diffusion-2", subfolder="scheduler" + ) + sd_pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2", scheduler=scheduler) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.enable_attention_slicing() + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "a photograph of an astronaut riding a horse" + generator = torch.manual_seed(0) + image = sd_pipe( + [prompt], generator=generator, guidance_scale=7.5, num_inference_steps=5, output_type="numpy" + ).images + + image_slice = image[0, 253:256, 253:256, -1] + assert image.shape == (1, 768, 768, 3) + expected_slice = np.array([0.3303, 0.3184, 0.3291, 0.3300, 0.3256, 0.3113, 0.2965, 0.3134, 0.3192]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_attention_slicing_v_pred(self): + torch.cuda.reset_peak_memory_stats() + model_id = "stabilityai/stable-diffusion-2" + pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt = "a photograph of an astronaut riding a horse" + + # make attention efficient + pipe.enable_attention_slicing() + generator = torch.manual_seed(0) + output_chunked = pipe( + [prompt], generator=generator, guidance_scale=7.5, num_inference_steps=10, output_type="numpy" + ) + image_chunked = output_chunked.images + + mem_bytes = torch.cuda.max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + # make sure that less than 5.5 GB is allocated + assert mem_bytes < 5.5 * 10**9 + + # disable slicing + pipe.disable_attention_slicing() + generator = torch.manual_seed(0) + output = pipe([prompt], generator=generator, guidance_scale=7.5, num_inference_steps=10, output_type="numpy") + image = output.images + + # make sure that more than 5.5 GB is allocated + mem_bytes = torch.cuda.max_memory_allocated() + assert mem_bytes > 5.5 * 10**9 + assert np.abs(image_chunked.flatten() - image.flatten()).max() < 1e-3 + + def test_stable_diffusion_text2img_pipeline_v_pred_default(self): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/" + "sd2-text2img/astronaut_riding_a_horse_v_pred.npy" + ) + + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2") + pipe.to(torch_device) + pipe.enable_attention_slicing() + pipe.set_progress_bar_config(disable=None) + + prompt = "astronaut riding a horse" + + generator = torch.manual_seed(0) + output = pipe(prompt=prompt, guidance_scale=7.5, generator=generator, output_type="np") + image = output.images[0] + + assert image.shape == (768, 768, 3) + assert np.abs(expected_image - image).max() < 7.5e-2 + + def test_stable_diffusion_text2img_pipeline_v_pred_fp16(self): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/" + "sd2-text2img/astronaut_riding_a_horse_v_pred_fp16.npy" + ) + + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2", torch_dtype=torch.float16) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt = "astronaut riding a horse" + + generator = torch.manual_seed(0) + output = pipe(prompt=prompt, guidance_scale=7.5, generator=generator, output_type="np") + image = output.images[0] + + assert image.shape == (768, 768, 3) + assert np.abs(expected_image - image).max() < 7.5e-1 + + def test_stable_diffusion_text2img_intermediate_state_v_pred(self): + number_of_steps = 0 + + def test_callback_fn(step: int, timestep: int, latents: torch.FloatTensor) -> None: + test_callback_fn.has_been_called = True + nonlocal number_of_steps + number_of_steps += 1 + if step == 0: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 96, 96) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([0.7749, 0.0325, 0.5088, 0.1619, 0.3372, 0.3667, -0.5186, 0.6860, 1.4326]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + elif step == 19: + latents = latents.detach().cpu().numpy() + assert latents.shape == (1, 4, 96, 96) + latents_slice = latents[0, -3:, -3:, -1] + expected_slice = np.array([1.3887, 1.0273, 1.7266, 0.0726, 0.6611, 0.1598, -1.0547, 0.1522, 0.0227]) + + assert np.abs(latents_slice.flatten() - expected_slice).max() < 5e-2 + + test_callback_fn.has_been_called = False + + pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + + prompt = "Andromeda galaxy in a bottle" + + generator = torch.manual_seed(0) + pipe( + prompt=prompt, + num_inference_steps=20, + guidance_scale=7.5, + generator=generator, + callback=test_callback_fn, + callback_steps=1, + ) + assert test_callback_fn.has_been_called + assert number_of_steps == 20 + + def test_stable_diffusion_low_cpu_mem_usage_v_pred(self): + pipeline_id = "stabilityai/stable-diffusion-2" + + start_time = time.time() + pipeline_low_cpu_mem_usage = StableDiffusionPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16) + pipeline_low_cpu_mem_usage.to(torch_device) + low_cpu_mem_usage_time = time.time() - start_time + + start_time = time.time() + _ = StableDiffusionPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16, low_cpu_mem_usage=False) + normal_load_time = time.time() - start_time + + assert 2 * low_cpu_mem_usage_time < normal_load_time + + def test_stable_diffusion_pipeline_with_sequential_cpu_offloading_v_pred(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipeline_id = "stabilityai/stable-diffusion-2" + prompt = "Andromeda galaxy in a bottle" + + pipeline = StableDiffusionPipeline.from_pretrained(pipeline_id, torch_dtype=torch.float16) + pipeline = pipeline.to(torch_device) + pipeline.enable_attention_slicing(1) + pipeline.enable_sequential_cpu_offload() + + generator = torch.manual_seed(0) + _ = pipeline(prompt, generator=generator, num_inference_steps=5) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 2.8 GB is allocated + assert mem_bytes < 2.8 * 10**9 diff --git a/diffusers/tests/pipelines/stable_diffusion_safe/__init__.py b/diffusers/tests/pipelines/stable_diffusion_safe/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/stable_diffusion_safe/test_safe_diffusion.py b/diffusers/tests/pipelines/stable_diffusion_safe/test_safe_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..31f6e1972f7f4c958766ddee6118c5ecac8f02d4 --- /dev/null +++ b/diffusers/tests/pipelines/stable_diffusion_safe/test_safe_diffusion.py @@ -0,0 +1,439 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import tempfile +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import AutoencoderKL, DDIMScheduler, LMSDiscreteScheduler, PNDMScheduler, UNet2DConditionModel +from diffusers.pipelines.stable_diffusion_safe import StableDiffusionPipelineSafe as StableDiffusionPipeline +from diffusers.utils import floats_tensor, nightly, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class SafeDiffusionPipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + @property + def dummy_cond_unet(self): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModel(config) + + @property + def dummy_extractor(self): + def extract(*args, **kwargs): + class Out: + def __init__(self): + self.pixel_values = torch.ones([0]) + + def to(self, device): + self.pixel_values.to(device) + return self + + return Out() + + return extract + + def test_safe_diffusion_ddim(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = DDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + clip_sample=False, + set_alpha_to_one=False, + ) + + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=6.0, num_inference_steps=2, output_type="np") + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5644, 0.6018, 0.4799, 0.5267, 0.5585, 0.4641, 0.516, 0.4964, 0.4792]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_pndm(self): + device = "cpu" # ensure determinism for the device-dependent torch.Generator + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + generator = torch.Generator(device=device).manual_seed(0) + output = sd_pipe([prompt], generator=generator, guidance_scale=6.0, num_inference_steps=2, output_type="np") + + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = sd_pipe( + [prompt], + generator=generator, + guidance_scale=6.0, + num_inference_steps=2, + output_type="np", + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + expected_slice = np.array([0.5095, 0.5674, 0.4668, 0.5126, 0.5697, 0.4675, 0.5278, 0.4964, 0.4945]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_stable_diffusion_no_safety_checker(self): + pipe = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-lms-pipe", safety_checker=None + ) + assert isinstance(pipe, StableDiffusionPipeline) + assert isinstance(pipe.scheduler, LMSDiscreteScheduler) + assert pipe.safety_checker is None + + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None + + # check that there's no error when saving a pipeline with one of the models being None + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = StableDiffusionPipeline.from_pretrained(tmpdirname) + + # sanity check that the pipeline still works + assert pipe.safety_checker is None + image = pipe("example prompt", num_inference_steps=2).images[0] + assert image is not None + + @unittest.skipIf(torch_device != "cuda", "This test requires a GPU") + def test_stable_diffusion_fp16(self): + """Test that stable diffusion works with fp16""" + unet = self.dummy_cond_unet + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + # put models in fp16 + unet = unet.half() + vae = vae.half() + bert = bert.half() + + # make sure here that pndm scheduler skips prk + sd_pipe = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger" + image = sd_pipe([prompt], num_inference_steps=2, output_type="np").images + + assert image.shape == (1, 64, 64, 3) + + +@nightly +@require_torch_gpu +class SafeDiffusionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_harm_safe_stable_diffusion(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", safety_checker=None) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = ( + "portrait of girl with smokey eyes makeup in abandoned hotel, grange clothes, redshift, wide high angle" + " coloured polaroid photograph with flash, kodak film, hyper real, stunning moody cinematography, with" + " anamorphic lenses, by maripol, fallen angels by wong kar - wai, style of suspiria and neon demon and" + " children from bahnhof zoo, detailed " + ) + seed = 4003660346 + guidance_scale = 7 + + # without safety guidance (sld_guidance_scale = 0) + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=0, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = [0.2278, 0.2231, 0.2249, 0.2333, 0.2303, 0.1885, 0.2273, 0.2144, 0.2176] + + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + # without safety guidance (strong configuration) + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=2000, + sld_warmup_steps=7, + sld_threshold=0.025, + sld_momentum_scale=0.5, + sld_mom_beta=0.7, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = [0.2383, 0.2276, 0.236, 0.2192, 0.2186, 0.2053, 0.1971, 0.1901, 0.1719] + + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_nudity_safe_stable_diffusion(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", safety_checker=None) + sd_pipe.scheduler = LMSDiscreteScheduler.from_config(sd_pipe.scheduler.config) + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = "padme amidala taking a bath artwork, safe for work, no nudity" + seed = 2734971755 + guidance_scale = 7 + + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=0, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = [0.3502, 0.3622, 0.3396, 0.3642, 0.3478, 0.3318, 0.35, 0.3348, 0.3297] + + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=2000, + sld_warmup_steps=7, + sld_threshold=0.025, + sld_momentum_scale=0.5, + sld_mom_beta=0.7, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = [0.5531, 0.5206, 0.4895, 0.5156, 0.5182, 0.4751, 0.4802, 0.4803, 0.4443] + + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + + def test_nudity_safetychecker_safe_stable_diffusion(self): + sd_pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5") + sd_pipe = sd_pipe.to(torch_device) + sd_pipe.set_progress_bar_config(disable=None) + + prompt = ( + "the four horsewomen of the apocalypse, painting by tom of finland, gaston bussiere, craig mullins, j. c." + " leyendecker" + ) + seed = 1044355234 + guidance_scale = 12 + + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=0, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-7 + + generator = torch.manual_seed(seed) + output = sd_pipe( + [prompt], + generator=generator, + guidance_scale=guidance_scale, + num_inference_steps=50, + output_type="np", + width=512, + height=512, + sld_guidance_scale=2000, + sld_warmup_steps=7, + sld_threshold=0.025, + sld_momentum_scale=0.5, + sld_mom_beta=0.7, + ) + + image = output.images + image_slice = image[0, -3:, -3:, -1] + expected_slice = np.array([0.5818, 0.6285, 0.6835, 0.6019, 0.625, 0.6754, 0.6096, 0.6334, 0.6561]) + assert image.shape == (1, 512, 512, 3) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/unclip/__init__.py b/diffusers/tests/pipelines/unclip/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/unclip/test_unclip.py b/diffusers/tests/pipelines/unclip/test_unclip.py new file mode 100644 index 0000000000000000000000000000000000000000..daa083aa1e1af4dea0f17717aa75bbc97d5062ff --- /dev/null +++ b/diffusers/tests/pipelines/unclip/test_unclip.py @@ -0,0 +1,475 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModelWithProjection, CLIPTokenizer + +from diffusers import PriorTransformer, UnCLIPPipeline, UnCLIPScheduler, UNet2DConditionModel, UNet2DModel +from diffusers.pipelines.unclip.text_proj import UnCLIPTextProjModel +from diffusers.utils import load_numpy, nightly, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin, assert_mean_pixel_difference + + +class UnCLIPPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = UnCLIPPipeline + test_xformers_attention = False + + required_optional_params = [ + "generator", + "return_dict", + "prior_num_inference_steps", + "decoder_num_inference_steps", + "super_res_num_inference_steps", + ] + num_inference_steps_args = [ + "prior_num_inference_steps", + "decoder_num_inference_steps", + "super_res_num_inference_steps", + ] + + @property + def text_embedder_hidden_size(self): + return 32 + + @property + def time_input_dim(self): + return 32 + + @property + def block_out_channels_0(self): + return self.time_input_dim + + @property + def time_embed_dim(self): + return self.time_input_dim * 4 + + @property + def cross_attention_dim(self): + return 100 + + @property + def dummy_tokenizer(self): + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + return tokenizer + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=self.text_embedder_hidden_size, + projection_dim=self.text_embedder_hidden_size, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModelWithProjection(config) + + @property + def dummy_prior(self): + torch.manual_seed(0) + + model_kwargs = { + "num_attention_heads": 2, + "attention_head_dim": 12, + "embedding_dim": self.text_embedder_hidden_size, + "num_layers": 1, + } + + model = PriorTransformer(**model_kwargs) + return model + + @property + def dummy_text_proj(self): + torch.manual_seed(0) + + model_kwargs = { + "clip_embeddings_dim": self.text_embedder_hidden_size, + "time_embed_dim": self.time_embed_dim, + "cross_attention_dim": self.cross_attention_dim, + } + + model = UnCLIPTextProjModel(**model_kwargs) + return model + + @property + def dummy_decoder(self): + torch.manual_seed(0) + + model_kwargs = { + "sample_size": 32, + # RGB in channels + "in_channels": 3, + # Out channels is double in channels because predicts mean and variance + "out_channels": 6, + "down_block_types": ("ResnetDownsampleBlock2D", "SimpleCrossAttnDownBlock2D"), + "up_block_types": ("SimpleCrossAttnUpBlock2D", "ResnetUpsampleBlock2D"), + "mid_block_type": "UNetMidBlock2DSimpleCrossAttn", + "block_out_channels": (self.block_out_channels_0, self.block_out_channels_0 * 2), + "layers_per_block": 1, + "cross_attention_dim": self.cross_attention_dim, + "attention_head_dim": 4, + "resnet_time_scale_shift": "scale_shift", + "class_embed_type": "identity", + } + + model = UNet2DConditionModel(**model_kwargs) + return model + + @property + def dummy_super_res_kwargs(self): + return { + "sample_size": 64, + "layers_per_block": 1, + "down_block_types": ("ResnetDownsampleBlock2D", "ResnetDownsampleBlock2D"), + "up_block_types": ("ResnetUpsampleBlock2D", "ResnetUpsampleBlock2D"), + "block_out_channels": (self.block_out_channels_0, self.block_out_channels_0 * 2), + "in_channels": 6, + "out_channels": 3, + } + + @property + def dummy_super_res_first(self): + torch.manual_seed(0) + + model = UNet2DModel(**self.dummy_super_res_kwargs) + return model + + @property + def dummy_super_res_last(self): + # seeded differently to get different unet than `self.dummy_super_res_first` + torch.manual_seed(1) + + model = UNet2DModel(**self.dummy_super_res_kwargs) + return model + + def get_dummy_components(self): + prior = self.dummy_prior + decoder = self.dummy_decoder + text_proj = self.dummy_text_proj + text_encoder = self.dummy_text_encoder + tokenizer = self.dummy_tokenizer + super_res_first = self.dummy_super_res_first + super_res_last = self.dummy_super_res_last + + prior_scheduler = UnCLIPScheduler( + variance_type="fixed_small_log", + prediction_type="sample", + num_train_timesteps=1000, + clip_sample_range=5.0, + ) + + decoder_scheduler = UnCLIPScheduler( + variance_type="learned_range", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + super_res_scheduler = UnCLIPScheduler( + variance_type="fixed_small_log", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + components = { + "prior": prior, + "decoder": decoder, + "text_proj": text_proj, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "super_res_first": super_res_first, + "super_res_last": super_res_last, + "prior_scheduler": prior_scheduler, + "decoder_scheduler": decoder_scheduler, + "super_res_scheduler": super_res_scheduler, + } + + return components + + def get_dummy_inputs(self, device, seed=0): + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + inputs = { + "prompt": "horse", + "generator": generator, + "prior_num_inference_steps": 2, + "decoder_num_inference_steps": 2, + "super_res_num_inference_steps": 2, + "output_type": "numpy", + } + return inputs + + def test_unclip(self): + device = "cpu" + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + output = pipe(**self.get_dummy_inputs(device)) + image = output.images + + image_from_tuple = pipe( + **self.get_dummy_inputs(device), + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + + expected_slice = np.array( + [ + 0.9997, + 0.9988, + 0.0028, + 0.9997, + 0.9984, + 0.9965, + 0.0029, + 0.9986, + 0.0025, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_unclip_passed_text_embed(self): + device = torch.device("cpu") + + class DummyScheduler: + init_noise_sigma = 1 + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + prior = components["prior"] + decoder = components["decoder"] + super_res_first = components["super_res_first"] + tokenizer = components["tokenizer"] + text_encoder = components["text_encoder"] + + generator = torch.Generator(device=device).manual_seed(0) + dtype = prior.dtype + batch_size = 1 + + shape = (batch_size, prior.config.embedding_dim) + prior_latents = pipe.prepare_latents( + shape, dtype=dtype, device=device, generator=generator, latents=None, scheduler=DummyScheduler() + ) + shape = (batch_size, decoder.in_channels, decoder.sample_size, decoder.sample_size) + decoder_latents = pipe.prepare_latents( + shape, dtype=dtype, device=device, generator=generator, latents=None, scheduler=DummyScheduler() + ) + + shape = ( + batch_size, + super_res_first.in_channels // 2, + super_res_first.sample_size, + super_res_first.sample_size, + ) + super_res_latents = pipe.prepare_latents( + shape, dtype=dtype, device=device, generator=generator, latents=None, scheduler=DummyScheduler() + ) + + pipe.set_progress_bar_config(disable=None) + + prompt = "this is a prompt example" + + generator = torch.Generator(device=device).manual_seed(0) + output = pipe( + [prompt], + generator=generator, + prior_num_inference_steps=2, + decoder_num_inference_steps=2, + super_res_num_inference_steps=2, + prior_latents=prior_latents, + decoder_latents=decoder_latents, + super_res_latents=super_res_latents, + output_type="np", + ) + image = output.images + + text_inputs = tokenizer( + prompt, + padding="max_length", + max_length=tokenizer.model_max_length, + return_tensors="pt", + ) + text_model_output = text_encoder(text_inputs.input_ids) + text_attention_mask = text_inputs.attention_mask + + generator = torch.Generator(device=device).manual_seed(0) + image_from_text = pipe( + generator=generator, + prior_num_inference_steps=2, + decoder_num_inference_steps=2, + super_res_num_inference_steps=2, + prior_latents=prior_latents, + decoder_latents=decoder_latents, + super_res_latents=super_res_latents, + text_model_output=text_model_output, + text_attention_mask=text_attention_mask, + output_type="np", + )[0] + + # make sure passing text embeddings manually is identical + assert np.abs(image - image_from_text).max() < 1e-4 + + # Overriding PipelineTesterMixin::test_attention_slicing_forward_pass + # because UnCLIP GPU undeterminism requires a looser check. + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_attention_slicing_forward_pass(self): + test_max_difference = torch_device == "cpu" + + self._test_attention_slicing_forward_pass(test_max_difference=test_max_difference) + + # Overriding PipelineTesterMixin::test_inference_batch_single_identical + # because UnCLIP undeterminism requires a looser check. + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_inference_batch_single_identical(self): + test_max_difference = torch_device == "cpu" + relax_max_difference = True + + self._test_inference_batch_single_identical( + test_max_difference=test_max_difference, relax_max_difference=relax_max_difference + ) + + def test_inference_batch_consistent(self): + if torch_device == "mps": + # TODO: MPS errors with larger batch sizes + batch_sizes = [2, 3] + self._test_inference_batch_consistent(batch_sizes=batch_sizes) + else: + self._test_inference_batch_consistent() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_dict_tuple_outputs_equivalent(self): + return super().test_dict_tuple_outputs_equivalent() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_save_load_local(self): + return super().test_save_load_local() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_save_load_optional_components(self): + return super().test_save_load_optional_components() + + +@nightly +class UnCLIPPipelineCPUIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_unclip_karlo_cpu_fp32(self): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/unclip/karlo_v1_alpha_horse_cpu.npy" + ) + + pipeline = UnCLIPPipeline.from_pretrained("kakaobrain/karlo-v1-alpha") + pipeline.set_progress_bar_config(disable=None) + + generator = torch.manual_seed(0) + output = pipeline( + "horse", + num_images_per_prompt=1, + generator=generator, + output_type="np", + ) + + image = output.images[0] + + assert image.shape == (256, 256, 3) + assert np.abs(expected_image - image).max() < 1e-1 + + +@slow +@require_torch_gpu +class UnCLIPPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_unclip_karlo(self): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/unclip/karlo_v1_alpha_horse_fp16.npy" + ) + + pipeline = UnCLIPPipeline.from_pretrained("kakaobrain/karlo-v1-alpha", torch_dtype=torch.float16) + pipeline = pipeline.to(torch_device) + pipeline.set_progress_bar_config(disable=None) + + generator = torch.Generator(device="cpu").manual_seed(0) + output = pipeline( + "horse", + generator=generator, + output_type="np", + ) + + image = output.images[0] + + assert image.shape == (256, 256, 3) + + assert_mean_pixel_difference(image, expected_image) + + def test_unclip_pipeline_with_sequential_cpu_offloading(self): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() + torch.cuda.reset_peak_memory_stats() + + pipe = UnCLIPPipeline.from_pretrained("kakaobrain/karlo-v1-alpha", torch_dtype=torch.float16) + pipe = pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + pipe.enable_attention_slicing() + pipe.enable_sequential_cpu_offload() + + _ = pipe( + "horse", + num_images_per_prompt=1, + prior_num_inference_steps=2, + decoder_num_inference_steps=2, + super_res_num_inference_steps=2, + output_type="np", + ) + + mem_bytes = torch.cuda.max_memory_allocated() + # make sure that less than 7 GB is allocated + assert mem_bytes < 7 * 10**9 diff --git a/diffusers/tests/pipelines/unclip/test_unclip_image_variation.py b/diffusers/tests/pipelines/unclip/test_unclip_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..2fd7d091bf1ec2a110d1f79b100bffc66777a66d --- /dev/null +++ b/diffusers/tests/pipelines/unclip/test_unclip_image_variation.py @@ -0,0 +1,546 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import random +import unittest + +import numpy as np +import torch +from transformers import ( + CLIPImageProcessor, + CLIPTextConfig, + CLIPTextModelWithProjection, + CLIPTokenizer, + CLIPVisionConfig, + CLIPVisionModelWithProjection, +) + +from diffusers import ( + DiffusionPipeline, + UnCLIPImageVariationPipeline, + UnCLIPScheduler, + UNet2DConditionModel, + UNet2DModel, +) +from diffusers.pipelines.unclip.text_proj import UnCLIPTextProjModel +from diffusers.utils import floats_tensor, load_numpy, slow, torch_device +from diffusers.utils.testing_utils import load_image, require_torch_gpu + +from ...test_pipelines_common import PipelineTesterMixin, assert_mean_pixel_difference + + +class UnCLIPImageVariationPipelineFastTests(PipelineTesterMixin, unittest.TestCase): + pipeline_class = UnCLIPImageVariationPipeline + + required_optional_params = [ + "generator", + "return_dict", + "decoder_num_inference_steps", + "super_res_num_inference_steps", + ] + num_inference_steps_args = [ + "decoder_num_inference_steps", + "super_res_num_inference_steps", + ] + + @property + def text_embedder_hidden_size(self): + return 32 + + @property + def time_input_dim(self): + return 32 + + @property + def block_out_channels_0(self): + return self.time_input_dim + + @property + def time_embed_dim(self): + return self.time_input_dim * 4 + + @property + def cross_attention_dim(self): + return 100 + + @property + def dummy_tokenizer(self): + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + return tokenizer + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=self.text_embedder_hidden_size, + projection_dim=self.text_embedder_hidden_size, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModelWithProjection(config) + + @property + def dummy_image_encoder(self): + torch.manual_seed(0) + config = CLIPVisionConfig( + hidden_size=self.text_embedder_hidden_size, + projection_dim=self.text_embedder_hidden_size, + num_hidden_layers=5, + num_attention_heads=4, + image_size=32, + intermediate_size=37, + patch_size=1, + ) + return CLIPVisionModelWithProjection(config) + + @property + def dummy_text_proj(self): + torch.manual_seed(0) + + model_kwargs = { + "clip_embeddings_dim": self.text_embedder_hidden_size, + "time_embed_dim": self.time_embed_dim, + "cross_attention_dim": self.cross_attention_dim, + } + + model = UnCLIPTextProjModel(**model_kwargs) + return model + + @property + def dummy_decoder(self): + torch.manual_seed(0) + + model_kwargs = { + "sample_size": 32, + # RGB in channels + "in_channels": 3, + # Out channels is double in channels because predicts mean and variance + "out_channels": 6, + "down_block_types": ("ResnetDownsampleBlock2D", "SimpleCrossAttnDownBlock2D"), + "up_block_types": ("SimpleCrossAttnUpBlock2D", "ResnetUpsampleBlock2D"), + "mid_block_type": "UNetMidBlock2DSimpleCrossAttn", + "block_out_channels": (self.block_out_channels_0, self.block_out_channels_0 * 2), + "layers_per_block": 1, + "cross_attention_dim": self.cross_attention_dim, + "attention_head_dim": 4, + "resnet_time_scale_shift": "scale_shift", + "class_embed_type": "identity", + } + + model = UNet2DConditionModel(**model_kwargs) + return model + + @property + def dummy_super_res_kwargs(self): + return { + "sample_size": 64, + "layers_per_block": 1, + "down_block_types": ("ResnetDownsampleBlock2D", "ResnetDownsampleBlock2D"), + "up_block_types": ("ResnetUpsampleBlock2D", "ResnetUpsampleBlock2D"), + "block_out_channels": (self.block_out_channels_0, self.block_out_channels_0 * 2), + "in_channels": 6, + "out_channels": 3, + } + + @property + def dummy_super_res_first(self): + torch.manual_seed(0) + + model = UNet2DModel(**self.dummy_super_res_kwargs) + return model + + @property + def dummy_super_res_last(self): + # seeded differently to get different unet than `self.dummy_super_res_first` + torch.manual_seed(1) + + model = UNet2DModel(**self.dummy_super_res_kwargs) + return model + + def get_dummy_components(self): + decoder = self.dummy_decoder + text_proj = self.dummy_text_proj + text_encoder = self.dummy_text_encoder + tokenizer = self.dummy_tokenizer + super_res_first = self.dummy_super_res_first + super_res_last = self.dummy_super_res_last + + decoder_scheduler = UnCLIPScheduler( + variance_type="learned_range", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + super_res_scheduler = UnCLIPScheduler( + variance_type="fixed_small_log", + prediction_type="epsilon", + num_train_timesteps=1000, + ) + + feature_extractor = CLIPImageProcessor(crop_size=32, size=32) + + image_encoder = self.dummy_image_encoder + + return { + "decoder": decoder, + "text_encoder": text_encoder, + "tokenizer": tokenizer, + "text_proj": text_proj, + "feature_extractor": feature_extractor, + "image_encoder": image_encoder, + "super_res_first": super_res_first, + "super_res_last": super_res_last, + "decoder_scheduler": decoder_scheduler, + "super_res_scheduler": super_res_scheduler, + } + + def get_dummy_inputs(self, device, seed=0, pil_image=True): + input_image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) + if str(device).startswith("mps"): + generator = torch.manual_seed(seed) + else: + generator = torch.Generator(device=device).manual_seed(seed) + + if pil_image: + input_image = input_image * 0.5 + 0.5 + input_image = input_image.clamp(0, 1) + input_image = input_image.cpu().permute(0, 2, 3, 1).float().numpy() + input_image = DiffusionPipeline.numpy_to_pil(input_image)[0] + + return { + "image": input_image, + "generator": generator, + "decoder_num_inference_steps": 2, + "super_res_num_inference_steps": 2, + "output_type": "np", + } + + def test_unclip_image_variation_input_tensor(self): + device = "cpu" + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=False) + + output = pipe(**pipeline_inputs) + image = output.images + + tuple_pipeline_inputs = self.get_dummy_inputs(device, pil_image=False) + + image_from_tuple = pipe( + **tuple_pipeline_inputs, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + + expected_slice = np.array( + [ + 0.9997, + 0.0002, + 0.9997, + 0.9997, + 0.9969, + 0.0023, + 0.9997, + 0.9969, + 0.9970, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_unclip_image_variation_input_image(self): + device = "cpu" + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + + output = pipe(**pipeline_inputs) + image = output.images + + tuple_pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + + image_from_tuple = pipe( + **tuple_pipeline_inputs, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 64, 64, 3) + + expected_slice = np.array([0.9997, 0.0003, 0.9997, 0.9997, 0.9970, 0.0024, 0.9997, 0.9971, 0.9971]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_unclip_image_variation_input_list_images(self): + device = "cpu" + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + pipeline_inputs["image"] = [ + pipeline_inputs["image"], + pipeline_inputs["image"], + ] + + output = pipe(**pipeline_inputs) + image = output.images + + tuple_pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + tuple_pipeline_inputs["image"] = [ + tuple_pipeline_inputs["image"], + tuple_pipeline_inputs["image"], + ] + + image_from_tuple = pipe( + **tuple_pipeline_inputs, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (2, 64, 64, 3) + + expected_slice = np.array( + [ + 0.9997, + 0.9989, + 0.0008, + 0.0021, + 0.9960, + 0.0018, + 0.0014, + 0.0002, + 0.9933, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_unclip_image_variation_input_num_images_per_prompt(self): + device = "cpu" + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + pipeline_inputs["image"] = [ + pipeline_inputs["image"], + pipeline_inputs["image"], + ] + + output = pipe(**pipeline_inputs, num_images_per_prompt=2) + image = output.images + + tuple_pipeline_inputs = self.get_dummy_inputs(device, pil_image=True) + tuple_pipeline_inputs["image"] = [ + tuple_pipeline_inputs["image"], + tuple_pipeline_inputs["image"], + ] + + image_from_tuple = pipe( + **tuple_pipeline_inputs, + num_images_per_prompt=2, + return_dict=False, + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (4, 64, 64, 3) + + expected_slice = np.array( + [ + 0.9980, + 0.9997, + 0.0023, + 0.0029, + 0.9997, + 0.9985, + 0.9997, + 0.0010, + 0.9995, + ] + ) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_unclip_passed_image_embed(self): + device = torch.device("cpu") + + class DummyScheduler: + init_noise_sigma = 1 + + components = self.get_dummy_components() + + pipe = self.pipeline_class(**components) + pipe = pipe.to(device) + + pipe.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=device).manual_seed(0) + dtype = pipe.decoder.dtype + batch_size = 1 + + shape = (batch_size, pipe.decoder.in_channels, pipe.decoder.sample_size, pipe.decoder.sample_size) + decoder_latents = pipe.prepare_latents( + shape, dtype=dtype, device=device, generator=generator, latents=None, scheduler=DummyScheduler() + ) + + shape = ( + batch_size, + pipe.super_res_first.in_channels // 2, + pipe.super_res_first.sample_size, + pipe.super_res_first.sample_size, + ) + super_res_latents = pipe.prepare_latents( + shape, dtype=dtype, device=device, generator=generator, latents=None, scheduler=DummyScheduler() + ) + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=False) + + img_out_1 = pipe( + **pipeline_inputs, decoder_latents=decoder_latents, super_res_latents=super_res_latents + ).images + + pipeline_inputs = self.get_dummy_inputs(device, pil_image=False) + # Don't pass image, instead pass embedding + image = pipeline_inputs.pop("image") + image_embeddings = pipe.image_encoder(image).image_embeds + + img_out_2 = pipe( + **pipeline_inputs, + decoder_latents=decoder_latents, + super_res_latents=super_res_latents, + image_embeddings=image_embeddings, + ).images + + # make sure passing text embeddings manually is identical + assert np.abs(img_out_1 - img_out_2).max() < 1e-4 + + # Overriding PipelineTesterMixin::test_attention_slicing_forward_pass + # because UnCLIP GPU undeterminism requires a looser check. + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_attention_slicing_forward_pass(self): + test_max_difference = torch_device == "cpu" + + self._test_attention_slicing_forward_pass(test_max_difference=test_max_difference) + + # Overriding PipelineTesterMixin::test_inference_batch_single_identical + # because UnCLIP undeterminism requires a looser check. + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_inference_batch_single_identical(self): + test_max_difference = torch_device == "cpu" + relax_max_difference = True + + self._test_inference_batch_single_identical( + test_max_difference=test_max_difference, relax_max_difference=relax_max_difference + ) + + def test_inference_batch_consistent(self): + if torch_device == "mps": + # TODO: MPS errors with larger batch sizes + batch_sizes = [2, 3] + self._test_inference_batch_consistent(batch_sizes=batch_sizes) + else: + self._test_inference_batch_consistent() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_dict_tuple_outputs_equivalent(self): + return super().test_dict_tuple_outputs_equivalent() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_save_load_local(self): + return super().test_save_load_local() + + @unittest.skipIf(torch_device == "mps", reason="MPS inconsistent") + def test_save_load_optional_components(self): + return super().test_save_load_optional_components() + + +@slow +@require_torch_gpu +class UnCLIPImageVariationPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_unclip_image_variation_karlo(self): + input_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/unclip/cat.png" + ) + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/unclip/karlo_v1_alpha_cat_variation_fp16.npy" + ) + + pipeline = UnCLIPImageVariationPipeline.from_pretrained( + "kakaobrain/karlo-v1-alpha-image-variations", torch_dtype=torch.float16 + ) + pipeline = pipeline.to(torch_device) + pipeline.set_progress_bar_config(disable=None) + + generator = torch.Generator(device="cpu").manual_seed(0) + output = pipeline( + input_image, + generator=generator, + output_type="np", + ) + + image = output.images[0] + + assert image.shape == (256, 256, 3) + + assert_mean_pixel_difference(image, expected_image) diff --git a/diffusers/tests/pipelines/versatile_diffusion/__init__.py b/diffusers/tests/pipelines/versatile_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_dual_guided.py b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_dual_guided.py new file mode 100644 index 0000000000000000000000000000000000000000..5edebdde48ba5f6a8d9af58893a060871884177f --- /dev/null +++ b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_dual_guided.py @@ -0,0 +1,111 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import tempfile +import unittest + +import numpy as np +import torch + +from diffusers import VersatileDiffusionDualGuidedPipeline +from diffusers.utils.testing_utils import load_image, require_torch_gpu, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VersatileDiffusionDualGuidedPipelineFastTests(unittest.TestCase): + pass + + +@slow +@require_torch_gpu +class VersatileDiffusionDualGuidedPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_remove_unused_weights_save_load(self): + pipe = VersatileDiffusionDualGuidedPipeline.from_pretrained("shi-labs/versatile-diffusion") + # remove text_unet + pipe.remove_unused_weights() + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + second_prompt = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/versatile_diffusion/benz.jpg" + ) + + generator = torch.manual_seed(0) + image = pipe( + prompt="first prompt", + image=second_prompt, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=2, + output_type="numpy", + ).images + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = VersatileDiffusionDualGuidedPipeline.from_pretrained(tmpdirname) + + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = generator.manual_seed(0) + new_image = pipe( + prompt="first prompt", + image=second_prompt, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=2, + output_type="numpy", + ).images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't have the same forward pass" + + def test_inference_dual_guided(self): + pipe = VersatileDiffusionDualGuidedPipeline.from_pretrained("shi-labs/versatile-diffusion") + pipe.remove_unused_weights() + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + first_prompt = "cyberpunk 2077" + second_prompt = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/versatile_diffusion/benz.jpg" + ) + generator = torch.manual_seed(0) + image = pipe( + prompt=first_prompt, + image=second_prompt, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=50, + output_type="numpy", + ).images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0787, 0.0849, 0.0826, 0.0812, 0.0807, 0.0795, 0.0818, 0.0798, 0.0779]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_image_variation.py b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_image_variation.py new file mode 100644 index 0000000000000000000000000000000000000000..19a02b77c9e03d202b1578b0ec143e6f78b9cd1e --- /dev/null +++ b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_image_variation.py @@ -0,0 +1,57 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import numpy as np +import torch + +from diffusers import VersatileDiffusionImageVariationPipeline +from diffusers.utils.testing_utils import load_image, require_torch_gpu, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VersatileDiffusionImageVariationPipelineFastTests(unittest.TestCase): + pass + + +@slow +@require_torch_gpu +class VersatileDiffusionImageVariationPipelineIntegrationTests(unittest.TestCase): + def test_inference_image_variations(self): + pipe = VersatileDiffusionImageVariationPipeline.from_pretrained("shi-labs/versatile-diffusion") + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + image_prompt = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main/versatile_diffusion/benz.jpg" + ) + generator = torch.manual_seed(0) + image = pipe( + image=image_prompt, + generator=generator, + guidance_scale=7.5, + num_inference_steps=50, + output_type="numpy", + ).images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.0441, 0.0469, 0.0507, 0.0575, 0.0632, 0.0650, 0.0865, 0.0909, 0.0945]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_mega.py b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_mega.py new file mode 100644 index 0000000000000000000000000000000000000000..8bca6ce5de0d33e273b76c10883eab5015a5ad3c --- /dev/null +++ b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_mega.py @@ -0,0 +1,129 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import tempfile +import unittest + +import numpy as np +import torch + +from diffusers import VersatileDiffusionPipeline +from diffusers.utils.testing_utils import load_image, require_torch_gpu, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VersatileDiffusionMegaPipelineFastTests(unittest.TestCase): + pass + + +@slow +@require_torch_gpu +class VersatileDiffusionMegaPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_from_save_pretrained(self): + pipe = VersatileDiffusionPipeline.from_pretrained("shi-labs/versatile-diffusion", torch_dtype=torch.float16) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/blob/main/versatile_diffusion/benz.jpg" + ) + + generator = torch.manual_seed(0) + image = pipe.dual_guided( + prompt="first prompt", + image=prompt_image, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=2, + output_type="numpy", + ).images + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = VersatileDiffusionPipeline.from_pretrained(tmpdirname, torch_dtype=torch.float16) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = generator.manual_seed(0) + new_image = pipe.dual_guided( + prompt="first prompt", + image=prompt_image, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=2, + output_type="numpy", + ).images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't have the same forward pass" + + def test_inference_dual_guided_then_text_to_image(self): + pipe = VersatileDiffusionPipeline.from_pretrained("shi-labs/versatile-diffusion", torch_dtype=torch.float16) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt = "cyberpunk 2077" + init_image = load_image( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/blob/main/versatile_diffusion/benz.jpg" + ) + generator = torch.manual_seed(0) + image = pipe.dual_guided( + prompt=prompt, + image=init_image, + text_to_image_strength=0.75, + generator=generator, + guidance_scale=7.5, + num_inference_steps=50, + output_type="numpy", + ).images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.1448, 0.1619, 0.1741, 0.1086, 0.1147, 0.1128, 0.1199, 0.1165, 0.1001]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + prompt = "A painting of a squirrel eating a burger " + generator = torch.manual_seed(0) + image = pipe.text_to_image( + prompt=prompt, generator=generator, guidance_scale=7.5, num_inference_steps=50, output_type="numpy" + ).images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.3367, 0.3169, 0.2656, 0.3870, 0.4790, 0.3796, 0.4009, 0.4878, 0.4778]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 + + image = pipe.image_variation(init_image, generator=generator, output_type="numpy").images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.3076, 0.3123, 0.3284, 0.3782, 0.3770, 0.3894, 0.4297, 0.4331, 0.4456]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-1 diff --git a/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_text_to_image.py b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_text_to_image.py new file mode 100644 index 0000000000000000000000000000000000000000..9c4c2f2d993b988ff1cf4cd7bdb38a3fb083ab20 --- /dev/null +++ b/diffusers/tests/pipelines/versatile_diffusion/test_versatile_diffusion_text_to_image.py @@ -0,0 +1,85 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import tempfile +import unittest + +import numpy as np +import torch + +from diffusers import VersatileDiffusionTextToImagePipeline +from diffusers.utils.testing_utils import require_torch_gpu, slow, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VersatileDiffusionTextToImagePipelineFastTests(unittest.TestCase): + pass + + +@slow +@require_torch_gpu +class VersatileDiffusionTextToImagePipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_remove_unused_weights_save_load(self): + pipe = VersatileDiffusionTextToImagePipeline.from_pretrained("shi-labs/versatile-diffusion") + # remove text_unet + pipe.remove_unused_weights() + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger " + generator = torch.manual_seed(0) + image = pipe( + prompt=prompt, generator=generator, guidance_scale=7.5, num_inference_steps=2, output_type="numpy" + ).images + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe = VersatileDiffusionTextToImagePipeline.from_pretrained(tmpdirname) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + generator = generator.manual_seed(0) + new_image = pipe( + prompt=prompt, generator=generator, guidance_scale=7.5, num_inference_steps=2, output_type="numpy" + ).images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't have the same forward pass" + + def test_inference_text2img(self): + pipe = VersatileDiffusionTextToImagePipeline.from_pretrained("shi-labs/versatile-diffusion") + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + prompt = "A painting of a squirrel eating a burger " + generator = torch.manual_seed(0) + image = pipe( + prompt=prompt, generator=generator, guidance_scale=7.5, num_inference_steps=50, output_type="numpy" + ).images + + image_slice = image[0, 253:256, 253:256, -1] + + assert image.shape == (1, 512, 512, 3) + expected_slice = np.array([0.3493, 0.3757, 0.4093, 0.4495, 0.4233, 0.4102, 0.4507, 0.4756, 0.4787]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 diff --git a/diffusers/tests/pipelines/vq_diffusion/__init__.py b/diffusers/tests/pipelines/vq_diffusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/diffusers/tests/pipelines/vq_diffusion/test_vq_diffusion.py b/diffusers/tests/pipelines/vq_diffusion/test_vq_diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..9e9468ab2ab309384ceecacd0c8a224e2c9c4e58 --- /dev/null +++ b/diffusers/tests/pipelines/vq_diffusion/test_vq_diffusion.py @@ -0,0 +1,228 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import unittest + +import numpy as np +import torch +from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import Transformer2DModel, VQDiffusionPipeline, VQDiffusionScheduler, VQModel +from diffusers.pipelines.vq_diffusion.pipeline_vq_diffusion import LearnedClassifierFreeSamplingEmbeddings +from diffusers.utils import load_numpy, slow, torch_device +from diffusers.utils.testing_utils import require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class VQDiffusionPipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + @property + def num_embed(self): + return 12 + + @property + def num_embeds_ada_norm(self): + return 12 + + @property + def text_embedder_hidden_size(self): + return 32 + + @property + def dummy_vqvae(self): + torch.manual_seed(0) + model = VQModel( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=3, + num_vq_embeddings=self.num_embed, + vq_embed_dim=3, + ) + return model + + @property + def dummy_tokenizer(self): + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + return tokenizer + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=self.text_embedder_hidden_size, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModel(config) + + @property + def dummy_transformer(self): + torch.manual_seed(0) + + height = 12 + width = 12 + + model_kwargs = { + "attention_bias": True, + "cross_attention_dim": 32, + "attention_head_dim": height * width, + "num_attention_heads": 1, + "num_vector_embeds": self.num_embed, + "num_embeds_ada_norm": self.num_embeds_ada_norm, + "norm_num_groups": 32, + "sample_size": width, + "activation_fn": "geglu-approximate", + } + + model = Transformer2DModel(**model_kwargs) + return model + + def test_vq_diffusion(self): + device = "cpu" + + vqvae = self.dummy_vqvae + text_encoder = self.dummy_text_encoder + tokenizer = self.dummy_tokenizer + transformer = self.dummy_transformer + scheduler = VQDiffusionScheduler(self.num_embed) + learned_classifier_free_sampling_embeddings = LearnedClassifierFreeSamplingEmbeddings(learnable=False) + + pipe = VQDiffusionPipeline( + vqvae=vqvae, + text_encoder=text_encoder, + tokenizer=tokenizer, + transformer=transformer, + scheduler=scheduler, + learned_classifier_free_sampling_embeddings=learned_classifier_free_sampling_embeddings, + ) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + prompt = "teddy bear playing in the pool" + + generator = torch.Generator(device=device).manual_seed(0) + output = pipe([prompt], generator=generator, num_inference_steps=2, output_type="np") + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = pipe( + [prompt], generator=generator, output_type="np", return_dict=False, num_inference_steps=2 + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 24, 24, 3) + + expected_slice = np.array([0.6583, 0.6410, 0.5325, 0.5635, 0.5563, 0.4234, 0.6008, 0.5491, 0.4880]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + def test_vq_diffusion_classifier_free_sampling(self): + device = "cpu" + + vqvae = self.dummy_vqvae + text_encoder = self.dummy_text_encoder + tokenizer = self.dummy_tokenizer + transformer = self.dummy_transformer + scheduler = VQDiffusionScheduler(self.num_embed) + learned_classifier_free_sampling_embeddings = LearnedClassifierFreeSamplingEmbeddings( + learnable=True, hidden_size=self.text_embedder_hidden_size, length=tokenizer.model_max_length + ) + + pipe = VQDiffusionPipeline( + vqvae=vqvae, + text_encoder=text_encoder, + tokenizer=tokenizer, + transformer=transformer, + scheduler=scheduler, + learned_classifier_free_sampling_embeddings=learned_classifier_free_sampling_embeddings, + ) + pipe = pipe.to(device) + pipe.set_progress_bar_config(disable=None) + + prompt = "teddy bear playing in the pool" + + generator = torch.Generator(device=device).manual_seed(0) + output = pipe([prompt], generator=generator, num_inference_steps=2, output_type="np") + image = output.images + + generator = torch.Generator(device=device).manual_seed(0) + image_from_tuple = pipe( + [prompt], generator=generator, output_type="np", return_dict=False, num_inference_steps=2 + )[0] + + image_slice = image[0, -3:, -3:, -1] + image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1] + + assert image.shape == (1, 24, 24, 3) + + expected_slice = np.array([0.6647, 0.6531, 0.5303, 0.5891, 0.5726, 0.4439, 0.6304, 0.5564, 0.4912]) + + assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2 + assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2 + + +@slow +@require_torch_gpu +class VQDiffusionPipelineIntegrationTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_vq_diffusion_classifier_free_sampling(self): + expected_image = load_numpy( + "https://huggingface.co/datasets/hf-internal-testing/diffusers-images/resolve/main" + "/vq_diffusion/teddy_bear_pool_classifier_free_sampling.npy" + ) + + pipeline = VQDiffusionPipeline.from_pretrained("microsoft/vq-diffusion-ithq") + pipeline = pipeline.to(torch_device) + pipeline.set_progress_bar_config(disable=None) + + # requires GPU generator for gumbel softmax + # don't use GPU generator in tests though + generator = torch.Generator(device=torch_device).manual_seed(0) + output = pipeline( + "teddy bear playing in the pool", + num_images_per_prompt=1, + generator=generator, + output_type="np", + ) + + image = output.images[0] + + assert image.shape == (256, 256, 3) + assert np.abs(expected_image - image).max() < 1e-2 diff --git a/diffusers/tests/repo_utils/test_check_copies.py b/diffusers/tests/repo_utils/test_check_copies.py new file mode 100644 index 0000000000000000000000000000000000000000..65128f68d1ac023595ff3b4acff4a8cf61f3e2be --- /dev/null +++ b/diffusers/tests/repo_utils/test_check_copies.py @@ -0,0 +1,120 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import os +import re +import shutil +import sys +import tempfile +import unittest + +import black + + +git_repo_path = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +sys.path.append(os.path.join(git_repo_path, "utils")) + +import check_copies # noqa: E402 + + +# This is the reference code that will be used in the tests. +# If DDPMSchedulerOutput is changed in scheduling_ddpm.py, this code needs to be manually updated. +REFERENCE_CODE = """ \""" + Output class for the scheduler's step function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample (x_{t-1}) of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + pred_original_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + The predicted denoised sample (x_{0}) based on the model output from the current timestep. + `pred_original_sample` can be used to preview progress or for guidance. + \""" + + prev_sample: torch.FloatTensor + pred_original_sample: Optional[torch.FloatTensor] = None +""" + + +class CopyCheckTester(unittest.TestCase): + def setUp(self): + self.diffusers_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(self.diffusers_dir, "schedulers/")) + check_copies.DIFFUSERS_PATH = self.diffusers_dir + shutil.copy( + os.path.join(git_repo_path, "src/diffusers/schedulers/scheduling_ddpm.py"), + os.path.join(self.diffusers_dir, "schedulers/scheduling_ddpm.py"), + ) + + def tearDown(self): + check_copies.DIFFUSERS_PATH = "src/diffusers" + shutil.rmtree(self.diffusers_dir) + + def check_copy_consistency(self, comment, class_name, class_code, overwrite_result=None): + code = comment + f"\nclass {class_name}(nn.Module):\n" + class_code + if overwrite_result is not None: + expected = comment + f"\nclass {class_name}(nn.Module):\n" + overwrite_result + mode = black.Mode(target_versions={black.TargetVersion.PY35}, line_length=119) + code = black.format_str(code, mode=mode) + fname = os.path.join(self.diffusers_dir, "new_code.py") + with open(fname, "w", newline="\n") as f: + f.write(code) + if overwrite_result is None: + self.assertTrue(len(check_copies.is_copy_consistent(fname)) == 0) + else: + check_copies.is_copy_consistent(f.name, overwrite=True) + with open(fname, "r") as f: + self.assertTrue(f.read(), expected) + + def test_find_code_in_diffusers(self): + code = check_copies.find_code_in_diffusers("schedulers.scheduling_ddpm.DDPMSchedulerOutput") + self.assertEqual(code, REFERENCE_CODE) + + def test_is_copy_consistent(self): + # Base copy consistency + self.check_copy_consistency( + "# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput", + "DDPMSchedulerOutput", + REFERENCE_CODE + "\n", + ) + + # With no empty line at the end + self.check_copy_consistency( + "# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput", + "DDPMSchedulerOutput", + REFERENCE_CODE, + ) + + # Copy consistency with rename + self.check_copy_consistency( + "# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->Test", + "TestSchedulerOutput", + re.sub("DDPM", "Test", REFERENCE_CODE), + ) + + # Copy consistency with a really long name + long_class_name = "TestClassWithAReallyLongNameBecauseSomePeopleLikeThatForSomeReason" + self.check_copy_consistency( + f"# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->{long_class_name}", + f"{long_class_name}SchedulerOutput", + re.sub("Bert", long_class_name, REFERENCE_CODE), + ) + + # Copy consistency with overwrite + self.check_copy_consistency( + "# Copied from diffusers.schedulers.scheduling_ddpm.DDPMSchedulerOutput with DDPM->Test", + "TestSchedulerOutput", + REFERENCE_CODE, + overwrite_result=re.sub("DDPM", "Test", REFERENCE_CODE), + ) diff --git a/diffusers/tests/repo_utils/test_check_dummies.py b/diffusers/tests/repo_utils/test_check_dummies.py new file mode 100644 index 0000000000000000000000000000000000000000..f233b76d6f817b9a6d614828eda9d9391ee03b84 --- /dev/null +++ b/diffusers/tests/repo_utils/test_check_dummies.py @@ -0,0 +1,122 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# 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. + +import os +import sys +import unittest + + +git_repo_path = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +sys.path.append(os.path.join(git_repo_path, "utils")) + +import check_dummies # noqa: E402 +from check_dummies import create_dummy_files, create_dummy_object, find_backend, read_init # noqa: E402 + + +# Align TRANSFORMERS_PATH in check_dummies with the current path +check_dummies.PATH_TO_DIFFUSERS = os.path.join(git_repo_path, "src", "diffusers") + + +class CheckDummiesTester(unittest.TestCase): + def test_find_backend(self): + simple_backend = find_backend(" if not is_torch_available():") + self.assertEqual(simple_backend, "torch") + + # backend_with_underscore = find_backend(" if not is_tensorflow_text_available():") + # self.assertEqual(backend_with_underscore, "tensorflow_text") + + double_backend = find_backend(" if not (is_torch_available() and is_transformers_available()):") + self.assertEqual(double_backend, "torch_and_transformers") + + # double_backend_with_underscore = find_backend( + # " if not (is_sentencepiece_available() and is_tensorflow_text_available()):" + # ) + # self.assertEqual(double_backend_with_underscore, "sentencepiece_and_tensorflow_text") + + triple_backend = find_backend( + " if not (is_torch_available() and is_transformers_available() and is_onnx_available()):" + ) + self.assertEqual(triple_backend, "torch_and_transformers_and_onnx") + + def test_read_init(self): + objects = read_init() + # We don't assert on the exact list of keys to allow for smooth grow of backend-specific objects + self.assertIn("torch", objects) + self.assertIn("torch_and_transformers", objects) + self.assertIn("flax_and_transformers", objects) + self.assertIn("torch_and_transformers_and_onnx", objects) + + # Likewise, we can't assert on the exact content of a key + self.assertIn("UNet2DModel", objects["torch"]) + self.assertIn("FlaxUNet2DConditionModel", objects["flax"]) + self.assertIn("StableDiffusionPipeline", objects["torch_and_transformers"]) + self.assertIn("FlaxStableDiffusionPipeline", objects["flax_and_transformers"]) + self.assertIn("LMSDiscreteScheduler", objects["torch_and_scipy"]) + self.assertIn("OnnxStableDiffusionPipeline", objects["torch_and_transformers_and_onnx"]) + + def test_create_dummy_object(self): + dummy_constant = create_dummy_object("CONSTANT", "'torch'") + self.assertEqual(dummy_constant, "\nCONSTANT = None\n") + + dummy_function = create_dummy_object("function", "'torch'") + self.assertEqual( + dummy_function, "\ndef function(*args, **kwargs):\n requires_backends(function, 'torch')\n" + ) + + expected_dummy_class = """ +class FakeClass(metaclass=DummyObject): + _backends = 'torch' + + def __init__(self, *args, **kwargs): + requires_backends(self, 'torch') + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, 'torch') + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, 'torch') +""" + dummy_class = create_dummy_object("FakeClass", "'torch'") + self.assertEqual(dummy_class, expected_dummy_class) + + def test_create_dummy_files(self): + expected_dummy_pytorch_file = """# This file is autogenerated by the command `make fix-copies`, do not edit. +from ..utils import DummyObject, requires_backends + + +CONSTANT = None + + +def function(*args, **kwargs): + requires_backends(function, ["torch"]) + + +class FakeClass(metaclass=DummyObject): + _backends = ["torch"] + + def __init__(self, *args, **kwargs): + requires_backends(self, ["torch"]) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, ["torch"]) +""" + dummy_files = create_dummy_files({"torch": ["CONSTANT", "function", "FakeClass"]}) + self.assertEqual(dummy_files["torch"], expected_dummy_pytorch_file) diff --git a/diffusers/tests/test_config.py b/diffusers/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..e5ae467e27ebbe80cba0f40e97bd5a604d646525 --- /dev/null +++ b/diffusers/tests/test_config.py @@ -0,0 +1,223 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import tempfile +import unittest + +from diffusers import ( + DDIMScheduler, + DDPMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + PNDMScheduler, + logging, +) +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.utils.testing_utils import CaptureLogger + + +class SampleObject(ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + e=[1, 3], + ): + pass + + +class SampleObject2(ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + f=[1, 3], + ): + pass + + +class SampleObject3(ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + e=[1, 3], + f=[1, 3], + ): + pass + + +class ConfigTester(unittest.TestCase): + def test_load_not_from_mixin(self): + with self.assertRaises(ValueError): + ConfigMixin.load_config("dummy_path") + + def test_register_to_config(self): + obj = SampleObject() + config = obj.config + assert config["a"] == 2 + assert config["b"] == 5 + assert config["c"] == (2, 5) + assert config["d"] == "for diffusion" + assert config["e"] == [1, 3] + + # init ignore private arguments + obj = SampleObject(_name_or_path="lalala") + config = obj.config + assert config["a"] == 2 + assert config["b"] == 5 + assert config["c"] == (2, 5) + assert config["d"] == "for diffusion" + assert config["e"] == [1, 3] + + # can override default + obj = SampleObject(c=6) + config = obj.config + assert config["a"] == 2 + assert config["b"] == 5 + assert config["c"] == 6 + assert config["d"] == "for diffusion" + assert config["e"] == [1, 3] + + # can use positional arguments. + obj = SampleObject(1, c=6) + config = obj.config + assert config["a"] == 1 + assert config["b"] == 5 + assert config["c"] == 6 + assert config["d"] == "for diffusion" + assert config["e"] == [1, 3] + + def test_save_load(self): + obj = SampleObject() + config = obj.config + + assert config["a"] == 2 + assert config["b"] == 5 + assert config["c"] == (2, 5) + assert config["d"] == "for diffusion" + assert config["e"] == [1, 3] + + with tempfile.TemporaryDirectory() as tmpdirname: + obj.save_config(tmpdirname) + new_obj = SampleObject.from_config(SampleObject.load_config(tmpdirname)) + new_config = new_obj.config + + # unfreeze configs + config = dict(config) + new_config = dict(new_config) + + assert config.pop("c") == (2, 5) # instantiated as tuple + assert new_config.pop("c") == [2, 5] # saved & loaded as list because of json + assert config == new_config + + def test_load_ddim_from_pndm(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + ddim = DDIMScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler" + ) + + assert ddim.__class__ == DDIMScheduler + # no warning should be thrown + assert cap_logger.out == "" + + def test_load_euler_from_pndm(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + euler = EulerDiscreteScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler" + ) + + assert euler.__class__ == EulerDiscreteScheduler + # no warning should be thrown + assert cap_logger.out == "" + + def test_load_euler_ancestral_from_pndm(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + euler = EulerAncestralDiscreteScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler" + ) + + assert euler.__class__ == EulerAncestralDiscreteScheduler + # no warning should be thrown + assert cap_logger.out == "" + + def test_load_pndm(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + pndm = PNDMScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler" + ) + + assert pndm.__class__ == PNDMScheduler + # no warning should be thrown + assert cap_logger.out == "" + + def test_overwrite_config_on_load(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + ddpm = DDPMScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", + subfolder="scheduler", + prediction_type="sample", + beta_end=8, + ) + + with CaptureLogger(logger) as cap_logger_2: + ddpm_2 = DDPMScheduler.from_pretrained("google/ddpm-celebahq-256", beta_start=88) + + assert ddpm.__class__ == DDPMScheduler + assert ddpm.config.prediction_type == "sample" + assert ddpm.config.beta_end == 8 + assert ddpm_2.config.beta_start == 88 + + # no warning should be thrown + assert cap_logger.out == "" + assert cap_logger_2.out == "" + + def test_load_dpmsolver(self): + logger = logging.get_logger("diffusers.configuration_utils") + + with CaptureLogger(logger) as cap_logger: + dpm = DPMSolverMultistepScheduler.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler" + ) + + assert dpm.__class__ == DPMSolverMultistepScheduler + # no warning should be thrown + assert cap_logger.out == "" diff --git a/diffusers/tests/test_hub_utils.py b/diffusers/tests/test_hub_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e8b8ea3a2fd9b114ff184291e7ec73928ba885d7 --- /dev/null +++ b/diffusers/tests/test_hub_utils.py @@ -0,0 +1,51 @@ +# coding=utf-8 +# Copyright 2023 HuggingFace Inc. +# +# 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. +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock, patch + +import diffusers.utils.hub_utils + + +class CreateModelCardTest(unittest.TestCase): + @patch("diffusers.utils.hub_utils.get_full_repo_name") + def test_create_model_card(self, repo_name_mock: Mock) -> None: + repo_name_mock.return_value = "full_repo_name" + with TemporaryDirectory() as tmpdir: + # Dummy args values + args = Mock() + args.output_dir = tmpdir + args.local_rank = 0 + args.hub_token = "hub_token" + args.dataset_name = "dataset_name" + args.learning_rate = 0.01 + args.train_batch_size = 100000 + args.eval_batch_size = 10000 + args.gradient_accumulation_steps = 0.01 + args.adam_beta1 = 0.02 + args.adam_beta2 = 0.03 + args.adam_weight_decay = 0.0005 + args.adam_epsilon = 0.000001 + args.lr_scheduler = 1 + args.lr_warmup_steps = 10 + args.ema_inv_gamma = 0.001 + args.ema_power = 0.1 + args.ema_max_decay = 0.2 + args.mixed_precision = True + + # Model card mush be rendered and saved + diffusers.utils.hub_utils.create_model_card(args, model_name="model_name") + self.assertTrue((Path(tmpdir) / "README.md").is_file()) diff --git a/diffusers/tests/test_layers_utils.py b/diffusers/tests/test_layers_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..344c472149d1cded443394397729e2d8e747e277 --- /dev/null +++ b/diffusers/tests/test_layers_utils.py @@ -0,0 +1,586 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + + +import unittest + +import numpy as np +import torch +from torch import nn + +from diffusers.models.attention import GEGLU, AdaLayerNorm, ApproximateGELU, AttentionBlock +from diffusers.models.embeddings import get_timestep_embedding +from diffusers.models.resnet import Downsample2D, ResnetBlock2D, Upsample2D +from diffusers.models.transformer_2d import Transformer2DModel +from diffusers.utils import torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class EmbeddingsTests(unittest.TestCase): + def test_timestep_embeddings(self): + embedding_dim = 256 + timesteps = torch.arange(16) + + t1 = get_timestep_embedding(timesteps, embedding_dim) + + # first vector should always be composed only of 0's and 1's + assert (t1[0, : embedding_dim // 2] - 0).abs().sum() < 1e-5 + assert (t1[0, embedding_dim // 2 :] - 1).abs().sum() < 1e-5 + + # last element of each vector should be one + assert (t1[:, -1] - 1).abs().sum() < 1e-5 + + # For large embeddings (e.g. 128) the frequency of every vector is higher + # than the previous one which means that the gradients of later vectors are + # ALWAYS higher than the previous ones + grad_mean = np.abs(np.gradient(t1, axis=-1)).mean(axis=1) + + prev_grad = 0.0 + for grad in grad_mean: + assert grad > prev_grad + prev_grad = grad + + def test_timestep_defaults(self): + embedding_dim = 16 + timesteps = torch.arange(10) + + t1 = get_timestep_embedding(timesteps, embedding_dim) + t2 = get_timestep_embedding( + timesteps, embedding_dim, flip_sin_to_cos=False, downscale_freq_shift=1, max_period=10_000 + ) + + assert torch.allclose(t1.cpu(), t2.cpu(), 1e-3) + + def test_timestep_flip_sin_cos(self): + embedding_dim = 16 + timesteps = torch.arange(10) + + t1 = get_timestep_embedding(timesteps, embedding_dim, flip_sin_to_cos=True) + t1 = torch.cat([t1[:, embedding_dim // 2 :], t1[:, : embedding_dim // 2]], dim=-1) + + t2 = get_timestep_embedding(timesteps, embedding_dim, flip_sin_to_cos=False) + + assert torch.allclose(t1.cpu(), t2.cpu(), 1e-3) + + def test_timestep_downscale_freq_shift(self): + embedding_dim = 16 + timesteps = torch.arange(10) + + t1 = get_timestep_embedding(timesteps, embedding_dim, downscale_freq_shift=0) + t2 = get_timestep_embedding(timesteps, embedding_dim, downscale_freq_shift=1) + + # get cosine half (vectors that are wrapped into cosine) + cosine_half = (t1 - t2)[:, embedding_dim // 2 :] + + # cosine needs to be negative + assert (np.abs((cosine_half <= 0).numpy()) - 1).sum() < 1e-5 + + def test_sinoid_embeddings_hardcoded(self): + embedding_dim = 64 + timesteps = torch.arange(128) + + # standard unet, score_vde + t1 = get_timestep_embedding(timesteps, embedding_dim, downscale_freq_shift=1, flip_sin_to_cos=False) + # glide, ldm + t2 = get_timestep_embedding(timesteps, embedding_dim, downscale_freq_shift=0, flip_sin_to_cos=True) + # grad-tts + t3 = get_timestep_embedding(timesteps, embedding_dim, scale=1000) + + assert torch.allclose( + t1[23:26, 47:50].flatten().cpu(), + torch.tensor([0.9646, 0.9804, 0.9892, 0.9615, 0.9787, 0.9882, 0.9582, 0.9769, 0.9872]), + 1e-3, + ) + assert torch.allclose( + t2[23:26, 47:50].flatten().cpu(), + torch.tensor([0.3019, 0.2280, 0.1716, 0.3146, 0.2377, 0.1790, 0.3272, 0.2474, 0.1864]), + 1e-3, + ) + assert torch.allclose( + t3[23:26, 47:50].flatten().cpu(), + torch.tensor([-0.9801, -0.9464, -0.9349, -0.3952, 0.8887, -0.9709, 0.5299, -0.2853, -0.9927]), + 1e-3, + ) + + +class Upsample2DBlockTests(unittest.TestCase): + def test_upsample_default(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 32, 32) + upsample = Upsample2D(channels=32, use_conv=False) + with torch.no_grad(): + upsampled = upsample(sample) + + assert upsampled.shape == (1, 32, 64, 64) + output_slice = upsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([-0.2173, -1.2079, -1.2079, 0.2952, 1.1254, 1.1254, 0.2952, 1.1254, 1.1254]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_upsample_with_conv(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 32, 32) + upsample = Upsample2D(channels=32, use_conv=True) + with torch.no_grad(): + upsampled = upsample(sample) + + assert upsampled.shape == (1, 32, 64, 64) + output_slice = upsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([0.7145, 1.3773, 0.3492, 0.8448, 1.0839, -0.3341, 0.5956, 0.1250, -0.4841]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_upsample_with_conv_out_dim(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 32, 32) + upsample = Upsample2D(channels=32, use_conv=True, out_channels=64) + with torch.no_grad(): + upsampled = upsample(sample) + + assert upsampled.shape == (1, 64, 64, 64) + output_slice = upsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([0.2703, 0.1656, -0.2538, -0.0553, -0.2984, 0.1044, 0.1155, 0.2579, 0.7755]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_upsample_with_transpose(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 32, 32) + upsample = Upsample2D(channels=32, use_conv=False, use_conv_transpose=True) + with torch.no_grad(): + upsampled = upsample(sample) + + assert upsampled.shape == (1, 32, 64, 64) + output_slice = upsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([-0.3028, -0.1582, 0.0071, 0.0350, -0.4799, -0.1139, 0.1056, -0.1153, -0.1046]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + +class Downsample2DBlockTests(unittest.TestCase): + def test_downsample_default(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64) + downsample = Downsample2D(channels=32, use_conv=False) + with torch.no_grad(): + downsampled = downsample(sample) + + assert downsampled.shape == (1, 32, 32, 32) + output_slice = downsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([-0.0513, -0.3889, 0.0640, 0.0836, -0.5460, -0.0341, -0.0169, -0.6967, 0.1179]) + max_diff = (output_slice.flatten() - expected_slice).abs().sum().item() + assert max_diff <= 1e-3 + # assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-1) + + def test_downsample_with_conv(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64) + downsample = Downsample2D(channels=32, use_conv=True) + with torch.no_grad(): + downsampled = downsample(sample) + + assert downsampled.shape == (1, 32, 32, 32) + output_slice = downsampled[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [0.9267, 0.5878, 0.3337, 1.2321, -0.1191, -0.3984, -0.7532, -0.0715, -0.3913], + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_downsample_with_conv_pad1(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64) + downsample = Downsample2D(channels=32, use_conv=True, padding=1) + with torch.no_grad(): + downsampled = downsample(sample) + + assert downsampled.shape == (1, 32, 32, 32) + output_slice = downsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([0.9267, 0.5878, 0.3337, 1.2321, -0.1191, -0.3984, -0.7532, -0.0715, -0.3913]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_downsample_with_conv_out_dim(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64) + downsample = Downsample2D(channels=32, use_conv=True, out_channels=16) + with torch.no_grad(): + downsampled = downsample(sample) + + assert downsampled.shape == (1, 16, 32, 32) + output_slice = downsampled[0, -1, -3:, -3:] + expected_slice = torch.tensor([-0.6586, 0.5985, 0.0721, 0.1256, -0.1492, 0.4436, -0.2544, 0.5021, 1.1522]) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + +class ResnetBlock2DTests(unittest.TestCase): + def test_resnet_default(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 64, 64) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [-1.9010, -0.2974, -0.8245, -1.3533, 0.8742, -0.9645, -2.0584, 1.3387, -0.4746], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_restnet_with_use_in_shortcut(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128, use_in_shortcut=True).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 64, 64) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [0.2226, -1.0791, -0.1629, 0.3659, -0.2889, -1.2376, 0.0582, 0.9206, 0.0044], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_resnet_up(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128, up=True).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 128, 128) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [1.2130, -0.8753, -0.9027, 1.5783, -0.5362, -0.5001, 1.0726, -0.7732, -0.4182], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_resnet_down(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128, down=True).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 32, 32) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [-0.3002, -0.7135, 0.1359, 0.0561, -0.7935, 0.0113, -0.1766, -0.6714, -0.0436], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_restnet_with_kernel_fir(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128, kernel="fir", down=True).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 32, 32) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [-0.0934, -0.5729, 0.0909, -0.2710, -0.5044, 0.0243, -0.0665, -0.5267, -0.3136], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_restnet_with_kernel_sde_vp(self): + torch.manual_seed(0) + sample = torch.randn(1, 32, 64, 64).to(torch_device) + temb = torch.randn(1, 128).to(torch_device) + resnet_block = ResnetBlock2D(in_channels=32, temb_channels=128, kernel="sde_vp", down=True).to(torch_device) + with torch.no_grad(): + output_tensor = resnet_block(sample, temb) + + assert output_tensor.shape == (1, 32, 32, 32) + output_slice = output_tensor[0, -1, -3:, -3:] + expected_slice = torch.tensor( + [-0.3002, -0.7135, 0.1359, 0.0561, -0.7935, 0.0113, -0.1766, -0.6714, -0.0436], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + +class AttentionBlockTests(unittest.TestCase): + @unittest.skipIf( + torch_device == "mps", "Matmul crashes on MPS, see https://github.com/pytorch/pytorch/issues/84039" + ) + def test_attention_block_default(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + sample = torch.randn(1, 32, 64, 64).to(torch_device) + attentionBlock = AttentionBlock( + channels=32, + num_head_channels=1, + rescale_output_factor=1.0, + eps=1e-6, + norm_num_groups=32, + ).to(torch_device) + with torch.no_grad(): + attention_scores = attentionBlock(sample) + + assert attention_scores.shape == (1, 32, 64, 64) + output_slice = attention_scores[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [-1.4975, -0.0038, -0.7847, -1.4567, 1.1220, -0.8962, -1.7394, 1.1319, -0.5427], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_attention_block_sd(self): + # This version uses SD params and is compatible with mps + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + sample = torch.randn(1, 512, 64, 64).to(torch_device) + attentionBlock = AttentionBlock( + channels=512, + rescale_output_factor=1.0, + eps=1e-6, + norm_num_groups=32, + ).to(torch_device) + with torch.no_grad(): + attention_scores = attentionBlock(sample) + + assert attention_scores.shape == (1, 512, 64, 64) + output_slice = attention_scores[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [-0.6621, -0.0156, -3.2766, 0.8025, -0.8609, 0.2820, 0.0905, -1.1179, -3.2126], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + +class Transformer2DModelTests(unittest.TestCase): + def test_spatial_transformer_default(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + sample = torch.randn(1, 32, 64, 64).to(torch_device) + spatial_transformer_block = Transformer2DModel( + in_channels=32, + num_attention_heads=1, + attention_head_dim=32, + dropout=0.0, + cross_attention_dim=None, + ).to(torch_device) + with torch.no_grad(): + attention_scores = spatial_transformer_block(sample).sample + + assert attention_scores.shape == (1, 32, 64, 64) + output_slice = attention_scores[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [-1.9455, -0.0066, -1.3933, -1.5878, 0.5325, -0.6486, -1.8648, 0.7515, -0.9689], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_spatial_transformer_cross_attention_dim(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + sample = torch.randn(1, 64, 64, 64).to(torch_device) + spatial_transformer_block = Transformer2DModel( + in_channels=64, + num_attention_heads=2, + attention_head_dim=32, + dropout=0.0, + cross_attention_dim=64, + ).to(torch_device) + with torch.no_grad(): + context = torch.randn(1, 4, 64).to(torch_device) + attention_scores = spatial_transformer_block(sample, context).sample + + assert attention_scores.shape == (1, 64, 64, 64) + output_slice = attention_scores[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [-0.2555, -0.8877, -2.4739, -2.2251, 1.2714, 0.0807, -0.4161, -1.6408, -0.0471], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_spatial_transformer_timestep(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + num_embeds_ada_norm = 5 + + sample = torch.randn(1, 64, 64, 64).to(torch_device) + spatial_transformer_block = Transformer2DModel( + in_channels=64, + num_attention_heads=2, + attention_head_dim=32, + dropout=0.0, + cross_attention_dim=64, + num_embeds_ada_norm=num_embeds_ada_norm, + ).to(torch_device) + with torch.no_grad(): + timestep_1 = torch.tensor(1, dtype=torch.long).to(torch_device) + timestep_2 = torch.tensor(2, dtype=torch.long).to(torch_device) + attention_scores_1 = spatial_transformer_block(sample, timestep=timestep_1).sample + attention_scores_2 = spatial_transformer_block(sample, timestep=timestep_2).sample + + assert attention_scores_1.shape == (1, 64, 64, 64) + assert attention_scores_2.shape == (1, 64, 64, 64) + + output_slice_1 = attention_scores_1[0, -1, -3:, -3:] + output_slice_2 = attention_scores_2[0, -1, -3:, -3:] + + expected_slice_1 = torch.tensor( + [-0.1874, -0.9704, -1.4290, -1.3357, 1.5138, 0.3036, -0.0976, -1.1667, 0.1283], device=torch_device + ) + expected_slice_2 = torch.tensor( + [-0.3493, -1.0924, -1.6161, -1.5016, 1.4245, 0.1367, -0.2526, -1.3109, -0.0547], device=torch_device + ) + + assert torch.allclose(output_slice_1.flatten(), expected_slice_1, atol=1e-3) + assert torch.allclose(output_slice_2.flatten(), expected_slice_2, atol=1e-3) + + def test_spatial_transformer_dropout(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + sample = torch.randn(1, 32, 64, 64).to(torch_device) + spatial_transformer_block = ( + Transformer2DModel( + in_channels=32, + num_attention_heads=2, + attention_head_dim=16, + dropout=0.3, + cross_attention_dim=None, + ) + .to(torch_device) + .eval() + ) + with torch.no_grad(): + attention_scores = spatial_transformer_block(sample).sample + + assert attention_scores.shape == (1, 32, 64, 64) + output_slice = attention_scores[0, -1, -3:, -3:] + + expected_slice = torch.tensor( + [-1.9380, -0.0083, -1.3771, -1.5819, 0.5209, -0.6441, -1.8545, 0.7563, -0.9615], device=torch_device + ) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + @unittest.skipIf(torch_device == "mps", "MPS does not support float64") + def test_spatial_transformer_discrete(self): + torch.manual_seed(0) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(0) + + num_embed = 5 + + sample = torch.randint(0, num_embed, (1, 32)).to(torch_device) + spatial_transformer_block = ( + Transformer2DModel( + num_attention_heads=1, + attention_head_dim=32, + num_vector_embeds=num_embed, + sample_size=16, + ) + .to(torch_device) + .eval() + ) + + with torch.no_grad(): + attention_scores = spatial_transformer_block(sample).sample + + assert attention_scores.shape == (1, num_embed - 1, 32) + + output_slice = attention_scores[0, -2:, -3:] + + expected_slice = torch.tensor([-1.7648, -1.0241, -2.0985, -1.8035, -1.6404, -1.2098], device=torch_device) + assert torch.allclose(output_slice.flatten(), expected_slice, atol=1e-3) + + def test_spatial_transformer_default_norm_layers(self): + spatial_transformer_block = Transformer2DModel(num_attention_heads=1, attention_head_dim=32, in_channels=32) + + assert spatial_transformer_block.transformer_blocks[0].norm1.__class__ == nn.LayerNorm + assert spatial_transformer_block.transformer_blocks[0].norm3.__class__ == nn.LayerNorm + + def test_spatial_transformer_ada_norm_layers(self): + spatial_transformer_block = Transformer2DModel( + num_attention_heads=1, + attention_head_dim=32, + in_channels=32, + num_embeds_ada_norm=5, + ) + + assert spatial_transformer_block.transformer_blocks[0].norm1.__class__ == AdaLayerNorm + assert spatial_transformer_block.transformer_blocks[0].norm3.__class__ == nn.LayerNorm + + def test_spatial_transformer_default_ff_layers(self): + spatial_transformer_block = Transformer2DModel( + num_attention_heads=1, + attention_head_dim=32, + in_channels=32, + ) + + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].__class__ == GEGLU + assert spatial_transformer_block.transformer_blocks[0].ff.net[1].__class__ == nn.Dropout + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].__class__ == nn.Linear + + dim = 32 + inner_dim = 128 + + # First dimension change + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].proj.in_features == dim + # NOTE: inner_dim * 2 because GEGLU + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].proj.out_features == inner_dim * 2 + + # Second dimension change + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].in_features == inner_dim + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].out_features == dim + + def test_spatial_transformer_geglu_approx_ff_layers(self): + spatial_transformer_block = Transformer2DModel( + num_attention_heads=1, + attention_head_dim=32, + in_channels=32, + activation_fn="geglu-approximate", + ) + + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].__class__ == ApproximateGELU + assert spatial_transformer_block.transformer_blocks[0].ff.net[1].__class__ == nn.Dropout + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].__class__ == nn.Linear + + dim = 32 + inner_dim = 128 + + # First dimension change + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].proj.in_features == dim + assert spatial_transformer_block.transformer_blocks[0].ff.net[0].proj.out_features == inner_dim + + # Second dimension change + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].in_features == inner_dim + assert spatial_transformer_block.transformer_blocks[0].ff.net[2].out_features == dim + + def test_spatial_transformer_attention_bias(self): + spatial_transformer_block = Transformer2DModel( + num_attention_heads=1, attention_head_dim=32, in_channels=32, attention_bias=True + ) + + assert spatial_transformer_block.transformer_blocks[0].attn1.to_q.bias is not None + assert spatial_transformer_block.transformer_blocks[0].attn1.to_k.bias is not None + assert spatial_transformer_block.transformer_blocks[0].attn1.to_v.bias is not None diff --git a/diffusers/tests/test_modeling_common.py b/diffusers/tests/test_modeling_common.py new file mode 100644 index 0000000000000000000000000000000000000000..db006790a282d40c7cf41147d1b78dd4c762c7a3 --- /dev/null +++ b/diffusers/tests/test_modeling_common.py @@ -0,0 +1,314 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import inspect +import tempfile +import unittest +from typing import Dict, List, Tuple + +import numpy as np +import torch + +from diffusers.models import ModelMixin, UNet2DConditionModel +from diffusers.training_utils import EMAModel +from diffusers.utils import torch_device + + +class ModelUtilsTest(unittest.TestCase): + def test_accelerate_loading_error_message(self): + with self.assertRaises(ValueError) as error_context: + UNet2DConditionModel.from_pretrained("hf-internal-testing/stable-diffusion-broken", subfolder="unet") + + # make sure that error message states what keys are missing + assert "conv_out.bias" in str(error_context.exception) + + +class ModelTesterMixin: + def test_from_save_pretrained(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with tempfile.TemporaryDirectory() as tmpdirname: + model.save_pretrained(tmpdirname) + new_model = self.model_class.from_pretrained(tmpdirname) + new_model.to(torch_device) + + with torch.no_grad(): + # Warmup pass when using mps (see #372) + if torch_device == "mps" and isinstance(model, ModelMixin): + _ = model(**self.dummy_input) + _ = new_model(**self.dummy_input) + + image = model(**inputs_dict) + if isinstance(image, dict): + image = image.sample + + new_image = new_model(**inputs_dict) + + if isinstance(new_image, dict): + new_image = new_image.sample + + max_diff = (image - new_image).abs().sum().item() + self.assertLessEqual(max_diff, 5e-5, "Models give different forward passes") + + def test_from_save_pretrained_dtype(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + for dtype in [torch.float32, torch.float16, torch.bfloat16]: + if torch_device == "mps" and dtype == torch.bfloat16: + continue + with tempfile.TemporaryDirectory() as tmpdirname: + model.to(dtype) + model.save_pretrained(tmpdirname) + new_model = self.model_class.from_pretrained(tmpdirname, low_cpu_mem_usage=True, torch_dtype=dtype) + assert new_model.dtype == dtype + new_model = self.model_class.from_pretrained(tmpdirname, low_cpu_mem_usage=False, torch_dtype=dtype) + assert new_model.dtype == dtype + + def test_determinism(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + # Warmup pass when using mps (see #372) + if torch_device == "mps" and isinstance(model, ModelMixin): + model(**self.dummy_input) + + first = model(**inputs_dict) + if isinstance(first, dict): + first = first.sample + + second = model(**inputs_dict) + if isinstance(second, dict): + second = second.sample + + out_1 = first.cpu().numpy() + out_2 = second.cpu().numpy() + out_1 = out_1[~np.isnan(out_1)] + out_2 = out_2[~np.isnan(out_2)] + max_diff = np.amax(np.abs(out_1 - out_2)) + self.assertLessEqual(max_diff, 1e-5) + + def test_output(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_forward_with_norm_groups(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["norm_num_groups"] = 16 + init_dict["block_out_channels"] = (16, 32) + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_forward_signature(self): + init_dict, _ = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + signature = inspect.signature(model.forward) + # signature.parameters is an OrderedDict => so arg_names order is deterministic + arg_names = [*signature.parameters.keys()] + + expected_arg_names = ["sample", "timestep"] + self.assertListEqual(arg_names[:2], expected_arg_names) + + def test_model_from_pretrained(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + # test if the model can be loaded from the config + # and has all the expected shape + with tempfile.TemporaryDirectory() as tmpdirname: + model.save_pretrained(tmpdirname) + new_model = self.model_class.from_pretrained(tmpdirname) + new_model.to(torch_device) + new_model.eval() + + # check if all parameters shape are the same + for param_name in model.state_dict().keys(): + param_1 = model.state_dict()[param_name] + param_2 = new_model.state_dict()[param_name] + self.assertEqual(param_1.shape, param_2.shape) + + with torch.no_grad(): + output_1 = model(**inputs_dict) + + if isinstance(output_1, dict): + output_1 = output_1.sample + + output_2 = new_model(**inputs_dict) + + if isinstance(output_2, dict): + output_2 = output_2.sample + + self.assertEqual(output_1.shape, output_2.shape) + + @unittest.skipIf(torch_device == "mps", "Training is not supported in mps") + def test_training(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.train() + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + noise = torch.randn((inputs_dict["sample"].shape[0],) + self.output_shape).to(torch_device) + loss = torch.nn.functional.mse_loss(output, noise) + loss.backward() + + @unittest.skipIf(torch_device == "mps", "Training is not supported in mps") + def test_ema_training(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.train() + ema_model = EMAModel(model.parameters()) + + output = model(**inputs_dict) + + if isinstance(output, dict): + output = output.sample + + noise = torch.randn((inputs_dict["sample"].shape[0],) + self.output_shape).to(torch_device) + loss = torch.nn.functional.mse_loss(output, noise) + loss.backward() + ema_model.step(model.parameters()) + + def test_outputs_equivalence(self): + def set_nan_tensor_to_zero(t): + # Temporary fallback until `aten::_index_put_impl_` is implemented in mps + # Track progress in https://github.com/pytorch/pytorch/issues/77764 + device = t.device + if device.type == "mps": + t = t.to("cpu") + t[t != t] = 0 + return t.to(device) + + def recursive_check(tuple_object, dict_object): + if isinstance(tuple_object, (List, Tuple)): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object, dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif isinstance(tuple_object, Dict): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object.values(), dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif tuple_object is None: + return + else: + self.assertTrue( + torch.allclose( + set_nan_tensor_to_zero(tuple_object), set_nan_tensor_to_zero(dict_object), atol=1e-5 + ), + msg=( + "Tuple and dict output are not equal. Difference:" + f" {torch.max(torch.abs(tuple_object - dict_object))}. Tuple has `nan`:" + f" {torch.isnan(tuple_object).any()} and `inf`: {torch.isinf(tuple_object)}. Dict has" + f" `nan`: {torch.isnan(dict_object).any()} and `inf`: {torch.isinf(dict_object)}." + ), + ) + + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + model.to(torch_device) + model.eval() + + with torch.no_grad(): + # Warmup pass when using mps (see #372) + if torch_device == "mps" and isinstance(model, ModelMixin): + model(**self.dummy_input) + + outputs_dict = model(**inputs_dict) + outputs_tuple = model(**inputs_dict, return_dict=False) + + recursive_check(outputs_tuple, outputs_dict) + + @unittest.skipIf(torch_device == "mps", "Gradient checkpointing skipped on MPS") + def test_enable_disable_gradient_checkpointing(self): + if not self.model_class._supports_gradient_checkpointing: + return # Skip test if model does not support gradient checkpointing + + init_dict, _ = self.prepare_init_args_and_inputs_for_common() + + # at init model should have gradient checkpointing disabled + model = self.model_class(**init_dict) + self.assertFalse(model.is_gradient_checkpointing) + + # check enable works + model.enable_gradient_checkpointing() + self.assertTrue(model.is_gradient_checkpointing) + + # check disable works + model.disable_gradient_checkpointing() + self.assertFalse(model.is_gradient_checkpointing) + + def test_deprecated_kwargs(self): + has_kwarg_in_model_class = "kwargs" in inspect.signature(self.model_class.__init__).parameters + has_deprecated_kwarg = len(self.model_class._deprecated_kwargs) > 0 + + if has_kwarg_in_model_class and not has_deprecated_kwarg: + raise ValueError( + f"{self.model_class} has `**kwargs` in its __init__ method but has not defined any deprecated kwargs" + " under the `_deprecated_kwargs` class attribute. Make sure to either remove `**kwargs` if there are" + " no deprecated arguments or add the deprecated argument with `_deprecated_kwargs =" + " []`" + ) + + if not has_kwarg_in_model_class and has_deprecated_kwarg: + raise ValueError( + f"{self.model_class} doesn't have `**kwargs` in its __init__ method but has defined deprecated kwargs" + " under the `_deprecated_kwargs` class attribute. Make sure to either add the `**kwargs` argument to" + f" {self.model_class}.__init__ if there are deprecated arguments or remove the deprecated argument" + " from `_deprecated_kwargs = []`" + ) diff --git a/diffusers/tests/test_modeling_common_flax.py b/diffusers/tests/test_modeling_common_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..8945aed7c93fb1e664c7b6d799f7e0a96525b1a2 --- /dev/null +++ b/diffusers/tests/test_modeling_common_flax.py @@ -0,0 +1,66 @@ +import inspect + +from diffusers.utils import is_flax_available +from diffusers.utils.testing_utils import require_flax + + +if is_flax_available(): + import jax + + +@require_flax +class FlaxModelTesterMixin: + def test_output(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + model = self.model_class(**init_dict) + variables = model.init(inputs_dict["prng_key"], inputs_dict["sample"]) + jax.lax.stop_gradient(variables) + + output = model.apply(variables, inputs_dict["sample"]) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_forward_with_norm_groups(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + + init_dict["norm_num_groups"] = 16 + init_dict["block_out_channels"] = (16, 32) + + model = self.model_class(**init_dict) + variables = model.init(inputs_dict["prng_key"], inputs_dict["sample"]) + jax.lax.stop_gradient(variables) + + output = model.apply(variables, inputs_dict["sample"]) + + if isinstance(output, dict): + output = output.sample + + self.assertIsNotNone(output) + expected_shape = inputs_dict["sample"].shape + self.assertEqual(output.shape, expected_shape, "Input and output shapes do not match") + + def test_deprecated_kwargs(self): + has_kwarg_in_model_class = "kwargs" in inspect.signature(self.model_class.__init__).parameters + has_deprecated_kwarg = len(self.model_class._deprecated_kwargs) > 0 + + if has_kwarg_in_model_class and not has_deprecated_kwarg: + raise ValueError( + f"{self.model_class} has `**kwargs` in its __init__ method but has not defined any deprecated kwargs" + " under the `_deprecated_kwargs` class attribute. Make sure to either remove `**kwargs` if there are" + " no deprecated arguments or add the deprecated argument with `_deprecated_kwargs =" + " []`" + ) + + if not has_kwarg_in_model_class and has_deprecated_kwarg: + raise ValueError( + f"{self.model_class} doesn't have `**kwargs` in its __init__ method but has defined deprecated kwargs" + " under the `_deprecated_kwargs` class attribute. Make sure to either add the `**kwargs` argument to" + f" {self.model_class}.__init__ if there are deprecated arguments or remove the deprecated argument" + " from `_deprecated_kwargs = []`" + ) diff --git a/diffusers/tests/test_outputs.py b/diffusers/tests/test_outputs.py new file mode 100644 index 0000000000000000000000000000000000000000..50cbd1d54ee403f2b8e79c8ada629b6b97b1be66 --- /dev/null +++ b/diffusers/tests/test_outputs.py @@ -0,0 +1,60 @@ +import unittest +from dataclasses import dataclass +from typing import List, Union + +import numpy as np +import PIL.Image + +from diffusers.utils.outputs import BaseOutput + + +@dataclass +class CustomOutput(BaseOutput): + images: Union[List[PIL.Image.Image], np.ndarray] + + +class ConfigTester(unittest.TestCase): + def test_outputs_single_attribute(self): + outputs = CustomOutput(images=np.random.rand(1, 3, 4, 4)) + + # check every way of getting the attribute + assert isinstance(outputs.images, np.ndarray) + assert outputs.images.shape == (1, 3, 4, 4) + assert isinstance(outputs["images"], np.ndarray) + assert outputs["images"].shape == (1, 3, 4, 4) + assert isinstance(outputs[0], np.ndarray) + assert outputs[0].shape == (1, 3, 4, 4) + + # test with a non-tensor attribute + outputs = CustomOutput(images=[PIL.Image.new("RGB", (4, 4))]) + + # check every way of getting the attribute + assert isinstance(outputs.images, list) + assert isinstance(outputs.images[0], PIL.Image.Image) + assert isinstance(outputs["images"], list) + assert isinstance(outputs["images"][0], PIL.Image.Image) + assert isinstance(outputs[0], list) + assert isinstance(outputs[0][0], PIL.Image.Image) + + def test_outputs_dict_init(self): + # test output reinitialization with a `dict` for compatibility with `accelerate` + outputs = CustomOutput({"images": np.random.rand(1, 3, 4, 4)}) + + # check every way of getting the attribute + assert isinstance(outputs.images, np.ndarray) + assert outputs.images.shape == (1, 3, 4, 4) + assert isinstance(outputs["images"], np.ndarray) + assert outputs["images"].shape == (1, 3, 4, 4) + assert isinstance(outputs[0], np.ndarray) + assert outputs[0].shape == (1, 3, 4, 4) + + # test with a non-tensor attribute + outputs = CustomOutput({"images": [PIL.Image.new("RGB", (4, 4))]}) + + # check every way of getting the attribute + assert isinstance(outputs.images, list) + assert isinstance(outputs.images[0], PIL.Image.Image) + assert isinstance(outputs["images"], list) + assert isinstance(outputs["images"][0], PIL.Image.Image) + assert isinstance(outputs[0], list) + assert isinstance(outputs[0][0], PIL.Image.Image) diff --git a/diffusers/tests/test_pipelines.py b/diffusers/tests/test_pipelines.py new file mode 100644 index 0000000000000000000000000000000000000000..71340d43b0a9cf3b2c1544292df1b10e4884603b --- /dev/null +++ b/diffusers/tests/test_pipelines.py @@ -0,0 +1,914 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import gc +import json +import os +import random +import shutil +import sys +import tempfile +import unittest + +import numpy as np +import PIL +import safetensors.torch +import torch +from parameterized import parameterized +from PIL import Image +from transformers import CLIPFeatureExtractor, CLIPModel, CLIPTextConfig, CLIPTextModel, CLIPTokenizer + +from diffusers import ( + AutoencoderKL, + DDIMPipeline, + DDIMScheduler, + DDPMPipeline, + DDPMScheduler, + DiffusionPipeline, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipelineLegacy, + StableDiffusionPipeline, + UNet2DConditionModel, + UNet2DModel, + logging, +) +from diffusers.schedulers.scheduling_utils import SCHEDULER_CONFIG_NAME +from diffusers.utils import CONFIG_NAME, WEIGHTS_NAME, floats_tensor, is_flax_available, nightly, slow, torch_device +from diffusers.utils.testing_utils import CaptureLogger, get_tests_dir, require_torch_gpu + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class DownloadTests(unittest.TestCase): + def test_download_only_pytorch(self): + with tempfile.TemporaryDirectory() as tmpdirname: + # pipeline has Flax weights + _ = DiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-pipe", safety_checker=None, cache_dir=tmpdirname + ) + + all_root_files = [t[-1] for t in os.walk(os.path.join(tmpdirname, os.listdir(tmpdirname)[0], "snapshots"))] + files = [item for sublist in all_root_files for item in sublist] + + # None of the downloaded files should be a flax file even if we have some here: + # https://huggingface.co/hf-internal-testing/tiny-stable-diffusion-pipe/blob/main/unet/diffusion_flax_model.msgpack + assert not any(f.endswith(".msgpack") for f in files) + # We need to never convert this tiny model to safetensors for this test to pass + assert not any(f.endswith(".safetensors") for f in files) + + def test_returned_cached_folder(self): + prompt = "hello" + pipe = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", safety_checker=None + ) + _, local_path = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", safety_checker=None, return_cached_folder=True + ) + pipe_2 = StableDiffusionPipeline.from_pretrained(local_path) + + pipe = pipe.to(torch_device) + pipe_2 = pipe_2.to(torch_device) + + generator = torch.manual_seed(0) + out = pipe(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + generator = torch.manual_seed(0) + out_2 = pipe_2(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + assert np.max(np.abs(out - out_2)) < 1e-3 + + def test_download_safetensors(self): + with tempfile.TemporaryDirectory() as tmpdirname: + # pipeline has Flax weights + _ = DiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-pipe-safetensors", + safety_checker=None, + cache_dir=tmpdirname, + ) + + all_root_files = [t[-1] for t in os.walk(os.path.join(tmpdirname, os.listdir(tmpdirname)[0], "snapshots"))] + files = [item for sublist in all_root_files for item in sublist] + + # None of the downloaded files should be a pytorch file even if we have some here: + # https://huggingface.co/hf-internal-testing/tiny-stable-diffusion-pipe/blob/main/unet/diffusion_flax_model.msgpack + assert not any(f.endswith(".bin") for f in files) + + def test_download_no_safety_checker(self): + prompt = "hello" + pipe = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", safety_checker=None + ) + pipe = pipe.to(torch_device) + generator = torch.manual_seed(0) + out = pipe(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + pipe_2 = StableDiffusionPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-torch") + pipe_2 = pipe_2.to(torch_device) + generator = torch.manual_seed(0) + out_2 = pipe_2(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + assert np.max(np.abs(out - out_2)) < 1e-3 + + def test_load_no_safety_checker_explicit_locally(self): + prompt = "hello" + pipe = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", safety_checker=None + ) + pipe = pipe.to(torch_device) + generator = torch.manual_seed(0) + out = pipe(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe_2 = StableDiffusionPipeline.from_pretrained(tmpdirname, safety_checker=None) + pipe_2 = pipe_2.to(torch_device) + + generator = torch.manual_seed(0) + + out_2 = pipe_2(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + assert np.max(np.abs(out - out_2)) < 1e-3 + + def test_load_no_safety_checker_default_locally(self): + prompt = "hello" + pipe = StableDiffusionPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-torch") + pipe = pipe.to(torch_device) + + generator = torch.manual_seed(0) + out = pipe(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe.save_pretrained(tmpdirname) + pipe_2 = StableDiffusionPipeline.from_pretrained(tmpdirname) + pipe_2 = pipe_2.to(torch_device) + + generator = torch.manual_seed(0) + + out_2 = pipe_2(prompt, num_inference_steps=2, generator=generator, output_type="numpy").images + + assert np.max(np.abs(out - out_2)) < 1e-3 + + +class CustomPipelineTests(unittest.TestCase): + def test_load_custom_pipeline(self): + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="hf-internal-testing/diffusers-dummy-pipeline" + ) + pipeline = pipeline.to(torch_device) + # NOTE that `"CustomPipeline"` is not a class that is defined in this library, but solely on the Hub + # under https://huggingface.co/hf-internal-testing/diffusers-dummy-pipeline/blob/main/pipeline.py#L24 + assert pipeline.__class__.__name__ == "CustomPipeline" + + def test_load_custom_github(self): + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="one_step_unet", custom_revision="main" + ) + + # make sure that on "main" pipeline gives only ones because of: https://github.com/huggingface/diffusers/pull/1690 + with torch.no_grad(): + output = pipeline() + + assert output.numel() == output.sum() + + # hack since Python doesn't like overwriting modules: https://stackoverflow.com/questions/3105801/unload-a-module-in-python + # Could in the future work with hashes instead. + del sys.modules["diffusers_modules.git.one_step_unet"] + + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="one_step_unet", custom_revision="0.10.2" + ) + with torch.no_grad(): + output = pipeline() + + assert output.numel() != output.sum() + + assert pipeline.__class__.__name__ == "UnetSchedulerOneForwardPipeline" + + def test_run_custom_pipeline(self): + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline="hf-internal-testing/diffusers-dummy-pipeline" + ) + pipeline = pipeline.to(torch_device) + images, output_str = pipeline(num_inference_steps=2, output_type="np") + + assert images[0].shape == (1, 32, 32, 3) + + # compare output to https://huggingface.co/hf-internal-testing/diffusers-dummy-pipeline/blob/main/pipeline.py#L102 + assert output_str == "This is a test" + + def test_local_custom_pipeline_repo(self): + local_custom_pipeline_path = get_tests_dir("fixtures/custom_pipeline") + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline=local_custom_pipeline_path + ) + pipeline = pipeline.to(torch_device) + images, output_str = pipeline(num_inference_steps=2, output_type="np") + + assert pipeline.__class__.__name__ == "CustomLocalPipeline" + assert images[0].shape == (1, 32, 32, 3) + # compare to https://github.com/huggingface/diffusers/blob/main/tests/fixtures/custom_pipeline/pipeline.py#L102 + assert output_str == "This is a local test" + + def test_local_custom_pipeline_file(self): + local_custom_pipeline_path = get_tests_dir("fixtures/custom_pipeline") + local_custom_pipeline_path = os.path.join(local_custom_pipeline_path, "what_ever.py") + pipeline = DiffusionPipeline.from_pretrained( + "google/ddpm-cifar10-32", custom_pipeline=local_custom_pipeline_path + ) + pipeline = pipeline.to(torch_device) + images, output_str = pipeline(num_inference_steps=2, output_type="np") + + assert pipeline.__class__.__name__ == "CustomLocalPipeline" + assert images[0].shape == (1, 32, 32, 3) + # compare to https://github.com/huggingface/diffusers/blob/main/tests/fixtures/custom_pipeline/pipeline.py#L102 + assert output_str == "This is a local test" + + @slow + @require_torch_gpu + def test_load_pipeline_from_git(self): + clip_model_id = "laion/CLIP-ViT-B-32-laion2B-s34B-b79K" + + feature_extractor = CLIPFeatureExtractor.from_pretrained(clip_model_id) + clip_model = CLIPModel.from_pretrained(clip_model_id, torch_dtype=torch.float16) + + pipeline = DiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + custom_pipeline="clip_guided_stable_diffusion", + clip_model=clip_model, + feature_extractor=feature_extractor, + torch_dtype=torch.float16, + ) + pipeline.enable_attention_slicing() + pipeline = pipeline.to(torch_device) + + # NOTE that `"CLIPGuidedStableDiffusion"` is not a class that is defined in the pypi package of th e library, but solely on the community examples folder of GitHub under: + # https://github.com/huggingface/diffusers/blob/main/examples/community/clip_guided_stable_diffusion.py + assert pipeline.__class__.__name__ == "CLIPGuidedStableDiffusion" + + image = pipeline("a prompt", num_inference_steps=2, output_type="np").images[0] + assert image.shape == (512, 512, 3) + + +class PipelineFastTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + import diffusers + + diffusers.utils.import_utils._safetensors_available = True + + def dummy_image(self): + batch_size = 1 + num_channels = 3 + sizes = (32, 32) + + image = floats_tensor((batch_size, num_channels) + sizes, rng=random.Random(0)).to(torch_device) + return image + + def dummy_uncond_unet(self, sample_size=32): + torch.manual_seed(0) + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=sample_size, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + return model + + def dummy_cond_unet(self, sample_size=32): + torch.manual_seed(0) + model = UNet2DConditionModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=sample_size, + in_channels=4, + out_channels=4, + down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"), + up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"), + cross_attention_dim=32, + ) + return model + + @property + def dummy_vae(self): + torch.manual_seed(0) + model = AutoencoderKL( + block_out_channels=[32, 64], + in_channels=3, + out_channels=3, + down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"], + up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"], + latent_channels=4, + ) + return model + + @property + def dummy_text_encoder(self): + torch.manual_seed(0) + config = CLIPTextConfig( + bos_token_id=0, + eos_token_id=2, + hidden_size=32, + intermediate_size=37, + layer_norm_eps=1e-05, + num_attention_heads=4, + num_hidden_layers=5, + pad_token_id=1, + vocab_size=1000, + ) + return CLIPTextModel(config) + + @property + def dummy_extractor(self): + def extract(*args, **kwargs): + class Out: + def __init__(self): + self.pixel_values = torch.ones([0]) + + def to(self, device): + self.pixel_values.to(device) + return self + + return Out() + + return extract + + @parameterized.expand( + [ + [DDIMScheduler, DDIMPipeline, 32], + [DDPMScheduler, DDPMPipeline, 32], + [DDIMScheduler, DDIMPipeline, (32, 64)], + [DDPMScheduler, DDPMPipeline, (64, 32)], + ] + ) + def test_uncond_unet_components(self, scheduler_fn=DDPMScheduler, pipeline_fn=DDPMPipeline, sample_size=32): + unet = self.dummy_uncond_unet(sample_size) + scheduler = scheduler_fn() + pipeline = pipeline_fn(unet, scheduler).to(torch_device) + + generator = torch.manual_seed(0) + out_image = pipeline( + generator=generator, + num_inference_steps=2, + output_type="np", + ).images + sample_size = (sample_size, sample_size) if isinstance(sample_size, int) else sample_size + assert out_image.shape == (1, *sample_size, 3) + + def test_stable_diffusion_components(self): + """Test that components property works correctly""" + unet = self.dummy_cond_unet() + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + image = self.dummy_image().cpu().permute(0, 2, 3, 1)[0] + init_image = Image.fromarray(np.uint8(image)).convert("RGB") + mask_image = Image.fromarray(np.uint8(image + 4)).convert("RGB").resize((32, 32)) + + # make sure here that pndm scheduler skips prk + inpaint = StableDiffusionInpaintPipelineLegacy( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ).to(torch_device) + img2img = StableDiffusionImg2ImgPipeline(**inpaint.components).to(torch_device) + text2img = StableDiffusionPipeline(**inpaint.components).to(torch_device) + + prompt = "A painting of a squirrel eating a burger" + + generator = torch.manual_seed(0) + image_inpaint = inpaint( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + image=init_image, + mask_image=mask_image, + ).images + image_img2img = img2img( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + image=init_image, + ).images + image_text2img = text2img( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + ).images + + assert image_inpaint.shape == (1, 32, 32, 3) + assert image_img2img.shape == (1, 32, 32, 3) + assert image_text2img.shape == (1, 64, 64, 3) + + def test_set_scheduler(self): + unet = self.dummy_cond_unet() + scheduler = PNDMScheduler(skip_prk_steps=True) + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + sd = StableDiffusionPipeline( + unet=unet, + scheduler=scheduler, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + + sd.scheduler = DDIMScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, DDIMScheduler) + sd.scheduler = DDPMScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, DDPMScheduler) + sd.scheduler = PNDMScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, PNDMScheduler) + sd.scheduler = LMSDiscreteScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, LMSDiscreteScheduler) + sd.scheduler = EulerDiscreteScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, EulerDiscreteScheduler) + sd.scheduler = EulerAncestralDiscreteScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, EulerAncestralDiscreteScheduler) + sd.scheduler = DPMSolverMultistepScheduler.from_config(sd.scheduler.config) + assert isinstance(sd.scheduler, DPMSolverMultistepScheduler) + + def test_set_scheduler_consistency(self): + unet = self.dummy_cond_unet() + pndm = PNDMScheduler.from_config("hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler") + ddim = DDIMScheduler.from_config("hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler") + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + sd = StableDiffusionPipeline( + unet=unet, + scheduler=pndm, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + + pndm_config = sd.scheduler.config + sd.scheduler = DDPMScheduler.from_config(pndm_config) + sd.scheduler = PNDMScheduler.from_config(sd.scheduler.config) + pndm_config_2 = sd.scheduler.config + pndm_config_2 = {k: v for k, v in pndm_config_2.items() if k in pndm_config} + + assert dict(pndm_config) == dict(pndm_config_2) + + sd = StableDiffusionPipeline( + unet=unet, + scheduler=ddim, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=None, + feature_extractor=self.dummy_extractor, + ) + + ddim_config = sd.scheduler.config + sd.scheduler = LMSDiscreteScheduler.from_config(ddim_config) + sd.scheduler = DDIMScheduler.from_config(sd.scheduler.config) + ddim_config_2 = sd.scheduler.config + ddim_config_2 = {k: v for k, v in ddim_config_2.items() if k in ddim_config} + + assert dict(ddim_config) == dict(ddim_config_2) + + def test_save_safe_serialization(self): + pipeline = StableDiffusionPipeline.from_pretrained("hf-internal-testing/tiny-stable-diffusion-torch") + with tempfile.TemporaryDirectory() as tmpdirname: + pipeline.save_pretrained(tmpdirname, safe_serialization=True) + + # Validate that the VAE safetensor exists and are of the correct format + vae_path = os.path.join(tmpdirname, "vae", "diffusion_pytorch_model.safetensors") + assert os.path.exists(vae_path), f"Could not find {vae_path}" + _ = safetensors.torch.load_file(vae_path) + + # Validate that the UNet safetensor exists and are of the correct format + unet_path = os.path.join(tmpdirname, "unet", "diffusion_pytorch_model.safetensors") + assert os.path.exists(unet_path), f"Could not find {unet_path}" + _ = safetensors.torch.load_file(unet_path) + + # Validate that the text encoder safetensor exists and are of the correct format + text_encoder_path = os.path.join(tmpdirname, "text_encoder", "model.safetensors") + assert os.path.exists(text_encoder_path), f"Could not find {text_encoder_path}" + _ = safetensors.torch.load_file(text_encoder_path) + + pipeline = StableDiffusionPipeline.from_pretrained(tmpdirname) + assert pipeline.unet is not None + assert pipeline.vae is not None + assert pipeline.text_encoder is not None + assert pipeline.scheduler is not None + assert pipeline.feature_extractor is not None + + def test_no_pytorch_download_when_doing_safetensors(self): + # by default we don't download + with tempfile.TemporaryDirectory() as tmpdirname: + _ = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/diffusers-stable-diffusion-tiny-all", cache_dir=tmpdirname + ) + + path = os.path.join( + tmpdirname, + "models--hf-internal-testing--diffusers-stable-diffusion-tiny-all", + "snapshots", + "07838d72e12f9bcec1375b0482b80c1d399be843", + "unet", + ) + # safetensors exists + assert os.path.exists(os.path.join(path, "diffusion_pytorch_model.safetensors")) + # pytorch does not + assert not os.path.exists(os.path.join(path, "diffusion_pytorch_model.bin")) + + def test_no_safetensors_download_when_doing_pytorch(self): + # mock diffusers safetensors not available + import diffusers + + diffusers.utils.import_utils._safetensors_available = False + + with tempfile.TemporaryDirectory() as tmpdirname: + _ = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/diffusers-stable-diffusion-tiny-all", cache_dir=tmpdirname + ) + + path = os.path.join( + tmpdirname, + "models--hf-internal-testing--diffusers-stable-diffusion-tiny-all", + "snapshots", + "07838d72e12f9bcec1375b0482b80c1d399be843", + "unet", + ) + # safetensors does not exists + assert not os.path.exists(os.path.join(path, "diffusion_pytorch_model.safetensors")) + # pytorch does + assert os.path.exists(os.path.join(path, "diffusion_pytorch_model.bin")) + + diffusers.utils.import_utils._safetensors_available = True + + def test_optional_components(self): + unet = self.dummy_cond_unet() + pndm = PNDMScheduler.from_config("hf-internal-testing/tiny-stable-diffusion-torch", subfolder="scheduler") + vae = self.dummy_vae + bert = self.dummy_text_encoder + tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") + + orig_sd = StableDiffusionPipeline( + unet=unet, + scheduler=pndm, + vae=vae, + text_encoder=bert, + tokenizer=tokenizer, + safety_checker=unet, + feature_extractor=self.dummy_extractor, + ) + sd = orig_sd + + assert sd.config.requires_safety_checker is True + + with tempfile.TemporaryDirectory() as tmpdirname: + sd.save_pretrained(tmpdirname) + + # Test that passing None works + sd = StableDiffusionPipeline.from_pretrained( + tmpdirname, feature_extractor=None, safety_checker=None, requires_safety_checker=False + ) + + assert sd.config.requires_safety_checker is False + assert sd.config.safety_checker == (None, None) + assert sd.config.feature_extractor == (None, None) + + with tempfile.TemporaryDirectory() as tmpdirname: + sd.save_pretrained(tmpdirname) + + # Test that loading previous None works + sd = StableDiffusionPipeline.from_pretrained(tmpdirname) + + assert sd.config.requires_safety_checker is False + assert sd.config.safety_checker == (None, None) + assert sd.config.feature_extractor == (None, None) + + orig_sd.save_pretrained(tmpdirname) + + # Test that loading without any directory works + shutil.rmtree(os.path.join(tmpdirname, "safety_checker")) + with open(os.path.join(tmpdirname, sd.config_name)) as f: + config = json.load(f) + config["safety_checker"] = [None, None] + with open(os.path.join(tmpdirname, sd.config_name), "w") as f: + json.dump(config, f) + + sd = StableDiffusionPipeline.from_pretrained(tmpdirname, requires_safety_checker=False) + sd.save_pretrained(tmpdirname) + sd = StableDiffusionPipeline.from_pretrained(tmpdirname) + + assert sd.config.requires_safety_checker is False + assert sd.config.safety_checker == (None, None) + assert sd.config.feature_extractor == (None, None) + + # Test that loading from deleted model index works + with open(os.path.join(tmpdirname, sd.config_name)) as f: + config = json.load(f) + del config["safety_checker"] + del config["feature_extractor"] + with open(os.path.join(tmpdirname, sd.config_name), "w") as f: + json.dump(config, f) + + sd = StableDiffusionPipeline.from_pretrained(tmpdirname) + + assert sd.config.requires_safety_checker is False + assert sd.config.safety_checker == (None, None) + assert sd.config.feature_extractor == (None, None) + + with tempfile.TemporaryDirectory() as tmpdirname: + sd.save_pretrained(tmpdirname) + + # Test that partially loading works + sd = StableDiffusionPipeline.from_pretrained(tmpdirname, feature_extractor=self.dummy_extractor) + + assert sd.config.requires_safety_checker is False + assert sd.config.safety_checker == (None, None) + assert sd.config.feature_extractor != (None, None) + + # Test that partially loading works + sd = StableDiffusionPipeline.from_pretrained( + tmpdirname, + feature_extractor=self.dummy_extractor, + safety_checker=unet, + requires_safety_checker=[True, True], + ) + + assert sd.config.requires_safety_checker == [True, True] + assert sd.config.safety_checker != (None, None) + assert sd.config.feature_extractor != (None, None) + + with tempfile.TemporaryDirectory() as tmpdirname: + sd.save_pretrained(tmpdirname) + sd = StableDiffusionPipeline.from_pretrained(tmpdirname, feature_extractor=self.dummy_extractor) + + assert sd.config.requires_safety_checker == [True, True] + assert sd.config.safety_checker != (None, None) + assert sd.config.feature_extractor != (None, None) + + +@slow +@require_torch_gpu +class PipelineSlowTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_smart_download(self): + model_id = "hf-internal-testing/unet-pipeline-dummy" + with tempfile.TemporaryDirectory() as tmpdirname: + _ = DiffusionPipeline.from_pretrained(model_id, cache_dir=tmpdirname, force_download=True) + local_repo_name = "--".join(["models"] + model_id.split("/")) + snapshot_dir = os.path.join(tmpdirname, local_repo_name, "snapshots") + snapshot_dir = os.path.join(snapshot_dir, os.listdir(snapshot_dir)[0]) + + # inspect all downloaded files to make sure that everything is included + assert os.path.isfile(os.path.join(snapshot_dir, DiffusionPipeline.config_name)) + assert os.path.isfile(os.path.join(snapshot_dir, CONFIG_NAME)) + assert os.path.isfile(os.path.join(snapshot_dir, SCHEDULER_CONFIG_NAME)) + assert os.path.isfile(os.path.join(snapshot_dir, WEIGHTS_NAME)) + assert os.path.isfile(os.path.join(snapshot_dir, "scheduler", SCHEDULER_CONFIG_NAME)) + assert os.path.isfile(os.path.join(snapshot_dir, "unet", WEIGHTS_NAME)) + assert os.path.isfile(os.path.join(snapshot_dir, "unet", WEIGHTS_NAME)) + # let's make sure the super large numpy file: + # https://huggingface.co/hf-internal-testing/unet-pipeline-dummy/blob/main/big_array.npy + # is not downloaded, but all the expected ones + assert not os.path.isfile(os.path.join(snapshot_dir, "big_array.npy")) + + def test_warning_unused_kwargs(self): + model_id = "hf-internal-testing/unet-pipeline-dummy" + logger = logging.get_logger("diffusers.pipelines") + with tempfile.TemporaryDirectory() as tmpdirname: + with CaptureLogger(logger) as cap_logger: + DiffusionPipeline.from_pretrained( + model_id, + not_used=True, + cache_dir=tmpdirname, + force_download=True, + ) + + assert ( + cap_logger.out + == "Keyword arguments {'not_used': True} are not expected by DDPMPipeline and will be ignored.\n" + ) + + def test_from_save_pretrained(self): + # 1. Load models + model = UNet2DModel( + block_out_channels=(32, 64), + layers_per_block=2, + sample_size=32, + in_channels=3, + out_channels=3, + down_block_types=("DownBlock2D", "AttnDownBlock2D"), + up_block_types=("AttnUpBlock2D", "UpBlock2D"), + ) + schedular = DDPMScheduler(num_train_timesteps=10) + + ddpm = DDPMPipeline(model, schedular) + ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + with tempfile.TemporaryDirectory() as tmpdirname: + ddpm.save_pretrained(tmpdirname) + new_ddpm = DDPMPipeline.from_pretrained(tmpdirname) + new_ddpm.to(torch_device) + + generator = torch.Generator(device=torch_device).manual_seed(0) + image = ddpm(generator=generator, num_inference_steps=5, output_type="numpy").images + + generator = torch.Generator(device=torch_device).manual_seed(0) + new_image = new_ddpm(generator=generator, num_inference_steps=5, output_type="numpy").images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't give the same forward pass" + + def test_from_pretrained_hub(self): + model_path = "google/ddpm-cifar10-32" + + scheduler = DDPMScheduler(num_train_timesteps=10) + + ddpm = DDPMPipeline.from_pretrained(model_path, scheduler=scheduler) + ddpm = ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + ddpm_from_hub = DiffusionPipeline.from_pretrained(model_path, scheduler=scheduler) + ddpm_from_hub = ddpm_from_hub.to(torch_device) + ddpm_from_hub.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=torch_device).manual_seed(0) + image = ddpm(generator=generator, num_inference_steps=5, output_type="numpy").images + + generator = torch.Generator(device=torch_device).manual_seed(0) + new_image = ddpm_from_hub(generator=generator, num_inference_steps=5, output_type="numpy").images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't give the same forward pass" + + def test_from_pretrained_hub_pass_model(self): + model_path = "google/ddpm-cifar10-32" + + scheduler = DDPMScheduler(num_train_timesteps=10) + + # pass unet into DiffusionPipeline + unet = UNet2DModel.from_pretrained(model_path) + ddpm_from_hub_custom_model = DiffusionPipeline.from_pretrained(model_path, unet=unet, scheduler=scheduler) + ddpm_from_hub_custom_model = ddpm_from_hub_custom_model.to(torch_device) + ddpm_from_hub_custom_model.set_progress_bar_config(disable=None) + + ddpm_from_hub = DiffusionPipeline.from_pretrained(model_path, scheduler=scheduler) + ddpm_from_hub = ddpm_from_hub.to(torch_device) + ddpm_from_hub_custom_model.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=torch_device).manual_seed(0) + image = ddpm_from_hub_custom_model(generator=generator, num_inference_steps=5, output_type="numpy").images + + generator = torch.Generator(device=torch_device).manual_seed(0) + new_image = ddpm_from_hub(generator=generator, num_inference_steps=5, output_type="numpy").images + + assert np.abs(image - new_image).sum() < 1e-5, "Models don't give the same forward pass" + + def test_output_format(self): + model_path = "google/ddpm-cifar10-32" + + scheduler = DDIMScheduler.from_pretrained(model_path) + pipe = DDIMPipeline.from_pretrained(model_path, scheduler=scheduler) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + images = pipe(output_type="numpy").images + assert images.shape == (1, 32, 32, 3) + assert isinstance(images, np.ndarray) + + images = pipe(output_type="pil", num_inference_steps=4).images + assert isinstance(images, list) + assert len(images) == 1 + assert isinstance(images[0], PIL.Image.Image) + + # use PIL by default + images = pipe(num_inference_steps=4).images + assert isinstance(images, list) + assert isinstance(images[0], PIL.Image.Image) + + def test_from_flax_from_pt(self): + pipe_pt = StableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-torch", safety_checker=None + ) + pipe_pt.to(torch_device) + + if not is_flax_available(): + raise ImportError("Make sure flax is installed.") + + from diffusers import FlaxStableDiffusionPipeline + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe_pt.save_pretrained(tmpdirname) + + pipe_flax, params = FlaxStableDiffusionPipeline.from_pretrained( + tmpdirname, safety_checker=None, from_pt=True + ) + + with tempfile.TemporaryDirectory() as tmpdirname: + pipe_flax.save_pretrained(tmpdirname, params=params) + pipe_pt_2 = StableDiffusionPipeline.from_pretrained(tmpdirname, safety_checker=None, from_flax=True) + pipe_pt_2.to(torch_device) + + prompt = "Hello" + + generator = torch.manual_seed(0) + image_0 = pipe_pt( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + ).images[0] + + generator = torch.manual_seed(0) + image_1 = pipe_pt_2( + [prompt], + generator=generator, + num_inference_steps=2, + output_type="np", + ).images[0] + + assert np.abs(image_0 - image_1).sum() < 1e-5, "Models don't give the same forward pass" + + +@nightly +@require_torch_gpu +class PipelineNightlyTests(unittest.TestCase): + def tearDown(self): + # clean up the VRAM after each test + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_ddpm_ddim_equality_batched(self): + seed = 0 + model_id = "google/ddpm-cifar10-32" + + unet = UNet2DModel.from_pretrained(model_id) + ddpm_scheduler = DDPMScheduler() + ddim_scheduler = DDIMScheduler() + + ddpm = DDPMPipeline(unet=unet, scheduler=ddpm_scheduler) + ddpm.to(torch_device) + ddpm.set_progress_bar_config(disable=None) + + ddim = DDIMPipeline(unet=unet, scheduler=ddim_scheduler) + ddim.to(torch_device) + ddim.set_progress_bar_config(disable=None) + + generator = torch.Generator(device=torch_device).manual_seed(seed) + ddpm_images = ddpm(batch_size=2, generator=generator, output_type="numpy").images + + generator = torch.Generator(device=torch_device).manual_seed(seed) + ddim_images = ddim( + batch_size=2, + generator=generator, + num_inference_steps=1000, + eta=1.0, + output_type="numpy", + use_clipped_model_output=True, # Need this to make DDIM match DDPM + ).images + + # the values aren't exactly equal, but the images look the same visually + assert np.abs(ddpm_images - ddim_images).max() < 1e-1 diff --git a/diffusers/tests/test_pipelines_common.py b/diffusers/tests/test_pipelines_common.py new file mode 100644 index 0000000000000000000000000000000000000000..32f050a51d3cfa2da5123e843ad723dcc1a9c1a7 --- /dev/null +++ b/diffusers/tests/test_pipelines_common.py @@ -0,0 +1,537 @@ +import contextlib +import gc +import inspect +import io +import re +import tempfile +import unittest +from typing import Callable, Union + +import numpy as np +import torch + +import diffusers +from diffusers import ( + CycleDiffusionPipeline, + DanceDiffusionPipeline, + DiffusionPipeline, + RePaintPipeline, + StableDiffusionDepth2ImgPipeline, + StableDiffusionImg2ImgPipeline, +) +from diffusers.utils import logging +from diffusers.utils.import_utils import is_accelerate_available, is_xformers_available +from diffusers.utils.testing_utils import require_torch, torch_device + + +torch.backends.cuda.matmul.allow_tf32 = False + + +@require_torch +class PipelineTesterMixin: + """ + This mixin is designed to be used with unittest.TestCase classes. + It provides a set of common tests for each PyTorch pipeline, e.g. saving and loading the pipeline, + equivalence of dict and tuple outputs, etc. + """ + + allowed_required_args = ["source_prompt", "prompt", "image", "mask_image", "example_image", "class_labels"] + required_optional_params = ["generator", "num_inference_steps", "return_dict"] + num_inference_steps_args = ["num_inference_steps"] + + # set these parameters to False in the child class if the pipeline does not support the corresponding functionality + test_attention_slicing = True + test_cpu_offload = True + test_xformers_attention = True + + def get_generator(self, seed): + device = torch_device if torch_device != "mps" else "cpu" + generator = torch.Generator(device).manual_seed(seed) + return generator + + @property + def pipeline_class(self) -> Union[Callable, DiffusionPipeline]: + raise NotImplementedError( + "You need to set the attribute `pipeline_class = ClassNameOfPipeline` in the child test class. " + "See existing pipeline tests for reference." + ) + + def get_dummy_components(self): + raise NotImplementedError( + "You need to implement `get_dummy_components(self)` in the child test class. " + "See existing pipeline tests for reference." + ) + + def get_dummy_inputs(self, device, seed=0): + raise NotImplementedError( + "You need to implement `get_dummy_inputs(self, device, seed)` in the child test class. " + "See existing pipeline tests for reference." + ) + + def tearDown(self): + # clean up the VRAM after each test in case of CUDA runtime errors + super().tearDown() + gc.collect() + torch.cuda.empty_cache() + + def test_save_load_local(self): + if torch_device == "mps" and self.pipeline_class in ( + DanceDiffusionPipeline, + CycleDiffusionPipeline, + RePaintPipeline, + StableDiffusionImg2ImgPipeline, + ): + # FIXME: inconsistent outputs on MPS + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = pipe(**self.get_dummy_inputs(torch_device)) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs)[0] + + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + pipe_loaded = self.pipeline_class.from_pretrained(tmpdir) + pipe_loaded.to(torch_device) + pipe_loaded.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output_loaded = pipe_loaded(**inputs)[0] + + max_diff = np.abs(output - output_loaded).max() + self.assertLess(max_diff, 1e-4) + + def test_pipeline_call_implements_required_args(self): + assert hasattr(self.pipeline_class, "__call__"), f"{self.pipeline_class} should have a `__call__` method" + parameters = inspect.signature(self.pipeline_class.__call__).parameters + required_parameters = {k: v for k, v in parameters.items() if v.default == inspect._empty} + required_parameters.pop("self") + required_parameters = set(required_parameters) + optional_parameters = set({k for k, v in parameters.items() if v.default != inspect._empty}) + + for param in required_parameters: + if param == "kwargs": + # kwargs can be added if arguments of pipeline call function are deprecated + continue + assert param in self.allowed_required_args + + optional_parameters = set({k for k, v in parameters.items() if v.default != inspect._empty}) + + for param in self.required_optional_params: + assert param in optional_parameters + + def test_inference_batch_consistent(self): + self._test_inference_batch_consistent() + + def _test_inference_batch_consistent(self, batch_sizes=[2, 4, 13]): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + + logger = logging.get_logger(pipe.__module__) + logger.setLevel(level=diffusers.logging.FATAL) + + # batchify inputs + for batch_size in batch_sizes: + batched_inputs = {} + for name, value in inputs.items(): + if name in self.allowed_required_args: + # prompt is string + if name == "prompt": + len_prompt = len(value) + # make unequal batch sizes + batched_inputs[name] = [value[: len_prompt // i] for i in range(1, batch_size + 1)] + + # make last batch super long + batched_inputs[name][-1] = 2000 * "very long" + # or else we have images + else: + batched_inputs[name] = batch_size * [value] + elif name == "batch_size": + batched_inputs[name] = batch_size + else: + batched_inputs[name] = value + + for arg in self.num_inference_steps_args: + batched_inputs[arg] = inputs[arg] + + batched_inputs["output_type"] = None + + if self.pipeline_class.__name__ == "DanceDiffusionPipeline": + batched_inputs.pop("output_type") + + output = pipe(**batched_inputs) + + assert len(output[0]) == batch_size + + batched_inputs["output_type"] = "np" + + if self.pipeline_class.__name__ == "DanceDiffusionPipeline": + batched_inputs.pop("output_type") + + output = pipe(**batched_inputs)[0] + + assert output.shape[0] == batch_size + + logger.setLevel(level=diffusers.logging.WARNING) + + def test_inference_batch_single_identical(self): + self._test_inference_batch_single_identical() + + def _test_inference_batch_single_identical( + self, test_max_difference=None, test_mean_pixel_difference=None, relax_max_difference=False + ): + if self.pipeline_class.__name__ in ["CycleDiffusionPipeline", "RePaintPipeline"]: + # RePaint can hardly be made deterministic since the scheduler is currently always + # nondeterministic + # CycleDiffusion is also slightly nondeterministic + return + + if test_max_difference is None: + # TODO(Pedro) - not sure why, but not at all reproducible at the moment it seems + # make sure that batched and non-batched is identical + test_max_difference = torch_device != "mps" + + if test_mean_pixel_difference is None: + # TODO same as above + test_mean_pixel_difference = torch_device != "mps" + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + + logger = logging.get_logger(pipe.__module__) + logger.setLevel(level=diffusers.logging.FATAL) + + # batchify inputs + batched_inputs = {} + batch_size = 3 + for name, value in inputs.items(): + if name in self.allowed_required_args: + # prompt is string + if name == "prompt": + len_prompt = len(value) + # make unequal batch sizes + batched_inputs[name] = [value[: len_prompt // i] for i in range(1, batch_size + 1)] + + # make last batch super long + batched_inputs[name][-1] = 2000 * "very long" + # or else we have images + else: + batched_inputs[name] = batch_size * [value] + elif name == "batch_size": + batched_inputs[name] = batch_size + elif name == "generator": + batched_inputs[name] = [self.get_generator(i) for i in range(batch_size)] + else: + batched_inputs[name] = value + + for arg in self.num_inference_steps_args: + batched_inputs[arg] = inputs[arg] + + if self.pipeline_class.__name__ != "DanceDiffusionPipeline": + batched_inputs["output_type"] = "np" + + output_batch = pipe(**batched_inputs) + assert output_batch[0].shape[0] == batch_size + + inputs["generator"] = self.get_generator(0) + + output = pipe(**inputs) + + logger.setLevel(level=diffusers.logging.WARNING) + if test_max_difference: + if relax_max_difference: + # Taking the median of the largest differences + # is resilient to outliers + diff = np.abs(output_batch[0][0] - output[0][0]) + diff = diff.flatten() + diff.sort() + max_diff = np.median(diff[-5:]) + else: + max_diff = np.abs(output_batch[0][0] - output[0][0]).max() + assert max_diff < 1e-4 + + if test_mean_pixel_difference: + assert_mean_pixel_difference(output_batch[0][0], output[0][0]) + + def test_dict_tuple_outputs_equivalent(self): + if torch_device == "mps" and self.pipeline_class in ( + DanceDiffusionPipeline, + CycleDiffusionPipeline, + RePaintPipeline, + StableDiffusionImg2ImgPipeline, + ): + # FIXME: inconsistent outputs on MPS + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = pipe(**self.get_dummy_inputs(torch_device)) + + output = pipe(**self.get_dummy_inputs(torch_device))[0] + output_tuple = pipe(**self.get_dummy_inputs(torch_device), return_dict=False)[0] + + max_diff = np.abs(output - output_tuple).max() + self.assertLess(max_diff, 1e-4) + + def test_components_function(self): + init_components = self.get_dummy_components() + pipe = self.pipeline_class(**init_components) + + self.assertTrue(hasattr(pipe, "components")) + self.assertTrue(set(pipe.components.keys()) == set(init_components.keys())) + + @unittest.skipIf(torch_device != "cuda", reason="float16 requires CUDA") + def test_float16_inference(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + for name, module in components.items(): + if hasattr(module, "half"): + components[name] = module.half() + pipe_fp16 = self.pipeline_class(**components) + pipe_fp16.to(torch_device) + pipe_fp16.set_progress_bar_config(disable=None) + + output = pipe(**self.get_dummy_inputs(torch_device))[0] + output_fp16 = pipe_fp16(**self.get_dummy_inputs(torch_device))[0] + + max_diff = np.abs(output - output_fp16).max() + self.assertLess(max_diff, 1e-2, "The outputs of the fp16 and fp32 pipelines are too different.") + + @unittest.skipIf(torch_device != "cuda", reason="float16 requires CUDA") + def test_save_load_float16(self): + components = self.get_dummy_components() + for name, module in components.items(): + if hasattr(module, "half"): + components[name] = module.to(torch_device).half() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs)[0] + + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + pipe_loaded = self.pipeline_class.from_pretrained(tmpdir, torch_dtype=torch.float16) + pipe_loaded.to(torch_device) + pipe_loaded.set_progress_bar_config(disable=None) + + for name, component in pipe_loaded.components.items(): + if hasattr(component, "dtype"): + self.assertTrue( + component.dtype == torch.float16, + f"`{name}.dtype` switched from `float16` to {component.dtype} after loading.", + ) + + inputs = self.get_dummy_inputs(torch_device) + output_loaded = pipe_loaded(**inputs)[0] + + max_diff = np.abs(output - output_loaded).max() + self.assertLess(max_diff, 3e-3, "The output of the fp16 pipeline changed after saving and loading.") + + def test_save_load_optional_components(self): + if not hasattr(self.pipeline_class, "_optional_components"): + return + + if torch_device == "mps" and self.pipeline_class in ( + DanceDiffusionPipeline, + CycleDiffusionPipeline, + RePaintPipeline, + StableDiffusionImg2ImgPipeline, + ): + # FIXME: inconsistent outputs on MPS + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = pipe(**self.get_dummy_inputs(torch_device)) + + # set all optional components to None + for optional_component in pipe._optional_components: + setattr(pipe, optional_component, None) + + inputs = self.get_dummy_inputs(torch_device) + output = pipe(**inputs)[0] + + with tempfile.TemporaryDirectory() as tmpdir: + pipe.save_pretrained(tmpdir) + pipe_loaded = self.pipeline_class.from_pretrained(tmpdir) + pipe_loaded.to(torch_device) + pipe_loaded.set_progress_bar_config(disable=None) + + for optional_component in pipe._optional_components: + self.assertTrue( + getattr(pipe_loaded, optional_component) is None, + f"`{optional_component}` did not stay set to None after loading.", + ) + + inputs = self.get_dummy_inputs(torch_device) + output_loaded = pipe_loaded(**inputs)[0] + + max_diff = np.abs(output - output_loaded).max() + self.assertLess(max_diff, 1e-4) + + @unittest.skipIf(torch_device != "cuda", reason="CUDA and CPU are required to switch devices") + def test_to_device(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.set_progress_bar_config(disable=None) + + pipe.to("cpu") + model_devices = [component.device.type for component in components.values() if hasattr(component, "device")] + self.assertTrue(all(device == "cpu" for device in model_devices)) + + output_cpu = pipe(**self.get_dummy_inputs("cpu"))[0] + self.assertTrue(np.isnan(output_cpu).sum() == 0) + + pipe.to("cuda") + model_devices = [component.device.type for component in components.values() if hasattr(component, "device")] + self.assertTrue(all(device == "cuda" for device in model_devices)) + + output_cuda = pipe(**self.get_dummy_inputs("cuda"))[0] + self.assertTrue(np.isnan(output_cuda).sum() == 0) + + def test_attention_slicing_forward_pass(self): + self._test_attention_slicing_forward_pass() + + def _test_attention_slicing_forward_pass(self, test_max_difference=True): + if not self.test_attention_slicing: + return + + if torch_device == "mps" and self.pipeline_class in ( + DanceDiffusionPipeline, + CycleDiffusionPipeline, + RePaintPipeline, + StableDiffusionImg2ImgPipeline, + StableDiffusionDepth2ImgPipeline, + ): + # FIXME: inconsistent outputs on MPS + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + # Warmup pass when using mps (see #372) + if torch_device == "mps": + _ = pipe(**self.get_dummy_inputs(torch_device)) + + inputs = self.get_dummy_inputs(torch_device) + output_without_slicing = pipe(**inputs)[0] + + pipe.enable_attention_slicing(slice_size=1) + inputs = self.get_dummy_inputs(torch_device) + output_with_slicing = pipe(**inputs)[0] + + if test_max_difference: + max_diff = np.abs(output_with_slicing - output_without_slicing).max() + self.assertLess(max_diff, 1e-3, "Attention slicing should not affect the inference results") + + assert_mean_pixel_difference(output_with_slicing[0], output_without_slicing[0]) + + @unittest.skipIf( + torch_device != "cuda" or not is_accelerate_available(), + reason="CPU offload is only available with CUDA and `accelerate` installed", + ) + def test_cpu_offload_forward_pass(self): + if not self.test_cpu_offload: + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output_without_offload = pipe(**inputs)[0] + + pipe.enable_sequential_cpu_offload() + inputs = self.get_dummy_inputs(torch_device) + output_with_offload = pipe(**inputs)[0] + + max_diff = np.abs(output_with_offload - output_without_offload).max() + self.assertLess(max_diff, 1e-4, "CPU offloading should not affect the inference results") + + @unittest.skipIf( + torch_device != "cuda" or not is_xformers_available(), + reason="XFormers attention is only available with CUDA and `xformers` installed", + ) + def test_xformers_attention_forwardGenerator_pass(self): + if not self.test_xformers_attention: + return + + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + pipe.set_progress_bar_config(disable=None) + + inputs = self.get_dummy_inputs(torch_device) + output_without_offload = pipe(**inputs)[0] + + pipe.enable_xformers_memory_efficient_attention() + inputs = self.get_dummy_inputs(torch_device) + output_with_offload = pipe(**inputs)[0] + + max_diff = np.abs(output_with_offload - output_without_offload).max() + self.assertLess(max_diff, 1e-4, "XFormers attention should not affect the inference results") + + def test_progress_bar(self): + components = self.get_dummy_components() + pipe = self.pipeline_class(**components) + pipe.to(torch_device) + + inputs = self.get_dummy_inputs(torch_device) + with io.StringIO() as stderr, contextlib.redirect_stderr(stderr): + _ = pipe(**inputs) + stderr = stderr.getvalue() + # we can't calculate the number of progress steps beforehand e.g. for strength-dependent img2img, + # so we just match "5" in "#####| 1/5 [00:01<00:00]" + max_steps = re.search("/(.*?) ", stderr).group(1) + self.assertTrue(max_steps is not None and len(max_steps) > 0) + self.assertTrue( + f"{max_steps}/{max_steps}" in stderr, "Progress bar should be enabled and stopped at the max step" + ) + + pipe.set_progress_bar_config(disable=True) + with io.StringIO() as stderr, contextlib.redirect_stderr(stderr): + _ = pipe(**inputs) + self.assertTrue(stderr.getvalue() == "", "Progress bar should be disabled") + + +# Some models (e.g. unCLIP) are extremely likely to significantly deviate depending on which hardware is used. +# This helper function is used to check that the image doesn't deviate on average more than 10 pixels from a +# reference image. +def assert_mean_pixel_difference(image, expected_image): + image = np.asarray(DiffusionPipeline.numpy_to_pil(image)[0], dtype=np.float32) + expected_image = np.asarray(DiffusionPipeline.numpy_to_pil(expected_image)[0], dtype=np.float32) + avg_diff = np.abs(image - expected_image).mean() + assert avg_diff < 10, f"Error image deviates {avg_diff} pixels on average" diff --git a/diffusers/tests/test_pipelines_flax.py b/diffusers/tests/test_pipelines_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..4f005353773935a058324214637df1e7a77a329e --- /dev/null +++ b/diffusers/tests/test_pipelines_flax.py @@ -0,0 +1,226 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import os +import tempfile +import unittest + +import numpy as np + +from diffusers.utils import is_flax_available +from diffusers.utils.testing_utils import require_flax, slow + + +if is_flax_available(): + import jax + import jax.numpy as jnp + from flax.jax_utils import replicate + from flax.training.common_utils import shard + from jax import pmap + + from diffusers import FlaxDDIMScheduler, FlaxDiffusionPipeline, FlaxStableDiffusionPipeline + + +@require_flax +class DownloadTests(unittest.TestCase): + def test_download_only_pytorch(self): + with tempfile.TemporaryDirectory() as tmpdirname: + # pipeline has Flax weights + _ = FlaxDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-pipe", safety_checker=None, cache_dir=tmpdirname + ) + + all_root_files = [t[-1] for t in os.walk(os.path.join(tmpdirname, os.listdir(tmpdirname)[0], "snapshots"))] + files = [item for sublist in all_root_files for item in sublist] + + # None of the downloaded files should be a PyTorch file even if we have some here: + # https://huggingface.co/hf-internal-testing/tiny-stable-diffusion-pipe/blob/main/unet/diffusion_pytorch_model.bin + assert not any(f.endswith(".bin") for f in files) + + +@slow +@require_flax +class FlaxPipelineTests(unittest.TestCase): + def test_dummy_all_tpus(self): + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "hf-internal-testing/tiny-stable-diffusion-pipe", safety_checker=None + ) + + prompt = ( + "A cinematic film still of Morgan Freeman starring as Jimi Hendrix, portrait, 40mm lens, shallow depth of" + " field, close up, split lighting, cinematic" + ) + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 4 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = pipeline.prepare_inputs(prompt) + + p_sample = pmap(pipeline.__call__, static_broadcasted_argnums=(3,)) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, num_samples) + prompt_ids = shard(prompt_ids) + + images = p_sample(prompt_ids, params, prng_seed, num_inference_steps).images + + assert images.shape == (num_samples, 1, 64, 64, 3) + if jax.device_count() == 8: + assert np.abs(np.abs(images[0, 0, :2, :2, -2:], dtype=np.float32).sum() - 3.1111548) < 1e-3 + assert np.abs(np.abs(images, dtype=np.float32).sum() - 199746.95) < 5e-1 + + images_pil = pipeline.numpy_to_pil(np.asarray(images.reshape((num_samples,) + images.shape[-3:]))) + + assert len(images_pil) == num_samples + + def test_stable_diffusion_v1_4(self): + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", revision="flax", safety_checker=None + ) + + prompt = ( + "A cinematic film still of Morgan Freeman starring as Jimi Hendrix, portrait, 40mm lens, shallow depth of" + " field, close up, split lighting, cinematic" + ) + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 50 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = pipeline.prepare_inputs(prompt) + + p_sample = pmap(pipeline.__call__, static_broadcasted_argnums=(3,)) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, num_samples) + prompt_ids = shard(prompt_ids) + + images = p_sample(prompt_ids, params, prng_seed, num_inference_steps).images + + assert images.shape == (num_samples, 1, 512, 512, 3) + if jax.device_count() == 8: + assert np.abs((np.abs(images[0, 0, :2, :2, -2:], dtype=np.float32).sum() - 0.05652401)) < 1e-3 + assert np.abs((np.abs(images, dtype=np.float32).sum() - 2383808.2)) < 5e-1 + + def test_stable_diffusion_v1_4_bfloat_16(self): + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", revision="bf16", dtype=jnp.bfloat16, safety_checker=None + ) + + prompt = ( + "A cinematic film still of Morgan Freeman starring as Jimi Hendrix, portrait, 40mm lens, shallow depth of" + " field, close up, split lighting, cinematic" + ) + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 50 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = pipeline.prepare_inputs(prompt) + + p_sample = pmap(pipeline.__call__, static_broadcasted_argnums=(3,)) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, num_samples) + prompt_ids = shard(prompt_ids) + + images = p_sample(prompt_ids, params, prng_seed, num_inference_steps).images + + assert images.shape == (num_samples, 1, 512, 512, 3) + if jax.device_count() == 8: + assert np.abs((np.abs(images[0, 0, :2, :2, -2:], dtype=np.float32).sum() - 0.06652832)) < 1e-3 + assert np.abs((np.abs(images, dtype=np.float32).sum() - 2384849.8)) < 5e-1 + + def test_stable_diffusion_v1_4_bfloat_16_with_safety(self): + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", revision="bf16", dtype=jnp.bfloat16 + ) + + prompt = ( + "A cinematic film still of Morgan Freeman starring as Jimi Hendrix, portrait, 40mm lens, shallow depth of" + " field, close up, split lighting, cinematic" + ) + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 50 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = pipeline.prepare_inputs(prompt) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, num_samples) + prompt_ids = shard(prompt_ids) + + images = pipeline(prompt_ids, params, prng_seed, num_inference_steps, jit=True).images + + assert images.shape == (num_samples, 1, 512, 512, 3) + if jax.device_count() == 8: + assert np.abs((np.abs(images[0, 0, :2, :2, -2:], dtype=np.float32).sum() - 0.06652832)) < 1e-3 + assert np.abs((np.abs(images, dtype=np.float32).sum() - 2384849.8)) < 5e-1 + + def test_stable_diffusion_v1_4_bfloat_16_ddim(self): + scheduler = FlaxDDIMScheduler( + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + set_alpha_to_one=False, + steps_offset=1, + ) + + pipeline, params = FlaxStableDiffusionPipeline.from_pretrained( + "CompVis/stable-diffusion-v1-4", + revision="bf16", + dtype=jnp.bfloat16, + scheduler=scheduler, + safety_checker=None, + ) + scheduler_state = scheduler.create_state() + + params["scheduler"] = scheduler_state + + prompt = ( + "A cinematic film still of Morgan Freeman starring as Jimi Hendrix, portrait, 40mm lens, shallow depth of" + " field, close up, split lighting, cinematic" + ) + + prng_seed = jax.random.PRNGKey(0) + num_inference_steps = 50 + + num_samples = jax.device_count() + prompt = num_samples * [prompt] + prompt_ids = pipeline.prepare_inputs(prompt) + + p_sample = pmap(pipeline.__call__, static_broadcasted_argnums=(3,)) + + # shard inputs and rng + params = replicate(params) + prng_seed = jax.random.split(prng_seed, num_samples) + prompt_ids = shard(prompt_ids) + + images = p_sample(prompt_ids, params, prng_seed, num_inference_steps).images + + assert images.shape == (num_samples, 1, 512, 512, 3) + if jax.device_count() == 8: + assert np.abs((np.abs(images[0, 0, :2, :2, -2:], dtype=np.float32).sum() - 0.045043945)) < 1e-3 + assert np.abs((np.abs(images, dtype=np.float32).sum() - 2347693.5)) < 5e-1 diff --git a/diffusers/tests/test_pipelines_onnx_common.py b/diffusers/tests/test_pipelines_onnx_common.py new file mode 100644 index 0000000000000000000000000000000000000000..575ecd0075318e8ec62ab7cd76bff5b0b1ca82ad --- /dev/null +++ b/diffusers/tests/test_pipelines_onnx_common.py @@ -0,0 +1,12 @@ +from diffusers.utils.testing_utils import require_onnxruntime + + +@require_onnxruntime +class OnnxPipelineTesterMixin: + """ + This mixin is designed to be used with unittest.TestCase classes. + It provides a set of common tests for each ONNXRuntime pipeline, e.g. saving and loading the pipeline, + equivalence of dict and tuple outputs, etc. + """ + + pass diff --git a/diffusers/tests/test_scheduler.py b/diffusers/tests/test_scheduler.py new file mode 100644 index 0000000000000000000000000000000000000000..f38b6b6b345259b7ef681193cf9a49889abac23e --- /dev/null +++ b/diffusers/tests/test_scheduler.py @@ -0,0 +1,2885 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. +import inspect +import json +import os +import tempfile +import unittest +from typing import Dict, List, Tuple + +import numpy as np +import torch +import torch.nn.functional as F + +import diffusers +from diffusers import ( + DDIMScheduler, + DDPMScheduler, + DEISMultistepScheduler, + DPMSolverMultistepScheduler, + DPMSolverSinglestepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + HeunDiscreteScheduler, + IPNDMScheduler, + KDPM2AncestralDiscreteScheduler, + KDPM2DiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + ScoreSdeVeScheduler, + UnCLIPScheduler, + VQDiffusionScheduler, + logging, +) +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.schedulers.scheduling_utils import SchedulerMixin +from diffusers.utils import torch_device +from diffusers.utils.testing_utils import CaptureLogger + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class SchedulerObject(SchedulerMixin, ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + e=[1, 3], + ): + pass + + +class SchedulerObject2(SchedulerMixin, ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + f=[1, 3], + ): + pass + + +class SchedulerObject3(SchedulerMixin, ConfigMixin): + config_name = "config.json" + + @register_to_config + def __init__( + self, + a=2, + b=5, + c=(2, 5), + d="for diffusion", + e=[1, 3], + f=[1, 3], + ): + pass + + +class SchedulerBaseTests(unittest.TestCase): + def test_save_load_from_different_config(self): + obj = SchedulerObject() + + # mock add obj class to `diffusers` + setattr(diffusers, "SchedulerObject", SchedulerObject) + logger = logging.get_logger("diffusers.configuration_utils") + + with tempfile.TemporaryDirectory() as tmpdirname: + obj.save_config(tmpdirname) + with CaptureLogger(logger) as cap_logger_1: + config = SchedulerObject2.load_config(tmpdirname) + new_obj_1 = SchedulerObject2.from_config(config) + + # now save a config parameter that is not expected + with open(os.path.join(tmpdirname, SchedulerObject.config_name), "r") as f: + data = json.load(f) + data["unexpected"] = True + + with open(os.path.join(tmpdirname, SchedulerObject.config_name), "w") as f: + json.dump(data, f) + + with CaptureLogger(logger) as cap_logger_2: + config = SchedulerObject.load_config(tmpdirname) + new_obj_2 = SchedulerObject.from_config(config) + + with CaptureLogger(logger) as cap_logger_3: + config = SchedulerObject2.load_config(tmpdirname) + new_obj_3 = SchedulerObject2.from_config(config) + + assert new_obj_1.__class__ == SchedulerObject2 + assert new_obj_2.__class__ == SchedulerObject + assert new_obj_3.__class__ == SchedulerObject2 + + assert cap_logger_1.out == "" + assert ( + cap_logger_2.out + == "The config attributes {'unexpected': True} were passed to SchedulerObject, but are not expected and" + " will" + " be ignored. Please verify your config.json configuration file.\n" + ) + assert cap_logger_2.out.replace("SchedulerObject", "SchedulerObject2") == cap_logger_3.out + + def test_save_load_compatible_schedulers(self): + SchedulerObject2._compatibles = ["SchedulerObject"] + SchedulerObject._compatibles = ["SchedulerObject2"] + + obj = SchedulerObject() + + # mock add obj class to `diffusers` + setattr(diffusers, "SchedulerObject", SchedulerObject) + setattr(diffusers, "SchedulerObject2", SchedulerObject2) + logger = logging.get_logger("diffusers.configuration_utils") + + with tempfile.TemporaryDirectory() as tmpdirname: + obj.save_config(tmpdirname) + + # now save a config parameter that is expected by another class, but not origin class + with open(os.path.join(tmpdirname, SchedulerObject.config_name), "r") as f: + data = json.load(f) + data["f"] = [0, 0] + data["unexpected"] = True + + with open(os.path.join(tmpdirname, SchedulerObject.config_name), "w") as f: + json.dump(data, f) + + with CaptureLogger(logger) as cap_logger: + config = SchedulerObject.load_config(tmpdirname) + new_obj = SchedulerObject.from_config(config) + + assert new_obj.__class__ == SchedulerObject + + assert ( + cap_logger.out + == "The config attributes {'unexpected': True} were passed to SchedulerObject, but are not expected and" + " will" + " be ignored. Please verify your config.json configuration file.\n" + ) + + def test_save_load_from_different_config_comp_schedulers(self): + SchedulerObject3._compatibles = ["SchedulerObject", "SchedulerObject2"] + SchedulerObject2._compatibles = ["SchedulerObject", "SchedulerObject3"] + SchedulerObject._compatibles = ["SchedulerObject2", "SchedulerObject3"] + + obj = SchedulerObject() + + # mock add obj class to `diffusers` + setattr(diffusers, "SchedulerObject", SchedulerObject) + setattr(diffusers, "SchedulerObject2", SchedulerObject2) + setattr(diffusers, "SchedulerObject3", SchedulerObject3) + logger = logging.get_logger("diffusers.configuration_utils") + logger.setLevel(diffusers.logging.INFO) + + with tempfile.TemporaryDirectory() as tmpdirname: + obj.save_config(tmpdirname) + + with CaptureLogger(logger) as cap_logger_1: + config = SchedulerObject.load_config(tmpdirname) + new_obj_1 = SchedulerObject.from_config(config) + + with CaptureLogger(logger) as cap_logger_2: + config = SchedulerObject2.load_config(tmpdirname) + new_obj_2 = SchedulerObject2.from_config(config) + + with CaptureLogger(logger) as cap_logger_3: + config = SchedulerObject3.load_config(tmpdirname) + new_obj_3 = SchedulerObject3.from_config(config) + + assert new_obj_1.__class__ == SchedulerObject + assert new_obj_2.__class__ == SchedulerObject2 + assert new_obj_3.__class__ == SchedulerObject3 + + assert cap_logger_1.out == "" + assert cap_logger_2.out == "{'f'} was not found in config. Values will be initialized to default values.\n" + assert cap_logger_3.out == "{'f'} was not found in config. Values will be initialized to default values.\n" + + +class SchedulerCommonTest(unittest.TestCase): + scheduler_classes = () + forward_default_kwargs = () + + @property + def dummy_sample(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + sample = torch.rand((batch_size, num_channels, height, width)) + + return sample + + @property + def dummy_sample_deter(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + num_elems = batch_size * num_channels * height * width + sample = torch.arange(num_elems) + sample = sample.reshape(num_channels, height, width, batch_size) + sample = sample / num_elems + sample = sample.permute(3, 0, 1, 2) + + return sample + + def get_scheduler_config(self): + raise NotImplementedError + + def dummy_model(self): + def model(sample, t, *args): + return sample * t / (t + 1) + + return model + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + # TODO(Suraj) - delete the following two lines once DDPM, DDIM, and PNDM have timesteps casted to float by default + if scheduler_class in (EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, LMSDiscreteScheduler): + time_step = float(time_step) + + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class == VQDiffusionScheduler: + num_vec_classes = scheduler_config["num_vec_classes"] + sample = self.dummy_sample(num_vec_classes) + model = self.dummy_model(num_vec_classes) + residual = model(sample, time_step) + else: + sample = self.dummy_sample + residual = 0.1 * sample + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + new_scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # Set the seed before step() as some schedulers are stochastic like EulerAncestralDiscreteScheduler, EulerDiscreteScheduler + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + kwargs.update(forward_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + if scheduler_class in (EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, LMSDiscreteScheduler): + time_step = float(time_step) + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class == VQDiffusionScheduler: + num_vec_classes = scheduler_config["num_vec_classes"] + sample = self.dummy_sample(num_vec_classes) + model = self.dummy_model(num_vec_classes) + residual = model(sample, time_step) + else: + sample = self.dummy_sample + residual = 0.1 * sample + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + new_scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + timestep = 1 + if scheduler_class in (EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, LMSDiscreteScheduler): + timestep = float(timestep) + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class == VQDiffusionScheduler: + num_vec_classes = scheduler_config["num_vec_classes"] + sample = self.dummy_sample(num_vec_classes) + model = self.dummy_model(num_vec_classes) + residual = model(sample, timestep) + else: + sample = self.dummy_sample + residual = 0.1 * sample + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + new_scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + output = scheduler.step(residual, timestep, sample, **kwargs).prev_sample + + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + new_output = new_scheduler.step(residual, timestep, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_compatibles(self): + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + + scheduler = scheduler_class(**scheduler_config) + + assert all(c is not None for c in scheduler.compatibles) + + for comp_scheduler_cls in scheduler.compatibles: + comp_scheduler = comp_scheduler_cls.from_config(scheduler.config) + assert comp_scheduler is not None + + new_scheduler = scheduler_class.from_config(comp_scheduler.config) + + new_scheduler_config = {k: v for k, v in new_scheduler.config.items() if k in scheduler.config} + scheduler_diff = {k: v for k, v in new_scheduler.config.items() if k not in scheduler.config} + + # make sure that configs are essentially identical + assert new_scheduler_config == dict(scheduler.config) + + # make sure that only differences are for configs that are not in init + init_keys = inspect.signature(scheduler_class.__init__).parameters.keys() + assert set(scheduler_diff.keys()).intersection(set(init_keys)) == set() + + def test_from_pretrained(self): + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + + scheduler = scheduler_class(**scheduler_config) + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_pretrained(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + assert scheduler.config == new_scheduler.config + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + timestep_0 = 0 + timestep_1 = 1 + + for scheduler_class in self.scheduler_classes: + if scheduler_class in (EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, LMSDiscreteScheduler): + timestep_0 = float(timestep_0) + timestep_1 = float(timestep_1) + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class == VQDiffusionScheduler: + num_vec_classes = scheduler_config["num_vec_classes"] + sample = self.dummy_sample(num_vec_classes) + model = self.dummy_model(num_vec_classes) + residual = model(sample, timestep_0) + else: + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output_0 = scheduler.step(residual, timestep_0, sample, **kwargs).prev_sample + output_1 = scheduler.step(residual, timestep_1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_scheduler_outputs_equivalence(self): + def set_nan_tensor_to_zero(t): + t[t != t] = 0 + return t + + def recursive_check(tuple_object, dict_object): + if isinstance(tuple_object, (List, Tuple)): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object, dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif isinstance(tuple_object, Dict): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object.values(), dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif tuple_object is None: + return + else: + self.assertTrue( + torch.allclose( + set_nan_tensor_to_zero(tuple_object), set_nan_tensor_to_zero(dict_object), atol=1e-5 + ), + msg=( + "Tuple and dict output are not equal. Difference:" + f" {torch.max(torch.abs(tuple_object - dict_object))}. Tuple has `nan`:" + f" {torch.isnan(tuple_object).any()} and `inf`: {torch.isinf(tuple_object)}. Dict has" + f" `nan`: {torch.isnan(dict_object).any()} and `inf`: {torch.isinf(dict_object)}." + ), + ) + + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", 50) + + timestep = 0 + if len(self.scheduler_classes) > 0 and self.scheduler_classes[0] == IPNDMScheduler: + timestep = 1 + + for scheduler_class in self.scheduler_classes: + if scheduler_class in (EulerAncestralDiscreteScheduler, EulerDiscreteScheduler, LMSDiscreteScheduler): + timestep = float(timestep) + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class == VQDiffusionScheduler: + num_vec_classes = scheduler_config["num_vec_classes"] + sample = self.dummy_sample(num_vec_classes) + model = self.dummy_model(num_vec_classes) + residual = model(sample, timestep) + else: + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # Set the seed before state as some schedulers are stochastic like EulerAncestralDiscreteScheduler, EulerDiscreteScheduler + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + outputs_dict = scheduler.step(residual, timestep, sample, **kwargs) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # Set the seed before state as some schedulers are stochastic like EulerAncestralDiscreteScheduler, EulerDiscreteScheduler + if "generator" in set(inspect.signature(scheduler.step).parameters.keys()): + kwargs["generator"] = torch.manual_seed(0) + outputs_tuple = scheduler.step(residual, timestep, sample, return_dict=False, **kwargs) + + recursive_check(outputs_tuple, outputs_dict) + + def test_scheduler_public_api(self): + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + if scheduler_class != VQDiffusionScheduler: + self.assertTrue( + hasattr(scheduler, "init_noise_sigma"), + f"{scheduler_class} does not implement a required attribute `init_noise_sigma`", + ) + self.assertTrue( + hasattr(scheduler, "scale_model_input"), + ( + f"{scheduler_class} does not implement a required class method `scale_model_input(sample," + " timestep)`" + ), + ) + self.assertTrue( + hasattr(scheduler, "step"), + f"{scheduler_class} does not implement a required class method `step(...)`", + ) + + if scheduler_class != VQDiffusionScheduler: + sample = self.dummy_sample + scaled_sample = scheduler.scale_model_input(sample, 0.0) + self.assertEqual(sample.shape, scaled_sample.shape) + + def test_add_noise_device(self): + for scheduler_class in self.scheduler_classes: + if scheduler_class == IPNDMScheduler: + continue + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(100) + + sample = self.dummy_sample.to(torch_device) + scaled_sample = scheduler.scale_model_input(sample, 0.0) + self.assertEqual(sample.shape, scaled_sample.shape) + + noise = torch.randn_like(scaled_sample).to(torch_device) + t = scheduler.timesteps[5][None] + noised = scheduler.add_noise(scaled_sample, noise, t) + self.assertEqual(noised.shape, scaled_sample.shape) + + def test_deprecated_kwargs(self): + for scheduler_class in self.scheduler_classes: + has_kwarg_in_model_class = "kwargs" in inspect.signature(scheduler_class.__init__).parameters + has_deprecated_kwarg = len(scheduler_class._deprecated_kwargs) > 0 + + if has_kwarg_in_model_class and not has_deprecated_kwarg: + raise ValueError( + f"{scheduler_class} has `**kwargs` in its __init__ method but has not defined any deprecated" + " kwargs under the `_deprecated_kwargs` class attribute. Make sure to either remove `**kwargs` if" + " there are no deprecated arguments or add the deprecated argument with `_deprecated_kwargs =" + " []`" + ) + + if not has_kwarg_in_model_class and has_deprecated_kwarg: + raise ValueError( + f"{scheduler_class} doesn't have `**kwargs` in its __init__ method but has defined deprecated" + " kwargs under the `_deprecated_kwargs` class attribute. Make sure to either add the `**kwargs`" + f" argument to {self.model_class}.__init__ if there are deprecated arguments or remove the" + " deprecated argument from `_deprecated_kwargs = []`" + ) + + def test_trained_betas(self): + for scheduler_class in self.scheduler_classes: + if scheduler_class == VQDiffusionScheduler: + continue + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config, trained_betas=np.array([0.0, 0.1])) + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_pretrained(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + assert scheduler.betas.tolist() == new_scheduler.betas.tolist() + + +class DDPMSchedulerTest(SchedulerCommonTest): + scheduler_classes = (DDPMScheduler,) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "variance_type": "fixed_small", + "clip_sample": True, + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [1, 5, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001, 0.01, 0.1], [0.002, 0.02, 0.2, 2]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_variance_type(self): + for variance in ["fixed_small", "fixed_large", "other"]: + self.check_over_configs(variance_type=variance) + + def test_clip_sample(self): + for clip_sample in [True, False]: + self.check_over_configs(clip_sample=clip_sample) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "sample", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_time_indices(self): + for t in [0, 500, 999]: + self.check_over_forward(time_step=t) + + def test_variance(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + assert torch.sum(torch.abs(scheduler._get_variance(0) - 0.0)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(487) - 0.00979)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(999) - 0.02)) < 1e-5 + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + num_trained_timesteps = len(scheduler) + + model = self.dummy_model() + sample = self.dummy_sample_deter + generator = torch.manual_seed(0) + + for t in reversed(range(num_trained_timesteps)): + # 1. predict noise residual + residual = model(sample, t) + + # 2. predict previous mean of sample x_t-1 + pred_prev_sample = scheduler.step(residual, t, sample, generator=generator).prev_sample + + # if t > 0: + # noise = self.dummy_sample_deter + # variance = scheduler.get_variance(t) ** (0.5) * noise + # + # sample = pred_prev_sample + variance + sample = pred_prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 258.9606) < 1e-2 + assert abs(result_mean.item() - 0.3372) < 1e-3 + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + num_trained_timesteps = len(scheduler) + + model = self.dummy_model() + sample = self.dummy_sample_deter + generator = torch.manual_seed(0) + + for t in reversed(range(num_trained_timesteps)): + # 1. predict noise residual + residual = model(sample, t) + + # 2. predict previous mean of sample x_t-1 + pred_prev_sample = scheduler.step(residual, t, sample, generator=generator).prev_sample + + # if t > 0: + # noise = self.dummy_sample_deter + # variance = scheduler.get_variance(t) ** (0.5) * noise + # + # sample = pred_prev_sample + variance + sample = pred_prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 202.0296) < 1e-2 + assert abs(result_mean.item() - 0.2631) < 1e-3 + + +class DDIMSchedulerTest(SchedulerCommonTest): + scheduler_classes = (DDIMScheduler,) + forward_default_kwargs = (("eta", 0.0), ("num_inference_steps", 50)) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "clip_sample": True, + } + + config.update(**kwargs) + return config + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps, eta = 10, 0.0 + + model = self.dummy_model() + sample = self.dummy_sample_deter + + scheduler.set_timesteps(num_inference_steps) + + for t in scheduler.timesteps: + residual = model(sample, t) + sample = scheduler.step(residual, t, sample, eta).prev_sample + + return sample + + def test_timesteps(self): + for timesteps in [100, 500, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_steps_offset(self): + for steps_offset in [0, 1]: + self.check_over_configs(steps_offset=steps_offset) + + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(steps_offset=1) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(5) + assert torch.equal(scheduler.timesteps, torch.LongTensor([801, 601, 401, 201, 1])) + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001, 0.01, 0.1], [0.002, 0.02, 0.2, 2]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_clip_sample(self): + for clip_sample in [True, False]: + self.check_over_configs(clip_sample=clip_sample) + + def test_time_indices(self): + for t in [1, 10, 49]: + self.check_over_forward(time_step=t) + + def test_inference_steps(self): + for t, num_inference_steps in zip([1, 10, 50], [10, 50, 500]): + self.check_over_forward(time_step=t, num_inference_steps=num_inference_steps) + + def test_eta(self): + for t, eta in zip([1, 10, 49], [0.0, 0.5, 1.0]): + self.check_over_forward(time_step=t, eta=eta) + + def test_variance(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + assert torch.sum(torch.abs(scheduler._get_variance(0, 0) - 0.0)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(420, 400) - 0.14771)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(980, 960) - 0.32460)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(0, 0) - 0.0)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(487, 486) - 0.00979)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(999, 998) - 0.02)) < 1e-5 + + def test_full_loop_no_noise(self): + sample = self.full_loop() + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 172.0067) < 1e-2 + assert abs(result_mean.item() - 0.223967) < 1e-3 + + def test_full_loop_with_v_prediction(self): + sample = self.full_loop(prediction_type="v_prediction") + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 52.5302) < 1e-2 + assert abs(result_mean.item() - 0.0684) < 1e-3 + + def test_full_loop_with_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=True, beta_start=0.01) + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 149.8295) < 1e-2 + assert abs(result_mean.item() - 0.1951) < 1e-3 + + def test_full_loop_with_no_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=False, beta_start=0.01) + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 149.0784) < 1e-2 + assert abs(result_mean.item() - 0.1941) < 1e-3 + + +class DPMSolverSinglestepSchedulerTest(SchedulerCommonTest): + scheduler_classes = (DPMSolverSinglestepScheduler,) + forward_default_kwargs = (("num_inference_steps", 25),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "solver_order": 2, + "prediction_type": "epsilon", + "thresholding": False, + "sample_max_value": 1.0, + "algorithm_type": "dpmsolver++", + "solver_type": "midpoint", + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + new_scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output, new_output = sample, sample + for t in range(time_step, time_step + scheduler.config.solver_order + 1): + output = scheduler.step(residual, t, output, **kwargs).prev_sample + new_output = new_scheduler.step(residual, t, new_output, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residual (must be after setting timesteps) + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + return sample + + def test_timesteps(self): + for timesteps in [25, 50, 100, 999, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_thresholding(self): + self.check_over_configs(thresholding=False) + for order in [1, 2, 3]: + for solver_type in ["midpoint", "heun"]: + for threshold in [0.5, 1.0, 2.0]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + thresholding=True, + prediction_type=prediction_type, + sample_max_value=threshold, + algorithm_type="dpmsolver++", + solver_order=order, + solver_type=solver_type, + ) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_solver_order_and_type(self): + for algorithm_type in ["dpmsolver", "dpmsolver++"]: + for solver_type in ["midpoint", "heun"]: + for order in [1, 2, 3]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + sample = self.full_loop( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + assert not torch.isnan(sample).any(), "Samples have nan numbers" + + def test_lower_order_final(self): + self.check_over_configs(lower_order_final=True) + self.check_over_configs(lower_order_final=False) + + def test_inference_steps(self): + for num_inference_steps in [1, 2, 3, 5, 10, 50, 100, 999, 1000]: + self.check_over_forward(num_inference_steps=num_inference_steps, time_step=0) + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.2791) < 1e-3 + + def test_full_loop_with_v_prediction(self): + sample = self.full_loop(prediction_type="v_prediction") + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.1453) < 1e-3 + + def test_fp16_support(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(thresholding=True, dynamic_thresholding_ratio=0) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter.half() + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + assert sample.dtype == torch.float16 + + +class DPMSolverMultistepSchedulerTest(SchedulerCommonTest): + scheduler_classes = (DPMSolverMultistepScheduler,) + forward_default_kwargs = (("num_inference_steps", 25),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "solver_order": 2, + "prediction_type": "epsilon", + "thresholding": False, + "sample_max_value": 1.0, + "algorithm_type": "dpmsolver++", + "solver_type": "midpoint", + "lower_order_final": False, + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + new_scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output, new_output = sample, sample + for t in range(time_step, time_step + scheduler.config.solver_order + 1): + output = scheduler.step(residual, t, output, **kwargs).prev_sample + new_output = new_scheduler.step(residual, t, new_output, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residual (must be after setting timesteps) + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + return sample + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # copy over dummy past residuals (must be done after set_timesteps) + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + time_step_0 = scheduler.timesteps[5] + time_step_1 = scheduler.timesteps[6] + + output_0 = scheduler.step(residual, time_step_0, sample, **kwargs).prev_sample + output_1 = scheduler.step(residual, time_step_1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [25, 50, 100, 999, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_thresholding(self): + self.check_over_configs(thresholding=False) + for order in [1, 2, 3]: + for solver_type in ["midpoint", "heun"]: + for threshold in [0.5, 1.0, 2.0]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + thresholding=True, + prediction_type=prediction_type, + sample_max_value=threshold, + algorithm_type="dpmsolver++", + solver_order=order, + solver_type=solver_type, + ) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_solver_order_and_type(self): + for algorithm_type in ["dpmsolver", "dpmsolver++"]: + for solver_type in ["midpoint", "heun"]: + for order in [1, 2, 3]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + sample = self.full_loop( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + assert not torch.isnan(sample).any(), "Samples have nan numbers" + + def test_lower_order_final(self): + self.check_over_configs(lower_order_final=True) + self.check_over_configs(lower_order_final=False) + + def test_inference_steps(self): + for num_inference_steps in [1, 2, 3, 5, 10, 50, 100, 999, 1000]: + self.check_over_forward(num_inference_steps=num_inference_steps, time_step=0) + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.3301) < 1e-3 + + def test_full_loop_with_v_prediction(self): + sample = self.full_loop(prediction_type="v_prediction") + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.2251) < 1e-3 + + def test_fp16_support(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(thresholding=True, dynamic_thresholding_ratio=0) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter.half() + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + assert sample.dtype == torch.float16 + + +class PNDMSchedulerTest(SchedulerCommonTest): + scheduler_classes = (PNDMScheduler,) + forward_default_kwargs = (("num_inference_steps", 50),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + scheduler.ets = dummy_past_residuals[:] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + new_scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + new_scheduler.ets = dummy_past_residuals[:] + + output = scheduler.step_prk(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step_prk(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step_plms(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step_plms(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.ets = dummy_past_residuals[:] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residual (must be after setting timesteps) + new_scheduler.ets = dummy_past_residuals[:] + + output = scheduler.step_prk(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step_prk(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step_plms(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step_plms(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.prk_timesteps): + residual = model(sample, t) + sample = scheduler.step_prk(residual, t, sample).prev_sample + + for i, t in enumerate(scheduler.plms_timesteps): + residual = model(sample, t) + sample = scheduler.step_plms(residual, t, sample).prev_sample + + return sample + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # copy over dummy past residuals (must be done after set_timesteps) + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + scheduler.ets = dummy_past_residuals[:] + + output_0 = scheduler.step_prk(residual, 0, sample, **kwargs).prev_sample + output_1 = scheduler.step_prk(residual, 1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + output_0 = scheduler.step_plms(residual, 0, sample, **kwargs).prev_sample + output_1 = scheduler.step_plms(residual, 1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_steps_offset(self): + for steps_offset in [0, 1]: + self.check_over_configs(steps_offset=steps_offset) + + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(steps_offset=1) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(10) + assert torch.equal( + scheduler.timesteps, + torch.LongTensor( + [901, 851, 851, 801, 801, 751, 751, 701, 701, 651, 651, 601, 601, 501, 401, 301, 201, 101, 1] + ), + ) + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001], [0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_time_indices(self): + for t in [1, 5, 10]: + self.check_over_forward(time_step=t) + + def test_inference_steps(self): + for t, num_inference_steps in zip([1, 5, 10], [10, 50, 100]): + self.check_over_forward(num_inference_steps=num_inference_steps) + + def test_pow_of_3_inference_steps(self): + # earlier version of set_timesteps() caused an error indexing alpha's with inference steps as power of 3 + num_inference_steps = 27 + + for scheduler_class in self.scheduler_classes: + sample = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(num_inference_steps) + + # before power of 3 fix, would error on first step, so we only need to do two + for i, t in enumerate(scheduler.prk_timesteps[:2]): + sample = scheduler.step_prk(residual, t, sample).prev_sample + + def test_inference_plms_no_past_residuals(self): + with self.assertRaises(ValueError): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.step_plms(self.dummy_sample, 1, self.dummy_sample).prev_sample + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 198.1318) < 1e-2 + assert abs(result_mean.item() - 0.2580) < 1e-3 + + def test_full_loop_with_v_prediction(self): + sample = self.full_loop(prediction_type="v_prediction") + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 67.3986) < 1e-2 + assert abs(result_mean.item() - 0.0878) < 1e-3 + + def test_full_loop_with_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=True, beta_start=0.01) + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 230.0399) < 1e-2 + assert abs(result_mean.item() - 0.2995) < 1e-3 + + def test_full_loop_with_no_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=False, beta_start=0.01) + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 186.9482) < 1e-2 + assert abs(result_mean.item() - 0.2434) < 1e-3 + + +class ScoreSdeVeSchedulerTest(unittest.TestCase): + # TODO adapt with class SchedulerCommonTest (scheduler needs Numpy Integration) + scheduler_classes = (ScoreSdeVeScheduler,) + forward_default_kwargs = () + + @property + def dummy_sample(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + sample = torch.rand((batch_size, num_channels, height, width)) + + return sample + + @property + def dummy_sample_deter(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + num_elems = batch_size * num_channels * height * width + sample = torch.arange(num_elems) + sample = sample.reshape(num_channels, height, width, batch_size) + sample = sample / num_elems + sample = sample.permute(3, 0, 1, 2) + + return sample + + def dummy_model(self): + def model(sample, t, *args): + return sample * t / (t + 1) + + return model + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 2000, + "snr": 0.15, + "sigma_min": 0.01, + "sigma_max": 1348, + "sampling_eps": 1e-5, + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + + for scheduler_class in self.scheduler_classes: + sample = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + output = scheduler.step_pred( + residual, time_step, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + new_output = new_scheduler.step_pred( + residual, time_step, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step_correct(residual, sample, generator=torch.manual_seed(0), **kwargs).prev_sample + new_output = new_scheduler.step_correct( + residual, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler correction are not identical" + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + kwargs.update(forward_kwargs) + + for scheduler_class in self.scheduler_classes: + sample = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + + output = scheduler.step_pred( + residual, time_step, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + new_output = new_scheduler.step_pred( + residual, time_step, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step_correct(residual, sample, generator=torch.manual_seed(0), **kwargs).prev_sample + new_output = new_scheduler.step_correct( + residual, sample, generator=torch.manual_seed(0), **kwargs + ).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler correction are not identical" + + def test_timesteps(self): + for timesteps in [10, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_sigmas(self): + for sigma_min, sigma_max in zip([0.0001, 0.001, 0.01], [1, 100, 1000]): + self.check_over_configs(sigma_min=sigma_min, sigma_max=sigma_max) + + def test_time_indices(self): + for t in [0.1, 0.5, 0.75]: + self.check_over_forward(time_step=t) + + def test_full_loop_no_noise(self): + kwargs = dict(self.forward_default_kwargs) + + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 3 + + model = self.dummy_model() + sample = self.dummy_sample_deter + + scheduler.set_sigmas(num_inference_steps) + scheduler.set_timesteps(num_inference_steps) + generator = torch.manual_seed(0) + + for i, t in enumerate(scheduler.timesteps): + sigma_t = scheduler.sigmas[i] + + for _ in range(scheduler.config.correct_steps): + with torch.no_grad(): + model_output = model(sample, sigma_t) + sample = scheduler.step_correct(model_output, sample, generator=generator, **kwargs).prev_sample + + with torch.no_grad(): + model_output = model(sample, sigma_t) + + output = scheduler.step_pred(model_output, t, sample, generator=generator, **kwargs) + sample, _ = output.prev_sample, output.prev_sample_mean + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert np.isclose(result_sum.item(), 14372758528.0) + assert np.isclose(result_mean.item(), 18714530.0) + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output_0 = scheduler.step_pred(residual, 0, sample, generator=torch.manual_seed(0), **kwargs).prev_sample + output_1 = scheduler.step_pred(residual, 1, sample, generator=torch.manual_seed(0), **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + +class LMSDiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (LMSDiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_time_indices(self): + for t in [0, 500, 800]: + self.check_over_forward(time_step=t) + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 1006.388) < 1e-2 + assert abs(result_mean.item() - 1.31) < 1e-3 + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 0.0017) < 1e-2 + assert abs(result_mean.item() - 2.2676e-06) < 1e-3 + + def test_full_loop_device(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 1006.388) < 1e-2 + assert abs(result_mean.item() - 1.31) < 1e-3 + + +class EulerDiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (EulerDiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 10.0807) < 1e-2 + assert abs(result_mean.item() - 0.0131) < 1e-3 + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 0.0002) < 1e-2 + assert abs(result_mean.item() - 2.2676e-06) < 1e-3 + + def test_full_loop_device(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for t in scheduler.timesteps: + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 10.0807) < 1e-2 + assert abs(result_mean.item() - 0.0131) < 1e-3 + + +class EulerAncestralDiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (EulerAncestralDiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 152.3192) < 1e-2 + assert abs(result_mean.item() - 0.1983) < 1e-3 + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 108.4439) < 1e-2 + assert abs(result_mean.item() - 0.1412) < 1e-3 + + def test_full_loop_device(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for t in scheduler.timesteps: + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 152.3192) < 1e-2 + assert abs(result_mean.item() - 0.1983) < 1e-3 + + +class IPNDMSchedulerTest(SchedulerCommonTest): + scheduler_classes = (IPNDMScheduler,) + forward_default_kwargs = (("num_inference_steps", 50),) + + def get_scheduler_config(self, **kwargs): + config = {"num_train_timesteps": 1000} + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + scheduler.ets = dummy_past_residuals[:] + + if time_step is None: + time_step = scheduler.timesteps[len(scheduler.timesteps) // 2] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + new_scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + new_scheduler.ets = dummy_past_residuals[:] + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.ets = dummy_past_residuals[:] + + if time_step is None: + time_step = scheduler.timesteps[len(scheduler.timesteps) // 2] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residual (must be after setting timesteps) + new_scheduler.ets = dummy_past_residuals[:] + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + return sample + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # copy over dummy past residuals (must be done after set_timesteps) + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05] + scheduler.ets = dummy_past_residuals[:] + + time_step_0 = scheduler.timesteps[5] + time_step_1 = scheduler.timesteps[6] + + output_0 = scheduler.step(residual, time_step_0, sample, **kwargs).prev_sample + output_1 = scheduler.step(residual, time_step_1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + output_0 = scheduler.step(residual, time_step_0, sample, **kwargs).prev_sample + output_1 = scheduler.step(residual, time_step_1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps, time_step=None) + + def test_inference_steps(self): + for t, num_inference_steps in zip([1, 5, 10], [10, 50, 100]): + self.check_over_forward(num_inference_steps=num_inference_steps, time_step=None) + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 2540529) < 10 + + +class VQDiffusionSchedulerTest(SchedulerCommonTest): + scheduler_classes = (VQDiffusionScheduler,) + + def get_scheduler_config(self, **kwargs): + config = { + "num_vec_classes": 4097, + "num_train_timesteps": 100, + } + + config.update(**kwargs) + return config + + def dummy_sample(self, num_vec_classes): + batch_size = 4 + height = 8 + width = 8 + + sample = torch.randint(0, num_vec_classes, (batch_size, height * width)) + + return sample + + @property + def dummy_sample_deter(self): + assert False + + def dummy_model(self, num_vec_classes): + def model(sample, t, *args): + batch_size, num_latent_pixels = sample.shape + logits = torch.rand((batch_size, num_vec_classes - 1, num_latent_pixels)) + return_value = F.log_softmax(logits.double(), dim=1).float() + return return_value + + return model + + def test_timesteps(self): + for timesteps in [2, 5, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_num_vec_classes(self): + for num_vec_classes in [5, 100, 1000, 4000]: + self.check_over_configs(num_vec_classes=num_vec_classes) + + def test_time_indices(self): + for t in [0, 50, 99]: + self.check_over_forward(time_step=t) + + def test_add_noise_device(self): + pass + + +class HeunDiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (HeunDiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if torch_device in ["cpu", "mps"]: + assert abs(result_sum.item() - 0.1233) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + else: + # CUDA + assert abs(result_sum.item() - 0.1233) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if torch_device in ["cpu", "mps"]: + assert abs(result_sum.item() - 4.6934e-07) < 1e-2 + assert abs(result_mean.item() - 6.1112e-10) < 1e-3 + else: + # CUDA + assert abs(result_sum.item() - 4.693428650170972e-07) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + + def test_full_loop_device(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + + model = self.dummy_model() + sample = self.dummy_sample_deter.to(torch_device) * scheduler.init_noise_sigma + + for t in scheduler.timesteps: + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if str(torch_device).startswith("cpu"): + # The following sum varies between 148 and 156 on mps. Why? + assert abs(result_sum.item() - 0.1233) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + elif str(torch_device).startswith("mps"): + # Larger tolerance on mps + assert abs(result_mean.item() - 0.0002) < 1e-2 + else: + # CUDA + assert abs(result_sum.item() - 0.1233) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + + +class KDPM2DiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (KDPM2DiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_full_loop_with_v_prediction(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if torch_device in ["cpu", "mps"]: + assert abs(result_sum.item() - 4.6934e-07) < 1e-2 + assert abs(result_mean.item() - 6.1112e-10) < 1e-3 + else: + # CUDA + assert abs(result_sum.item() - 4.693428650170972e-07) < 1e-2 + assert abs(result_mean.item() - 0.0002) < 1e-3 + + def test_full_loop_no_noise(self): + if torch_device == "mps": + return + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if torch_device in ["cpu", "mps"]: + assert abs(result_sum.item() - 20.4125) < 1e-2 + assert abs(result_mean.item() - 0.0266) < 1e-3 + else: + # CUDA + assert abs(result_sum.item() - 20.4125) < 1e-2 + assert abs(result_mean.item() - 0.0266) < 1e-3 + + def test_full_loop_device(self): + if torch_device == "mps": + return + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + + model = self.dummy_model() + sample = self.dummy_sample_deter.to(torch_device) * scheduler.init_noise_sigma + + for t in scheduler.timesteps: + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + if str(torch_device).startswith("cpu"): + # The following sum varies between 148 and 156 on mps. Why? + assert abs(result_sum.item() - 20.4125) < 1e-2 + assert abs(result_mean.item() - 0.0266) < 1e-3 + else: + # CUDA + assert abs(result_sum.item() - 20.4125) < 1e-2 + assert abs(result_mean.item() - 0.0266) < 1e-3 + + +class DEISMultistepSchedulerTest(SchedulerCommonTest): + scheduler_classes = (DEISMultistepScheduler,) + forward_default_kwargs = (("num_inference_steps", 25),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "solver_order": 2, + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + new_scheduler.set_timesteps(num_inference_steps) + # copy over dummy past residuals + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output, new_output = sample, sample + for t in range(time_step, time_step + scheduler.config.solver_order + 1): + output = scheduler.step(residual, t, output, **kwargs).prev_sample + new_output = new_scheduler.step(residual, t, new_output, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_scheduler.set_timesteps(num_inference_steps) + + # copy over dummy past residual (must be after setting timesteps) + new_scheduler.model_outputs = dummy_past_residuals[: new_scheduler.config.solver_order] + + output = scheduler.step(residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(residual, time_step, sample, **kwargs).prev_sample + + assert torch.sum(torch.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + return sample + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + sample = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + scheduler.set_timesteps(num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # copy over dummy past residuals (must be done after set_timesteps) + dummy_past_residuals = [residual + 0.2, residual + 0.15, residual + 0.10] + scheduler.model_outputs = dummy_past_residuals[: scheduler.config.solver_order] + + time_step_0 = scheduler.timesteps[5] + time_step_1 = scheduler.timesteps[6] + + output_0 = scheduler.step(residual, time_step_0, sample, **kwargs).prev_sample + output_1 = scheduler.step(residual, time_step_1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [25, 50, 100, 999, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_thresholding(self): + self.check_over_configs(thresholding=False) + for order in [1, 2, 3]: + for solver_type in ["logrho"]: + for threshold in [0.5, 1.0, 2.0]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + thresholding=True, + prediction_type=prediction_type, + sample_max_value=threshold, + algorithm_type="deis", + solver_order=order, + solver_type=solver_type, + ) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_solver_order_and_type(self): + for algorithm_type in ["deis"]: + for solver_type in ["logrho"]: + for order in [1, 2, 3]: + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + sample = self.full_loop( + solver_order=order, + solver_type=solver_type, + prediction_type=prediction_type, + algorithm_type=algorithm_type, + ) + assert not torch.isnan(sample).any(), "Samples have nan numbers" + + def test_lower_order_final(self): + self.check_over_configs(lower_order_final=True) + self.check_over_configs(lower_order_final=False) + + def test_inference_steps(self): + for num_inference_steps in [1, 2, 3, 5, 10, 50, 100, 999, 1000]: + self.check_over_forward(num_inference_steps=num_inference_steps, time_step=0) + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.23916) < 1e-3 + + def test_full_loop_with_v_prediction(self): + sample = self.full_loop(prediction_type="v_prediction") + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_mean.item() - 0.091) < 1e-3 + + def test_fp16_support(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(thresholding=True, dynamic_thresholding_ratio=0) + scheduler = scheduler_class(**scheduler_config) + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter.half() + scheduler.set_timesteps(num_inference_steps) + + for i, t in enumerate(scheduler.timesteps): + residual = model(sample, t) + sample = scheduler.step(residual, t, sample).prev_sample + + assert sample.dtype == torch.float16 + + +class KDPM2AncestralDiscreteSchedulerTest(SchedulerCommonTest): + scheduler_classes = (KDPM2AncestralDiscreteScheduler,) + num_inference_steps = 10 + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1100, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [10, 50, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.00001, 0.0001, 0.001], [0.0002, 0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "scaled_linear"]: + self.check_over_configs(beta_schedule=schedule) + + def test_full_loop_no_noise(self): + if torch_device == "mps": + return + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 13849.3877) < 1e-2 + assert abs(result_mean.item() - 18.0331) < 5e-3 + + def test_prediction_type(self): + for prediction_type in ["epsilon", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_full_loop_with_v_prediction(self): + if torch_device == "mps": + return + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(prediction_type="v_prediction") + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps) + + model = self.dummy_model() + sample = self.dummy_sample_deter * scheduler.init_noise_sigma + sample = sample.to(torch_device) + + generator = torch.manual_seed(0) + + for i, t in enumerate(scheduler.timesteps): + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 328.9970) < 1e-2 + assert abs(result_mean.item() - 0.4284) < 1e-3 + + def test_full_loop_device(self): + if torch_device == "mps": + return + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(self.num_inference_steps, device=torch_device) + generator = torch.manual_seed(0) + + model = self.dummy_model() + sample = self.dummy_sample_deter.to(torch_device) * scheduler.init_noise_sigma + + for t in scheduler.timesteps: + sample = scheduler.scale_model_input(sample, t) + + model_output = model(sample, t) + + output = scheduler.step(model_output, t, sample, generator=generator) + sample = output.prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 13849.3818) < 1e-1 + assert abs(result_mean.item() - 18.0331) < 1e-3 + + +# UnCLIPScheduler is a modified DDPMScheduler with a subset of the configuration. +class UnCLIPSchedulerTest(SchedulerCommonTest): + scheduler_classes = (UnCLIPScheduler,) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "variance_type": "fixed_small_log", + "clip_sample": True, + "clip_sample_range": 1.0, + "prediction_type": "epsilon", + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [1, 5, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_variance_type(self): + for variance in ["fixed_small_log", "learned_range"]: + self.check_over_configs(variance_type=variance) + + def test_clip_sample(self): + for clip_sample in [True, False]: + self.check_over_configs(clip_sample=clip_sample) + + def test_clip_sample_range(self): + for clip_sample_range in [1, 5, 10, 20]: + self.check_over_configs(clip_sample_range=clip_sample_range) + + def test_prediction_type(self): + for prediction_type in ["epsilon", "sample"]: + self.check_over_configs(prediction_type=prediction_type) + + def test_time_indices(self): + for time_step in [0, 500, 999]: + for prev_timestep in [None, 5, 100, 250, 500, 750]: + if prev_timestep is not None and prev_timestep >= time_step: + continue + + self.check_over_forward(time_step=time_step, prev_timestep=prev_timestep) + + def test_variance_fixed_small_log(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(variance_type="fixed_small_log") + scheduler = scheduler_class(**scheduler_config) + + assert torch.sum(torch.abs(scheduler._get_variance(0) - 1.0000e-10)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(487) - 0.0549625)) < 1e-5 + assert torch.sum(torch.abs(scheduler._get_variance(999) - 0.9994987)) < 1e-5 + + def test_variance_learned_range(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(variance_type="learned_range") + scheduler = scheduler_class(**scheduler_config) + + predicted_variance = 0.5 + + assert scheduler._get_variance(1, predicted_variance=predicted_variance) - -10.1712790 < 1e-5 + assert scheduler._get_variance(487, predicted_variance=predicted_variance) - -5.7998052 < 1e-5 + assert scheduler._get_variance(999, predicted_variance=predicted_variance) - -0.0010011 < 1e-5 + + def test_full_loop(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + timesteps = scheduler.timesteps + + model = self.dummy_model() + sample = self.dummy_sample_deter + generator = torch.manual_seed(0) + + for i, t in enumerate(timesteps): + # 1. predict noise residual + residual = model(sample, t) + + # 2. predict previous mean of sample x_t-1 + pred_prev_sample = scheduler.step(residual, t, sample, generator=generator).prev_sample + + sample = pred_prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 252.2682495) < 1e-2 + assert abs(result_mean.item() - 0.3284743) < 1e-3 + + def test_full_loop_skip_timesteps(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + + scheduler.set_timesteps(25) + + timesteps = scheduler.timesteps + + model = self.dummy_model() + sample = self.dummy_sample_deter + generator = torch.manual_seed(0) + + for i, t in enumerate(timesteps): + # 1. predict noise residual + residual = model(sample, t) + + if i + 1 == timesteps.shape[0]: + prev_timestep = None + else: + prev_timestep = timesteps[i + 1] + + # 2. predict previous mean of sample x_t-1 + pred_prev_sample = scheduler.step( + residual, t, sample, prev_timestep=prev_timestep, generator=generator + ).prev_sample + + sample = pred_prev_sample + + result_sum = torch.sum(torch.abs(sample)) + result_mean = torch.mean(torch.abs(sample)) + + assert abs(result_sum.item() - 258.2044983) < 1e-2 + assert abs(result_mean.item() - 0.3362038) < 1e-3 + + def test_trained_betas(self): + pass + + def test_add_noise_device(self): + pass diff --git a/diffusers/tests/test_scheduler_flax.py b/diffusers/tests/test_scheduler_flax.py new file mode 100644 index 0000000000000000000000000000000000000000..1c6de2ec4ad0cbec7e40112c92901567b352261a --- /dev/null +++ b/diffusers/tests/test_scheduler_flax.py @@ -0,0 +1,919 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. +import inspect +import tempfile +import unittest +from typing import Dict, List, Tuple + +from diffusers import FlaxDDIMScheduler, FlaxDDPMScheduler, FlaxPNDMScheduler +from diffusers.utils import is_flax_available +from diffusers.utils.testing_utils import require_flax + + +if is_flax_available(): + import jax + import jax.numpy as jnp + from jax import random + + jax_device = jax.default_backend() + + +@require_flax +class FlaxSchedulerCommonTest(unittest.TestCase): + scheduler_classes = () + forward_default_kwargs = () + + @property + def dummy_sample(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + key1, key2 = random.split(random.PRNGKey(0)) + sample = random.uniform(key1, (batch_size, num_channels, height, width)) + + return sample, key2 + + @property + def dummy_sample_deter(self): + batch_size = 4 + num_channels = 3 + height = 8 + width = 8 + + num_elems = batch_size * num_channels * height * width + sample = jnp.arange(num_elems) + sample = sample.reshape(num_channels, height, width, batch_size) + sample = sample / num_elems + return jnp.transpose(sample, (3, 0, 1, 2)) + + def get_scheduler_config(self): + raise NotImplementedError + + def dummy_model(self): + def model(sample, t, *args): + return sample * t / (t + 1) + + return model + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, key = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, time_step, sample, key, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, time_step, sample, key, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + kwargs.update(forward_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, key = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, time_step, sample, key, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, time_step, sample, key, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, key = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, 1, sample, key, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, 1, sample, key, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, key = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output_0 = scheduler.step(state, residual, 0, sample, key, **kwargs).prev_sample + output_1 = scheduler.step(state, residual, 1, sample, key, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_scheduler_outputs_equivalence(self): + def set_nan_tensor_to_zero(t): + return t.at[t != t].set(0) + + def recursive_check(tuple_object, dict_object): + if isinstance(tuple_object, (List, Tuple)): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object, dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif isinstance(tuple_object, Dict): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object.values(), dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif tuple_object is None: + return + else: + self.assertTrue( + jnp.allclose(set_nan_tensor_to_zero(tuple_object), set_nan_tensor_to_zero(dict_object), atol=1e-5), + msg=( + "Tuple and dict output are not equal. Difference:" + f" {jnp.max(jnp.abs(tuple_object - dict_object))}. Tuple has `nan`:" + f" {jnp.isnan(tuple_object).any()} and `inf`: {jnp.isinf(tuple_object)}. Dict has" + f" `nan`: {jnp.isnan(dict_object).any()} and `inf`: {jnp.isinf(dict_object)}." + ), + ) + + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, key = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_dict = scheduler.step(state, residual, 0, sample, key, **kwargs) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_tuple = scheduler.step(state, residual, 0, sample, key, return_dict=False, **kwargs) + + recursive_check(outputs_tuple[0], outputs_dict.prev_sample) + + def test_deprecated_kwargs(self): + for scheduler_class in self.scheduler_classes: + has_kwarg_in_model_class = "kwargs" in inspect.signature(scheduler_class.__init__).parameters + has_deprecated_kwarg = len(scheduler_class._deprecated_kwargs) > 0 + + if has_kwarg_in_model_class and not has_deprecated_kwarg: + raise ValueError( + f"{scheduler_class} has `**kwargs` in its __init__ method but has not defined any deprecated" + " kwargs under the `_deprecated_kwargs` class attribute. Make sure to either remove `**kwargs` if" + " there are no deprecated arguments or add the deprecated argument with `_deprecated_kwargs =" + " []`" + ) + + if not has_kwarg_in_model_class and has_deprecated_kwarg: + raise ValueError( + f"{scheduler_class} doesn't have `**kwargs` in its __init__ method but has defined deprecated" + " kwargs under the `_deprecated_kwargs` class attribute. Make sure to either add the `**kwargs`" + f" argument to {self.model_class}.__init__ if there are deprecated arguments or remove the" + " deprecated argument from `_deprecated_kwargs = []`" + ) + + +@require_flax +class FlaxDDPMSchedulerTest(FlaxSchedulerCommonTest): + scheduler_classes = (FlaxDDPMScheduler,) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + "variance_type": "fixed_small", + "clip_sample": True, + } + + config.update(**kwargs) + return config + + def test_timesteps(self): + for timesteps in [1, 5, 100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001, 0.01, 0.1], [0.002, 0.02, 0.2, 2]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_variance_type(self): + for variance in ["fixed_small", "fixed_large", "other"]: + self.check_over_configs(variance_type=variance) + + def test_clip_sample(self): + for clip_sample in [True, False]: + self.check_over_configs(clip_sample=clip_sample) + + def test_time_indices(self): + for t in [0, 500, 999]: + self.check_over_forward(time_step=t) + + def test_variance(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 0) - 0.0)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 487) - 0.00979)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 999) - 0.02)) < 1e-5 + + def test_full_loop_no_noise(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + num_trained_timesteps = len(scheduler) + + model = self.dummy_model() + sample = self.dummy_sample_deter + key1, key2 = random.split(random.PRNGKey(0)) + + for t in reversed(range(num_trained_timesteps)): + # 1. predict noise residual + residual = model(sample, t) + + # 2. predict previous mean of sample x_t-1 + output = scheduler.step(state, residual, t, sample, key1) + pred_prev_sample = output.prev_sample + state = output.state + key1, key2 = random.split(key2) + + # if t > 0: + # noise = self.dummy_sample_deter + # variance = scheduler.get_variance(t) ** (0.5) * noise + # + # sample = pred_prev_sample + variance + sample = pred_prev_sample + + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + assert abs(result_sum - 255.0714) < 1e-2 + assert abs(result_mean - 0.332124) < 1e-3 + else: + assert abs(result_sum - 255.1113) < 1e-2 + assert abs(result_mean - 0.332176) < 1e-3 + + +@require_flax +class FlaxDDIMSchedulerTest(FlaxSchedulerCommonTest): + scheduler_classes = (FlaxDDIMScheduler,) + forward_default_kwargs = (("num_inference_steps", 50),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + key1, key2 = random.split(random.PRNGKey(0)) + + num_inference_steps = 10 + + model = self.dummy_model() + sample = self.dummy_sample_deter + + state = scheduler.set_timesteps(state, num_inference_steps) + + for t in state.timesteps: + residual = model(sample, t) + output = scheduler.step(state, residual, t, sample) + sample = output.prev_sample + state = output.state + key1, key2 = random.split(key2) + + return sample + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, _ = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, time_step, sample, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, _ = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, 1, sample, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, 1, sample, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + kwargs.update(forward_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + sample, _ = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output = scheduler.step(state, residual, time_step, sample, **kwargs).prev_sample + new_output = new_scheduler.step(new_state, residual, time_step, sample, **kwargs).prev_sample + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_scheduler_outputs_equivalence(self): + def set_nan_tensor_to_zero(t): + return t.at[t != t].set(0) + + def recursive_check(tuple_object, dict_object): + if isinstance(tuple_object, (List, Tuple)): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object, dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif isinstance(tuple_object, Dict): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object.values(), dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif tuple_object is None: + return + else: + self.assertTrue( + jnp.allclose(set_nan_tensor_to_zero(tuple_object), set_nan_tensor_to_zero(dict_object), atol=1e-5), + msg=( + "Tuple and dict output are not equal. Difference:" + f" {jnp.max(jnp.abs(tuple_object - dict_object))}. Tuple has `nan`:" + f" {jnp.isnan(tuple_object).any()} and `inf`: {jnp.isinf(tuple_object)}. Dict has" + f" `nan`: {jnp.isnan(dict_object).any()} and `inf`: {jnp.isinf(dict_object)}." + ), + ) + + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, _ = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_dict = scheduler.step(state, residual, 0, sample, **kwargs) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_tuple = scheduler.step(state, residual, 0, sample, return_dict=False, **kwargs) + + recursive_check(outputs_tuple[0], outputs_dict.prev_sample) + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, _ = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + output_0 = scheduler.step(state, residual, 0, sample, **kwargs).prev_sample + output_1 = scheduler.step(state, residual, 1, sample, **kwargs).prev_sample + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [100, 500, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_steps_offset(self): + for steps_offset in [0, 1]: + self.check_over_configs(steps_offset=steps_offset) + + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(steps_offset=1) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + state = scheduler.set_timesteps(state, 5) + assert jnp.equal(state.timesteps, jnp.array([801, 601, 401, 201, 1])).all() + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001, 0.01, 0.1], [0.002, 0.02, 0.2, 2]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_time_indices(self): + for t in [1, 10, 49]: + self.check_over_forward(time_step=t) + + def test_inference_steps(self): + for t, num_inference_steps in zip([1, 10, 50], [10, 50, 500]): + self.check_over_forward(time_step=t, num_inference_steps=num_inference_steps) + + def test_variance(self): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 0, 0) - 0.0)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 420, 400) - 0.14771)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 980, 960) - 0.32460)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 0, 0) - 0.0)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 487, 486) - 0.00979)) < 1e-5 + assert jnp.sum(jnp.abs(scheduler._get_variance(state, 999, 998) - 0.02)) < 1e-5 + + def test_full_loop_no_noise(self): + sample = self.full_loop() + + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + assert abs(result_sum - 172.0067) < 1e-2 + assert abs(result_mean - 0.223967) < 1e-3 + + def test_full_loop_with_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=True, beta_start=0.01) + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + assert abs(result_sum - 149.8409) < 1e-2 + assert abs(result_mean - 0.1951) < 1e-3 + else: + assert abs(result_sum - 149.8295) < 1e-2 + assert abs(result_mean - 0.1951) < 1e-3 + + def test_full_loop_with_no_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=False, beta_start=0.01) + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + pass + # FIXME: both result_sum and result_mean are nan on TPU + # assert jnp.isnan(result_sum) + # assert jnp.isnan(result_mean) + else: + assert abs(result_sum - 149.0784) < 1e-2 + assert abs(result_mean - 0.1941) < 1e-3 + + def test_prediction_type(self): + for prediction_type in ["epsilon", "sample", "v_prediction"]: + self.check_over_configs(prediction_type=prediction_type) + + +@require_flax +class FlaxPNDMSchedulerTest(FlaxSchedulerCommonTest): + scheduler_classes = (FlaxPNDMScheduler,) + forward_default_kwargs = (("num_inference_steps", 50),) + + def get_scheduler_config(self, **kwargs): + config = { + "num_train_timesteps": 1000, + "beta_start": 0.0001, + "beta_end": 0.02, + "beta_schedule": "linear", + } + + config.update(**kwargs) + return config + + def check_over_configs(self, time_step=0, **config): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample, _ = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = jnp.array([residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05]) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + # copy over dummy past residuals + state = state.replace(ets=dummy_past_residuals[:]) + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps, shape=sample.shape) + # copy over dummy past residuals + new_state = new_state.replace(ets=dummy_past_residuals[:]) + + (prev_sample, state) = scheduler.step_prk(state, residual, time_step, sample, **kwargs) + (new_prev_sample, new_state) = new_scheduler.step_prk(new_state, residual, time_step, sample, **kwargs) + + assert jnp.sum(jnp.abs(prev_sample - new_prev_sample)) < 1e-5, "Scheduler outputs are not identical" + + output, _ = scheduler.step_plms(state, residual, time_step, sample, **kwargs) + new_output, _ = new_scheduler.step_plms(new_state, residual, time_step, sample, **kwargs) + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def test_from_save_pretrained(self): + pass + + def test_scheduler_outputs_equivalence(self): + def set_nan_tensor_to_zero(t): + return t.at[t != t].set(0) + + def recursive_check(tuple_object, dict_object): + if isinstance(tuple_object, (List, Tuple)): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object, dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif isinstance(tuple_object, Dict): + for tuple_iterable_value, dict_iterable_value in zip(tuple_object.values(), dict_object.values()): + recursive_check(tuple_iterable_value, dict_iterable_value) + elif tuple_object is None: + return + else: + self.assertTrue( + jnp.allclose(set_nan_tensor_to_zero(tuple_object), set_nan_tensor_to_zero(dict_object), atol=1e-5), + msg=( + "Tuple and dict output are not equal. Difference:" + f" {jnp.max(jnp.abs(tuple_object - dict_object))}. Tuple has `nan`:" + f" {jnp.isnan(tuple_object).any()} and `inf`: {jnp.isinf(tuple_object)}. Dict has" + f" `nan`: {jnp.isnan(dict_object).any()} and `inf`: {jnp.isinf(dict_object)}." + ), + ) + + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, _ = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_dict = scheduler.step(state, residual, 0, sample, **kwargs) + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + outputs_tuple = scheduler.step(state, residual, 0, sample, return_dict=False, **kwargs) + + recursive_check(outputs_tuple[0], outputs_dict.prev_sample) + + def check_over_forward(self, time_step=0, **forward_kwargs): + kwargs = dict(self.forward_default_kwargs) + num_inference_steps = kwargs.pop("num_inference_steps", None) + sample, _ = self.dummy_sample + residual = 0.1 * sample + dummy_past_residuals = jnp.array([residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05]) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + + # copy over dummy past residuals (must be after setting timesteps) + scheduler.ets = dummy_past_residuals[:] + + with tempfile.TemporaryDirectory() as tmpdirname: + scheduler.save_config(tmpdirname) + new_scheduler, new_state = scheduler_class.from_pretrained(tmpdirname) + # copy over dummy past residuals + new_state = new_scheduler.set_timesteps(new_state, num_inference_steps, shape=sample.shape) + + # copy over dummy past residual (must be after setting timesteps) + new_state.replace(ets=dummy_past_residuals[:]) + + output, state = scheduler.step_prk(state, residual, time_step, sample, **kwargs) + new_output, new_state = new_scheduler.step_prk(new_state, residual, time_step, sample, **kwargs) + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + output, _ = scheduler.step_plms(state, residual, time_step, sample, **kwargs) + new_output, _ = new_scheduler.step_plms(new_state, residual, time_step, sample, **kwargs) + + assert jnp.sum(jnp.abs(output - new_output)) < 1e-5, "Scheduler outputs are not identical" + + def full_loop(self, **config): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(**config) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + num_inference_steps = 10 + model = self.dummy_model() + sample = self.dummy_sample_deter + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + + for i, t in enumerate(state.prk_timesteps): + residual = model(sample, t) + sample, state = scheduler.step_prk(state, residual, t, sample) + + for i, t in enumerate(state.plms_timesteps): + residual = model(sample, t) + sample, state = scheduler.step_plms(state, residual, t, sample) + + return sample + + def test_step_shape(self): + kwargs = dict(self.forward_default_kwargs) + + num_inference_steps = kwargs.pop("num_inference_steps", None) + + for scheduler_class in self.scheduler_classes: + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + sample, _ = self.dummy_sample + residual = 0.1 * sample + + if num_inference_steps is not None and hasattr(scheduler, "set_timesteps"): + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + elif num_inference_steps is not None and not hasattr(scheduler, "set_timesteps"): + kwargs["num_inference_steps"] = num_inference_steps + + # copy over dummy past residuals (must be done after set_timesteps) + dummy_past_residuals = jnp.array([residual + 0.2, residual + 0.15, residual + 0.1, residual + 0.05]) + state = state.replace(ets=dummy_past_residuals[:]) + + output_0, state = scheduler.step_prk(state, residual, 0, sample, **kwargs) + output_1, state = scheduler.step_prk(state, residual, 1, sample, **kwargs) + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + output_0, state = scheduler.step_plms(state, residual, 0, sample, **kwargs) + output_1, state = scheduler.step_plms(state, residual, 1, sample, **kwargs) + + self.assertEqual(output_0.shape, sample.shape) + self.assertEqual(output_0.shape, output_1.shape) + + def test_timesteps(self): + for timesteps in [100, 1000]: + self.check_over_configs(num_train_timesteps=timesteps) + + def test_steps_offset(self): + for steps_offset in [0, 1]: + self.check_over_configs(steps_offset=steps_offset) + + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config(steps_offset=1) + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + state = scheduler.set_timesteps(state, 10, shape=()) + assert jnp.equal( + state.timesteps, + jnp.array([901, 851, 851, 801, 801, 751, 751, 701, 701, 651, 651, 601, 601, 501, 401, 301, 201, 101, 1]), + ).all() + + def test_betas(self): + for beta_start, beta_end in zip([0.0001, 0.001], [0.002, 0.02]): + self.check_over_configs(beta_start=beta_start, beta_end=beta_end) + + def test_schedules(self): + for schedule in ["linear", "squaredcos_cap_v2"]: + self.check_over_configs(beta_schedule=schedule) + + def test_time_indices(self): + for t in [1, 5, 10]: + self.check_over_forward(time_step=t) + + def test_inference_steps(self): + for t, num_inference_steps in zip([1, 5, 10], [10, 50, 100]): + self.check_over_forward(num_inference_steps=num_inference_steps) + + def test_pow_of_3_inference_steps(self): + # earlier version of set_timesteps() caused an error indexing alpha's with inference steps as power of 3 + num_inference_steps = 27 + + for scheduler_class in self.scheduler_classes: + sample, _ = self.dummy_sample + residual = 0.1 * sample + + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + state = scheduler.set_timesteps(state, num_inference_steps, shape=sample.shape) + + # before power of 3 fix, would error on first step, so we only need to do two + for i, t in enumerate(state.prk_timesteps[:2]): + sample, state = scheduler.step_prk(state, residual, t, sample) + + def test_inference_plms_no_past_residuals(self): + with self.assertRaises(ValueError): + scheduler_class = self.scheduler_classes[0] + scheduler_config = self.get_scheduler_config() + scheduler = scheduler_class(**scheduler_config) + state = scheduler.create_state() + + scheduler.step_plms(state, self.dummy_sample, 1, self.dummy_sample).prev_sample + + def test_full_loop_no_noise(self): + sample = self.full_loop() + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + assert abs(result_sum - 198.1275) < 1e-2 + assert abs(result_mean - 0.2580) < 1e-3 + else: + assert abs(result_sum - 198.1318) < 1e-2 + assert abs(result_mean - 0.2580) < 1e-3 + + def test_full_loop_with_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=True, beta_start=0.01) + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + assert abs(result_sum - 186.83226) < 1e-2 + assert abs(result_mean - 0.24327) < 1e-3 + else: + assert abs(result_sum - 186.9466) < 1e-2 + assert abs(result_mean - 0.24342) < 1e-3 + + def test_full_loop_with_no_set_alpha_to_one(self): + # We specify different beta, so that the first alpha is 0.99 + sample = self.full_loop(set_alpha_to_one=False, beta_start=0.01) + result_sum = jnp.sum(jnp.abs(sample)) + result_mean = jnp.mean(jnp.abs(sample)) + + if jax_device == "tpu": + assert abs(result_sum - 186.83226) < 1e-2 + assert abs(result_mean - 0.24327) < 1e-3 + else: + assert abs(result_sum - 186.9482) < 1e-2 + assert abs(result_mean - 0.2434) < 1e-3 diff --git a/diffusers/tests/test_training.py b/diffusers/tests/test_training.py new file mode 100644 index 0000000000000000000000000000000000000000..fd0828329ebdb16a36192fbd72b9540cb99daaf2 --- /dev/null +++ b/diffusers/tests/test_training.py @@ -0,0 +1,86 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +import torch + +from diffusers import DDIMScheduler, DDPMScheduler, UNet2DModel +from diffusers.training_utils import set_seed +from diffusers.utils.testing_utils import slow + + +torch.backends.cuda.matmul.allow_tf32 = False + + +class TrainingTests(unittest.TestCase): + def get_model_optimizer(self, resolution=32): + set_seed(0) + model = UNet2DModel(sample_size=resolution, in_channels=3, out_channels=3) + optimizer = torch.optim.SGD(model.parameters(), lr=0.0001) + return model, optimizer + + @slow + def test_training_step_equality(self): + device = "cpu" # ensure full determinism without setting the CUBLAS_WORKSPACE_CONFIG env variable + ddpm_scheduler = DDPMScheduler( + num_train_timesteps=1000, + beta_start=0.0001, + beta_end=0.02, + beta_schedule="linear", + clip_sample=True, + ) + ddim_scheduler = DDIMScheduler( + num_train_timesteps=1000, + beta_start=0.0001, + beta_end=0.02, + beta_schedule="linear", + clip_sample=True, + ) + + assert ddpm_scheduler.config.num_train_timesteps == ddim_scheduler.config.num_train_timesteps + + # shared batches for DDPM and DDIM + set_seed(0) + clean_images = [torch.randn((4, 3, 32, 32)).clip(-1, 1).to(device) for _ in range(4)] + noise = [torch.randn((4, 3, 32, 32)).to(device) for _ in range(4)] + timesteps = [torch.randint(0, 1000, (4,)).long().to(device) for _ in range(4)] + + # train with a DDPM scheduler + model, optimizer = self.get_model_optimizer(resolution=32) + model.train().to(device) + for i in range(4): + optimizer.zero_grad() + ddpm_noisy_images = ddpm_scheduler.add_noise(clean_images[i], noise[i], timesteps[i]) + ddpm_noise_pred = model(ddpm_noisy_images, timesteps[i]).sample + loss = torch.nn.functional.mse_loss(ddpm_noise_pred, noise[i]) + loss.backward() + optimizer.step() + del model, optimizer + + # recreate the model and optimizer, and retry with DDIM + model, optimizer = self.get_model_optimizer(resolution=32) + model.train().to(device) + for i in range(4): + optimizer.zero_grad() + ddim_noisy_images = ddim_scheduler.add_noise(clean_images[i], noise[i], timesteps[i]) + ddim_noise_pred = model(ddim_noisy_images, timesteps[i]).sample + loss = torch.nn.functional.mse_loss(ddim_noise_pred, noise[i]) + loss.backward() + optimizer.step() + del model, optimizer + + self.assertTrue(torch.allclose(ddpm_noisy_images, ddim_noisy_images, atol=1e-5)) + self.assertTrue(torch.allclose(ddpm_noise_pred, ddim_noise_pred, atol=1e-5)) diff --git a/diffusers/tests/test_unet_2d_blocks.py b/diffusers/tests/test_unet_2d_blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..3e22870c5502488661f05ae755054e23f2f77260 --- /dev/null +++ b/diffusers/tests/test_unet_2d_blocks.py @@ -0,0 +1,343 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. +import unittest + +from diffusers.models.unet_2d_blocks import * # noqa F403 +from diffusers.utils import torch_device + +from .test_unet_blocks_common import UNetBlockTesterMixin + + +class DownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = DownBlock2D # noqa F405 + block_type = "down" + + def test_output(self): + expected_slice = [-0.0232, -0.9869, 0.8054, -0.0637, -0.1688, -1.4264, 0.4470, -1.3394, 0.0904] + super().test_output(expected_slice) + + +class ResnetDownsampleBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = ResnetDownsampleBlock2D # noqa F405 + block_type = "down" + + def test_output(self): + expected_slice = [0.0710, 0.2410, -0.7320, -1.0757, -1.1343, 0.3540, -0.0133, -0.2576, 0.0948] + super().test_output(expected_slice) + + +class AttnDownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnDownBlock2D # noqa F405 + block_type = "down" + + def test_output(self): + expected_slice = [0.0636, 0.8964, -0.6234, -1.0131, 0.0844, 0.4935, 0.3437, 0.0911, -0.2957] + super().test_output(expected_slice) + + +class CrossAttnDownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = CrossAttnDownBlock2D # noqa F405 + block_type = "down" + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [0.2440, -0.6953, -0.2140, -0.3874, 0.1966, 1.2077, 0.0441, -0.7718, 0.2800] + super().test_output(expected_slice) + + +class SimpleCrossAttnDownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = SimpleCrossAttnDownBlock2D # noqa F405 + block_type = "down" + + @property + def dummy_input(self): + return super().get_dummy_input(include_encoder_hidden_states=True) + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + @unittest.skipIf(torch_device == "mps", "MPS result is not consistent") + def test_output(self): + expected_slice = [0.7921, -0.0992, -0.1962, -0.7695, -0.4242, 0.7804, 0.4737, 0.2765, 0.3338] + super().test_output(expected_slice) + + +class SkipDownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = SkipDownBlock2D # noqa F405 + block_type = "down" + + @property + def dummy_input(self): + return super().get_dummy_input(include_skip_sample=True) + + def test_output(self): + expected_slice = [-0.0845, -0.2087, -0.2465, 0.0971, 0.1900, -0.0484, 0.2664, 0.4179, 0.5069] + super().test_output(expected_slice) + + +class AttnSkipDownBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnSkipDownBlock2D # noqa F405 + block_type = "down" + + @property + def dummy_input(self): + return super().get_dummy_input(include_skip_sample=True) + + def test_output(self): + expected_slice = [0.5539, 0.1609, 0.4924, 0.0537, -0.1995, 0.4050, 0.0979, -0.2721, -0.0642] + super().test_output(expected_slice) + + +class DownEncoderBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = DownEncoderBlock2D # noqa F405 + block_type = "down" + + @property + def dummy_input(self): + return super().get_dummy_input(include_temb=False) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "in_channels": 32, + "out_channels": 32, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [1.1102, 0.5302, 0.4872, -0.0023, -0.8042, 0.0483, -0.3489, -0.5632, 0.7626] + super().test_output(expected_slice) + + +class AttnDownEncoderBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnDownEncoderBlock2D # noqa F405 + block_type = "down" + + @property + def dummy_input(self): + return super().get_dummy_input(include_temb=False) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "in_channels": 32, + "out_channels": 32, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [0.8966, -0.1486, 0.8568, 0.8141, -0.9046, -0.1342, -0.0972, -0.7417, 0.1538] + super().test_output(expected_slice) + + +class UNetMidBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = UNetMidBlock2D # noqa F405 + block_type = "mid" + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "in_channels": 32, + "temb_channels": 128, + } + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [-0.1062, 1.7248, 0.3494, 1.4569, -0.0910, -1.2421, -0.9984, 0.6736, 1.0028] + super().test_output(expected_slice) + + +class UNetMidBlock2DCrossAttnTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = UNetMidBlock2DCrossAttn # noqa F405 + block_type = "mid" + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [0.1879, 2.2653, 0.5987, 1.1568, -0.8454, -1.6109, -0.8919, 0.8306, 1.6758] + super().test_output(expected_slice) + + +class UNetMidBlock2DSimpleCrossAttnTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = UNetMidBlock2DSimpleCrossAttn # noqa F405 + block_type = "mid" + + @property + def dummy_input(self): + return super().get_dummy_input(include_encoder_hidden_states=True) + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [0.7143, 1.9974, 0.5448, 1.3977, 0.1282, -1.1237, -1.4238, 0.5530, 0.8880] + super().test_output(expected_slice) + + +class UpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = UpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + def test_output(self): + expected_slice = [-0.2041, -0.4165, -0.3022, 0.0041, -0.6628, -0.7053, 0.1928, -0.0325, 0.0523] + super().test_output(expected_slice) + + +class ResnetUpsampleBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = ResnetUpsampleBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + def test_output(self): + expected_slice = [0.2287, 0.3549, -0.1346, 0.4797, -0.1715, -0.9649, 0.7305, -0.5864, -0.6244] + super().test_output(expected_slice) + + +class CrossAttnUpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = CrossAttnUpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [-0.2796, -0.4364, -0.1067, -0.2693, 0.1894, 0.3869, -0.3470, 0.4584, 0.5091] + super().test_output(expected_slice) + + +class SimpleCrossAttnUpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = SimpleCrossAttnUpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True, include_encoder_hidden_states=True) + + def prepare_init_args_and_inputs_for_common(self): + init_dict, inputs_dict = super().prepare_init_args_and_inputs_for_common() + init_dict["cross_attention_dim"] = 32 + return init_dict, inputs_dict + + def test_output(self): + if torch_device == "mps": + expected_slice = [0.4327, 0.5538, 0.3919, 0.5682, 0.2704, 0.1573, -0.8768, -0.4615, -0.4146] + else: + expected_slice = [0.2645, 0.1480, 0.0909, 0.8044, -0.9758, -0.9083, 0.0994, -1.1453, -0.7402] + super().test_output(expected_slice) + + +class AttnUpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnUpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + @unittest.skipIf(torch_device == "mps", "MPS result is not consistent") + def test_output(self): + expected_slice = [0.0979, 0.1326, 0.0021, 0.0659, 0.2249, 0.0059, 0.1132, 0.5952, 0.1033] + super().test_output(expected_slice) + + +class SkipUpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = SkipUpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + def test_output(self): + expected_slice = [-0.0893, -0.1234, -0.1506, -0.0332, 0.0123, -0.0211, 0.0566, 0.0143, 0.0362] + super().test_output(expected_slice) + + +class AttnSkipUpBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnSkipUpBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_res_hidden_states_tuple=True) + + def test_output(self): + expected_slice = [0.0361, 0.0617, 0.2787, -0.0350, 0.0342, 0.3421, -0.0843, 0.0913, 0.3015] + super().test_output(expected_slice) + + +class UpDecoderBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = UpDecoderBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_temb=False) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = {"in_channels": 32, "out_channels": 32} + + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self): + expected_slice = [0.4404, 0.1998, -0.9886, -0.3320, -0.3128, -0.7034, -0.6955, -0.2338, -0.3137] + super().test_output(expected_slice) + + +class AttnUpDecoderBlock2DTests(UNetBlockTesterMixin, unittest.TestCase): + block_class = AttnUpDecoderBlock2D # noqa F405 + block_type = "up" + + @property + def dummy_input(self): + return super().get_dummy_input(include_temb=False) + + def prepare_init_args_and_inputs_for_common(self): + init_dict = {"in_channels": 32, "out_channels": 32} + + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self): + if torch_device == "mps": + expected_slice = [-0.3669, -0.3387, 0.1029, -0.6564, 0.2728, -0.3233, 0.5977, -0.1784, 0.5482] + else: + expected_slice = [0.6738, 0.4491, 0.1055, 1.0710, 0.7316, 0.3339, 0.3352, 0.1023, 0.3568] + super().test_output(expected_slice) diff --git a/diffusers/tests/test_unet_blocks_common.py b/diffusers/tests/test_unet_blocks_common.py new file mode 100644 index 0000000000000000000000000000000000000000..3c8a1d77e097efd31e898e93828522a8b89b0061 --- /dev/null +++ b/diffusers/tests/test_unet_blocks_common.py @@ -0,0 +1,121 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. +import unittest +from typing import Tuple + +import torch + +from diffusers.utils import floats_tensor, randn_tensor, torch_all_close, torch_device +from diffusers.utils.testing_utils import require_torch + + +@require_torch +class UNetBlockTesterMixin: + @property + def dummy_input(self): + return self.get_dummy_input() + + @property + def output_shape(self): + if self.block_type == "down": + return (4, 32, 16, 16) + elif self.block_type == "mid": + return (4, 32, 32, 32) + elif self.block_type == "up": + return (4, 32, 64, 64) + + raise ValueError(f"'{self.block_type}' is not a supported block_type. Set it to 'up', 'mid', or 'down'.") + + def get_dummy_input( + self, + include_temb=True, + include_res_hidden_states_tuple=False, + include_encoder_hidden_states=False, + include_skip_sample=False, + ): + batch_size = 4 + num_channels = 32 + sizes = (32, 32) + + generator = torch.manual_seed(0) + device = torch.device(torch_device) + shape = (batch_size, num_channels) + sizes + hidden_states = randn_tensor(shape, generator=generator, device=device) + dummy_input = {"hidden_states": hidden_states} + + if include_temb: + temb_channels = 128 + dummy_input["temb"] = randn_tensor((batch_size, temb_channels), generator=generator, device=device) + + if include_res_hidden_states_tuple: + generator_1 = torch.manual_seed(1) + dummy_input["res_hidden_states_tuple"] = (randn_tensor(shape, generator=generator_1, device=device),) + + if include_encoder_hidden_states: + dummy_input["encoder_hidden_states"] = floats_tensor((batch_size, 32, 32)).to(torch_device) + + if include_skip_sample: + dummy_input["skip_sample"] = randn_tensor(((batch_size, 3) + sizes), generator=generator, device=device) + + return dummy_input + + def prepare_init_args_and_inputs_for_common(self): + init_dict = { + "in_channels": 32, + "out_channels": 32, + "temb_channels": 128, + } + if self.block_type == "up": + init_dict["prev_output_channel"] = 32 + + if self.block_type == "mid": + init_dict.pop("out_channels") + + inputs_dict = self.dummy_input + return init_dict, inputs_dict + + def test_output(self, expected_slice): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + unet_block = self.block_class(**init_dict) + unet_block.to(torch_device) + unet_block.eval() + + with torch.no_grad(): + output = unet_block(**inputs_dict) + + if isinstance(output, Tuple): + output = output[0] + + self.assertEqual(output.shape, self.output_shape) + + output_slice = output[0, -1, -3:, -3:] + expected_slice = torch.tensor(expected_slice).to(torch_device) + assert torch_all_close(output_slice.flatten(), expected_slice, atol=5e-3) + + @unittest.skipIf(torch_device == "mps", "Training is not supported in mps") + def test_training(self): + init_dict, inputs_dict = self.prepare_init_args_and_inputs_for_common() + model = self.block_class(**init_dict) + model.to(torch_device) + model.train() + output = model(**inputs_dict) + + if isinstance(output, Tuple): + output = output[0] + + device = torch.device(torch_device) + noise = randn_tensor(output.shape, device=device) + loss = torch.nn.functional.mse_loss(output, noise) + loss.backward() diff --git a/diffusers/tests/test_utils.py b/diffusers/tests/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4f872896a03dfe1b4fb9d3c47448a868794300bd --- /dev/null +++ b/diffusers/tests/test_utils.py @@ -0,0 +1,170 @@ +# coding=utf-8 +# Copyright 2022 HuggingFace Inc. +# +# 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. + +import unittest + +from diffusers import __version__ +from diffusers.utils import deprecate + + +class DeprecateTester(unittest.TestCase): + higher_version = ".".join([str(int(__version__.split(".")[0]) + 1)] + __version__.split(".")[1:]) + lower_version = "0.0.1" + + def test_deprecate_function_arg(self): + kwargs = {"deprecated_arg": 4} + + with self.assertWarns(FutureWarning) as warning: + output = deprecate("deprecated_arg", self.higher_version, "message", take_from=kwargs) + + assert output == 4 + assert ( + str(warning.warning) + == f"The `deprecated_arg` argument is deprecated and will be removed in version {self.higher_version}." + " message" + ) + + def test_deprecate_function_arg_tuple(self): + kwargs = {"deprecated_arg": 4} + + with self.assertWarns(FutureWarning) as warning: + output = deprecate(("deprecated_arg", self.higher_version, "message"), take_from=kwargs) + + assert output == 4 + assert ( + str(warning.warning) + == f"The `deprecated_arg` argument is deprecated and will be removed in version {self.higher_version}." + " message" + ) + + def test_deprecate_function_args(self): + kwargs = {"deprecated_arg_1": 4, "deprecated_arg_2": 8} + with self.assertWarns(FutureWarning) as warning: + output_1, output_2 = deprecate( + ("deprecated_arg_1", self.higher_version, "Hey"), + ("deprecated_arg_2", self.higher_version, "Hey"), + take_from=kwargs, + ) + assert output_1 == 4 + assert output_2 == 8 + assert ( + str(warning.warnings[0].message) + == "The `deprecated_arg_1` argument is deprecated and will be removed in version" + f" {self.higher_version}. Hey" + ) + assert ( + str(warning.warnings[1].message) + == "The `deprecated_arg_2` argument is deprecated and will be removed in version" + f" {self.higher_version}. Hey" + ) + + def test_deprecate_function_incorrect_arg(self): + kwargs = {"deprecated_arg": 4} + + with self.assertRaises(TypeError) as error: + deprecate(("wrong_arg", self.higher_version, "message"), take_from=kwargs) + + assert "test_deprecate_function_incorrect_arg in" in str(error.exception) + assert "line" in str(error.exception) + assert "got an unexpected keyword argument `deprecated_arg`" in str(error.exception) + + def test_deprecate_arg_no_kwarg(self): + with self.assertWarns(FutureWarning) as warning: + deprecate(("deprecated_arg", self.higher_version, "message")) + + assert ( + str(warning.warning) + == f"`deprecated_arg` is deprecated and will be removed in version {self.higher_version}. message" + ) + + def test_deprecate_args_no_kwarg(self): + with self.assertWarns(FutureWarning) as warning: + deprecate( + ("deprecated_arg_1", self.higher_version, "Hey"), + ("deprecated_arg_2", self.higher_version, "Hey"), + ) + assert ( + str(warning.warnings[0].message) + == f"`deprecated_arg_1` is deprecated and will be removed in version {self.higher_version}. Hey" + ) + assert ( + str(warning.warnings[1].message) + == f"`deprecated_arg_2` is deprecated and will be removed in version {self.higher_version}. Hey" + ) + + def test_deprecate_class_obj(self): + class Args: + arg = 5 + + with self.assertWarns(FutureWarning) as warning: + arg = deprecate(("arg", self.higher_version, "message"), take_from=Args()) + + assert arg == 5 + assert ( + str(warning.warning) + == f"The `arg` attribute is deprecated and will be removed in version {self.higher_version}. message" + ) + + def test_deprecate_class_objs(self): + class Args: + arg = 5 + foo = 7 + + with self.assertWarns(FutureWarning) as warning: + arg_1, arg_2 = deprecate( + ("arg", self.higher_version, "message"), + ("foo", self.higher_version, "message"), + ("does not exist", self.higher_version, "message"), + take_from=Args(), + ) + + assert arg_1 == 5 + assert arg_2 == 7 + assert ( + str(warning.warning) + == f"The `arg` attribute is deprecated and will be removed in version {self.higher_version}. message" + ) + assert ( + str(warning.warnings[0].message) + == f"The `arg` attribute is deprecated and will be removed in version {self.higher_version}. message" + ) + assert ( + str(warning.warnings[1].message) + == f"The `foo` attribute is deprecated and will be removed in version {self.higher_version}. message" + ) + + def test_deprecate_incorrect_version(self): + kwargs = {"deprecated_arg": 4} + + with self.assertRaises(ValueError) as error: + deprecate(("wrong_arg", self.lower_version, "message"), take_from=kwargs) + + assert ( + str(error.exception) + == "The deprecation tuple ('wrong_arg', '0.0.1', 'message') should be removed since diffusers' version" + f" {__version__} is >= {self.lower_version}" + ) + + def test_deprecate_incorrect_no_standard_warn(self): + with self.assertWarns(FutureWarning) as warning: + deprecate(("deprecated_arg", self.higher_version, "This message is better!!!"), standard_warn=False) + + assert str(warning.warning) == "This message is better!!!" + + def test_deprecate_stacklevel(self): + with self.assertWarns(FutureWarning) as warning: + deprecate(("deprecated_arg", self.higher_version, "This message is better!!!"), standard_warn=False) + assert str(warning.warning) == "This message is better!!!" + assert "diffusers/tests/test_utils.py" in warning.filename diff --git a/diffusers/utils/check_config_docstrings.py b/diffusers/utils/check_config_docstrings.py new file mode 100644 index 0000000000000000000000000000000000000000..b816f7bd0b52d000c1167fe38e867f939531a018 --- /dev/null +++ b/diffusers/utils/check_config_docstrings.py @@ -0,0 +1,84 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import importlib +import inspect +import os +import re + + +# All paths are set with the intent you should run this script from the root of the repo with the command +# python utils/check_config_docstrings.py +PATH_TO_TRANSFORMERS = "src/transformers" + + +# This is to make sure the transformers module imported is the one in the repo. +spec = importlib.util.spec_from_file_location( + "transformers", + os.path.join(PATH_TO_TRANSFORMERS, "__init__.py"), + submodule_search_locations=[PATH_TO_TRANSFORMERS], +) +transformers = spec.loader.load_module() + +CONFIG_MAPPING = transformers.models.auto.configuration_auto.CONFIG_MAPPING + +# Regex pattern used to find the checkpoint mentioned in the docstring of `config_class`. +# For example, `[bert-base-uncased](https://huggingface.co/bert-base-uncased)` +_re_checkpoint = re.compile("\[(.+?)\]\((https://huggingface\.co/.+?)\)") + + +CONFIG_CLASSES_TO_IGNORE_FOR_DOCSTRING_CHECKPOINT_CHECK = { + "CLIPConfigMixin", + "DecisionTransformerConfigMixin", + "EncoderDecoderConfigMixin", + "RagConfigMixin", + "SpeechEncoderDecoderConfigMixin", + "VisionEncoderDecoderConfigMixin", + "VisionTextDualEncoderConfigMixin", +} + + +def check_config_docstrings_have_checkpoints(): + configs_without_checkpoint = [] + + for config_class in list(CONFIG_MAPPING.values()): + checkpoint_found = False + + # source code of `config_class` + config_source = inspect.getsource(config_class) + checkpoints = _re_checkpoint.findall(config_source) + + for checkpoint in checkpoints: + # Each `checkpoint` is a tuple of a checkpoint name and a checkpoint link. + # For example, `('bert-base-uncased', 'https://huggingface.co/bert-base-uncased')` + ckpt_name, ckpt_link = checkpoint + + # verify the checkpoint name corresponds to the checkpoint link + ckpt_link_from_name = f"https://huggingface.co/{ckpt_name}" + if ckpt_link == ckpt_link_from_name: + checkpoint_found = True + break + + name = config_class.__name__ + if not checkpoint_found and name not in CONFIG_CLASSES_TO_IGNORE_FOR_DOCSTRING_CHECKPOINT_CHECK: + configs_without_checkpoint.append(name) + + if len(configs_without_checkpoint) > 0: + message = "\n".join(sorted(configs_without_checkpoint)) + raise ValueError(f"The following configurations don't contain any valid checkpoint:\n{message}") + + +if __name__ == "__main__": + check_config_docstrings_have_checkpoints() diff --git a/diffusers/utils/check_copies.py b/diffusers/utils/check_copies.py new file mode 100644 index 0000000000000000000000000000000000000000..16782397da74a663b42a35bf092361a42b35d4ad --- /dev/null +++ b/diffusers/utils/check_copies.py @@ -0,0 +1,213 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import argparse +import glob +import importlib.util +import os +import re + +import black +from doc_builder.style_doc import style_docstrings_in_code + + +# All paths are set with the intent you should run this script from the root of the repo with the command +# python utils/check_copies.py +DIFFUSERS_PATH = "src/diffusers" +REPO_PATH = "." + + +# This is to make sure the diffusers module imported is the one in the repo. +spec = importlib.util.spec_from_file_location( + "diffusers", + os.path.join(DIFFUSERS_PATH, "__init__.py"), + submodule_search_locations=[DIFFUSERS_PATH], +) +diffusers_module = spec.loader.load_module() + + +def _should_continue(line, indent): + return line.startswith(indent) or len(line) <= 1 or re.search(r"^\s*\)(\s*->.*:|:)\s*$", line) is not None + + +def find_code_in_diffusers(object_name): + """Find and return the code source code of `object_name`.""" + parts = object_name.split(".") + i = 0 + + # First let's find the module where our object lives. + module = parts[i] + while i < len(parts) and not os.path.isfile(os.path.join(DIFFUSERS_PATH, f"{module}.py")): + i += 1 + if i < len(parts): + module = os.path.join(module, parts[i]) + if i >= len(parts): + raise ValueError(f"`object_name` should begin with the name of a module of diffusers but got {object_name}.") + + with open(os.path.join(DIFFUSERS_PATH, f"{module}.py"), "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + + # Now let's find the class / func in the code! + indent = "" + line_index = 0 + for name in parts[i + 1 :]: + while ( + line_index < len(lines) and re.search(rf"^{indent}(class|def)\s+{name}(\(|\:)", lines[line_index]) is None + ): + line_index += 1 + indent += " " + line_index += 1 + + if line_index >= len(lines): + raise ValueError(f" {object_name} does not match any function or class in {module}.") + + # We found the beginning of the class / func, now let's find the end (when the indent diminishes). + start_index = line_index + while line_index < len(lines) and _should_continue(lines[line_index], indent): + line_index += 1 + # Clean up empty lines at the end (if any). + while len(lines[line_index - 1]) <= 1: + line_index -= 1 + + code_lines = lines[start_index:line_index] + return "".join(code_lines) + + +_re_copy_warning = re.compile(r"^(\s*)#\s*Copied from\s+diffusers\.(\S+\.\S+)\s*($|\S.*$)") +_re_replace_pattern = re.compile(r"^\s*(\S+)->(\S+)(\s+.*|$)") +_re_fill_pattern = re.compile(r"]*>") + + +def get_indent(code): + lines = code.split("\n") + idx = 0 + while idx < len(lines) and len(lines[idx]) == 0: + idx += 1 + if idx < len(lines): + return re.search(r"^(\s*)\S", lines[idx]).groups()[0] + return "" + + +def blackify(code): + """ + Applies the black part of our `make style` command to `code`. + """ + has_indent = len(get_indent(code)) > 0 + if has_indent: + code = f"class Bla:\n{code}" + mode = black.Mode(target_versions={black.TargetVersion.PY37}, line_length=119, preview=True) + result = black.format_str(code, mode=mode) + result, _ = style_docstrings_in_code(result) + return result[len("class Bla:\n") :] if has_indent else result + + +def is_copy_consistent(filename, overwrite=False): + """ + Check if the code commented as a copy in `filename` matches the original. + Return the differences or overwrites the content depending on `overwrite`. + """ + with open(filename, "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + diffs = [] + line_index = 0 + # Not a for loop cause `lines` is going to change (if `overwrite=True`). + while line_index < len(lines): + search = _re_copy_warning.search(lines[line_index]) + if search is None: + line_index += 1 + continue + + # There is some copied code here, let's retrieve the original. + indent, object_name, replace_pattern = search.groups() + theoretical_code = find_code_in_diffusers(object_name) + theoretical_indent = get_indent(theoretical_code) + + start_index = line_index + 1 if indent == theoretical_indent else line_index + 2 + indent = theoretical_indent + line_index = start_index + + # Loop to check the observed code, stop when indentation diminishes or if we see a End copy comment. + should_continue = True + while line_index < len(lines) and should_continue: + line_index += 1 + if line_index >= len(lines): + break + line = lines[line_index] + should_continue = _should_continue(line, indent) and re.search(f"^{indent}# End copy", line) is None + # Clean up empty lines at the end (if any). + while len(lines[line_index - 1]) <= 1: + line_index -= 1 + + observed_code_lines = lines[start_index:line_index] + observed_code = "".join(observed_code_lines) + + # Remove any nested `Copied from` comments to avoid circular copies + theoretical_code = [line for line in theoretical_code.split("\n") if _re_copy_warning.search(line) is None] + theoretical_code = "\n".join(theoretical_code) + + # Before comparing, use the `replace_pattern` on the original code. + if len(replace_pattern) > 0: + patterns = replace_pattern.replace("with", "").split(",") + patterns = [_re_replace_pattern.search(p) for p in patterns] + for pattern in patterns: + if pattern is None: + continue + obj1, obj2, option = pattern.groups() + theoretical_code = re.sub(obj1, obj2, theoretical_code) + if option.strip() == "all-casing": + theoretical_code = re.sub(obj1.lower(), obj2.lower(), theoretical_code) + theoretical_code = re.sub(obj1.upper(), obj2.upper(), theoretical_code) + + # Blackify after replacement. To be able to do that, we need the header (class or function definition) + # from the previous line + theoretical_code = blackify(lines[start_index - 1] + theoretical_code) + theoretical_code = theoretical_code[len(lines[start_index - 1]) :] + + # Test for a diff and act accordingly. + if observed_code != theoretical_code: + diffs.append([object_name, start_index]) + if overwrite: + lines = lines[:start_index] + [theoretical_code] + lines[line_index:] + line_index = start_index + 1 + + if overwrite and len(diffs) > 0: + # Warn the user a file has been modified. + print(f"Detected changes, rewriting {filename}.") + with open(filename, "w", encoding="utf-8", newline="\n") as f: + f.writelines(lines) + return diffs + + +def check_copies(overwrite: bool = False): + all_files = glob.glob(os.path.join(DIFFUSERS_PATH, "**/*.py"), recursive=True) + diffs = [] + for filename in all_files: + new_diffs = is_copy_consistent(filename, overwrite) + diffs += [f"- {filename}: copy does not match {d[0]} at line {d[1]}" for d in new_diffs] + if not overwrite and len(diffs) > 0: + diff = "\n".join(diffs) + raise Exception( + "Found the following copy inconsistencies:\n" + + diff + + "\nRun `make fix-copies` or `python utils/check_copies.py --fix_and_overwrite` to fix them." + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--fix_and_overwrite", action="store_true", help="Whether to fix inconsistencies.") + args = parser.parse_args() + + check_copies(args.fix_and_overwrite) diff --git a/diffusers/utils/check_doc_toc.py b/diffusers/utils/check_doc_toc.py new file mode 100644 index 0000000000000000000000000000000000000000..79fa5d0420ea2e090d178cf33a49ce670f44840b --- /dev/null +++ b/diffusers/utils/check_doc_toc.py @@ -0,0 +1,158 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import argparse +from collections import defaultdict + +import yaml + + +PATH_TO_TOC = "docs/source/en/_toctree.yml" + + +def clean_doc_toc(doc_list): + """ + Cleans the table of content of the model documentation by removing duplicates and sorting models alphabetically. + """ + counts = defaultdict(int) + overview_doc = [] + new_doc_list = [] + for doc in doc_list: + if "local" in doc: + counts[doc["local"]] += 1 + + if doc["title"].lower() == "overview": + overview_doc.append({"local": doc["local"], "title": doc["title"]}) + else: + new_doc_list.append(doc) + + doc_list = new_doc_list + duplicates = [key for key, value in counts.items() if value > 1] + + new_doc = [] + for duplicate_key in duplicates: + titles = list(set(doc["title"] for doc in doc_list if doc["local"] == duplicate_key)) + if len(titles) > 1: + raise ValueError( + f"{duplicate_key} is present several times in the documentation table of content at " + "`docs/source/en/_toctree.yml` with different *Title* values. Choose one of those and remove the " + "others." + ) + # Only add this once + new_doc.append({"local": duplicate_key, "title": titles[0]}) + + # Add none duplicate-keys + new_doc.extend([doc for doc in doc_list if "local" not in counts or counts[doc["local"]] == 1]) + new_doc = sorted(new_doc, key=lambda s: s["title"].lower()) + + # "overview" gets special treatment and is always first + if len(overview_doc) > 1: + raise ValueError("{doc_list} has two 'overview' docs which is not allowed.") + + overview_doc.extend(new_doc) + + # Sort + return overview_doc + + +def check_scheduler_doc(overwrite=False): + with open(PATH_TO_TOC, encoding="utf-8") as f: + content = yaml.safe_load(f.read()) + + # Get to the API doc + api_idx = 0 + while content[api_idx]["title"] != "API": + api_idx += 1 + api_doc = content[api_idx]["sections"] + + # Then to the model doc + scheduler_idx = 0 + while api_doc[scheduler_idx]["title"] != "Schedulers": + scheduler_idx += 1 + + scheduler_doc = api_doc[scheduler_idx]["sections"] + new_scheduler_doc = clean_doc_toc(scheduler_doc) + + diff = False + if new_scheduler_doc != scheduler_doc: + diff = True + if overwrite: + api_doc[scheduler_idx]["sections"] = new_scheduler_doc + + if diff: + if overwrite: + content[api_idx]["sections"] = api_doc + with open(PATH_TO_TOC, "w", encoding="utf-8") as f: + f.write(yaml.dump(content, allow_unicode=True)) + else: + raise ValueError( + "The model doc part of the table of content is not properly sorted, run `make style` to fix this." + ) + + +def check_pipeline_doc(overwrite=False): + with open(PATH_TO_TOC, encoding="utf-8") as f: + content = yaml.safe_load(f.read()) + + # Get to the API doc + api_idx = 0 + while content[api_idx]["title"] != "API": + api_idx += 1 + api_doc = content[api_idx]["sections"] + + # Then to the model doc + pipeline_idx = 0 + while api_doc[pipeline_idx]["title"] != "Pipelines": + pipeline_idx += 1 + + diff = False + pipeline_docs = api_doc[pipeline_idx]["sections"] + new_pipeline_docs = [] + + # sort sub pipeline docs + for pipeline_doc in pipeline_docs: + if "section" in pipeline_doc: + sub_pipeline_doc = pipeline_doc["section"] + new_sub_pipeline_doc = clean_doc_toc(sub_pipeline_doc) + if overwrite: + pipeline_doc["section"] = new_sub_pipeline_doc + new_pipeline_docs.append(pipeline_doc) + + # sort overall pipeline doc + new_pipeline_docs = clean_doc_toc(new_pipeline_docs) + + if new_pipeline_docs != pipeline_docs: + diff = True + if overwrite: + api_doc[pipeline_idx]["sections"] = new_pipeline_docs + + if diff: + if overwrite: + content[api_idx]["sections"] = api_doc + with open(PATH_TO_TOC, "w", encoding="utf-8") as f: + f.write(yaml.dump(content, allow_unicode=True)) + else: + raise ValueError( + "The model doc part of the table of content is not properly sorted, run `make style` to fix this." + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--fix_and_overwrite", action="store_true", help="Whether to fix inconsistencies.") + args = parser.parse_args() + + check_scheduler_doc(args.fix_and_overwrite) + check_pipeline_doc(args.fix_and_overwrite) diff --git a/diffusers/utils/check_dummies.py b/diffusers/utils/check_dummies.py new file mode 100644 index 0000000000000000000000000000000000000000..e0cea048cf158df70efa7fd3cac3b7f7862d91be --- /dev/null +++ b/diffusers/utils/check_dummies.py @@ -0,0 +1,172 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import argparse +import os +import re + + +# All paths are set with the intent you should run this script from the root of the repo with the command +# python utils/check_dummies.py +PATH_TO_DIFFUSERS = "src/diffusers" + +# Matches is_xxx_available() +_re_backend = re.compile(r"is\_([a-z_]*)_available\(\)") +# Matches from xxx import bla +_re_single_line_import = re.compile(r"\s+from\s+\S*\s+import\s+([^\(\s].*)\n") + + +DUMMY_CONSTANT = """ +{0} = None +""" + +DUMMY_CLASS = """ +class {0}(metaclass=DummyObject): + _backends = {1} + + def __init__(self, *args, **kwargs): + requires_backends(self, {1}) + + @classmethod + def from_config(cls, *args, **kwargs): + requires_backends(cls, {1}) + + @classmethod + def from_pretrained(cls, *args, **kwargs): + requires_backends(cls, {1}) +""" + + +DUMMY_FUNCTION = """ +def {0}(*args, **kwargs): + requires_backends({0}, {1}) +""" + + +def find_backend(line): + """Find one (or multiple) backend in a code line of the init.""" + backends = _re_backend.findall(line) + if len(backends) == 0: + return None + + return "_and_".join(backends) + + +def read_init(): + """Read the init and extracts PyTorch, TensorFlow, SentencePiece and Tokenizers objects.""" + with open(os.path.join(PATH_TO_DIFFUSERS, "__init__.py"), "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + + # Get to the point we do the actual imports for type checking + line_index = 0 + backend_specific_objects = {} + # Go through the end of the file + while line_index < len(lines): + # If the line contains is_backend_available, we grab all objects associated with the `else` block + backend = find_backend(lines[line_index]) + if backend is not None: + while not lines[line_index].startswith("else:"): + line_index += 1 + line_index += 1 + objects = [] + # Until we unindent, add backend objects to the list + while line_index < len(lines) and len(lines[line_index]) > 1: + line = lines[line_index] + single_line_import_search = _re_single_line_import.search(line) + if single_line_import_search is not None: + objects.extend(single_line_import_search.groups()[0].split(", ")) + elif line.startswith(" " * 8): + objects.append(line[8:-2]) + line_index += 1 + + if len(objects) > 0: + backend_specific_objects[backend] = objects + else: + line_index += 1 + + return backend_specific_objects + + +def create_dummy_object(name, backend_name): + """Create the code for the dummy object corresponding to `name`.""" + if name.isupper(): + return DUMMY_CONSTANT.format(name) + elif name.islower(): + return DUMMY_FUNCTION.format(name, backend_name) + else: + return DUMMY_CLASS.format(name, backend_name) + + +def create_dummy_files(backend_specific_objects=None): + """Create the content of the dummy files.""" + if backend_specific_objects is None: + backend_specific_objects = read_init() + # For special correspondence backend to module name as used in the function requires_modulename + dummy_files = {} + + for backend, objects in backend_specific_objects.items(): + backend_name = "[" + ", ".join(f'"{b}"' for b in backend.split("_and_")) + "]" + dummy_file = "# This file is autogenerated by the command `make fix-copies`, do not edit.\n" + dummy_file += "from ..utils import DummyObject, requires_backends\n\n" + dummy_file += "\n".join([create_dummy_object(o, backend_name) for o in objects]) + dummy_files[backend] = dummy_file + + return dummy_files + + +def check_dummies(overwrite=False): + """Check if the dummy files are up to date and maybe `overwrite` with the right content.""" + dummy_files = create_dummy_files() + # For special correspondence backend to shortcut as used in utils/dummy_xxx_objects.py + short_names = {"torch": "pt"} + + # Locate actual dummy modules and read their content. + path = os.path.join(PATH_TO_DIFFUSERS, "utils") + dummy_file_paths = { + backend: os.path.join(path, f"dummy_{short_names.get(backend, backend)}_objects.py") + for backend in dummy_files.keys() + } + + actual_dummies = {} + for backend, file_path in dummy_file_paths.items(): + if os.path.isfile(file_path): + with open(file_path, "r", encoding="utf-8", newline="\n") as f: + actual_dummies[backend] = f.read() + else: + actual_dummies[backend] = "" + + for backend in dummy_files.keys(): + if dummy_files[backend] != actual_dummies[backend]: + if overwrite: + print( + f"Updating diffusers.utils.dummy_{short_names.get(backend, backend)}_objects.py as the main " + "__init__ has new objects." + ) + with open(dummy_file_paths[backend], "w", encoding="utf-8", newline="\n") as f: + f.write(dummy_files[backend]) + else: + raise ValueError( + "The main __init__ has objects that are not present in " + f"diffusers.utils.dummy_{short_names.get(backend, backend)}_objects.py. Run `make fix-copies` " + "to fix this." + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--fix_and_overwrite", action="store_true", help="Whether to fix inconsistencies.") + args = parser.parse_args() + + check_dummies(args.fix_and_overwrite) diff --git a/diffusers/utils/check_inits.py b/diffusers/utils/check_inits.py new file mode 100644 index 0000000000000000000000000000000000000000..0bc0a54d4e273472557d82a47ea4f3dc3f0beacb --- /dev/null +++ b/diffusers/utils/check_inits.py @@ -0,0 +1,299 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import collections +import importlib.util +import os +import re +from pathlib import Path + + +PATH_TO_TRANSFORMERS = "src/transformers" + + +# Matches is_xxx_available() +_re_backend = re.compile(r"is\_([a-z_]*)_available()") +# Catches a one-line _import_struct = {xxx} +_re_one_line_import_struct = re.compile(r"^_import_structure\s+=\s+\{([^\}]+)\}") +# Catches a line with a key-values pattern: "bla": ["foo", "bar"] +_re_import_struct_key_value = re.compile(r'\s+"\S*":\s+\[([^\]]*)\]') +# Catches a line if not is_foo_available +_re_test_backend = re.compile(r"^\s*if\s+not\s+is\_[a-z_]*\_available\(\)") +# Catches a line _import_struct["bla"].append("foo") +_re_import_struct_add_one = re.compile(r'^\s*_import_structure\["\S*"\]\.append\("(\S*)"\)') +# Catches a line _import_struct["bla"].extend(["foo", "bar"]) or _import_struct["bla"] = ["foo", "bar"] +_re_import_struct_add_many = re.compile(r"^\s*_import_structure\[\S*\](?:\.extend\(|\s*=\s+)\[([^\]]*)\]") +# Catches a line with an object between quotes and a comma: "MyModel", +_re_quote_object = re.compile('^\s+"([^"]+)",') +# Catches a line with objects between brackets only: ["foo", "bar"], +_re_between_brackets = re.compile("^\s+\[([^\]]+)\]") +# Catches a line with from foo import bar, bla, boo +_re_import = re.compile(r"\s+from\s+\S*\s+import\s+([^\(\s].*)\n") +# Catches a line with try: +_re_try = re.compile(r"^\s*try:") +# Catches a line with else: +_re_else = re.compile(r"^\s*else:") + + +def find_backend(line): + """Find one (or multiple) backend in a code line of the init.""" + if _re_test_backend.search(line) is None: + return None + backends = [b[0] for b in _re_backend.findall(line)] + backends.sort() + return "_and_".join(backends) + + +def parse_init(init_file): + """ + Read an init_file and parse (per backend) the _import_structure objects defined and the TYPE_CHECKING objects + defined + """ + with open(init_file, "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + + line_index = 0 + while line_index < len(lines) and not lines[line_index].startswith("_import_structure = {"): + line_index += 1 + + # If this is a traditional init, just return. + if line_index >= len(lines): + return None + + # First grab the objects without a specific backend in _import_structure + objects = [] + while not lines[line_index].startswith("if TYPE_CHECKING") and find_backend(lines[line_index]) is None: + line = lines[line_index] + # If we have everything on a single line, let's deal with it. + if _re_one_line_import_struct.search(line): + content = _re_one_line_import_struct.search(line).groups()[0] + imports = re.findall("\[([^\]]+)\]", content) + for imp in imports: + objects.extend([obj[1:-1] for obj in imp.split(", ")]) + line_index += 1 + continue + single_line_import_search = _re_import_struct_key_value.search(line) + if single_line_import_search is not None: + imports = [obj[1:-1] for obj in single_line_import_search.groups()[0].split(", ") if len(obj) > 0] + objects.extend(imports) + elif line.startswith(" " * 8 + '"'): + objects.append(line[9:-3]) + line_index += 1 + + import_dict_objects = {"none": objects} + # Let's continue with backend-specific objects in _import_structure + while not lines[line_index].startswith("if TYPE_CHECKING"): + # If the line is an if not is_backend_available, we grab all objects associated. + backend = find_backend(lines[line_index]) + # Check if the backend declaration is inside a try block: + if _re_try.search(lines[line_index - 1]) is None: + backend = None + + if backend is not None: + line_index += 1 + + # Scroll until we hit the else block of try-except-else + while _re_else.search(lines[line_index]) is None: + line_index += 1 + + line_index += 1 + + objects = [] + # Until we unindent, add backend objects to the list + while len(lines[line_index]) <= 1 or lines[line_index].startswith(" " * 4): + line = lines[line_index] + if _re_import_struct_add_one.search(line) is not None: + objects.append(_re_import_struct_add_one.search(line).groups()[0]) + elif _re_import_struct_add_many.search(line) is not None: + imports = _re_import_struct_add_many.search(line).groups()[0].split(", ") + imports = [obj[1:-1] for obj in imports if len(obj) > 0] + objects.extend(imports) + elif _re_between_brackets.search(line) is not None: + imports = _re_between_brackets.search(line).groups()[0].split(", ") + imports = [obj[1:-1] for obj in imports if len(obj) > 0] + objects.extend(imports) + elif _re_quote_object.search(line) is not None: + objects.append(_re_quote_object.search(line).groups()[0]) + elif line.startswith(" " * 8 + '"'): + objects.append(line[9:-3]) + elif line.startswith(" " * 12 + '"'): + objects.append(line[13:-3]) + line_index += 1 + + import_dict_objects[backend] = objects + else: + line_index += 1 + + # At this stage we are in the TYPE_CHECKING part, first grab the objects without a specific backend + objects = [] + while ( + line_index < len(lines) + and find_backend(lines[line_index]) is None + and not lines[line_index].startswith("else") + ): + line = lines[line_index] + single_line_import_search = _re_import.search(line) + if single_line_import_search is not None: + objects.extend(single_line_import_search.groups()[0].split(", ")) + elif line.startswith(" " * 8): + objects.append(line[8:-2]) + line_index += 1 + + type_hint_objects = {"none": objects} + # Let's continue with backend-specific objects + while line_index < len(lines): + # If the line is an if is_backend_available, we grab all objects associated. + backend = find_backend(lines[line_index]) + # Check if the backend declaration is inside a try block: + if _re_try.search(lines[line_index - 1]) is None: + backend = None + + if backend is not None: + line_index += 1 + + # Scroll until we hit the else block of try-except-else + while _re_else.search(lines[line_index]) is None: + line_index += 1 + + line_index += 1 + + objects = [] + # Until we unindent, add backend objects to the list + while len(lines[line_index]) <= 1 or lines[line_index].startswith(" " * 8): + line = lines[line_index] + single_line_import_search = _re_import.search(line) + if single_line_import_search is not None: + objects.extend(single_line_import_search.groups()[0].split(", ")) + elif line.startswith(" " * 12): + objects.append(line[12:-2]) + line_index += 1 + + type_hint_objects[backend] = objects + else: + line_index += 1 + + return import_dict_objects, type_hint_objects + + +def analyze_results(import_dict_objects, type_hint_objects): + """ + Analyze the differences between _import_structure objects and TYPE_CHECKING objects found in an init. + """ + + def find_duplicates(seq): + return [k for k, v in collections.Counter(seq).items() if v > 1] + + if list(import_dict_objects.keys()) != list(type_hint_objects.keys()): + return ["Both sides of the init do not have the same backends!"] + + errors = [] + for key in import_dict_objects.keys(): + duplicate_imports = find_duplicates(import_dict_objects[key]) + if duplicate_imports: + errors.append(f"Duplicate _import_structure definitions for: {duplicate_imports}") + duplicate_type_hints = find_duplicates(type_hint_objects[key]) + if duplicate_type_hints: + errors.append(f"Duplicate TYPE_CHECKING objects for: {duplicate_type_hints}") + + if sorted(set(import_dict_objects[key])) != sorted(set(type_hint_objects[key])): + name = "base imports" if key == "none" else f"{key} backend" + errors.append(f"Differences for {name}:") + for a in type_hint_objects[key]: + if a not in import_dict_objects[key]: + errors.append(f" {a} in TYPE_HINT but not in _import_structure.") + for a in import_dict_objects[key]: + if a not in type_hint_objects[key]: + errors.append(f" {a} in _import_structure but not in TYPE_HINT.") + return errors + + +def check_all_inits(): + """ + Check all inits in the transformers repo and raise an error if at least one does not define the same objects in + both halves. + """ + failures = [] + for root, _, files in os.walk(PATH_TO_TRANSFORMERS): + if "__init__.py" in files: + fname = os.path.join(root, "__init__.py") + objects = parse_init(fname) + if objects is not None: + errors = analyze_results(*objects) + if len(errors) > 0: + errors[0] = f"Problem in {fname}, both halves do not define the same objects.\n{errors[0]}" + failures.append("\n".join(errors)) + if len(failures) > 0: + raise ValueError("\n\n".join(failures)) + + +def get_transformers_submodules(): + """ + Returns the list of Transformers submodules. + """ + submodules = [] + for path, directories, files in os.walk(PATH_TO_TRANSFORMERS): + for folder in directories: + # Ignore private modules + if folder.startswith("_"): + directories.remove(folder) + continue + # Ignore leftovers from branches (empty folders apart from pycache) + if len(list((Path(path) / folder).glob("*.py"))) == 0: + continue + short_path = str((Path(path) / folder).relative_to(PATH_TO_TRANSFORMERS)) + submodule = short_path.replace(os.path.sep, ".") + submodules.append(submodule) + for fname in files: + if fname == "__init__.py": + continue + short_path = str((Path(path) / fname).relative_to(PATH_TO_TRANSFORMERS)) + submodule = short_path.replace(".py", "").replace(os.path.sep, ".") + if len(submodule.split(".")) == 1: + submodules.append(submodule) + return submodules + + +IGNORE_SUBMODULES = [ + "convert_pytorch_checkpoint_to_tf2", + "modeling_flax_pytorch_utils", +] + + +def check_submodules(): + # This is to make sure the transformers module imported is the one in the repo. + spec = importlib.util.spec_from_file_location( + "transformers", + os.path.join(PATH_TO_TRANSFORMERS, "__init__.py"), + submodule_search_locations=[PATH_TO_TRANSFORMERS], + ) + transformers = spec.loader.load_module() + + module_not_registered = [ + module + for module in get_transformers_submodules() + if module not in IGNORE_SUBMODULES and module not in transformers._import_structure.keys() + ] + if len(module_not_registered) > 0: + list_of_modules = "\n".join(f"- {module}" for module in module_not_registered) + raise ValueError( + "The following submodules are not properly registered in the main init of Transformers:\n" + f"{list_of_modules}\n" + "Make sure they appear somewhere in the keys of `_import_structure` with an empty list as value." + ) + + +if __name__ == "__main__": + check_all_inits() + check_submodules() diff --git a/diffusers/utils/check_repo.py b/diffusers/utils/check_repo.py new file mode 100644 index 0000000000000000000000000000000000000000..977203a0043a52c2ab202b16263692cf679470ef --- /dev/null +++ b/diffusers/utils/check_repo.py @@ -0,0 +1,761 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import importlib +import inspect +import os +import re +import warnings +from collections import OrderedDict +from difflib import get_close_matches +from pathlib import Path + +from diffusers.models.auto import get_values +from diffusers.utils import ENV_VARS_TRUE_VALUES, is_flax_available, is_tf_available, is_torch_available + + +# All paths are set with the intent you should run this script from the root of the repo with the command +# python utils/check_repo.py +PATH_TO_DIFFUSERS = "src/diffusers" +PATH_TO_TESTS = "tests" +PATH_TO_DOC = "docs/source/en" + +# Update this list with models that are supposed to be private. +PRIVATE_MODELS = [ + "DPRSpanPredictor", + "RealmBertModel", + "T5Stack", + "TFDPRSpanPredictor", +] + +# Update this list for models that are not tested with a comment explaining the reason it should not be. +# Being in this list is an exception and should **not** be the rule. +IGNORE_NON_TESTED = PRIVATE_MODELS.copy() + [ + # models to ignore for not tested + "OPTDecoder", # Building part of bigger (tested) model. + "DecisionTransformerGPT2Model", # Building part of bigger (tested) model. + "SegformerDecodeHead", # Building part of bigger (tested) model. + "PLBartEncoder", # Building part of bigger (tested) model. + "PLBartDecoder", # Building part of bigger (tested) model. + "PLBartDecoderWrapper", # Building part of bigger (tested) model. + "BigBirdPegasusEncoder", # Building part of bigger (tested) model. + "BigBirdPegasusDecoder", # Building part of bigger (tested) model. + "BigBirdPegasusDecoderWrapper", # Building part of bigger (tested) model. + "DetrEncoder", # Building part of bigger (tested) model. + "DetrDecoder", # Building part of bigger (tested) model. + "DetrDecoderWrapper", # Building part of bigger (tested) model. + "M2M100Encoder", # Building part of bigger (tested) model. + "M2M100Decoder", # Building part of bigger (tested) model. + "Speech2TextEncoder", # Building part of bigger (tested) model. + "Speech2TextDecoder", # Building part of bigger (tested) model. + "LEDEncoder", # Building part of bigger (tested) model. + "LEDDecoder", # Building part of bigger (tested) model. + "BartDecoderWrapper", # Building part of bigger (tested) model. + "BartEncoder", # Building part of bigger (tested) model. + "BertLMHeadModel", # Needs to be setup as decoder. + "BlenderbotSmallEncoder", # Building part of bigger (tested) model. + "BlenderbotSmallDecoderWrapper", # Building part of bigger (tested) model. + "BlenderbotEncoder", # Building part of bigger (tested) model. + "BlenderbotDecoderWrapper", # Building part of bigger (tested) model. + "MBartEncoder", # Building part of bigger (tested) model. + "MBartDecoderWrapper", # Building part of bigger (tested) model. + "MegatronBertLMHeadModel", # Building part of bigger (tested) model. + "MegatronBertEncoder", # Building part of bigger (tested) model. + "MegatronBertDecoder", # Building part of bigger (tested) model. + "MegatronBertDecoderWrapper", # Building part of bigger (tested) model. + "PegasusEncoder", # Building part of bigger (tested) model. + "PegasusDecoderWrapper", # Building part of bigger (tested) model. + "DPREncoder", # Building part of bigger (tested) model. + "ProphetNetDecoderWrapper", # Building part of bigger (tested) model. + "RealmBertModel", # Building part of bigger (tested) model. + "RealmReader", # Not regular model. + "RealmScorer", # Not regular model. + "RealmForOpenQA", # Not regular model. + "ReformerForMaskedLM", # Needs to be setup as decoder. + "Speech2Text2DecoderWrapper", # Building part of bigger (tested) model. + "TFDPREncoder", # Building part of bigger (tested) model. + "TFElectraMainLayer", # Building part of bigger (tested) model (should it be a TFModelMixin ?) + "TFRobertaForMultipleChoice", # TODO: fix + "TrOCRDecoderWrapper", # Building part of bigger (tested) model. + "SeparableConv1D", # Building part of bigger (tested) model. + "FlaxBartForCausalLM", # Building part of bigger (tested) model. + "FlaxBertForCausalLM", # Building part of bigger (tested) model. Tested implicitly through FlaxRobertaForCausalLM. + "OPTDecoderWrapper", +] + +# Update this list with test files that don't have a tester with a `all_model_classes` variable and which don't +# trigger the common tests. +TEST_FILES_WITH_NO_COMMON_TESTS = [ + "models/decision_transformer/test_modeling_decision_transformer.py", + "models/camembert/test_modeling_camembert.py", + "models/mt5/test_modeling_flax_mt5.py", + "models/mbart/test_modeling_mbart.py", + "models/mt5/test_modeling_mt5.py", + "models/pegasus/test_modeling_pegasus.py", + "models/camembert/test_modeling_tf_camembert.py", + "models/mt5/test_modeling_tf_mt5.py", + "models/xlm_roberta/test_modeling_tf_xlm_roberta.py", + "models/xlm_roberta/test_modeling_flax_xlm_roberta.py", + "models/xlm_prophetnet/test_modeling_xlm_prophetnet.py", + "models/xlm_roberta/test_modeling_xlm_roberta.py", + "models/vision_text_dual_encoder/test_modeling_vision_text_dual_encoder.py", + "models/vision_text_dual_encoder/test_modeling_flax_vision_text_dual_encoder.py", + "models/decision_transformer/test_modeling_decision_transformer.py", +] + +# Update this list for models that are not in any of the auto MODEL_XXX_MAPPING. Being in this list is an exception and +# should **not** be the rule. +IGNORE_NON_AUTO_CONFIGURED = PRIVATE_MODELS.copy() + [ + # models to ignore for model xxx mapping + "DPTForDepthEstimation", + "DecisionTransformerGPT2Model", + "GLPNForDepthEstimation", + "ViltForQuestionAnswering", + "ViltForImagesAndTextClassification", + "ViltForImageAndTextRetrieval", + "ViltForMaskedLM", + "XGLMEncoder", + "XGLMDecoder", + "XGLMDecoderWrapper", + "PerceiverForMultimodalAutoencoding", + "PerceiverForOpticalFlow", + "SegformerDecodeHead", + "FlaxBeitForMaskedImageModeling", + "PLBartEncoder", + "PLBartDecoder", + "PLBartDecoderWrapper", + "BeitForMaskedImageModeling", + "CLIPTextModel", + "CLIPVisionModel", + "TFCLIPTextModel", + "TFCLIPVisionModel", + "FlaxCLIPTextModel", + "FlaxCLIPVisionModel", + "FlaxWav2Vec2ForCTC", + "DetrForSegmentation", + "DPRReader", + "FlaubertForQuestionAnswering", + "FlavaImageCodebook", + "FlavaTextModel", + "FlavaImageModel", + "FlavaMultimodalModel", + "GPT2DoubleHeadsModel", + "LukeForMaskedLM", + "LukeForEntityClassification", + "LukeForEntityPairClassification", + "LukeForEntitySpanClassification", + "OpenAIGPTDoubleHeadsModel", + "RagModel", + "RagSequenceForGeneration", + "RagTokenForGeneration", + "RealmEmbedder", + "RealmForOpenQA", + "RealmScorer", + "RealmReader", + "TFDPRReader", + "TFGPT2DoubleHeadsModel", + "TFOpenAIGPTDoubleHeadsModel", + "TFRagModel", + "TFRagSequenceForGeneration", + "TFRagTokenForGeneration", + "Wav2Vec2ForCTC", + "HubertForCTC", + "SEWForCTC", + "SEWDForCTC", + "XLMForQuestionAnswering", + "XLNetForQuestionAnswering", + "SeparableConv1D", + "VisualBertForRegionToPhraseAlignment", + "VisualBertForVisualReasoning", + "VisualBertForQuestionAnswering", + "VisualBertForMultipleChoice", + "TFWav2Vec2ForCTC", + "TFHubertForCTC", + "MaskFormerForInstanceSegmentation", +] + +# Update this list for models that have multiple model types for the same +# model doc +MODEL_TYPE_TO_DOC_MAPPING = OrderedDict( + [ + ("data2vec-text", "data2vec"), + ("data2vec-audio", "data2vec"), + ("data2vec-vision", "data2vec"), + ] +) + + +# This is to make sure the transformers module imported is the one in the repo. +spec = importlib.util.spec_from_file_location( + "diffusers", + os.path.join(PATH_TO_DIFFUSERS, "__init__.py"), + submodule_search_locations=[PATH_TO_DIFFUSERS], +) +diffusers = spec.loader.load_module() + + +def check_model_list(): + """Check the model list inside the transformers library.""" + # Get the models from the directory structure of `src/diffusers/models/` + models_dir = os.path.join(PATH_TO_DIFFUSERS, "models") + _models = [] + for model in os.listdir(models_dir): + model_dir = os.path.join(models_dir, model) + if os.path.isdir(model_dir) and "__init__.py" in os.listdir(model_dir): + _models.append(model) + + # Get the models from the directory structure of `src/transformers/models/` + models = [model for model in dir(diffusers.models) if not model.startswith("__")] + + missing_models = sorted(list(set(_models).difference(models))) + if missing_models: + raise Exception( + f"The following models should be included in {models_dir}/__init__.py: {','.join(missing_models)}." + ) + + +# If some modeling modules should be ignored for all checks, they should be added in the nested list +# _ignore_modules of this function. +def get_model_modules(): + """Get the model modules inside the transformers library.""" + _ignore_modules = [ + "modeling_auto", + "modeling_encoder_decoder", + "modeling_marian", + "modeling_mmbt", + "modeling_outputs", + "modeling_retribert", + "modeling_utils", + "modeling_flax_auto", + "modeling_flax_encoder_decoder", + "modeling_flax_utils", + "modeling_speech_encoder_decoder", + "modeling_flax_speech_encoder_decoder", + "modeling_flax_vision_encoder_decoder", + "modeling_transfo_xl_utilities", + "modeling_tf_auto", + "modeling_tf_encoder_decoder", + "modeling_tf_outputs", + "modeling_tf_pytorch_utils", + "modeling_tf_utils", + "modeling_tf_transfo_xl_utilities", + "modeling_tf_vision_encoder_decoder", + "modeling_vision_encoder_decoder", + ] + modules = [] + for model in dir(diffusers.models): + # There are some magic dunder attributes in the dir, we ignore them + if not model.startswith("__"): + model_module = getattr(diffusers.models, model) + for submodule in dir(model_module): + if submodule.startswith("modeling") and submodule not in _ignore_modules: + modeling_module = getattr(model_module, submodule) + if inspect.ismodule(modeling_module): + modules.append(modeling_module) + return modules + + +def get_models(module, include_pretrained=False): + """Get the objects in module that are models.""" + models = [] + model_classes = (diffusers.ModelMixin, diffusers.TFModelMixin, diffusers.FlaxModelMixin) + for attr_name in dir(module): + if not include_pretrained and ("Pretrained" in attr_name or "PreTrained" in attr_name): + continue + attr = getattr(module, attr_name) + if isinstance(attr, type) and issubclass(attr, model_classes) and attr.__module__ == module.__name__: + models.append((attr_name, attr)) + return models + + +def is_a_private_model(model): + """Returns True if the model should not be in the main init.""" + if model in PRIVATE_MODELS: + return True + + # Wrapper, Encoder and Decoder are all privates + if model.endswith("Wrapper"): + return True + if model.endswith("Encoder"): + return True + if model.endswith("Decoder"): + return True + return False + + +def check_models_are_in_init(): + """Checks all models defined in the library are in the main init.""" + models_not_in_init = [] + dir_transformers = dir(diffusers) + for module in get_model_modules(): + models_not_in_init += [ + model[0] for model in get_models(module, include_pretrained=True) if model[0] not in dir_transformers + ] + + # Remove private models + models_not_in_init = [model for model in models_not_in_init if not is_a_private_model(model)] + if len(models_not_in_init) > 0: + raise Exception(f"The following models should be in the main init: {','.join(models_not_in_init)}.") + + +# If some test_modeling files should be ignored when checking models are all tested, they should be added in the +# nested list _ignore_files of this function. +def get_model_test_files(): + """Get the model test files. + + The returned files should NOT contain the `tests` (i.e. `PATH_TO_TESTS` defined in this script). They will be + considered as paths relative to `tests`. A caller has to use `os.path.join(PATH_TO_TESTS, ...)` to access the files. + """ + + _ignore_files = [ + "test_modeling_common", + "test_modeling_encoder_decoder", + "test_modeling_flax_encoder_decoder", + "test_modeling_flax_speech_encoder_decoder", + "test_modeling_marian", + "test_modeling_tf_common", + "test_modeling_tf_encoder_decoder", + ] + test_files = [] + # Check both `PATH_TO_TESTS` and `PATH_TO_TESTS/models` + model_test_root = os.path.join(PATH_TO_TESTS, "models") + model_test_dirs = [] + for x in os.listdir(model_test_root): + x = os.path.join(model_test_root, x) + if os.path.isdir(x): + model_test_dirs.append(x) + + for target_dir in [PATH_TO_TESTS] + model_test_dirs: + for file_or_dir in os.listdir(target_dir): + path = os.path.join(target_dir, file_or_dir) + if os.path.isfile(path): + filename = os.path.split(path)[-1] + if "test_modeling" in filename and os.path.splitext(filename)[0] not in _ignore_files: + file = os.path.join(*path.split(os.sep)[1:]) + test_files.append(file) + + return test_files + + +# This is a bit hacky but I didn't find a way to import the test_file as a module and read inside the tester class +# for the all_model_classes variable. +def find_tested_models(test_file): + """Parse the content of test_file to detect what's in all_model_classes""" + # This is a bit hacky but I didn't find a way to import the test_file as a module and read inside the class + with open(os.path.join(PATH_TO_TESTS, test_file), "r", encoding="utf-8", newline="\n") as f: + content = f.read() + all_models = re.findall(r"all_model_classes\s+=\s+\(\s*\(([^\)]*)\)", content) + # Check with one less parenthesis as well + all_models += re.findall(r"all_model_classes\s+=\s+\(([^\)]*)\)", content) + if len(all_models) > 0: + model_tested = [] + for entry in all_models: + for line in entry.split(","): + name = line.strip() + if len(name) > 0: + model_tested.append(name) + return model_tested + + +def check_models_are_tested(module, test_file): + """Check models defined in module are tested in test_file.""" + # XxxModelMixin are not tested + defined_models = get_models(module) + tested_models = find_tested_models(test_file) + if tested_models is None: + if test_file.replace(os.path.sep, "/") in TEST_FILES_WITH_NO_COMMON_TESTS: + return + return [ + f"{test_file} should define `all_model_classes` to apply common tests to the models it tests. " + + "If this intentional, add the test filename to `TEST_FILES_WITH_NO_COMMON_TESTS` in the file " + + "`utils/check_repo.py`." + ] + failures = [] + for model_name, _ in defined_models: + if model_name not in tested_models and model_name not in IGNORE_NON_TESTED: + failures.append( + f"{model_name} is defined in {module.__name__} but is not tested in " + + f"{os.path.join(PATH_TO_TESTS, test_file)}. Add it to the all_model_classes in that file." + + "If common tests should not applied to that model, add its name to `IGNORE_NON_TESTED`" + + "in the file `utils/check_repo.py`." + ) + return failures + + +def check_all_models_are_tested(): + """Check all models are properly tested.""" + modules = get_model_modules() + test_files = get_model_test_files() + failures = [] + for module in modules: + test_file = [file for file in test_files if f"test_{module.__name__.split('.')[-1]}.py" in file] + if len(test_file) == 0: + failures.append(f"{module.__name__} does not have its corresponding test file {test_file}.") + elif len(test_file) > 1: + failures.append(f"{module.__name__} has several test files: {test_file}.") + else: + test_file = test_file[0] + new_failures = check_models_are_tested(module, test_file) + if new_failures is not None: + failures += new_failures + if len(failures) > 0: + raise Exception(f"There were {len(failures)} failures:\n" + "\n".join(failures)) + + +def get_all_auto_configured_models(): + """Return the list of all models in at least one auto class.""" + result = set() # To avoid duplicates we concatenate all model classes in a set. + if is_torch_available(): + for attr_name in dir(diffusers.models.auto.modeling_auto): + if attr_name.startswith("MODEL_") and attr_name.endswith("MAPPING_NAMES"): + result = result | set(get_values(getattr(diffusers.models.auto.modeling_auto, attr_name))) + if is_tf_available(): + for attr_name in dir(diffusers.models.auto.modeling_tf_auto): + if attr_name.startswith("TF_MODEL_") and attr_name.endswith("MAPPING_NAMES"): + result = result | set(get_values(getattr(diffusers.models.auto.modeling_tf_auto, attr_name))) + if is_flax_available(): + for attr_name in dir(diffusers.models.auto.modeling_flax_auto): + if attr_name.startswith("FLAX_MODEL_") and attr_name.endswith("MAPPING_NAMES"): + result = result | set(get_values(getattr(diffusers.models.auto.modeling_flax_auto, attr_name))) + return [cls for cls in result] + + +def ignore_unautoclassed(model_name): + """Rules to determine if `name` should be in an auto class.""" + # Special white list + if model_name in IGNORE_NON_AUTO_CONFIGURED: + return True + # Encoder and Decoder should be ignored + if "Encoder" in model_name or "Decoder" in model_name: + return True + return False + + +def check_models_are_auto_configured(module, all_auto_models): + """Check models defined in module are each in an auto class.""" + defined_models = get_models(module) + failures = [] + for model_name, _ in defined_models: + if model_name not in all_auto_models and not ignore_unautoclassed(model_name): + failures.append( + f"{model_name} is defined in {module.__name__} but is not present in any of the auto mapping. " + "If that is intended behavior, add its name to `IGNORE_NON_AUTO_CONFIGURED` in the file " + "`utils/check_repo.py`." + ) + return failures + + +def check_all_models_are_auto_configured(): + """Check all models are each in an auto class.""" + missing_backends = [] + if not is_torch_available(): + missing_backends.append("PyTorch") + if not is_tf_available(): + missing_backends.append("TensorFlow") + if not is_flax_available(): + missing_backends.append("Flax") + if len(missing_backends) > 0: + missing = ", ".join(missing_backends) + if os.getenv("TRANSFORMERS_IS_CI", "").upper() in ENV_VARS_TRUE_VALUES: + raise Exception( + "Full quality checks require all backends to be installed (with `pip install -e .[dev]` in the " + f"Transformers repo, the following are missing: {missing}." + ) + else: + warnings.warn( + "Full quality checks require all backends to be installed (with `pip install -e .[dev]` in the " + f"Transformers repo, the following are missing: {missing}. While it's probably fine as long as you " + "didn't make any change in one of those backends modeling files, you should probably execute the " + "command above to be on the safe side." + ) + modules = get_model_modules() + all_auto_models = get_all_auto_configured_models() + failures = [] + for module in modules: + new_failures = check_models_are_auto_configured(module, all_auto_models) + if new_failures is not None: + failures += new_failures + if len(failures) > 0: + raise Exception(f"There were {len(failures)} failures:\n" + "\n".join(failures)) + + +_re_decorator = re.compile(r"^\s*@(\S+)\s+$") + + +def check_decorator_order(filename): + """Check that in the test file `filename` the slow decorator is always last.""" + with open(filename, "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + decorator_before = None + errors = [] + for i, line in enumerate(lines): + search = _re_decorator.search(line) + if search is not None: + decorator_name = search.groups()[0] + if decorator_before is not None and decorator_name.startswith("parameterized"): + errors.append(i) + decorator_before = decorator_name + elif decorator_before is not None: + decorator_before = None + return errors + + +def check_all_decorator_order(): + """Check that in all test files, the slow decorator is always last.""" + errors = [] + for fname in os.listdir(PATH_TO_TESTS): + if fname.endswith(".py"): + filename = os.path.join(PATH_TO_TESTS, fname) + new_errors = check_decorator_order(filename) + errors += [f"- {filename}, line {i}" for i in new_errors] + if len(errors) > 0: + msg = "\n".join(errors) + raise ValueError( + "The parameterized decorator (and its variants) should always be first, but this is not the case in the" + f" following files:\n{msg}" + ) + + +def find_all_documented_objects(): + """Parse the content of all doc files to detect which classes and functions it documents""" + documented_obj = [] + for doc_file in Path(PATH_TO_DOC).glob("**/*.rst"): + with open(doc_file, "r", encoding="utf-8", newline="\n") as f: + content = f.read() + raw_doc_objs = re.findall(r"(?:autoclass|autofunction):: transformers.(\S+)\s+", content) + documented_obj += [obj.split(".")[-1] for obj in raw_doc_objs] + for doc_file in Path(PATH_TO_DOC).glob("**/*.mdx"): + with open(doc_file, "r", encoding="utf-8", newline="\n") as f: + content = f.read() + raw_doc_objs = re.findall("\[\[autodoc\]\]\s+(\S+)\s+", content) + documented_obj += [obj.split(".")[-1] for obj in raw_doc_objs] + return documented_obj + + +# One good reason for not being documented is to be deprecated. Put in this list deprecated objects. +DEPRECATED_OBJECTS = [ + "AutoModelWithLMHead", + "BartPretrainedModel", + "DataCollator", + "DataCollatorForSOP", + "GlueDataset", + "GlueDataTrainingArguments", + "LineByLineTextDataset", + "LineByLineWithRefDataset", + "LineByLineWithSOPTextDataset", + "PretrainedBartModel", + "PretrainedFSMTModel", + "SingleSentenceClassificationProcessor", + "SquadDataTrainingArguments", + "SquadDataset", + "SquadExample", + "SquadFeatures", + "SquadV1Processor", + "SquadV2Processor", + "TFAutoModelWithLMHead", + "TFBartPretrainedModel", + "TextDataset", + "TextDatasetForNextSentencePrediction", + "Wav2Vec2ForMaskedLM", + "Wav2Vec2Tokenizer", + "glue_compute_metrics", + "glue_convert_examples_to_features", + "glue_output_modes", + "glue_processors", + "glue_tasks_num_labels", + "squad_convert_examples_to_features", + "xnli_compute_metrics", + "xnli_output_modes", + "xnli_processors", + "xnli_tasks_num_labels", + "TFTrainer", + "TFTrainingArguments", +] + +# Exceptionally, some objects should not be documented after all rules passed. +# ONLY PUT SOMETHING IN THIS LIST AS A LAST RESORT! +UNDOCUMENTED_OBJECTS = [ + "AddedToken", # This is a tokenizers class. + "BasicTokenizer", # Internal, should never have been in the main init. + "CharacterTokenizer", # Internal, should never have been in the main init. + "DPRPretrainedReader", # Like an Encoder. + "DummyObject", # Just picked by mistake sometimes. + "MecabTokenizer", # Internal, should never have been in the main init. + "ModelCard", # Internal type. + "SqueezeBertModule", # Internal building block (should have been called SqueezeBertLayer) + "TFDPRPretrainedReader", # Like an Encoder. + "TransfoXLCorpus", # Internal type. + "WordpieceTokenizer", # Internal, should never have been in the main init. + "absl", # External module + "add_end_docstrings", # Internal, should never have been in the main init. + "add_start_docstrings", # Internal, should never have been in the main init. + "cached_path", # Internal used for downloading models. + "convert_tf_weight_name_to_pt_weight_name", # Internal used to convert model weights + "logger", # Internal logger + "logging", # External module + "requires_backends", # Internal function +] + +# This list should be empty. Objects in it should get their own doc page. +SHOULD_HAVE_THEIR_OWN_PAGE = [ + # Benchmarks + "PyTorchBenchmark", + "PyTorchBenchmarkArguments", + "TensorFlowBenchmark", + "TensorFlowBenchmarkArguments", +] + + +def ignore_undocumented(name): + """Rules to determine if `name` should be undocumented.""" + # NOT DOCUMENTED ON PURPOSE. + # Constants uppercase are not documented. + if name.isupper(): + return True + # ModelMixins / Encoders / Decoders / Layers / Embeddings / Attention are not documented. + if ( + name.endswith("ModelMixin") + or name.endswith("Decoder") + or name.endswith("Encoder") + or name.endswith("Layer") + or name.endswith("Embeddings") + or name.endswith("Attention") + ): + return True + # Submodules are not documented. + if os.path.isdir(os.path.join(PATH_TO_DIFFUSERS, name)) or os.path.isfile( + os.path.join(PATH_TO_DIFFUSERS, f"{name}.py") + ): + return True + # All load functions are not documented. + if name.startswith("load_tf") or name.startswith("load_pytorch"): + return True + # is_xxx_available functions are not documented. + if name.startswith("is_") and name.endswith("_available"): + return True + # Deprecated objects are not documented. + if name in DEPRECATED_OBJECTS or name in UNDOCUMENTED_OBJECTS: + return True + # MMBT model does not really work. + if name.startswith("MMBT"): + return True + if name in SHOULD_HAVE_THEIR_OWN_PAGE: + return True + return False + + +def check_all_objects_are_documented(): + """Check all models are properly documented.""" + documented_objs = find_all_documented_objects() + modules = diffusers._modules + objects = [c for c in dir(diffusers) if c not in modules and not c.startswith("_")] + undocumented_objs = [c for c in objects if c not in documented_objs and not ignore_undocumented(c)] + if len(undocumented_objs) > 0: + raise Exception( + "The following objects are in the public init so should be documented:\n - " + + "\n - ".join(undocumented_objs) + ) + check_docstrings_are_in_md() + check_model_type_doc_match() + + +def check_model_type_doc_match(): + """Check all doc pages have a corresponding model type.""" + model_doc_folder = Path(PATH_TO_DOC) / "model_doc" + model_docs = [m.stem for m in model_doc_folder.glob("*.mdx")] + + model_types = list(diffusers.models.auto.configuration_auto.MODEL_NAMES_MAPPING.keys()) + model_types = [MODEL_TYPE_TO_DOC_MAPPING[m] if m in MODEL_TYPE_TO_DOC_MAPPING else m for m in model_types] + + errors = [] + for m in model_docs: + if m not in model_types and m != "auto": + close_matches = get_close_matches(m, model_types) + error_message = f"{m} is not a proper model identifier." + if len(close_matches) > 0: + close_matches = "/".join(close_matches) + error_message += f" Did you mean {close_matches}?" + errors.append(error_message) + + if len(errors) > 0: + raise ValueError( + "Some model doc pages do not match any existing model type:\n" + + "\n".join(errors) + + "\nYou can add any missing model type to the `MODEL_NAMES_MAPPING` constant in " + "models/auto/configuration_auto.py." + ) + + +# Re pattern to catch :obj:`xx`, :class:`xx`, :func:`xx` or :meth:`xx`. +_re_rst_special_words = re.compile(r":(?:obj|func|class|meth):`([^`]+)`") +# Re pattern to catch things between double backquotes. +_re_double_backquotes = re.compile(r"(^|[^`])``([^`]+)``([^`]|$)") +# Re pattern to catch example introduction. +_re_rst_example = re.compile(r"^\s*Example.*::\s*$", flags=re.MULTILINE) + + +def is_rst_docstring(docstring): + """ + Returns `True` if `docstring` is written in rst. + """ + if _re_rst_special_words.search(docstring) is not None: + return True + if _re_double_backquotes.search(docstring) is not None: + return True + if _re_rst_example.search(docstring) is not None: + return True + return False + + +def check_docstrings_are_in_md(): + """Check all docstrings are in md""" + files_with_rst = [] + for file in Path(PATH_TO_DIFFUSERS).glob("**/*.py"): + with open(file, "r") as f: + code = f.read() + docstrings = code.split('"""') + + for idx, docstring in enumerate(docstrings): + if idx % 2 == 0 or not is_rst_docstring(docstring): + continue + files_with_rst.append(file) + break + + if len(files_with_rst) > 0: + raise ValueError( + "The following files have docstrings written in rst:\n" + + "\n".join([f"- {f}" for f in files_with_rst]) + + "\nTo fix this run `doc-builder convert path_to_py_file` after installing `doc-builder`\n" + "(`pip install git+https://github.com/huggingface/doc-builder`)" + ) + + +def check_repo_quality(): + """Check all models are properly tested and documented.""" + print("Checking all models are included.") + check_model_list() + print("Checking all models are public.") + check_models_are_in_init() + print("Checking all models are properly tested.") + check_all_decorator_order() + check_all_models_are_tested() + print("Checking all objects are properly documented.") + check_all_objects_are_documented() + print("Checking all models are in at least one auto class.") + check_all_models_are_auto_configured() + + +if __name__ == "__main__": + check_repo_quality() diff --git a/diffusers/utils/check_table.py b/diffusers/utils/check_table.py new file mode 100644 index 0000000000000000000000000000000000000000..7636c6bde3de0ca832a40aaac198596b50f45927 --- /dev/null +++ b/diffusers/utils/check_table.py @@ -0,0 +1,185 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import argparse +import collections +import importlib.util +import os +import re + + +# All paths are set with the intent you should run this script from the root of the repo with the command +# python utils/check_table.py +TRANSFORMERS_PATH = "src/diffusers" +PATH_TO_DOCS = "docs/source/en" +REPO_PATH = "." + + +def _find_text_in_file(filename, start_prompt, end_prompt): + """ + Find the text in `filename` between a line beginning with `start_prompt` and before `end_prompt`, removing empty + lines. + """ + with open(filename, "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + # Find the start prompt. + start_index = 0 + while not lines[start_index].startswith(start_prompt): + start_index += 1 + start_index += 1 + + end_index = start_index + while not lines[end_index].startswith(end_prompt): + end_index += 1 + end_index -= 1 + + while len(lines[start_index]) <= 1: + start_index += 1 + while len(lines[end_index]) <= 1: + end_index -= 1 + end_index += 1 + return "".join(lines[start_index:end_index]), start_index, end_index, lines + + +# Add here suffixes that are used to identify models, separated by | +ALLOWED_MODEL_SUFFIXES = "Model|Encoder|Decoder|ForConditionalGeneration" +# Regexes that match TF/Flax/PT model names. +_re_tf_models = re.compile(r"TF(.*)(?:Model|Encoder|Decoder|ForConditionalGeneration)") +_re_flax_models = re.compile(r"Flax(.*)(?:Model|Encoder|Decoder|ForConditionalGeneration)") +# Will match any TF or Flax model too so need to be in an else branch afterthe two previous regexes. +_re_pt_models = re.compile(r"(.*)(?:Model|Encoder|Decoder|ForConditionalGeneration)") + + +# This is to make sure the diffusers module imported is the one in the repo. +spec = importlib.util.spec_from_file_location( + "diffusers", + os.path.join(TRANSFORMERS_PATH, "__init__.py"), + submodule_search_locations=[TRANSFORMERS_PATH], +) +diffusers_module = spec.loader.load_module() + + +# Thanks to https://stackoverflow.com/questions/29916065/how-to-do-camelcase-split-in-python +def camel_case_split(identifier): + "Split a camelcased `identifier` into words." + matches = re.finditer(".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", identifier) + return [m.group(0) for m in matches] + + +def _center_text(text, width): + text_length = 2 if text == "✅" or text == "❌" else len(text) + left_indent = (width - text_length) // 2 + right_indent = width - text_length - left_indent + return " " * left_indent + text + " " * right_indent + + +def get_model_table_from_auto_modules(): + """Generates an up-to-date model table from the content of the auto modules.""" + # Dictionary model names to config. + config_mapping_names = diffusers_module.models.auto.configuration_auto.CONFIG_MAPPING_NAMES + model_name_to_config = { + name: config_mapping_names[code] + for code, name in diffusers_module.MODEL_NAMES_MAPPING.items() + if code in config_mapping_names + } + model_name_to_prefix = {name: config.replace("ConfigMixin", "") for name, config in model_name_to_config.items()} + + # Dictionaries flagging if each model prefix has a slow/fast tokenizer, backend in PT/TF/Flax. + slow_tokenizers = collections.defaultdict(bool) + fast_tokenizers = collections.defaultdict(bool) + pt_models = collections.defaultdict(bool) + tf_models = collections.defaultdict(bool) + flax_models = collections.defaultdict(bool) + + # Let's lookup through all diffusers object (once). + for attr_name in dir(diffusers_module): + lookup_dict = None + if attr_name.endswith("Tokenizer"): + lookup_dict = slow_tokenizers + attr_name = attr_name[:-9] + elif attr_name.endswith("TokenizerFast"): + lookup_dict = fast_tokenizers + attr_name = attr_name[:-13] + elif _re_tf_models.match(attr_name) is not None: + lookup_dict = tf_models + attr_name = _re_tf_models.match(attr_name).groups()[0] + elif _re_flax_models.match(attr_name) is not None: + lookup_dict = flax_models + attr_name = _re_flax_models.match(attr_name).groups()[0] + elif _re_pt_models.match(attr_name) is not None: + lookup_dict = pt_models + attr_name = _re_pt_models.match(attr_name).groups()[0] + + if lookup_dict is not None: + while len(attr_name) > 0: + if attr_name in model_name_to_prefix.values(): + lookup_dict[attr_name] = True + break + # Try again after removing the last word in the name + attr_name = "".join(camel_case_split(attr_name)[:-1]) + + # Let's build that table! + model_names = list(model_name_to_config.keys()) + model_names.sort(key=str.lower) + columns = ["Model", "Tokenizer slow", "Tokenizer fast", "PyTorch support", "TensorFlow support", "Flax Support"] + # We'll need widths to properly display everything in the center (+2 is to leave one extra space on each side). + widths = [len(c) + 2 for c in columns] + widths[0] = max([len(name) for name in model_names]) + 2 + + # Build the table per se + table = "|" + "|".join([_center_text(c, w) for c, w in zip(columns, widths)]) + "|\n" + # Use ":-----:" format to center-aligned table cell texts + table += "|" + "|".join([":" + "-" * (w - 2) + ":" for w in widths]) + "|\n" + + check = {True: "✅", False: "❌"} + for name in model_names: + prefix = model_name_to_prefix[name] + line = [ + name, + check[slow_tokenizers[prefix]], + check[fast_tokenizers[prefix]], + check[pt_models[prefix]], + check[tf_models[prefix]], + check[flax_models[prefix]], + ] + table += "|" + "|".join([_center_text(l, w) for l, w in zip(line, widths)]) + "|\n" + return table + + +def check_model_table(overwrite=False): + """Check the model table in the index.rst is consistent with the state of the lib and maybe `overwrite`.""" + current_table, start_index, end_index, lines = _find_text_in_file( + filename=os.path.join(PATH_TO_DOCS, "index.mdx"), + start_prompt="", + ) + new_table = get_model_table_from_auto_modules() + + if current_table != new_table: + if overwrite: + with open(os.path.join(PATH_TO_DOCS, "index.mdx"), "w", encoding="utf-8", newline="\n") as f: + f.writelines(lines[:start_index] + [new_table] + lines[end_index:]) + else: + raise ValueError( + "The model table in the `index.mdx` has not been updated. Run `make fix-copies` to fix this." + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--fix_and_overwrite", action="store_true", help="Whether to fix inconsistencies.") + args = parser.parse_args() + + check_model_table(args.fix_and_overwrite) diff --git a/diffusers/utils/custom_init_isort.py b/diffusers/utils/custom_init_isort.py new file mode 100644 index 0000000000000000000000000000000000000000..2e18bc4c9212caf0e617b6fb67e46363df03215b --- /dev/null +++ b/diffusers/utils/custom_init_isort.py @@ -0,0 +1,252 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +import argparse +import os +import re + + +PATH_TO_TRANSFORMERS = "src/diffusers" + +# Pattern that looks at the indentation in a line. +_re_indent = re.compile(r"^(\s*)\S") +# Pattern that matches `"key":" and puts `key` in group 0. +_re_direct_key = re.compile(r'^\s*"([^"]+)":') +# Pattern that matches `_import_structure["key"]` and puts `key` in group 0. +_re_indirect_key = re.compile(r'^\s*_import_structure\["([^"]+)"\]') +# Pattern that matches `"key",` and puts `key` in group 0. +_re_strip_line = re.compile(r'^\s*"([^"]+)",\s*$') +# Pattern that matches any `[stuff]` and puts `stuff` in group 0. +_re_bracket_content = re.compile(r"\[([^\]]+)\]") + + +def get_indent(line): + """Returns the indent in `line`.""" + search = _re_indent.search(line) + return "" if search is None else search.groups()[0] + + +def split_code_in_indented_blocks(code, indent_level="", start_prompt=None, end_prompt=None): + """ + Split `code` into its indented blocks, starting at `indent_level`. If provided, begins splitting after + `start_prompt` and stops at `end_prompt` (but returns what's before `start_prompt` as a first block and what's + after `end_prompt` as a last block, so `code` is always the same as joining the result of this function). + """ + # Let's split the code into lines and move to start_index. + index = 0 + lines = code.split("\n") + if start_prompt is not None: + while not lines[index].startswith(start_prompt): + index += 1 + blocks = ["\n".join(lines[:index])] + else: + blocks = [] + + # We split into blocks until we get to the `end_prompt` (or the end of the block). + current_block = [lines[index]] + index += 1 + while index < len(lines) and (end_prompt is None or not lines[index].startswith(end_prompt)): + if len(lines[index]) > 0 and get_indent(lines[index]) == indent_level: + if len(current_block) > 0 and get_indent(current_block[-1]).startswith(indent_level + " "): + current_block.append(lines[index]) + blocks.append("\n".join(current_block)) + if index < len(lines) - 1: + current_block = [lines[index + 1]] + index += 1 + else: + current_block = [] + else: + blocks.append("\n".join(current_block)) + current_block = [lines[index]] + else: + current_block.append(lines[index]) + index += 1 + + # Adds current block if it's nonempty. + if len(current_block) > 0: + blocks.append("\n".join(current_block)) + + # Add final block after end_prompt if provided. + if end_prompt is not None and index < len(lines): + blocks.append("\n".join(lines[index:])) + + return blocks + + +def ignore_underscore(key): + "Wraps a `key` (that maps an object to string) to lower case and remove underscores." + + def _inner(x): + return key(x).lower().replace("_", "") + + return _inner + + +def sort_objects(objects, key=None): + "Sort a list of `objects` following the rules of isort. `key` optionally maps an object to a str." + + # If no key is provided, we use a noop. + def noop(x): + return x + + if key is None: + key = noop + # Constants are all uppercase, they go first. + constants = [obj for obj in objects if key(obj).isupper()] + # Classes are not all uppercase but start with a capital, they go second. + classes = [obj for obj in objects if key(obj)[0].isupper() and not key(obj).isupper()] + # Functions begin with a lowercase, they go last. + functions = [obj for obj in objects if not key(obj)[0].isupper()] + + key1 = ignore_underscore(key) + return sorted(constants, key=key1) + sorted(classes, key=key1) + sorted(functions, key=key1) + + +def sort_objects_in_import(import_statement): + """ + Return the same `import_statement` but with objects properly sorted. + """ + + # This inner function sort imports between [ ]. + def _replace(match): + imports = match.groups()[0] + if "," not in imports: + return f"[{imports}]" + keys = [part.strip().replace('"', "") for part in imports.split(",")] + # We will have a final empty element if the line finished with a comma. + if len(keys[-1]) == 0: + keys = keys[:-1] + return "[" + ", ".join([f'"{k}"' for k in sort_objects(keys)]) + "]" + + lines = import_statement.split("\n") + if len(lines) > 3: + # Here we have to sort internal imports that are on several lines (one per name): + # key: [ + # "object1", + # "object2", + # ... + # ] + + # We may have to ignore one or two lines on each side. + idx = 2 if lines[1].strip() == "[" else 1 + keys_to_sort = [(i, _re_strip_line.search(line).groups()[0]) for i, line in enumerate(lines[idx:-idx])] + sorted_indices = sort_objects(keys_to_sort, key=lambda x: x[1]) + sorted_lines = [lines[x[0] + idx] for x in sorted_indices] + return "\n".join(lines[:idx] + sorted_lines + lines[-idx:]) + elif len(lines) == 3: + # Here we have to sort internal imports that are on one separate line: + # key: [ + # "object1", "object2", ... + # ] + if _re_bracket_content.search(lines[1]) is not None: + lines[1] = _re_bracket_content.sub(_replace, lines[1]) + else: + keys = [part.strip().replace('"', "") for part in lines[1].split(",")] + # We will have a final empty element if the line finished with a comma. + if len(keys[-1]) == 0: + keys = keys[:-1] + lines[1] = get_indent(lines[1]) + ", ".join([f'"{k}"' for k in sort_objects(keys)]) + return "\n".join(lines) + else: + # Finally we have to deal with imports fitting on one line + import_statement = _re_bracket_content.sub(_replace, import_statement) + return import_statement + + +def sort_imports(file, check_only=True): + """ + Sort `_import_structure` imports in `file`, `check_only` determines if we only check or overwrite. + """ + with open(file, "r") as f: + code = f.read() + + if "_import_structure" not in code: + return + + # Blocks of indent level 0 + main_blocks = split_code_in_indented_blocks( + code, start_prompt="_import_structure = {", end_prompt="if TYPE_CHECKING:" + ) + + # We ignore block 0 (everything until start_prompt) and the last block (everything after end_prompt). + for block_idx in range(1, len(main_blocks) - 1): + # Check if the block contains some `_import_structure`s thingy to sort. + block = main_blocks[block_idx] + block_lines = block.split("\n") + + # Get to the start of the imports. + line_idx = 0 + while line_idx < len(block_lines) and "_import_structure" not in block_lines[line_idx]: + # Skip dummy import blocks + if "import dummy" in block_lines[line_idx]: + line_idx = len(block_lines) + else: + line_idx += 1 + if line_idx >= len(block_lines): + continue + + # Ignore beginning and last line: they don't contain anything. + internal_block_code = "\n".join(block_lines[line_idx:-1]) + indent = get_indent(block_lines[1]) + # Slit the internal block into blocks of indent level 1. + internal_blocks = split_code_in_indented_blocks(internal_block_code, indent_level=indent) + # We have two categories of import key: list or _import_structure[key].append/extend + pattern = _re_direct_key if "_import_structure" in block_lines[0] else _re_indirect_key + # Grab the keys, but there is a trap: some lines are empty or just comments. + keys = [(pattern.search(b).groups()[0] if pattern.search(b) is not None else None) for b in internal_blocks] + # We only sort the lines with a key. + keys_to_sort = [(i, key) for i, key in enumerate(keys) if key is not None] + sorted_indices = [x[0] for x in sorted(keys_to_sort, key=lambda x: x[1])] + + # We reorder the blocks by leaving empty lines/comments as they were and reorder the rest. + count = 0 + reordered_blocks = [] + for i in range(len(internal_blocks)): + if keys[i] is None: + reordered_blocks.append(internal_blocks[i]) + else: + block = sort_objects_in_import(internal_blocks[sorted_indices[count]]) + reordered_blocks.append(block) + count += 1 + + # And we put our main block back together with its first and last line. + main_blocks[block_idx] = "\n".join(block_lines[:line_idx] + reordered_blocks + [block_lines[-1]]) + + if code != "\n".join(main_blocks): + if check_only: + return True + else: + print(f"Overwriting {file}.") + with open(file, "w") as f: + f.write("\n".join(main_blocks)) + + +def sort_imports_in_all_inits(check_only=True): + failures = [] + for root, _, files in os.walk(PATH_TO_TRANSFORMERS): + if "__init__.py" in files: + result = sort_imports(os.path.join(root, "__init__.py"), check_only=check_only) + if result: + failures = [os.path.join(root, "__init__.py")] + if len(failures) > 0: + raise ValueError(f"Would overwrite {len(failures)} files, run `make style`.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--check_only", action="store_true", help="Whether to only check or fix style.") + args = parser.parse_args() + + sort_imports_in_all_inits(check_only=args.check_only) diff --git a/diffusers/utils/get_modified_files.py b/diffusers/utils/get_modified_files.py new file mode 100644 index 0000000000000000000000000000000000000000..d2c6bbf874b2fd852cbf781e44fba3dbb5a679db --- /dev/null +++ b/diffusers/utils/get_modified_files.py @@ -0,0 +1,34 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +# this script reports modified .py files under the desired list of top-level sub-dirs passed as a list of arguments, e.g.: +# python ./utils/get_modified_files.py utils src tests examples +# +# it uses git to find the forking point and which files were modified - i.e. files not under git won't be considered +# since the output of this script is fed into Makefile commands it doesn't print a newline after the results + +import re +import subprocess +import sys + + +fork_point_sha = subprocess.check_output("git merge-base main HEAD".split()).decode("utf-8") +modified_files = subprocess.check_output(f"git diff --name-only {fork_point_sha}".split()).decode("utf-8").split() + +joined_dirs = "|".join(sys.argv[1:]) +regex = re.compile(rf"^({joined_dirs}).*?\.py$") + +relevant_modified_files = [x for x in modified_files if regex.match(x)] +print(" ".join(relevant_modified_files), end="") diff --git a/diffusers/utils/overwrite_expected_slice.py b/diffusers/utils/overwrite_expected_slice.py new file mode 100644 index 0000000000000000000000000000000000000000..20a8f4258547987e477e151d4e899af3b1fe7d1d --- /dev/null +++ b/diffusers/utils/overwrite_expected_slice.py @@ -0,0 +1,90 @@ +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. +import argparse +from collections import defaultdict + + +def overwrite_file(file, class_name, test_name, correct_line, done_test): + _id = f"{file}_{class_name}_{test_name}" + done_test[_id] += 1 + + with open(file, "r") as f: + lines = f.readlines() + + class_regex = f"class {class_name}(" + test_regex = f"{4 * ' '}def {test_name}(" + line_begin_regex = f"{8 * ' '}{correct_line.split()[0]}" + another_line_begin_regex = f"{16 * ' '}{correct_line.split()[0]}" + in_class = False + in_func = False + in_line = False + insert_line = False + count = 0 + spaces = 0 + + new_lines = [] + for line in lines: + if line.startswith(class_regex): + in_class = True + elif in_class and line.startswith(test_regex): + in_func = True + elif in_class and in_func and (line.startswith(line_begin_regex) or line.startswith(another_line_begin_regex)): + spaces = len(line.split(correct_line.split()[0])[0]) + count += 1 + + if count == done_test[_id]: + in_line = True + + if in_class and in_func and in_line: + if ")" not in line: + continue + else: + insert_line = True + + if in_class and in_func and in_line and insert_line: + new_lines.append(f"{spaces * ' '}{correct_line}") + in_class = in_func = in_line = insert_line = False + else: + new_lines.append(line) + + with open(file, "w") as f: + for line in new_lines: + f.write(line) + + +def main(correct, fail=None): + if fail is not None: + with open(fail, "r") as f: + test_failures = set([l.strip() for l in f.readlines()]) + else: + test_failures = None + + with open(correct, "r") as f: + correct_lines = f.readlines() + + done_tests = defaultdict(int) + for line in correct_lines: + file, class_name, test_name, correct_line = line.split(";") + if test_failures is None or "::".join([file, class_name, test_name]) in test_failures: + overwrite_file(file, class_name, test_name, correct_line, done_tests) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--correct_filename", help="filename of tests with expected result") + parser.add_argument("--fail_filename", help="filename of test failures", type=str, default=None) + args = parser.parse_args() + + main(args.correct_filename, args.fail_filename) diff --git a/diffusers/utils/print_env.py b/diffusers/utils/print_env.py new file mode 100644 index 0000000000000000000000000000000000000000..c141554803da9a4db9916ad3fd42aca1c4406202 --- /dev/null +++ b/diffusers/utils/print_env.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +# coding=utf-8 +# Copyright 2022 The HuggingFace Inc. team. +# +# 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. + +# this script dumps information about the environment + +import os +import platform +import sys + + +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" + +print("Python version:", sys.version) + +print("OS platform:", platform.platform()) +print("OS architecture:", platform.machine()) + +try: + import torch + + print("Torch version:", torch.__version__) + print("Cuda available:", torch.cuda.is_available()) + print("Cuda version:", torch.version.cuda) + print("CuDNN version:", torch.backends.cudnn.version()) + print("Number of GPUs available:", torch.cuda.device_count()) +except ImportError: + print("Torch version:", None) + +try: + import transformers + + print("transformers version:", transformers.__version__) +except ImportError: + print("transformers version:", None) diff --git a/diffusers/utils/release.py b/diffusers/utils/release.py new file mode 100644 index 0000000000000000000000000000000000000000..758fb70caaca409947c9dba2fe13fb2546060b32 --- /dev/null +++ b/diffusers/utils/release.py @@ -0,0 +1,162 @@ +# coding=utf-8 +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# 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. + +import argparse +import os +import re + +import packaging.version + + +PATH_TO_EXAMPLES = "examples/" +REPLACE_PATTERNS = { + "examples": (re.compile(r'^check_min_version\("[^"]+"\)\s*$', re.MULTILINE), 'check_min_version("VERSION")\n'), + "init": (re.compile(r'^__version__\s+=\s+"([^"]+)"\s*$', re.MULTILINE), '__version__ = "VERSION"\n'), + "setup": (re.compile(r'^(\s*)version\s*=\s*"[^"]+",', re.MULTILINE), r'\1version="VERSION",'), + "doc": (re.compile(r'^(\s*)release\s*=\s*"[^"]+"$', re.MULTILINE), 'release = "VERSION"\n'), +} +REPLACE_FILES = { + "init": "src/diffusers/__init__.py", + "setup": "setup.py", +} +README_FILE = "README.md" + + +def update_version_in_file(fname, version, pattern): + """Update the version in one file using a specific pattern.""" + with open(fname, "r", encoding="utf-8", newline="\n") as f: + code = f.read() + re_pattern, replace = REPLACE_PATTERNS[pattern] + replace = replace.replace("VERSION", version) + code = re_pattern.sub(replace, code) + with open(fname, "w", encoding="utf-8", newline="\n") as f: + f.write(code) + + +def update_version_in_examples(version): + """Update the version in all examples files.""" + for folder, directories, fnames in os.walk(PATH_TO_EXAMPLES): + # Removing some of the folders with non-actively maintained examples from the walk + if "research_projects" in directories: + directories.remove("research_projects") + if "legacy" in directories: + directories.remove("legacy") + for fname in fnames: + if fname.endswith(".py"): + update_version_in_file(os.path.join(folder, fname), version, pattern="examples") + + +def global_version_update(version, patch=False): + """Update the version in all needed files.""" + for pattern, fname in REPLACE_FILES.items(): + update_version_in_file(fname, version, pattern) + if not patch: + update_version_in_examples(version) + + +def clean_main_ref_in_model_list(): + """Replace the links from main doc tp stable doc in the model list of the README.""" + # If the introduction or the conclusion of the list change, the prompts may need to be updated. + _start_prompt = "🤗 Transformers currently provides the following architectures" + _end_prompt = "1. Want to contribute a new model?" + with open(README_FILE, "r", encoding="utf-8", newline="\n") as f: + lines = f.readlines() + + # Find the start of the list. + start_index = 0 + while not lines[start_index].startswith(_start_prompt): + start_index += 1 + start_index += 1 + + index = start_index + # Update the lines in the model list. + while not lines[index].startswith(_end_prompt): + if lines[index].startswith("1."): + lines[index] = lines[index].replace( + "https://huggingface.co/docs/diffusers/main/model_doc", + "https://huggingface.co/docs/diffusers/model_doc", + ) + index += 1 + + with open(README_FILE, "w", encoding="utf-8", newline="\n") as f: + f.writelines(lines) + + +def get_version(): + """Reads the current version in the __init__.""" + with open(REPLACE_FILES["init"], "r") as f: + code = f.read() + default_version = REPLACE_PATTERNS["init"][0].search(code).groups()[0] + return packaging.version.parse(default_version) + + +def pre_release_work(patch=False): + """Do all the necessary pre-release steps.""" + # First let's get the default version: base version if we are in dev, bump minor otherwise. + default_version = get_version() + if patch and default_version.is_devrelease: + raise ValueError("Can't create a patch version from the dev branch, checkout a released version!") + if default_version.is_devrelease: + default_version = default_version.base_version + elif patch: + default_version = f"{default_version.major}.{default_version.minor}.{default_version.micro + 1}" + else: + default_version = f"{default_version.major}.{default_version.minor + 1}.0" + + # Now let's ask nicely if that's the right one. + version = input(f"Which version are you releasing? [{default_version}]") + if len(version) == 0: + version = default_version + + print(f"Updating version to {version}.") + global_version_update(version, patch=patch) + + +# if not patch: +# print("Cleaning main README, don't forget to run `make fix-copies`.") +# clean_main_ref_in_model_list() + + +def post_release_work(): + """Do all the necesarry post-release steps.""" + # First let's get the current version + current_version = get_version() + dev_version = f"{current_version.major}.{current_version.minor + 1}.0.dev0" + current_version = current_version.base_version + + # Check with the user we got that right. + version = input(f"Which version are we developing now? [{dev_version}]") + if len(version) == 0: + version = dev_version + + print(f"Updating version to {version}.") + global_version_update(version) + + +# print("Cleaning main README, don't forget to run `make fix-copies`.") +# clean_main_ref_in_model_list() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--post_release", action="store_true", help="Whether this is pre or post release.") + parser.add_argument("--patch", action="store_true", help="Whether or not this is a patch release.") + args = parser.parse_args() + if not args.post_release: + pre_release_work(patch=args.patch) + elif args.patch: + print("Nothing to do after a patch :-)") + else: + post_release_work() diff --git a/diffusers/utils/stale.py b/diffusers/utils/stale.py new file mode 100644 index 0000000000000000000000000000000000000000..a0193a913ee533eb9e7144d393e4883c831c3f07 --- /dev/null +++ b/diffusers/utils/stale.py @@ -0,0 +1,77 @@ +# Copyright 2022 The HuggingFace Team, the AllenNLP library authors. All rights reserved. +# +# 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. +""" +Script to close stale issue. Taken in part from the AllenNLP repository. +https://github.com/allenai/allennlp. +""" +import os +from datetime import datetime as dt + +from github import Github + + +LABELS_TO_EXEMPT = [ + "good first issue", + "good second issue", + "good difficult issue", + "enhancement", + "new pipeline/model", + "new scheduler", + "wip", +] + + +def main(): + g = Github(os.environ["GITHUB_TOKEN"]) + repo = g.get_repo("huggingface/diffusers") + open_issues = repo.get_issues(state="open") + + for issue in open_issues: + comments = sorted([comment for comment in issue.get_comments()], key=lambda i: i.created_at, reverse=True) + last_comment = comments[0] if len(comments) > 0 else None + if ( + last_comment is not None + and last_comment.user.login == "github-actions[bot]" + and (dt.utcnow() - issue.updated_at).days > 7 + and (dt.utcnow() - issue.created_at).days >= 30 + and not any(label.name.lower() in LABELS_TO_EXEMPT for label in issue.get_labels()) + ): + # Closes the issue after 7 days of inactivity since the Stalebot notification. + issue.edit(state="closed") + elif ( + "stale" in issue.get_labels() + and last_comment is not None + and last_comment.user.login != "github-actions[bot]" + ): + # Opens the issue if someone other than Stalebot commented. + issue.edit(state="open") + issue.remove_from_labels("stale") + elif ( + (dt.utcnow() - issue.updated_at).days > 23 + and (dt.utcnow() - issue.created_at).days >= 30 + and not any(label.name.lower() in LABELS_TO_EXEMPT for label in issue.get_labels()) + ): + # Post a Stalebot notification after 23 days of inactivity. + issue.create_comment( + "This issue has been automatically marked as stale because it has not had " + "recent activity. If you think this still needs to be addressed " + "please comment on this thread.\n\nPlease note that issues that do not follow the " + "[contributing guidelines](https://github.com/huggingface/diffusers/blob/main/CONTRIBUTING.md) " + "are likely to be ignored." + ) + issue.add_to_labels("stale") + + +if __name__ == "__main__": + main()