From 6f37b894957acf097a79d87f92f943b4f4384528 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:01:53 +0100 Subject: [PATCH 001/119] update --- MANIFEST.in | 6 + docs/examples/app_boring/.gitignore | 10 + docs/examples/app_boring/.lightning | 1 + docs/examples/app_boring/__init__.py | 0 docs/examples/app_boring/app.py | 57 ++++ docs/examples/app_boring/app_dynamic.py | 67 ++++ docs/examples/app_boring/scripts/serve.py | 29 ++ docs/examples/app_components/__init__.py | 0 .../app_components/python/__init__.py | 0 docs/examples/app_components/python/app.py | 24 ++ .../app_components/python/component_popen.py | 7 + .../app_components/python/component_tracer.py | 53 ++++ .../app_components/python/pl_script.py | 65 ++++ .../python/pytorch_lightning_script.py | 65 ++++ .../app_components/serve/gradio/app.py | 53 ++++ .../app_components/serve/gradio/beyonce.jpg | Bin 0 -> 132520 bytes .../serve/gradio/requirements.txt | 1 + docs/examples/app_dag/.gitignore | 6 + docs/examples/app_dag/.lightning | 1 + docs/examples/app_dag/.lightningignore | 8 + docs/examples/app_dag/app.py | 137 ++++++++ docs/examples/app_dag/processing.py | 14 + docs/examples/app_dag/requirements.txt | 2 + docs/examples/app_drive/.gitignore | 1 + docs/examples/app_drive/.lightning | 1 + docs/examples/app_drive/app.py | 51 +++ docs/examples/app_hpo/README.md | 64 ++++ docs/examples/app_hpo/app_wi_ui.py | 61 ++++ docs/examples/app_hpo/app_wo_ui.py | 58 ++++ docs/examples/app_hpo/download_data.py | 5 + docs/examples/app_hpo/hyperplot.py | 34 ++ docs/examples/app_hpo/objective.py | 63 ++++ docs/examples/app_hpo/pl_script.py | 43 +++ docs/examples/app_hpo/requirements.txt | 3 + docs/examples/app_hpo/utils.py | 54 ++++ docs/examples/app_layout/.lightning | 1 + docs/examples/app_layout/__init__.py | 0 docs/examples/app_layout/app.py | 101 ++++++ docs/examples/app_layout/ui1/index.html | 10 + docs/examples/app_layout/ui2/index.html | 10 + docs/examples/app_multi_node/.gitignore | 2 + docs/examples/app_multi_node/.lightning | 1 + docs/examples/app_multi_node/multi_node.py | 36 +++ docs/examples/app_multi_node/requirements.txt | 1 + docs/examples/app_payload/.lightning | 1 + docs/examples/app_payload/app.py | 31 ++ docs/examples/app_pickle_or_not/app.py | 55 ++++ .../app_pickle_or_not/requirements.txt | 0 docs/examples/app_v0/.gitignore | 2 + docs/examples/app_v0/README.md | 18 ++ docs/examples/app_v0/__init__.py | 0 docs/examples/app_v0/app.py | 49 +++ docs/examples/app_v0/emulate_ui.py | 19 ++ docs/examples/app_v0/requirements.txt | 1 + docs/examples/app_v0/ui/a/index.html | 1 + docs/examples/app_v0/ui/b/index.html | 1 + docs/source-app/_static/images/brandmark.png | Bin 0 -> 60816 bytes docs/source-app/_templates/classtemplate.rst | 2 +- .../_templates/theme_variables.jinja | 8 +- docs/source-app/{pages => }/basics.rst | 50 ++- docs/source-app/code_samples/basics/0.py | 4 +- docs/source-app/code_samples/basics/1.py | 4 +- .../code_samples/quickstart/app/app_0.py | 8 +- .../code_samples/quickstart/app/app_1.py | 14 +- .../code_samples/quickstart/app_01.py | 12 +- .../code_samples/quickstart/app_02.py | 10 +- .../code_samples/quickstart/app_03.py | 10 +- .../code_samples/quickstart/app_comp.py | 10 +- .../quickstart/hello_world/app.py | 6 +- .../quickstart/hello_world/app_ui.py | 14 +- docs/source-app/conf.py | 35 +- .../source-app/{pages => }/contribute_app.rst | 0 .../core_api/lightning_app/communication.rst | 135 +------- .../lightning_app/communication_content.rst | 123 +++++++ .../core_api/lightning_app/dynamic_work.rst | 188 +---------- .../lightning_app/dynamic_work_content.rst | 199 ++++++++++++ .../core_api/lightning_app/lightning_app.rst | 4 +- docs/source-app/core_api/lightning_flow.rst | 5 +- .../core_api/lightning_work/compute.rst | 99 +----- .../lightning_work/compute_content.rst | 100 ++++++ .../lightning_work/handling_app_exception.rst | 80 +---- .../handling_app_exception_content.rst | 74 +++++ .../lightning_work/lightning_work.rst | 4 +- .../core_api/lightning_work/payload.rst | 84 +---- .../lightning_work/payload_content.rst | 73 +++++ .../core_api/lightning_work/status.rst | 204 +----------- .../lightning_work/status_content.rst | 195 ++++++++++++ docs/source-app/examples/dag/dag.rst | 23 +- .../examples/dag/dag_from_scratch.rst | 11 +- docs/source-app/examples/data_explore_app.rst | 2 + docs/source-app/examples/etl_app.rst | 2 + docs/source-app/examples/file_server/app.py | 236 ++++++++++++++ .../examples/file_server/file_server.rst | 9 + .../file_server/file_server_content.rst | 82 +++++ .../file_server/file_server_step_1.rst | 11 + .../file_server/file_server_step_2.rst | 37 +++ .../file_server/file_server_step_3.rst | 16 + .../file_server/file_server_step_4.rst | 86 +++++ .../examples/github_repo_runner/.lightning | 1 + .../examples/github_repo_runner/app.py | 299 ++++++++++++++++++ .../github_repo_runner/github_repo_runner.rst | 12 + .../github_repo_runner_content.rst | 96 ++++++ .../github_repo_runner_step_1.rst | 62 ++++ .../github_repo_runner_step_2.rst | 68 ++++ .../github_repo_runner_step_3.rst | 62 ++++ .../github_repo_runner_step_4.rst | 93 ++++++ .../github_repo_runner_step_5.rst | 77 +++++ .../examples/hpo/build_from_scratch.rst | 41 +++ .../{tutorials => examples}/hpo/hpo.py | 17 +- docs/source-app/examples/hpo/hpo.rst | 79 +++++ docs/source-app/examples/hpo/hpo_wi.rst | 57 ++++ docs/source-app/examples/hpo/hpo_wo.rst | 57 ++++ .../source-app/examples/hpo/lightning_hpo.rst | 99 ++++++ .../examples/hpo/lightning_hpo_target.py | 53 ++++ .../{tutorials => examples}/hpo/objective.py | 12 +- .../examples/hpo/optuna_reference.py | 36 +++ docs/source-app/examples/model_deploy_app.rst | 3 - .../examples/model_server_app/app.py | 34 ++ .../model_server_app/load_testing.rst | 57 ++++ .../model_server_app/locust_component.py | 43 +++ .../examples/model_server_app/locustfile.py | 41 +++ .../examples/model_server_app/model_server.py | 90 ++++++ .../model_server_app/model_server.rst | 48 +++ .../model_server_app/model_server_app.rst | 13 + .../model_server_app_content.rst | 84 +++++ .../putting_everything_together.rst | 80 +++++ .../examples/model_server_app/train.py | 42 +++ .../examples/model_server_app/train.rst | 49 +++ .../source-app/examples/research_demo_app.rst | 2 + docs/source-app/glossary/app_tree.rst | 42 ++- .../build_config/build_config_advanced.rst | 1 - .../build_config/build_config_basic.rst | 16 +- .../build_config_intermediate.rst | 7 +- docs/source-app/glossary/dag.rst | 38 ++- docs/source-app/glossary/debug_app.rst | 2 + docs/source-app/glossary/distributed_fe.rst | 2 + .../glossary/distributed_hardware.rst | 2 + .../glossary/environment_variables.rst | 5 +- docs/source-app/glossary/event_loop.rst | 8 +- docs/source-app/glossary/fault_tolerance.rst | 2 + .../glossary/lightning_app_overview/index.rst | 1 + docs/source-app/glossary/scheduling.rst | 60 ++-- .../glossary/sharing_components.rst | 8 +- docs/source-app/glossary/storage/drive.rst | 201 +----------- .../glossary/storage/drive_content.rst | 199 ++++++++++++ docs/source-app/glossary/storage/path.rst | 12 +- docs/source-app/glossary/storage/storage.rst | 27 ++ docs/source-app/index.rst | 128 ++++---- .../{pages => }/install_beginner.rst | 22 +- docs/source-app/installation.rst | 34 ++ .../{pages/introduction.rst => intro.rst} | 19 +- docs/source-app/levels/advanced/index.rst | 74 +++++ docs/source-app/levels/advanced/level_17.rst | 10 + docs/source-app/levels/advanced/level_18.rst | 12 + docs/source-app/levels/advanced/level_19.rst | 10 + docs/source-app/levels/advanced/level_20.rst | 11 + docs/source-app/levels/advanced/level_21.rst | 11 + docs/source-app/levels/basic/index.rst | 92 ++++++ docs/source-app/levels/basic/level_1.rst | 99 ++++++ docs/source-app/levels/basic/level_2.rst | 36 +++ docs/source-app/levels/basic/level_3.rst | 56 ++++ docs/source-app/levels/basic/level_4.rst | 123 +++++++ docs/source-app/levels/basic/level_5.rst | 31 ++ docs/source-app/levels/basic/level_6.rst | 78 +++++ docs/source-app/levels/basic/level_7.rst | 36 +++ docs/source-app/levels/intermediate/index.rst | 109 +++++++ .../levels/intermediate/level_10.rst | 14 + .../levels/intermediate/level_11.rst | 12 + .../levels/intermediate/level_12.rst | 10 + .../levels/intermediate/level_13.rst | 10 + .../levels/intermediate/level_14.rst | 10 + .../levels/intermediate/level_15.rst | 10 + .../levels/intermediate/level_16.rst | 10 + .../levels/intermediate/level_8.rst | 10 + .../levels/intermediate/level_9.rst | 10 + .../{pages => }/lightning_apps_intro.rst | 151 ++++----- .../{pages => }/moving_to_the_cloud.rst | 34 +- docs/source-app/pages/installation.rst | 26 -- docs/source-app/{pages => }/quickstart.rst | 28 +- docs/source-app/read_me_first.rst | 61 ++++ docs/source-app/{pages => }/testing.rst | 27 +- docs/source-app/tutorials/hpo/hpo.rst | 146 --------- .../{pages => }/ui_and_frontends.rst | 4 +- .../source-app/workflows/access_app_state.rst | 61 ---- .../access_app_state/access_app_state.rst | 11 + .../access_app_state_content.rst | 70 ++++ .../workflows/access_app_state/app.py | 27 ++ .../workflows/add_components/index.rst | 4 +- .../workflows/add_server/any_server.rst | 41 ++- .../workflows/add_server/flask_basic.rst | 58 ++-- docs/source-app/workflows/add_web_link.rst | 24 +- .../add_web_ui/angular_js_intermediate.rst | 2 + .../workflows/add_web_ui/dash/basic.rst | 90 +++--- .../workflows/add_web_ui/dash/index.rst | 2 + .../add_web_ui/dash/intermediate.rst | 26 +- .../add_web_ui/dash/intermediate_plot.py | 88 ++++++ .../add_web_ui/dash/intermediate_state.py | 39 +++ .../workflows/add_web_ui/gradio/basic.rst | 48 ++- .../workflows/add_web_ui/gradio/index.rst | 2 + .../add_web_ui/gradio/intermediate.rst | 31 +- .../workflows/add_web_ui/html/basic.rst | 36 +-- .../workflows/add_web_ui/html/index.rst | 2 + .../source-app/workflows/add_web_ui/index.rst | 4 +- .../workflows/add_web_ui/index_content.rst | 18 -- .../integrate_any_javascript_framework.rst | 2 + .../workflows/add_web_ui/jupyter_basic.rst | 2 + ...ommunicate_between_react_and_lightning.rst | 12 +- .../react/connect_react_and_lightning.rst | 30 +- .../workflows/add_web_ui/react/index.rst | 2 + .../workflows/add_web_ui/streamlit/basic.rst | 55 ++-- .../workflows/add_web_ui/streamlit/index.rst | 2 + .../add_web_ui/streamlit/intermediate.rst | 24 +- .../add_web_ui/vue_js_intermediate.rst | 2 + .../arrange_tabs/arrange_app_basic.rst | 53 +--- .../from_pytorch_lightning_script.rst | 6 +- .../build_lightning_app/from_scratch.rst | 126 +------- .../from_scratch_content.rst | 118 +++++++ .../build_lightning_component/basic.rst | 205 +----------- .../from_scratch_component_content.rst | 193 +++++++++++ .../index_content.rst | 8 +- .../intermediate.rst | 20 +- .../publish_a_component.rst | 12 +- docs/source-app/workflows/debug_locally.rst | 2 + .../workflows/enable_fault_tolerance.rst | 2 + .../run_app_on_cloud/cloud_files.rst | 58 ++++ .../workflows/run_app_on_cloud/index.rst | 5 + .../run_app_on_cloud/index_content.rst | 115 +++++++ .../run_app_on_cloud/lightning_cloud.rst | 67 ++++ .../workflows/run_app_on_cloud/on_prem.rst | 6 + .../run_app_on_cloud/on_your_own_machine.rst | 23 ++ .../run_components_on_different_hardware.rst | 2 + .../workflows/run_work_in_parallel.rst | 57 +--- .../run_work_in_parallel_content.rst | 51 +++ docs/source-app/workflows/run_work_once.rst | 125 +------- .../workflows/run_work_once_content.rst | 149 +++++++++ docs/source-app/workflows/schedule_apps.rst | 2 + docs/source-app/workflows/share_app.rst | 2 +- .../share_files_between_components.rst | 64 ++-- .../share_files_between_components/app.py | 48 +++ docs/source-app/workflows/test_an_app.rst | 2 + setup.py | 2 +- 241 files changed, 7533 insertions(+), 2462 deletions(-) create mode 100644 docs/examples/app_boring/.gitignore create mode 100644 docs/examples/app_boring/.lightning create mode 100644 docs/examples/app_boring/__init__.py create mode 100644 docs/examples/app_boring/app.py create mode 100644 docs/examples/app_boring/app_dynamic.py create mode 100644 docs/examples/app_boring/scripts/serve.py create mode 100644 docs/examples/app_components/__init__.py create mode 100644 docs/examples/app_components/python/__init__.py create mode 100644 docs/examples/app_components/python/app.py create mode 100644 docs/examples/app_components/python/component_popen.py create mode 100644 docs/examples/app_components/python/component_tracer.py create mode 100644 docs/examples/app_components/python/pl_script.py create mode 100644 docs/examples/app_components/python/pytorch_lightning_script.py create mode 100644 docs/examples/app_components/serve/gradio/app.py create mode 100644 docs/examples/app_components/serve/gradio/beyonce.jpg create mode 100644 docs/examples/app_components/serve/gradio/requirements.txt create mode 100644 docs/examples/app_dag/.gitignore create mode 100644 docs/examples/app_dag/.lightning create mode 100644 docs/examples/app_dag/.lightningignore create mode 100644 docs/examples/app_dag/app.py create mode 100644 docs/examples/app_dag/processing.py create mode 100644 docs/examples/app_dag/requirements.txt create mode 100644 docs/examples/app_drive/.gitignore create mode 100644 docs/examples/app_drive/.lightning create mode 100644 docs/examples/app_drive/app.py create mode 100644 docs/examples/app_hpo/README.md create mode 100644 docs/examples/app_hpo/app_wi_ui.py create mode 100644 docs/examples/app_hpo/app_wo_ui.py create mode 100644 docs/examples/app_hpo/download_data.py create mode 100644 docs/examples/app_hpo/hyperplot.py create mode 100644 docs/examples/app_hpo/objective.py create mode 100644 docs/examples/app_hpo/pl_script.py create mode 100644 docs/examples/app_hpo/requirements.txt create mode 100644 docs/examples/app_hpo/utils.py create mode 100644 docs/examples/app_layout/.lightning create mode 100644 docs/examples/app_layout/__init__.py create mode 100644 docs/examples/app_layout/app.py create mode 100644 docs/examples/app_layout/ui1/index.html create mode 100644 docs/examples/app_layout/ui2/index.html create mode 100644 docs/examples/app_multi_node/.gitignore create mode 100644 docs/examples/app_multi_node/.lightning create mode 100644 docs/examples/app_multi_node/multi_node.py create mode 100644 docs/examples/app_multi_node/requirements.txt create mode 100644 docs/examples/app_payload/.lightning create mode 100644 docs/examples/app_payload/app.py create mode 100644 docs/examples/app_pickle_or_not/app.py create mode 100644 docs/examples/app_pickle_or_not/requirements.txt create mode 100644 docs/examples/app_v0/.gitignore create mode 100644 docs/examples/app_v0/README.md create mode 100644 docs/examples/app_v0/__init__.py create mode 100644 docs/examples/app_v0/app.py create mode 100644 docs/examples/app_v0/emulate_ui.py create mode 100644 docs/examples/app_v0/requirements.txt create mode 100644 docs/examples/app_v0/ui/a/index.html create mode 100644 docs/examples/app_v0/ui/b/index.html create mode 100644 docs/source-app/_static/images/brandmark.png rename docs/source-app/{pages => }/basics.rst (92%) rename docs/source-app/{pages => }/contribute_app.rst (100%) create mode 100644 docs/source-app/core_api/lightning_app/communication_content.rst create mode 100644 docs/source-app/core_api/lightning_app/dynamic_work_content.rst create mode 100644 docs/source-app/core_api/lightning_work/compute_content.rst create mode 100644 docs/source-app/core_api/lightning_work/handling_app_exception_content.rst create mode 100644 docs/source-app/core_api/lightning_work/payload_content.rst create mode 100644 docs/source-app/core_api/lightning_work/status_content.rst create mode 100644 docs/source-app/examples/file_server/app.py create mode 100644 docs/source-app/examples/file_server/file_server.rst create mode 100644 docs/source-app/examples/file_server/file_server_content.rst create mode 100644 docs/source-app/examples/file_server/file_server_step_1.rst create mode 100644 docs/source-app/examples/file_server/file_server_step_2.rst create mode 100644 docs/source-app/examples/file_server/file_server_step_3.rst create mode 100644 docs/source-app/examples/file_server/file_server_step_4.rst create mode 100644 docs/source-app/examples/github_repo_runner/.lightning create mode 100644 docs/source-app/examples/github_repo_runner/app.py create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst create mode 100644 docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst create mode 100644 docs/source-app/examples/hpo/build_from_scratch.rst rename docs/source-app/{tutorials => examples}/hpo/hpo.py (74%) create mode 100644 docs/source-app/examples/hpo/hpo.rst create mode 100644 docs/source-app/examples/hpo/hpo_wi.rst create mode 100644 docs/source-app/examples/hpo/hpo_wo.rst create mode 100644 docs/source-app/examples/hpo/lightning_hpo.rst create mode 100644 docs/source-app/examples/hpo/lightning_hpo_target.py rename docs/source-app/{tutorials => examples}/hpo/objective.py (55%) create mode 100644 docs/source-app/examples/hpo/optuna_reference.py delete mode 100644 docs/source-app/examples/model_deploy_app.rst create mode 100644 docs/source-app/examples/model_server_app/app.py create mode 100644 docs/source-app/examples/model_server_app/load_testing.rst create mode 100644 docs/source-app/examples/model_server_app/locust_component.py create mode 100644 docs/source-app/examples/model_server_app/locustfile.py create mode 100644 docs/source-app/examples/model_server_app/model_server.py create mode 100644 docs/source-app/examples/model_server_app/model_server.rst create mode 100644 docs/source-app/examples/model_server_app/model_server_app.rst create mode 100644 docs/source-app/examples/model_server_app/model_server_app_content.rst create mode 100644 docs/source-app/examples/model_server_app/putting_everything_together.rst create mode 100644 docs/source-app/examples/model_server_app/train.py create mode 100644 docs/source-app/examples/model_server_app/train.rst create mode 100644 docs/source-app/glossary/storage/drive_content.rst rename docs/source-app/{pages => }/install_beginner.rst (91%) create mode 100644 docs/source-app/installation.rst rename docs/source-app/{pages/introduction.rst => intro.rst} (95%) create mode 100644 docs/source-app/levels/advanced/index.rst create mode 100644 docs/source-app/levels/advanced/level_17.rst create mode 100644 docs/source-app/levels/advanced/level_18.rst create mode 100644 docs/source-app/levels/advanced/level_19.rst create mode 100644 docs/source-app/levels/advanced/level_20.rst create mode 100644 docs/source-app/levels/advanced/level_21.rst create mode 100644 docs/source-app/levels/basic/index.rst create mode 100644 docs/source-app/levels/basic/level_1.rst create mode 100644 docs/source-app/levels/basic/level_2.rst create mode 100644 docs/source-app/levels/basic/level_3.rst create mode 100644 docs/source-app/levels/basic/level_4.rst create mode 100644 docs/source-app/levels/basic/level_5.rst create mode 100644 docs/source-app/levels/basic/level_6.rst create mode 100644 docs/source-app/levels/basic/level_7.rst create mode 100644 docs/source-app/levels/intermediate/index.rst create mode 100644 docs/source-app/levels/intermediate/level_10.rst create mode 100644 docs/source-app/levels/intermediate/level_11.rst create mode 100644 docs/source-app/levels/intermediate/level_12.rst create mode 100644 docs/source-app/levels/intermediate/level_13.rst create mode 100644 docs/source-app/levels/intermediate/level_14.rst create mode 100644 docs/source-app/levels/intermediate/level_15.rst create mode 100644 docs/source-app/levels/intermediate/level_16.rst create mode 100644 docs/source-app/levels/intermediate/level_8.rst create mode 100644 docs/source-app/levels/intermediate/level_9.rst rename docs/source-app/{pages => }/lightning_apps_intro.rst (73%) rename docs/source-app/{pages => }/moving_to_the_cloud.rst (85%) delete mode 100644 docs/source-app/pages/installation.rst rename docs/source-app/{pages => }/quickstart.rst (92%) create mode 100644 docs/source-app/read_me_first.rst rename docs/source-app/{pages => }/testing.rst (95%) delete mode 100644 docs/source-app/tutorials/hpo/hpo.rst rename docs/source-app/{pages => }/ui_and_frontends.rst (82%) delete mode 100644 docs/source-app/workflows/access_app_state.rst create mode 100644 docs/source-app/workflows/access_app_state/access_app_state.rst create mode 100644 docs/source-app/workflows/access_app_state/access_app_state_content.rst create mode 100644 docs/source-app/workflows/access_app_state/app.py create mode 100644 docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py create mode 100644 docs/source-app/workflows/add_web_ui/dash/intermediate_state.py create mode 100644 docs/source-app/workflows/build_lightning_app/from_scratch_content.rst create mode 100644 docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/cloud_files.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/index.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/index_content.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/on_prem.rst create mode 100644 docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst create mode 100644 docs/source-app/workflows/run_work_in_parallel_content.rst create mode 100644 docs/source-app/workflows/run_work_once_content.rst create mode 100644 docs/source-app/workflows/share_files_between_components/app.py diff --git a/MANIFEST.in b/MANIFEST.in index a8dbcff69b631..37c72c103b69c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,9 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/docs/examples/app_boring/.gitignore b/docs/examples/app_boring/.gitignore new file mode 100644 index 0000000000000..94018704d9f90 --- /dev/null +++ b/docs/examples/app_boring/.gitignore @@ -0,0 +1,10 @@ +lightning_logs +*.pt +.storage/ +.shared/ +data +*.ckpt +redis-stable +node_modules +*.rdb +boring_file.txt diff --git a/docs/examples/app_boring/.lightning b/docs/examples/app_boring/.lightning new file mode 100644 index 0000000000000..c85414d8c498a --- /dev/null +++ b/docs/examples/app_boring/.lightning @@ -0,0 +1 @@ +name: boring-app diff --git a/docs/examples/app_boring/__init__.py b/docs/examples/app_boring/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_boring/app.py b/docs/examples/app_boring/app.py new file mode 100644 index 0000000000000..9ba11316c65a1 --- /dev/null +++ b/docs/examples/app_boring/app.py @@ -0,0 +1,57 @@ +import os + +import lightning as L +from lightning.app.components.python import TracerPythonScript +from lightning.app.storage.path import Path + +FILE_CONTENT = """ +Hello there! +This tab is currently an IFrame of the FastAPI Server running in `DestinationFileAndServeWork`. +Also, the content of this file was created in `SourceFileWork` and then transferred to `DestinationFileAndServeWork`. +Are you already 🤯 ? Stick with us, this is only the beginning. Lightning is 🚀. +""" + + +class SourceFileWork(L.LightningWork): + def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): + super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) + self.boring_path = None + + def run(self): + # This should be used as a REFERENCE to the file. + self.boring_path = "lit://boring_file.txt" + with open(self.boring_path, "w", encoding="utf-8") as f: + f.write(FILE_CONTENT) + + +class DestinationFileAndServeWork(TracerPythonScript): + def run(self, path: Path): + assert path.exists() + self.script_args += [f"--filepath={path}", f"--host={self.host}", f"--port={self.port}"] + super().run() + + +class BoringApp(L.LightningFlow): + def __init__(self): + super().__init__() + self.source_work = SourceFileWork() + self.dest_work = DestinationFileAndServeWork( + script_path=os.path.join(os.path.dirname(__file__), "scripts/serve.py"), + port=1111, + parallel=False, # runs until killed. + cloud_compute=L.CloudCompute(), + raise_exception=True, + ) + + def run(self): + self.source_work.run() + if self.source_work.has_succeeded: + # the flow passes the file from one work to another. + self.dest_work.run(self.source_work.boring_path) + self._exit("Boring App End") + + def configure_layout(self): + return {"name": "Boring Tab", "content": self.dest_work.url + "/file"} + + +app = L.LightningApp(BoringApp()) diff --git a/docs/examples/app_boring/app_dynamic.py b/docs/examples/app_boring/app_dynamic.py new file mode 100644 index 0000000000000..6e3fdfa3ccdee --- /dev/null +++ b/docs/examples/app_boring/app_dynamic.py @@ -0,0 +1,67 @@ +import os + +import lightning as L +from lightning.app.components.python import TracerPythonScript +from lightning.app.storage.path import Path +from lightning.app.structures import Dict + +FILE_CONTENT = """ +Hello there! +This tab is currently an IFrame of the FastAPI Server running in `DestinationFileAndServeWork`. +Also, the content of this file was created in `SourceFileWork` and then transferred to `DestinationFileAndServeWork`. +Are you already 🤯 ? Stick with us, this is only the beginning. Lightning is 🚀. +""" + + +class SourceFileWork(L.LightningWork): + def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): + super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) + self.boring_path = None + + def run(self): + # This should be used as a REFERENCE to the file. + self.boring_path = "lit://boring_file.txt" + with open(self.boring_path, "w") as f: + f.write(FILE_CONTENT) + + +class DestinationFileAndServeWork(TracerPythonScript): + def run(self, path: Path): + assert path.exists() + self.script_args += [f"--filepath={path}", f"--host={self.host}", f"--port={self.port}"] + super().run() + + +class BoringApp(L.LightningFlow): + def __init__(self): + super().__init__() + self.dict = Dict() + + def run(self): + # create dynamically the source_work at runtime + if "src_w" not in self.dict: + self.dict["src_w"] = SourceFileWork() + + self.dict["src_w"].run() + + if self.dict["src_w"].has_succeeded: + + # create dynamically the dst_w at runtime + if "dst_w" not in self.dict: + self.dict["dst_w"] = DestinationFileAndServeWork( + script_path=os.path.join(os.path.dirname(__file__), "scripts/serve.py"), + port=1111, + parallel=False, # runs until killed. + cloud_compute=L.CloudCompute(), + raise_exception=True, + ) + + # the flow passes the file from one work to another. + self.dict["dst_w"].run(self.dict["src_w"].boring_path) + self._exit("Boring App End") + + def configure_layout(self): + return {"name": "Boring Tab", "content": self.dict["dst_w"].url + "/file" if "dst_w" in self.dict else ""} + + +app = L.LightningApp(BoringApp()) diff --git a/docs/examples/app_boring/scripts/serve.py b/docs/examples/app_boring/scripts/serve.py new file mode 100644 index 0000000000000..17c431ca378ac --- /dev/null +++ b/docs/examples/app_boring/scripts/serve.py @@ -0,0 +1,29 @@ +import argparse +import os + +import uvicorn +from fastapi import FastAPI +from fastapi.requests import Request +from fastapi.responses import HTMLResponse + +if __name__ == "__main__": + + parser = argparse.ArgumentParser("Server Parser") + parser.add_argument("--filepath", type=str, help="Where to find the `filepath`") + parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host`") + parser.add_argument("--port", type=int, default="8888", help="Server port`") + hparams = parser.parse_args() + + fastapi_service = FastAPI() + + if not os.path.exists(str(hparams.filepath)): + content = ["The file wasn't transferred"] + else: + content = open(hparams.filepath).readlines() # read the file received from SourceWork. + + @fastapi_service.get("/file") + async def get_file_content(request: Request, response_class=HTMLResponse): + lines = "\n".join(["

" + line + "

" for line in content]) + return HTMLResponse(f"") + + uvicorn.run(app=fastapi_service, host=hparams.host, port=hparams.port) diff --git a/docs/examples/app_components/__init__.py b/docs/examples/app_components/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_components/python/__init__.py b/docs/examples/app_components/python/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_components/python/app.py b/docs/examples/app_components/python/app.py new file mode 100644 index 0000000000000..1386a699a09fb --- /dev/null +++ b/docs/examples/app_components/python/app.py @@ -0,0 +1,24 @@ +import os +from pathlib import Path + +import lightning as L +from examples.components.python.component_tracer import PLTracerPythonScript + + +class RootFlow(L.LightningFlow): + def __init__(self): + super().__init__() + script_path = Path(__file__).parent / "pl_script.py" + self.tracer_python_script = PLTracerPythonScript(script_path) + + def run(self): + assert os.getenv("GLOBAL_RANK", "0") == "0" + if not self.tracer_python_script.has_started: + self.tracer_python_script.run() + if self.tracer_python_script.has_succeeded: + self._exit("tracer script succeed") + if self.tracer_python_script.has_failed: + self._exit("tracer script failed") + + +app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_components/python/component_popen.py b/docs/examples/app_components/python/component_popen.py new file mode 100644 index 0000000000000..d3af5ee2d55c7 --- /dev/null +++ b/docs/examples/app_components/python/component_popen.py @@ -0,0 +1,7 @@ +from pathlib import Path + +from lightning.app.components.python import PopenPythonScript + +if __name__ == "__main__": + comp = PopenPythonScript(Path(__file__).parent / "pl_script.py") + comp.run() diff --git a/docs/examples/app_components/python/component_tracer.py b/docs/examples/app_components/python/component_tracer.py new file mode 100644 index 0000000000000..9edc48cf51a29 --- /dev/null +++ b/docs/examples/app_components/python/component_tracer.py @@ -0,0 +1,53 @@ +from lightning.app.components.python import TracerPythonScript +from lightning.app.storage.path import Path +from lightning.app.utilities.tracer import Tracer +from pytorch_lightning import Trainer + + +class PLTracerPythonScript(TracerPythonScript): + + """This component can be used for ANY PyTorch Lightning script to track its progress and extract its best model + path.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Define the component state. + self.global_step = None + self.best_model_path = None + + def configure_tracer(self) -> Tracer: + from pytorch_lightning.callbacks import Callback + + class MyInjectedCallback(Callback): + def __init__(self, lightning_work): + self.lightning_work = lightning_work + + def on_train_start(self, trainer, pl_module) -> None: + print("This code doesn't belong to the script but was injected.") + print("Even the Lightning Work is available and state transfer works !") + print(self.lightning_work) + + def on_batch_end(self, trainer, *_) -> None: + # On every batch end, collects some information. + # This is communicated automatically to the rest of the app, + # so you can track your training in real time in the Lightning App UI. + self.lightning_work.global_step = trainer.global_step + best_model_path = trainer.checkpoint_callback.best_model_path + if best_model_path: + self.lightning_work.best_model_path = Path(best_model_path) + + # This hook would be called every time + # before a Trainer `__init__` method is called. + + def trainer_pre_fn(trainer, *args, **kwargs): + kwargs["callbacks"] = kwargs.get("callbacks", []) + [MyInjectedCallback(self)] + return {}, args, kwargs + + tracer = super().configure_tracer() + tracer.add_traced(Trainer, "__init__", pre_fn=trainer_pre_fn) + return tracer + + +if __name__ == "__main__": + comp = PLTracerPythonScript(Path(__file__).parent / "pl_script.py") + res = comp.run() diff --git a/docs/examples/app_components/python/pl_script.py b/docs/examples/app_components/python/pl_script.py new file mode 100644 index 0000000000000..4ad17b459200c --- /dev/null +++ b/docs/examples/app_components/python/pl_script.py @@ -0,0 +1,65 @@ +import torch +from torch.utils.data import DataLoader, Dataset + +from pytorch_lightning import LightningModule, Trainer + + +class RandomDataset(Dataset): + def __init__(self, size: int, length: int): + self.len = length + self.data = torch.randn(length, size) + + def __getitem__(self, index): + return self.data[index] + + def __len__(self): + return self.len + + +class BoringModel(LightningModule): + def __init__(self): + super().__init__() + self.layer = torch.nn.Linear(32, 2) + + def forward(self, x): + return self.layer(x) + + def loss(self, batch, prediction): + # An arbitrary loss to have a loss that updates the model weights during `Trainer.fit` calls + return torch.nn.functional.mse_loss(prediction, torch.ones_like(prediction)) + + def training_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"x": loss} + + def test_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"y": loss} + + def configure_optimizers(self): + optimizer = torch.optim.SGD(self.layer.parameters(), lr=0.1) + lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1) + return [optimizer], [lr_scheduler] + + def train_dataloader(self): + return DataLoader(RandomDataset(32, 64)) + + val_dataloader = train_dataloader + test_dataloader = train_dataloader + predict_dataloader = train_dataloader + + +if __name__ == "__main__": + model = BoringModel() + trainer = Trainer(max_epochs=1, accelerator="cpu", devices=2, strategy="ddp") + trainer.fit(model) + trainer.validate(model) + trainer.test(model) + trainer.predict(model) diff --git a/docs/examples/app_components/python/pytorch_lightning_script.py b/docs/examples/app_components/python/pytorch_lightning_script.py new file mode 100644 index 0000000000000..4ad17b459200c --- /dev/null +++ b/docs/examples/app_components/python/pytorch_lightning_script.py @@ -0,0 +1,65 @@ +import torch +from torch.utils.data import DataLoader, Dataset + +from pytorch_lightning import LightningModule, Trainer + + +class RandomDataset(Dataset): + def __init__(self, size: int, length: int): + self.len = length + self.data = torch.randn(length, size) + + def __getitem__(self, index): + return self.data[index] + + def __len__(self): + return self.len + + +class BoringModel(LightningModule): + def __init__(self): + super().__init__() + self.layer = torch.nn.Linear(32, 2) + + def forward(self, x): + return self.layer(x) + + def loss(self, batch, prediction): + # An arbitrary loss to have a loss that updates the model weights during `Trainer.fit` calls + return torch.nn.functional.mse_loss(prediction, torch.ones_like(prediction)) + + def training_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"loss": loss} + + def validation_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"x": loss} + + def test_step(self, batch, batch_idx): + output = self(batch) + loss = self.loss(batch, output) + return {"y": loss} + + def configure_optimizers(self): + optimizer = torch.optim.SGD(self.layer.parameters(), lr=0.1) + lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1) + return [optimizer], [lr_scheduler] + + def train_dataloader(self): + return DataLoader(RandomDataset(32, 64)) + + val_dataloader = train_dataloader + test_dataloader = train_dataloader + predict_dataloader = train_dataloader + + +if __name__ == "__main__": + model = BoringModel() + trainer = Trainer(max_epochs=1, accelerator="cpu", devices=2, strategy="ddp") + trainer.fit(model) + trainer.validate(model) + trainer.test(model) + trainer.predict(model) diff --git a/docs/examples/app_components/serve/gradio/app.py b/docs/examples/app_components/serve/gradio/app.py new file mode 100644 index 0000000000000..7bb3e7bf790cb --- /dev/null +++ b/docs/examples/app_components/serve/gradio/app.py @@ -0,0 +1,53 @@ +from functools import partial + +import gradio as gr +import requests +import torch +from PIL import Image + +import lightning as L +from lightning.app.components.serve import ServeGradio + + +# Credit to @akhaliq for his inspiring work. +# Find his original code there: https://huggingface.co/spaces/akhaliq/AnimeGANv2/blob/main/app.py +class AnimeGANv2UI(ServeGradio): + + inputs = gr.inputs.Image(type="pil") + outputs = gr.outputs.Image(type="pil") + elon = "https://upload.wikimedia.org/wikipedia/commons/3/34/Elon_Musk_Royal_Society_%28crop2%29.jpg" + img = Image.open(requests.get(elon, stream=True).raw) + img.save("elon.jpg") + examples = [["elon.jpg"]] + + def __init__(self): + super().__init__() + self.ready = False + + def predict(self, img): + return self.model(img=img) + + def build_model(self): + repo = "AK391/animegan2-pytorch:main" + model = torch.hub.load(repo, "generator", device="cpu") + face2paint = torch.hub.load(repo, "face2paint", size=512, device="cpu") + self.ready = True + return partial(face2paint, model=model) + + +class RootFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.demo = AnimeGANv2UI() + + def run(self): + self.demo.run() + + def configure_layout(self): + tabs = [] + if self.demo.ready: + tabs.append({"name": "Home", "content": self.demo}) + return tabs + + +app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_components/serve/gradio/beyonce.jpg b/docs/examples/app_components/serve/gradio/beyonce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68b6084475b019bd37db953b87c37ec905b79b86 GIT binary patch literal 132520 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
t|(Tt)ka`yC-9-2TR)4O4mu-Wozl{ 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 self.total_trials: + self._exit() + + has_told_study = [] + + for trial_idx in range(self.num_trials): + work_name = f"objective_work_{trial_idx}" + if work_name not in self.ws: + objective_work = ObjectiveWork( + script_path=self.script_path, + data_dir=self.data_dir, + cloud_compute=L.CloudCompute("cpu"), + ) + self.ws[work_name] = objective_work + if not self.ws[work_name].has_started: + trial = self._study.ask(ObjectiveWork.distributions()) + self.ws[work_name].run(trial_id=trial._trial_id, **trial.params) + + if self.ws[work_name].metric and not self.ws[work_name].has_told_study: + self.hi_plot.data.append({"x": -1 * self.ws[work_name].metric, **self.ws[work_name].params}) + self._study.tell(self.ws[work_name].trial_id, self.ws[work_name].metric) + self.ws[work_name].has_told_study = True + + has_told_study.append(self.ws[work_name].has_told_study) + + if all(has_told_study): + self.num_trials += self.simultaneous_trials + + +if __name__ == "__main__": + app = L.LightningApp( + RootHPOFlow( + script_path=str(Path(__file__).parent / "pl_script.py"), + data_dir="data/hymenoptera_data_version_0", + total_trials=6, + simultaneous_trials=2, + ) + ) diff --git a/docs/examples/app_hpo/app_wo_ui.py b/docs/examples/app_hpo/app_wo_ui.py new file mode 100644 index 0000000000000..b8a8448668573 --- /dev/null +++ b/docs/examples/app_hpo/app_wo_ui.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import optuna +from objective import ObjectiveWork + +import lightning as L +from lightning.app.structures import Dict + + +class RootHPOFlow(L.LightningFlow): + def __init__(self, script_path, data_dir, total_trials, simultaneous_trials): + super().__init__() + self.script_path = script_path + self.data_dir = data_dir + self.total_trials = total_trials + self.simultaneous_trials = simultaneous_trials + self.num_trials = simultaneous_trials + self._study = optuna.create_study() + self.ws = Dict() + + def run(self): + if self.num_trials >= self.total_trials: + self._exit() + + has_told_study = [] + + for trial_idx in range(self.num_trials): + work_name = f"objective_work_{trial_idx}" + if work_name not in self.ws: + objective_work = ObjectiveWork( + script_path=self.script_path, + data_dir=self.data_dir, + cloud_compute=L.CloudCompute("cpu"), + ) + self.ws[work_name] = objective_work + if not self.ws[work_name].has_started: + trial = self._study.ask(ObjectiveWork.distributions()) + self.ws[work_name].run(trial_id=trial._trial_id, **trial.params) + + if self.ws[work_name].metric and not self.ws[work_name].has_told_study: + self._study.tell(self.ws[work_name].trial_id, self.ws[work_name].metric) + self.ws[work_name].has_told_study = True + + has_told_study.append(self.ws[work_name].has_told_study) + + if all(has_told_study): + self.num_trials += self.simultaneous_trials + + +if __name__ == "__main__": + app = L.LightningApp( + RootHPOFlow( + script_path=str(Path(__file__).parent / "pl_script.py"), + data_dir="data/hymenoptera_data_version_0", + total_trials=6, + simultaneous_trials=2, + ) + ) diff --git a/docs/examples/app_hpo/download_data.py b/docs/examples/app_hpo/download_data.py new file mode 100644 index 0000000000000..d82b86a9dee95 --- /dev/null +++ b/docs/examples/app_hpo/download_data.py @@ -0,0 +1,5 @@ +from utils import download_data + +data_dir = "hymenoptera_data_version_0" +download_url = f"https://pl-flash-data.s3.amazonaws.com/{data_dir}.zip" +download_data(download_url, "./data") diff --git a/docs/examples/app_hpo/hyperplot.py b/docs/examples/app_hpo/hyperplot.py new file mode 100644 index 0000000000000..105285822705c --- /dev/null +++ b/docs/examples/app_hpo/hyperplot.py @@ -0,0 +1,34 @@ +import lightning as L +from lightning.app.frontend.stream_lit import StreamlitFrontend +from lightning.app.utilities.state import AppState + + +class HiPlotFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.data = [] + + def run(self): + pass + + def configure_layout(self): + return StreamlitFrontend(render_fn=render_fn) + + +def render_fn(state: AppState): + import json + + import hiplot as hip + import streamlit as st + from streamlit_autorefresh import st_autorefresh + + st.set_page_config(layout="wide") + st_autorefresh(interval=1000, limit=None, key="refresh") + + if not state.data: + st.write("No data available yet ! Stay tuned") + return + + xp = hip.Experiment.from_iterable(state.data) + ret_val = xp.to_streamlit(ret="selected_uids", key="hip").display() + st.markdown("hiplot returned " + json.dumps(ret_val)) diff --git a/docs/examples/app_hpo/objective.py b/docs/examples/app_hpo/objective.py new file mode 100644 index 0000000000000..f2d1ebb6a747f --- /dev/null +++ b/docs/examples/app_hpo/objective.py @@ -0,0 +1,63 @@ +import os +import tempfile +from datetime import datetime +from typing import Optional + +import pandas as pd +import torch +from optuna.distributions import CategoricalDistribution, LogUniformDistribution +from torchmetrics import Accuracy + +import lightning as L +from lightning.app.components.python import TracerPythonScript + + +class ObjectiveWork(TracerPythonScript): + def __init__(self, script_path: str, data_dir: str, cloud_compute: Optional[L.CloudCompute]): + timestamp = datetime.now().strftime("%H:%M:%S") + tmpdir = tempfile.TemporaryDirectory().name + submission_path = os.path.join(tmpdir, f"{timestamp}.csv") + best_model_path = os.path.join(tmpdir, f"{timestamp}.model.pt") + super().__init__( + script_path, + script_args=[ + f"--train_data_path={data_dir}/train", + f"--test_data_path={data_dir}/test", + f"--submission_path={submission_path}", + f"--best_model_path={best_model_path}", + ], + cloud_compute=cloud_compute, + ) + self.data_dir = data_dir + self.best_model_path = best_model_path + self.submission_path = submission_path + self.metric = None + self.trial_id = None + self.metric = None + self.params = None + self.has_told_study = False + + def run(self, trial_id: int, **params): + self.trial_id = trial_id + self.params = params + self.script_args.extend([f"--{k}={v}" for k, v in params.items()]) + super().run() + self.compute_metric() + + def _to_labels(self, path: str): + return torch.from_numpy(pd.read_csv(path).label.values) + + def compute_metric(self): + self.metric = -1 * float( + Accuracy()( + self._to_labels(self.submission_path), + self._to_labels(f"{self.data_dir}/ground_truth.csv"), + ) + ) + + @staticmethod + def distributions(): + return { + "backbone": CategoricalDistribution(["resnet18", "resnet34"]), + "learning_rate": LogUniformDistribution(0.0001, 0.1), + } diff --git a/docs/examples/app_hpo/pl_script.py b/docs/examples/app_hpo/pl_script.py new file mode 100644 index 0000000000000..bbc453798431a --- /dev/null +++ b/docs/examples/app_hpo/pl_script.py @@ -0,0 +1,43 @@ +import argparse +import os + +import pandas as pd +import torch +from flash import Trainer +from flash.image import ImageClassificationData, ImageClassifier + +# Parse arguments provided by the Work. +parser = argparse.ArgumentParser() +parser.add_argument("--train_data_path", type=str, required=True) +parser.add_argument("--submission_path", type=str, required=True) +parser.add_argument("--test_data_path", type=str, required=True) +parser.add_argument("--best_model_path", type=str, required=True) +# Optional +parser.add_argument("--backbone", type=str, default="resnet18") +parser.add_argument("--learning_rate", type=float, default=0.01) +args = parser.parse_args() + + +datamodule = ImageClassificationData.from_folders( + train_folder=args.train_data_path, + batch_size=8, +) + +model = ImageClassifier(datamodule.num_classes, backbone=args.backbone) +trainer = Trainer(fast_dev_run=True) +trainer.fit(model, datamodule=datamodule) +trainer.save_checkpoint(args.best_model_path) + +datamodule = ImageClassificationData.from_folders( + predict_folder=args.test_data_path, + batch_size=8, +) + +predictions = Trainer().predict(model, datamodule=datamodule) +submission_data = [ + {"filename": os.path.basename(p["metadata"]["filepath"]), "label": torch.argmax(p["preds"]).item()} + for batch in predictions + for p in batch +] +df = pd.DataFrame(submission_data) +df.to_csv(args.submission_path, index=False) diff --git a/docs/examples/app_hpo/requirements.txt b/docs/examples/app_hpo/requirements.txt new file mode 100644 index 0000000000000..bd85880da2237 --- /dev/null +++ b/docs/examples/app_hpo/requirements.txt @@ -0,0 +1,3 @@ +optuna +lightning-flash[image,serve] == 0.7.0 +hiplot diff --git a/docs/examples/app_hpo/utils.py b/docs/examples/app_hpo/utils.py new file mode 100644 index 0000000000000..3e8960ea893fc --- /dev/null +++ b/docs/examples/app_hpo/utils.py @@ -0,0 +1,54 @@ +import os +import os.path +import tarfile +import zipfile + +import requests + + +def download_data(url: str, path: str = "data/", verbose: bool = False) -> None: + """Download file with progressbar. + + # Code taken from: https://gist.github.com/ruxi/5d6803c116ec1130d484a4ab8c00c603 + # __author__ = "github.com/ruxi" + # __license__ = "MIT" + + Usage: + download_file('http://web4host.net/5MB.zip') + """ + if url == "NEED_TO_BE_CREATED": + raise NotImplementedError + + if not os.path.exists(path): + os.makedirs(path) + local_filename = os.path.join(path, url.split("/")[-1]) + r = requests.get(url, stream=True, verify=False) + file_size = int(r.headers["Content-Length"]) if "Content-Length" in r.headers else 0 + chunk_size = 1024 + num_bars = int(file_size / chunk_size) + if verbose: + print(dict(file_size=file_size)) + print(dict(num_bars=num_bars)) + + if not os.path.exists(local_filename): + with open(local_filename, "wb") as fp: + for chunk in r.iter_content(chunk_size=chunk_size): + fp.write(chunk) # type: ignore + + def extract_tarfile(file_path: str, extract_path: str, mode: str): + if os.path.exists(file_path): + with tarfile.open(file_path, mode=mode) as tar_ref: + for member in tar_ref.getmembers(): + try: + tar_ref.extract(member, path=extract_path, set_attrs=False) + except PermissionError: + raise PermissionError(f"Could not extract tar file {file_path}") + + if ".zip" in local_filename: + if os.path.exists(local_filename): + with zipfile.ZipFile(local_filename, "r") as zip_ref: + zip_ref.extractall(path) + elif local_filename.endswith(".tar.gz") or local_filename.endswith(".tgz"): + extract_tarfile(local_filename, path, "r:gz") + elif local_filename.endswith(".tar.bz2") or local_filename.endswith(".tbz"): + extract_tarfile(local_filename, path, "r:bz2") diff --git a/docs/examples/app_layout/.lightning b/docs/examples/app_layout/.lightning new file mode 100644 index 0000000000000..48e8408f9e81e --- /dev/null +++ b/docs/examples/app_layout/.lightning @@ -0,0 +1 @@ +name: layout-example diff --git a/docs/examples/app_layout/__init__.py b/docs/examples/app_layout/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_layout/app.py b/docs/examples/app_layout/app.py new file mode 100644 index 0000000000000..b7feb3e6d07be --- /dev/null +++ b/docs/examples/app_layout/app.py @@ -0,0 +1,101 @@ +"""An example showcasing how `configure_layout` can be used to nest user interfaces of different flows. + +Run the app: + +lightning run app examples/layout/demo.py + +This starts one server for each flow that returns a UI. Access the UI at the link printed in the terminal. +""" + +import os +from time import sleep + +import lightning as L +from lightning.app.frontend.stream_lit import StreamlitFrontend +from lightning.app.frontend.web import StaticWebFrontend + + +class C11(L.LightningFlow): + def __init__(self): + super().__init__() + self.message = "Hello Streamlit!" + + def run(self): + pass + + def configure_layout(self): + return StreamlitFrontend(render_fn=render_c11) + + +def render_c11(state): + import streamlit as st + + st.write(state.message) + + +class C21(L.LightningFlow): + def __init__(self): + super().__init__() + + def run(self): + pass + + def configure_layout(self): + return StaticWebFrontend(os.path.join(os.path.dirname(__file__), "ui1")) + + +class C22(L.LightningFlow): + def __init__(self): + super().__init__() + + def run(self): + pass + + def configure_layout(self): + return StaticWebFrontend(os.path.join(os.path.dirname(__file__), "ui2")) + + +class C1(L.LightningFlow): + def __init__(self): + super().__init__() + self.c11 = C11() + + def run(self): + pass + + +class C2(L.LightningFlow): + def __init__(self): + super().__init__() + self.c21 = C21() + self.c22 = C22() + + def run(self): + pass + + def configure_layout(self): + return [ + dict(name="one", content=self.c21), + dict(name="two", content=self.c22), + ] + + +class Root(L.LightningFlow): + def __init__(self): + super().__init__() + self.c1 = C1() + self.c2 = C2() + + def run(self): + sleep(10) + self._exit("Layout End") + + def configure_layout(self): + return [ + dict(name="one", content=self.c1.c11), + dict(name="two", content=self.c2), + dict(name="three", content="https://lightning.ai"), + ] + + +app = L.LightningApp(Root()) diff --git a/docs/examples/app_layout/ui1/index.html b/docs/examples/app_layout/ui1/index.html new file mode 100644 index 0000000000000..7019634b87fd5 --- /dev/null +++ b/docs/examples/app_layout/ui1/index.html @@ -0,0 +1,10 @@ + + + + + One + + +One + + diff --git a/docs/examples/app_layout/ui2/index.html b/docs/examples/app_layout/ui2/index.html new file mode 100644 index 0000000000000..f9b6432e4963d --- /dev/null +++ b/docs/examples/app_layout/ui2/index.html @@ -0,0 +1,10 @@ + + + + + Two + + +Two + + diff --git a/docs/examples/app_multi_node/.gitignore b/docs/examples/app_multi_node/.gitignore new file mode 100644 index 0000000000000..33eb0ef33c61c --- /dev/null +++ b/docs/examples/app_multi_node/.gitignore @@ -0,0 +1,2 @@ +.storage/ +.shared/ diff --git a/docs/examples/app_multi_node/.lightning b/docs/examples/app_multi_node/.lightning new file mode 100644 index 0000000000000..7befcc74ea6d3 --- /dev/null +++ b/docs/examples/app_multi_node/.lightning @@ -0,0 +1 @@ +name: multi-node-demo diff --git a/docs/examples/app_multi_node/multi_node.py b/docs/examples/app_multi_node/multi_node.py new file mode 100644 index 0000000000000..adc8df1c74815 --- /dev/null +++ b/docs/examples/app_multi_node/multi_node.py @@ -0,0 +1,36 @@ +import lightning as L + + +class Work(L.LightningWork): + def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): + super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) + + def run(self, main_address="localhost", main_port=1111, world_size=1, rank=0, init=False): + if init: + return + + import torch.distributed + + print(f"Initializing process group: {main_address=}, {main_port=}, {world_size=}, {rank=}") + torch.distributed.init_process_group( + backend="gloo", init_method=f"tcp://{main_address}:{main_port}", world_size=world_size, rank=rank + ) + gathered = [torch.zeros(1) for _ in range(world_size)] + torch.distributed.all_gather(gathered, torch.tensor([rank]).float()) + print(gathered) + + +class MultiNodeDemo(L.LightningFlow): + def __init__(self): + super().__init__() + self.work0 = Work() + self.work1 = Work() + + def run(self): + self.work0.run(init=True) + if self.work0.internal_ip: + self.work0.run(main_address=self.work0.internal_ip, main_port=self.work0.port, world_size=2, rank=0) + self.work1.run(main_address=self.work0.internal_ip, main_port=self.work0.port, world_size=2, rank=1) + + +app = L.LightningApp(MultiNodeDemo()) diff --git a/docs/examples/app_multi_node/requirements.txt b/docs/examples/app_multi_node/requirements.txt new file mode 100644 index 0000000000000..12c6d5d5eac2a --- /dev/null +++ b/docs/examples/app_multi_node/requirements.txt @@ -0,0 +1 @@ +torch diff --git a/docs/examples/app_payload/.lightning b/docs/examples/app_payload/.lightning new file mode 100644 index 0000000000000..933d6ed9a73e1 --- /dev/null +++ b/docs/examples/app_payload/.lightning @@ -0,0 +1 @@ +name: payload diff --git a/docs/examples/app_payload/app.py b/docs/examples/app_payload/app.py new file mode 100644 index 0000000000000..66de76d964adc --- /dev/null +++ b/docs/examples/app_payload/app.py @@ -0,0 +1,31 @@ +import lightning as L +from lightning.app.storage.payload import Payload + + +class SourceFileWriterWork(L.LightningWork): + def __init__(self): + super().__init__() + self.value = None + + def run(self): + self.value = Payload(42) + + +class DestinationWork(L.LightningWork): + def run(self, payload): + assert payload.value == 42 + + +class RootFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.src = SourceFileWriterWork() + self.dst = DestinationWork() + + def run(self): + self.src.run() + self.dst.run(self.src.value) + self._exit("Application End!") + + +app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_pickle_or_not/app.py b/docs/examples/app_pickle_or_not/app.py new file mode 100644 index 0000000000000..bda24cb5b7967 --- /dev/null +++ b/docs/examples/app_pickle_or_not/app.py @@ -0,0 +1,55 @@ +import logging + +import lightning as L + +logger = logging.getLogger(__name__) + + +class PickleChecker(L.LightningWork): + def run(self, pickle_image: bytes): + parsed = self.parse_image(pickle_image) + if parsed == b"it is a pickle": + return True + elif parsed == b"it is not a pickle": + return False + else: + raise Exception("Couldn't parse the image") + + @staticmethod + def parse_image(image_str: bytes): + return image_str + + +class Slack(L.LightningFlow): + def __init__(self): + super().__init__() + + @staticmethod + def send_message(message): + logger.info(f"Sending message: {message}") + + def run(self): + pass + + +class RootComponent(L.LightningFlow): + def __init__(self): + super().__init__() + self.pickle_checker = PickleChecker() + self.slack = Slack() + self.counter = 3 + + def run(self): + if self.counter > 0: + logger.info(f"Running the app {self.counter}") + image_str = b"it is not a pickle" + if self.pickle_checker.run(image_str): + self.slack.send_message("It's a pickle!") + else: + self.slack.send_message("It's not a pickle!") + self.counter -= 1 + else: + self._exit("Pickle or Not End") + + +app = L.LightningApp(RootComponent()) diff --git a/docs/examples/app_pickle_or_not/requirements.txt b/docs/examples/app_pickle_or_not/requirements.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_v0/.gitignore b/docs/examples/app_v0/.gitignore new file mode 100644 index 0000000000000..186149fa056fe --- /dev/null +++ b/docs/examples/app_v0/.gitignore @@ -0,0 +1,2 @@ +.storage +.lightning diff --git a/docs/examples/app_v0/README.md b/docs/examples/app_v0/README.md new file mode 100644 index 0000000000000..516283ae9cedd --- /dev/null +++ b/docs/examples/app_v0/README.md @@ -0,0 +1,18 @@ +# v0 app + +This app is a flow-only app with nothing fancy. +This is meant to present the basic functionalities of the lightning framework. + +## Starting it + +Local + +```bash +lightning run app app.py +``` + +Cloud + +```bash +lightning run app app.py --cloud +``` diff --git a/docs/examples/app_v0/__init__.py b/docs/examples/app_v0/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/examples/app_v0/app.py b/docs/examples/app_v0/app.py new file mode 100644 index 0000000000000..26345f5b43e46 --- /dev/null +++ b/docs/examples/app_v0/app.py @@ -0,0 +1,49 @@ +# v0_app.py +import os +from datetime import datetime +from time import sleep + +import lightning as L +from lightning.app.frontend.web import StaticWebFrontend + + +class Word(L.LightningFlow): + def __init__(self, letter): + super().__init__() + self.letter = letter + self.repeats = letter + + def run(self): + self.repeats += self.letter + + def configure_layout(self): + return StaticWebFrontend(os.path.join(os.path.dirname(__file__), f"ui/{self.letter}")) + + +class V0App(L.LightningFlow): + def __init__(self): + super().__init__() + self.aas = Word("a") + self.bbs = Word("b") + self.counter = 0 + + def run(self): + now = datetime.now() + now = now.strftime("%H:%M:%S") + log = {"time": now, "a": self.aas.repeats, "b": self.bbs.repeats} + print(log) + self.aas.run() + self.bbs.run() + + sleep(2.0) + self.counter += 1 + + def configure_layout(self): + tab1 = {"name": "Tab_1", "content": self.aas} + tab2 = {"name": "Tab_2", "content": self.bbs} + tab3 = {"name": "Tab_3", "content": "https://tensorboard.dev/experiment/8m1aX0gcQ7aEmH0J7kbBtg/#scalars"} + + return [tab1, tab2, tab3] + + +app = L.LightningApp(V0App()) diff --git a/docs/examples/app_v0/emulate_ui.py b/docs/examples/app_v0/emulate_ui.py new file mode 100644 index 0000000000000..8a5b45c1c3904 --- /dev/null +++ b/docs/examples/app_v0/emulate_ui.py @@ -0,0 +1,19 @@ +from time import sleep + +import requests + +from lightning.app.utilities.state import headers_for + +headers = headers_for({}) +headers["X-Lightning-Type"] = "DEFAULT" + +res = requests.get("http://127.0.0.1:7501/state", headers=headers) + + +res = requests.post("http://127.0.0.1:7501/state", json={"stage": "running"}, headers=headers) +print(res) + +sleep(10) + +res = requests.post("http://127.0.0.1:7501/state", json={"stage": "stopping"}, headers=headers) +print(res) diff --git a/docs/examples/app_v0/requirements.txt b/docs/examples/app_v0/requirements.txt new file mode 100644 index 0000000000000..edfce786a4d18 --- /dev/null +++ b/docs/examples/app_v0/requirements.txt @@ -0,0 +1 @@ +py diff --git a/docs/examples/app_v0/ui/a/index.html b/docs/examples/app_v0/ui/a/index.html new file mode 100644 index 0000000000000..6ddb9a5a1323c --- /dev/null +++ b/docs/examples/app_v0/ui/a/index.html @@ -0,0 +1 @@ +
Hello from component A
diff --git a/docs/examples/app_v0/ui/b/index.html b/docs/examples/app_v0/ui/b/index.html new file mode 100644 index 0000000000000..3bfd9e24cb7f7 --- /dev/null +++ b/docs/examples/app_v0/ui/b/index.html @@ -0,0 +1 @@ +
Hello from component B
diff --git a/docs/source-app/_static/images/brandmark.png b/docs/source-app/_static/images/brandmark.png new file mode 100644 index 0000000000000000000000000000000000000000..76648a7ed99e019dff43a641562fc9b238fc7951 GIT binary patch literal 60816 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

|YPrMQ(76*=X&6xdryZZGOjzUA253wEM$}f}5 zX?7;)jugB3dxWM=an*6E=D#-|B|*DCK$K;6-sOM$(0RyznSq(EyhR@Iudiz-c*-oQ zuJ|eVUC_@xd{?a%o_Fe<9j`j*|YHRDX;p2PGi)k)TQ{uLz$ zPQphMqgn4T<2hqg^OCl4Qadz|`9XfLY@Ubb>}HgDHBJBPI4YaH0DUq?YRsa{4y?lJ6g^hu z$KvId8jSHV#ew4yet-o1ZT}mz&#rH8KsruLcvJY4YzoRk=>0x%gKFvQ9zAiT;lSTl zQQ1-j=>5;6#?IsMy;sIYNo+);BrW-lYm7V3Kp%JoS zeIXhD-ux~2z9zdOqbNjFtnTW!j*1ZSP(3=>u4I6*NTBO}pKvVG^_KscvVy)s$AdOr zwr)OZ)RrSk6m4+0Hn58;F94mGeN)~)-WE->p=2-&{|yiQ#3b$wX{8%xf4i}N8>gk1 z@(y}|;<(3u^Bt!t{q^E?JwN@@xi{#jeAM-*;7ks7#o zke{J~*+H*T954OPM7+EcMm}xSc5TQhurv2>FT(EQG{mF^2o%dX|0DmJRgE*@uOh)> z#FQ?!Wj`^eS$V&dyN%0?Bj_JeS`G;OHNDEtfL}Bwz`JSCg)%c-U^cQ8y*oq5*lS7# zXX^vo2|g3dt-j2re{Y5^2a1 z;lIU~cD-}iD_}8dH-Dt=_fj$a%(v)&J^c;xvPKl1w4Nw){aWWCurW=lJ>Q>^FJN#3 zgt2bb`BVR1M%!Gr&iWDSDFY_#%-^-7p@&}b!rx@HGfD>j84e=QHwp9$bSWbl~c8#sJ^H7vnez(5g=}ifS<@FmD~M9VZrcY1lI?2g>yhm3S8h zwobqJ_bU3rW$U=$e_GC5Xx3@4$o#t`w>!`2s2ThdEQ&)*no|y$SrEZ=TkG>E++#U@ zb$w_vz@^DeD^q2Fza*wy>K%aoU!~%UG~rLyHEN4-SLAYT z{yNKz=^O z3?>BeOD8aq*J0e1Z&L=`Shwf#e5_3V=e{qFMfEjYywd2El3 zjsCTj55mqz#gn-4v7!kIz`5i!aK(Wuyl2USGfIItXZ1OM+k~I*m{D623>KDsi<$tV zp3&r+f?!%PM&;GV)X!_d{s+z>hLA-Tc&kWHU zQ#r+z*kmF-z*R8xb+m^qVjl%$V06Jxum5;y~acwOJe n0e|m6T1r%*#(ygq9kI+J8x^wnv-?H_@_Wqj)5mg;THX48Rfu|9 literal 0 HcmV?d00001 diff --git a/docs/source-app/_templates/classtemplate.rst b/docs/source-app/_templates/classtemplate.rst index b99cdb3426519..00f2d0b767436 100644 --- a/docs/source-app/_templates/classtemplate.rst +++ b/docs/source-app/_templates/classtemplate.rst @@ -9,5 +9,5 @@ :members: .. - autogenerated from source/_templates/classtemplate.rst + autogenerated from source-app/_templates/classtemplate.rst note it does not have :inherited-members: diff --git a/docs/source-app/_templates/theme_variables.jinja b/docs/source-app/_templates/theme_variables.jinja index da4cf094f8743..203431909446f 100644 --- a/docs/source-app/_templates/theme_variables.jinja +++ b/docs/source-app/_templates/theme_variables.jinja @@ -1,8 +1,8 @@ {%- set external_urls = { - 'github': 'https://github.com/PytorchLightning/lightning', - 'github_issues': 'https://github.com/PytorchLightning/lightning/issues', - 'contributing': 'https://github.com/PytorchLightning/pytorch-lightning/blob/master/CONTRIBUTING.md', - 'governance': 'https://github.com/PytorchLightning/pytorch-lightning/blob/master/governance.md', + 'github': 'https://github.com/Lightning-AI/lightning', + 'github_issues': 'https://github.com/Lightning-AI/lightning/issues', + 'contributing': 'https://github.com/Lightning-AI/pytorch-lightning/blob/master/CONTRIBUTING.md', + 'governance': 'https://github.com/Lightning-AI/pytorch-lightning/blob/master/governance.md', 'docs': 'https://lightning.rtfd.io/en/latest', 'twitter': 'https://twitter.com/PyTorchLightnin', 'discuss': 'https://pytorch-lightning.slack.com', diff --git a/docs/source-app/pages/basics.rst b/docs/source-app/basics.rst similarity index 92% rename from docs/source-app/pages/basics.rst rename to docs/source-app/basics.rst index d0c6fa80f9470..f818c9ed7eae7 100644 --- a/docs/source-app/pages/basics.rst +++ b/docs/source-app/basics.rst @@ -8,22 +8,29 @@ Basics In this guide, we'll cover the basic terminology associated with the Lightning framework. +---- + +************** Lightning App -============= +************** The :class:`~lightning_app.core.app.LightningApp` runs a tree of one or more components that interact to create end-to-end applications. There are two kinds of components: :class:`~lightning_app.core.flow.LightningFlow` and :class:`~lightning_app.core.work.LightningWork`. This modular design enables you to reuse components created by other users. +---- Lightning Work ^^^^^^^^^^^^^^ The :class:`~lightning_app.core.work.LightningWork` component is a building block optimized for long-running jobs or integrating third-party services. LightningWork can be used for training large models, downloading a dataset, or any long-lasting operation. +---- + Lightning Flow ^^^^^^^^^^^^^^ The :class:`~lightning_app.core.flow.LightningFlow` component coordinates long-running tasks :class:`~lightning_app.core.work.LightningWork` and runs its children :class:`~lightning_app.core.flow.LightningFlow` components. +---- Lightning App Tree ^^^^^^^^^^^^^^^^^^ @@ -32,7 +39,7 @@ Components can be nested to form component trees where the LightningFlows are it Here's a basic application with four flows and two works: -.. literalinclude:: ../code_samples/quickstart/app_comp.py +.. literalinclude:: code_samples/quickstart/app_comp.py And here's its associated tree structure: @@ -42,6 +49,8 @@ And here's its associated tree structure: A Lightning App runs all flows into a single process. Its flows coordinate the execution of the works each running in their own independent processes. +---- + Lightning Distributed Event Loop ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -51,6 +60,7 @@ Drawing inspiration from modern web frameworks like `React.js `_. ---- -*********************************** -Why should components communicate ? -*********************************** - -When creating interactive apps with multiple components, you might want your components to share information with each other. You might to rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc. - -By design, the :class:`~lightning_app.core.flow.LightningFlow` communicates to all :class:`~lightning_app.core.flow.LightningWork` within the application, but :class:`~lightning_app.core.flow.LightningWork` can't communicate between each other directly, they need the flow as a proxy to do so. - -Once a ``LightningWork`` is running, any updates to its state is automatically communicated to the flow as a delta (using `DeepDiff `_). The state communication isn't bi-directional, it is only done from work to flow. - -Internally, the Lightning App is alternatively collecting deltas sent from all the registered ``LightningWorks`` and/or UI, and running the root flow run method of the app. - -******************************* -Communication From Work to Flow -******************************* - -Below, find an example to better understand this behavior. - -The ``WorkCounter`` increments a counter until 1 million and the ``Flow`` prints the work counter. - -As the work is running into its own process, its state changes is sent to the Flow which contains the latest value of the counter. - -.. code-block:: python - - import lightning_app as la - - - class WorkCounter(lapp.LightningWork): - def __init__(self): - super().__init__(parallel=True) - self.counter = 0 - - def run(self): - for _ in range(int(10e6)): - self.counter += 1 - - - class Flow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.w = WorkCounter() - - def run(self): - self.w.run() - print(self.w.counter) - - - app = lapp.LightningApp(Flow()) - - -A delta sent from the work to the flow looks like this: - -.. code-block:: python - - {"values_changed": {"root['works']['w']['vars']['counter']": {"new_value": 425}}} - -Here is the associated illustration: - -.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/deltas.gif - :alt: Mechanism showing how delta are sent. - :width: 100 % - - -******************************* -Communication From From to Work -******************************* - -Communication from the flow to the work while running isn't support yet. If your application requires this feature, please open an issue on Github. - -.. code-block:: python - - import lightning_app as la - from time import sleep - - - class WorkCounter(lapp.LightningWork): - def __init__(self): - super().__init__(parallel=True) - self.counter = 0 - - def run(self): - while True: - sleep(1) - print(f"Work {self.counter}") - - - class Flow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.w = WorkCounter() - - def run(self): - self.w.run() - sleep(1) - print(f"Flow {self.w.counter}") - self.w.counter += 1 - - - app = lapp.LightningApp(Flow()) - -As you can observe, there is a divergence between the value within the Work and the Flow. - -.. code-block:: console - - Flow 0 - Flow 1 - Flow 2 - Flow 3 - Work 0 - Flow 4 - Work 0 - Flow 5 - Work 0 - Flow 6 - Work 0 - Flow 7 - Work 0 - Flow 8 - Work 0 - Flow 9 - Work 0 - Flow 10 - -.. note:: Technically, the flow and works relies on queues to share data (multiprocessing locally and redis lists in the cloud). +.. include:: ../../core_api/lightning_app/communication_content.rst diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst new file mode 100644 index 0000000000000..edad7b5dc74ad --- /dev/null +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -0,0 +1,123 @@ + + +************************************************** +What communication between components does for you +************************************************** + +When creating interactive apps with multiple components, you might want your components to share information with each other. You might to rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc. + +By design, the :class:`~lightning_app.core.flow.LightningFlow` communicates to all :class:`~lightning_app.core.flow.LightningWork` within the application, but :class:`~lightning_app.core.flow.LightningWork` can't communicate between each other directly, they need the flow as a proxy to do so. + +Once a ``LightningWork`` is running, any updates to its state is automatically communicated to the flow as a delta (using `DeepDiff `_). The state communication isn't bi-directional, it is only done from work to flow. + +Internally, the Lightning App is alternatively collecting deltas sent from all the registered ``LightningWorks`` and/or UI, and running the root flow run method of the app. + +---- + +************************************************* +Communication from LightningWork to LightningFlow +************************************************* + +Here's an example to better understand communication from LightningWork to LightningFlow. + +The ``WorkCounter`` increments a counter until 1 million and the ``Flow`` prints the work counter. + +As the LightningWork is running into its own process, its state changes is sent to the LightningFlow which contains the latest value of the counter. + +.. code-block:: python + + import lightning as L + + class WorkCounter(L.LightningWork): + def __init__(self): + super().__init__(parallel=True) + self.counter = 0 + + def run(self): + for _ in range(int(10e6)): + self.counter += 1 + + class Flow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.w = WorkCounter() + + def run(self): + self.w.run() + print(self.w.counter) + + app = L.LightningApp(Flow()) + + +A delta sent from the LightningWork to the LightningFlow looks like this: + +.. code-block:: python + + {'values_changed': {"root['works']['w']['vars']['counter']": {'new_value': 425}}} + +Here is the associated illustration: + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/deltas.gif + :alt: Mechanism showing how delta are sent. + :width: 100 % + +---- + +************************************************* +Communication from LightningFlow to LightningWork +************************************************* + +Communication from the LightningFlow to the LightningWork while running **isn't support yet**. If your application requires this feature, please open an issue on Github. + +.. code-block:: python + + import lightning as L + from time import sleep + + class WorkCounter(L.LightningWork): + def __init__(self): + super().__init__(parallel=True) + self.counter = 0 + + def run(self): + while True: + sleep(1) + print(f"Work {self.counter}") + + class Flow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.w = WorkCounter() + + def run(self): + self.w.run() + sleep(1) + print(f"Flow {self.w.counter}") + self.w.counter += 1 + + app = L.LightningApp(Flow()) + +As you can observe, there is a divergence between the value within the LightningWork and the LightningFlow. + +.. code-block:: console + + Flow 0 + Flow 1 + Flow 2 + Flow 3 + Work 0 + Flow 4 + Work 0 + Flow 5 + Work 0 + Flow 6 + Work 0 + Flow 7 + Work 0 + Flow 8 + Work 0 + Flow 9 + Work 0 + Flow 10 diff --git a/docs/source-app/core_api/lightning_app/dynamic_work.rst b/docs/source-app/core_api/lightning_app/dynamic_work.rst index b4ad9d1144d5a..bf202aa590a79 100644 --- a/docs/source-app/core_api/lightning_app/dynamic_work.rst +++ b/docs/source-app/core_api/lightning_app/dynamic_work.rst @@ -1,187 +1,15 @@ :orphan: -############ -Dynamic Work -############ +.. _dynamic_work: -**Audience:** Users who want to learn how to create application which adapts to user demands. +##################### +Dynamic LightningWork +##################### -**Level:** Intermediate +**Audience:** Users who want to create applications that adapt to user demands. ----- - -*************************************************** -Why should I care about creating work dynamically ? -*************************************************** - -Imagine you want to create a research notebook app for your team, where every member can create multiple `JupyterLab `_ session on their hardware of choice. - -To allow every notebook to choose hardware, it needs to be set up in it's own :class:`~lightning_app.core.work.LightningWork`, but you can't know the number of notebooks user will need in advance. In this case you'll need to add ``LightningWorks`` dynamically at run time. - -This is what **dynamic works** enables. - -*************************** -When to use dynamic works ? -*************************** - -Dynamic works should be used anytime you want change the resources your application is using at runtime. - -******************* -How to add a work ? -******************* - -You can simply attach your components in the **run** method of a flow using python **hasattr**, **setattr** and **getattr** functions. - -.. code-block:: python - - class RootFlow(lapp.LightningFlow): - def run(self): - - if not hasattr(self, "work"): - setattr(self, "work", Work()) # The `Work` component is created and attached here. - getattr(self, "work").run() # Run the `Work` component. - -But it is usually more readable to use Lightning built-in :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` as follows: - -.. code-block:: python - - from lightning_app.structures import Dict - - - class RootFlow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.dict = Dict() - - def run(self): - if "work" not in self.dict: - self.dict["work"] = Work() # The `Work` component is attached here. - self.dict["work"].run() - - -******************** -How to stop a work ? -******************** - -In order to stop a work, simply use the work ``stop`` method as follows: - -.. code-block:: python - - class RootFlow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.work = Work() - - def run(self): - self.work.stop() - - -********************************** -Application Example with StreamLit -********************************** +**Level:** Advanced -.. - The entire application can be found `here `_. - -The Notebook Manager -^^^^^^^^^^^^^^^^^^^^ - -In the component below, we are dynamically creating ``JupyterLabWork`` every time as user clicks the ``Create Jupyter Notebook`` button. - -To do so, we are iterating over the list of ``jupyter_config_requests`` infinitely. - -.. code-block:: python - - import lightning_app as la - - - class JupyterLabManager(lapp.LightningFlow): - """This flow manages the users notebooks running within works.""" - - def __init__(self): - super().__init__() - self.jupyter_works = lapp.structures.Dict() - self.jupyter_config_requests = [] - - def run(self): - for idx, jupyter_config in enumerate(self.jupyter_config_requests): - - # The Jupyter Config has this form is: - # {"use_gpu": False/True, "token": None, "username": ..., "stop": False} - - # Step 1: Check if JupyterWork already exists for this username - username = jupyter_config["username"] - if username not in self.jupyter_works: - jupyter_config["ready"] = False - - # Set the hardware selected by the user: GPU or CPU. - cloud_compute = lapp.CloudCompute("gpu" if jupyter_config["use_gpu"] else "cpu-small") - - # Step 2: Create new JupyterWork dynamically ! - self.jupyter_works[username] = JupyterLabWork(cloud_compute=cloud_compute) - - # Step 3: Run the JupyterWork - self.jupyter_works[username].run() - - # Step 4: Store the notebook token in the associated config. - # We are using this to know when the notebook is ready - # and display the stop button on the UI. - if self.jupyter_works[username].token: - jupyter_config["token"] = self.jupyter_works[username].token - - # Step 5: Stop the work if the user requested it. - if jupyter_config["stop"]: - self.jupyter_works[username].stop() - self.jupyter_config_requests.pop(idx) - - def configure_layout(self): - return StreamlitFrontend(render_fn=render_fn) - - -The StreamLit UI -^^^^^^^^^^^^^^^^ - -In the UI below, we receive the **state** of the Jupyter Manager and it can be modified directly from the UI interaction. - -.. code-block:: python - - def render_fn(state): - import streamlit as st - - # Step 1: Enable users to select their notebooks and create them - column_1, column_2, column_3 = st.columns(3) - with column_1: - create_jupyter = st.button("Create Jupyter Notebook") - with column_2: - username = st.text_input("Enter your username", "tchaton") - assert username - with column_3: - use_gpu = st.checkbox("Use GPU") - - # Step 2: If a user clicked the button, add an element to the list of configs - # Note: state.jupyter_config_requests = ... will sent the state update to the component. - if create_jupyter: - new_config = [{"use_gpu": use_gpu, "token": None, "username": username, "stop": False}] - state.jupyter_config_requests = state.jupyter_config_requests + new_config - - # Step 3: List of running notebooks. - for idx, config in enumerate(state.jupyter_config_requests): - column_1, column_2, column_3 = st.columns(3) - with column_1: - if not idx: - st.write(f"Idx") - st.write(f"{idx}") - with column_2: - if not idx: - st.write(f"Use GPU") - st.write(config["use_gpu"]) - with column_3: - if not idx: - st.write(f"Stop") - if config["token"]: - should_stop = st.button("Stop this notebook") +---- - # Step 4: Change stop if the user clicked the button - if should_stop: - config["stop"] = should_stop - state.jupyter_config_requests = state.jupyter_config_requests +.. include:: dynamic_work_content.rst diff --git a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst new file mode 100644 index 0000000000000..293b29c7ca675 --- /dev/null +++ b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst @@ -0,0 +1,199 @@ +*************************************** +What Dynamic LightningWork does for you +*************************************** + +Dynamic LightningWork (Work) changes the resources your application uses while the application is running (aka at runtime). + +For example, imagine you want to create a research notebook app for your team. You want every member to be able to create multiple `JupyterLab `_ sessions on their hardware of choice. + +To allow every notebook to choose hardware, it needs to be set up in it's own :class:`~lightning_app.core.work.LightningWork`, but you can't know the number of notebooks user will need in advance. In this case you'll need to add ``LightningWorks`` dynamically at run time. + +---- + +***************** +Use Dynamic Works +***************** + +Dynamic Works should be used anytime you want change the resources your application is using while it is running (aka at runtime). + +You're usually going to use the ``start`` and ``stop`` methods together. + +---- + +Add a Dynamic Work +^^^^^^^^^^^^^^^^^^ + +There are a couple of ways you can add a dynamic Work: + +- Option 1: Attach your components in the **run** method using the Python functions. +- Option 2: Use the Lightning built-in classes :class:`~lightning.structures.Dict` or :class:`~lightning.structures.List`. + +.. note:: Using the Lightning built-in classes is usually easier to read. + +---- + +**OPTION 1:** Attach your components in the run method of a flow using the Python functions **hasattr**, **setattr**, and **getattr**: + +.. code-block:: python + + class RootFlow(lapp.LightningFlow): + + def run(self): + + if not hasattr(self, "work"): + # The `Work` component is created and attached here. + setattr(self, "work", Work()) + # Run the `Work` component. + getattr(self, "work").run() + +**OPTION 2:** Use the built-in Lightning classes :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` + +.. code-block:: python + + from lightning_app.structures import Dict + + class RootFlow(lapp.LightningFlow): + + def __init__(self): + super().__init__() + self.dict = Dict() + + def run(self): + if "work" not in self.dict: + # The `Work` component is attached here. + self.dict["work"] = Work() + self.dict["work"].run() + +---- + +Stop a Work +^^^^^^^^^^^ +Stop a work when you are concerned about cost. + +To stop a work, use the work ``stop`` method: + +.. code-block:: python + + class RootFlow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.work = Work() + + def run(self): + self.work.stop() + +---- + +********************* +Dynamic Work Examples +********************* + +.. + The entire application can be found `here `_. + +---- + +Dynamic Work with Jupyter Notebooks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In this example, we are dynamically creating ``JupyterLabWork`` every time a user clicks the **Create Jupyter Notebook** button. + +In order to do that, we are iterating over the list of ``jupyter_config_requests`` infinitely. + +.. code-block:: python + + import lightning as L + + class JupyterLabManager(L.LightningFlow): + + """This flow manages the users notebooks running within works."""" + + def __init__(self): + super().__init__() + self.jupyter_works = L.structures.Dict() + self.jupyter_config_requests = [] + + def run(self): + for idx, jupyter_config in enumerate(self.jupyter_config_requests): + + # The Jupyter Config has this form is: + # {"use_gpu": False/True, "token": None, "username": ..., "stop": False} + + # Step 1: Check if JupyterWork already exists for this username + username = jupyter_config["username"] + if username not in self.jupyter_works: + jupyter_config["ready"] = False + + # Set the hardware selected by the user: GPU or CPU. + cloud_compute = L.CloudCompute("gpu" if jupyter_config["use_gpu"] else "cpu-small") + + # Step 2: Create new JupyterWork dynamically ! + self.jupyter_works[username] = JupyterLabWork(cloud_compute=cloud_compute) + + # Step 3: Run the JupyterWork + self.jupyter_works[username].run() + + # Step 4: Store the notebook token in the associated config. + # We are using this to know when the notebook is ready + # and display the stop button on the UI. + if self.jupyter_works[username].token: + jupyter_config["token"] = self.jupyter_works[username].token + + # Step 5: Stop the work if the user requested it. + if jupyter_config['stop']: + self.jupyter_works[username].stop() + self.jupyter_config_requests.pop(idx) + + def configure_layout(self): + return StreamlitFrontend(render_fn=render_fn) + +---- + +Dynamic Works with StreamLit UI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Continuing from the Jupyter Notebook example, in the UI, we receive the **state** of the Jupyter Manager and the state can be modified directly from the UI. + +.. code-block:: python + + def render_fn(state): + import streamlit as st + + # Step 1: Enable users to select their notebooks and create them + column_1, column_2, column_3 = st.columns(3) + with column_1: + create_jupyter = st.button("Create Jupyter Notebook") + with column_2: + username = st.text_input('Enter your username', "tchaton") + assert username + with column_3: + use_gpu = st.checkbox('Use GPU') + + # Step 2: If a user clicked the button, add an element to the list of configs + # Note: state.jupyter_config_requests = ... will sent the state update to the component. + if create_jupyter: + new_config = [{"use_gpu": use_gpu, "token": None, "username": username, "stop": False}] + state.jupyter_config_requests = state.jupyter_config_requests + new_config + + # Step 3: List of running notebooks. + for idx, config in enumerate(state.jupyter_config_requests): + column_1, column_2, column_3 = st.columns(3) + with column_1: + if not idx: + st.write(f"Idx") + st.write(f"{idx}") + with column_2: + if not idx: + st.write(f"Use GPU") + st.write(config['use_gpu']) + with column_3: + if not idx: + st.write(f"Stop") + if config["token"]: + should_stop = st.button("Stop this notebook") + + # Step 4: Change stop if the user clicked the button + if should_stop: + config["stop"] = should_stop + state.jupyter_config_requests = state.jupyter_config_requests diff --git a/docs/source-app/core_api/lightning_app/lightning_app.rst b/docs/source-app/core_api/lightning_app/lightning_app.rst index 5415f0d9dcf0c..f4ad65268f7e4 100644 --- a/docs/source-app/core_api/lightning_app/lightning_app.rst +++ b/docs/source-app/core_api/lightning_app/lightning_app.rst @@ -7,7 +7,5 @@ LightningApp ############ -The :class:`~lightning_app.core.app.LightningApp` runs a tree of one or more components that interact to create end-to-end applications. There are two kinds of components: :class:`~lightning_app.core.flow.LightningFlow` and :class:`~lightning_app.core.work.LightningWork`. This modular design enables you to reuse components created by other users. - .. autoclass:: lightning_app.core.app.LightningApp - :noindex: + :exclude-members: _run, connect, get_component_by_name, maybe_apply_changes, set_state diff --git a/docs/source-app/core_api/lightning_flow.rst b/docs/source-app/core_api/lightning_flow.rst index 6776bfefcb91e..a6ee8b2eca340 100644 --- a/docs/source-app/core_api/lightning_flow.rst +++ b/docs/source-app/core_api/lightning_flow.rst @@ -4,8 +4,5 @@ LightningFlow ############# -The :class:`~lightning_app.core.flow.LightningFlow` component coordinates long-running tasks :class:`~lightning_app.core.work.LightningWork` and runs its children :class:`~lightning_app.core.flow.LightningFlow` components. - - .. autoclass:: lightning_app.core.flow.LightningFlow - :noindex: + :exclude-members: _attach_backend, _exit, _is_state_attribute, set_state diff --git a/docs/source-app/core_api/lightning_work/compute.rst b/docs/source-app/core_api/lightning_work/compute.rst index f41ca08380d99..89313c4878cec 100644 --- a/docs/source-app/core_api/lightning_work/compute.rst +++ b/docs/source-app/core_api/lightning_work/compute.rst @@ -8,103 +8,8 @@ Customize your Cloud Compute **Audience:** Users who want to select the hardware to run in the cloud. -**Level:** Basic +**Level:** Intermediate ---- -*************************************** -How can I customize my Work resources ? -*************************************** - -In the cloud, you can simply configure which machine to run on by passing -a :class:`~lightning_app.utilities.packaging.cloud_compute.CloudCompute` to your work ``__init__`` method: - -.. code-block:: python - - import lightning_app as la - - # Run on a free, shared CPU machine. This is the default for every LightningWork. - MyCustomWork(cloud_compute=lapp.CloudCompute()) - - # Run on a dedicated, medium-size CPU machine (see specs below) - MyCustomWork(cloud_compute=lapp.CloudCompute("cpu-medium")) - - # Run on cheap GPU machine with a single GPU (see specs below) - MyCustomWork(cloud_compute=lapp.CloudCompute("gpu")) - - # Run on a fast multi-GPU machine (see specs below) - MyCustomWork(cloud_compute=lapp.CloudCompute("gpu-fast-multi")) - - -Here is the full list of supported machine names: - -.. list-table:: Hardware by Accelerator Type - :widths: 25 25 25 25 - :header-rows: 1 - - * - Name - - # of CPUs - - GPUs - - Memory - * - default - - 2 - - 0 - - 3 GB - * - cpu-small - - 2 - - 0 - - 8 GB - * - cpu-medium - - 8 - - 0 - - 32 GB - * - gpu - - 4 - - 1 (T4, 16 GB) - - 16 GB - * - gpu-fast - - 8 - - 1 (V100, 16 GB) - - 61 GB - * - gpu-fast-multi - - 32 - - 4 (V100 16 GB) - - 244 GB - -The up-to-date prices for these instances can be found `here `_. - - -******************************************* -How can I run on spot/preemptible machine ? -******************************************* - -Most cloud provider offers ``preemptible`` (synonym of ``spot``) machine which are usually discounted up to 90 %. Those machines are cheaper but the cloud provider can retrieve them at any time. - -.. code-block:: python - - import lightning_app as la - - # Run on a single CPU - MyCustomWork(cloud_compute=lapp.CloudCompute("gpu", preemptible=True)) - - -*********************************** -How can I stop my work when idle ? -*********************************** - -By providing **idle_timeout=X Seconds**, the work is automatically stopped **X seconds** after doing nothing. - -.. code-block:: python - - import lightning_app as la - - # Run on a single CPU and turn down immediately when idle. - MyCustomWork(cloud_compute=lapp.CloudCompute("gpu", idle_timeout=0)) - - -############# -CloudCompute -############# - -.. autoclass:: lightning_app.utilities.packaging.cloud_compute.CloudCompute - :noindex: +.. include:: compute_content.rst diff --git a/docs/source-app/core_api/lightning_work/compute_content.rst b/docs/source-app/core_api/lightning_work/compute_content.rst new file mode 100644 index 0000000000000..b37c54da38008 --- /dev/null +++ b/docs/source-app/core_api/lightning_work/compute_content.rst @@ -0,0 +1,100 @@ + +*************************** +Customize my Work resources +*************************** + +In the cloud, you can simply configure which machine to run on by passing +a :class:`~lightning_app.utilities.packaging.cloud_compute.CloudCompute` to your work ``__init__`` method: + +.. code-block:: python + + import lightning as L + + # Run on a free, shared CPU machine. This is the default for every LightningWork. + MyCustomWork(cloud_compute=L.CloudCompute()) + + # Run on a dedicated, medium-size CPU machine (see specs below) + MyCustomWork(cloud_compute=L.CloudCompute("cpu-medium")) + + # Run on cheap GPU machine with a single GPU (see specs below) + MyCustomWork(cloud_compute=L.CloudCompute("gpu")) + + # Run on a fast multi-GPU machine (see specs below) + MyCustomWork(cloud_compute=L.CloudCompute("gpu-fast-multi")) + + +Here is the full list of supported machine names: + +.. list-table:: Hardware by Accelerator Type + :widths: 25 25 25 25 + :header-rows: 1 + + * - Name + - # of CPUs + - GPUs + - Memory + * - default + - 2 + - 0 + - 3 GB + * - cpu-small + - 2 + - 0 + - 8 GB + * - cpu-medium + - 8 + - 0 + - 32 GB + * - gpu + - 4 + - 1 (T4, 16 GB) + - 16 GB + * - gpu-fast + - 8 + - 1 (V100, 16 GB) + - 61 GB + * - gpu-fast-multi + - 32 + - 4 (V100 16 GB) + - 244 GB + +The up-to-date prices for these instances can be found `here `_. + +---- + +******************************* +Run on spot/preemptible machine +******************************* + +Most cloud provider offers ``preemptible`` (synonym of ``spot``) machines that are usually discounted by up to 90 %. Those machines are cheaper but the cloud provider can retrieve them at any time. + +.. code-block:: python + + import lightning as L + + # Run on a single CPU + MyCustomWork(cloud_compute=L.CloudCompute("gpu", preemptible=True)) + +---- + +********************** +Stop my work when idle +********************** + +By providing **idle_timeout=X Seconds**, the work is automatically stopped **X seconds** after doing nothing. + +.. code-block:: python + + import lightning as L + + # Run on a single CPU and turn down immediately when idle. + MyCustomWork(cloud_compute=L.CloudCompute("gpu", idle_timeout=0)) + +---- + +############# +CloudCompute +############# + +.. autoclass:: lightning_app.utilities.packaging.cloud_compute.CloudCompute + :noindex: diff --git a/docs/source-app/core_api/lightning_work/handling_app_exception.rst b/docs/source-app/core_api/lightning_work/handling_app_exception.rst index 60f432e8177ae..20c9b618d97aa 100644 --- a/docs/source-app/core_api/lightning_work/handling_app_exception.rst +++ b/docs/source-app/core_api/lightning_work/handling_app_exception.rst @@ -1,83 +1,13 @@ :orphan: -######################## -Handling App Exceptions -######################## +############################### +Handle Lightning App exceptions +############################### -**Audience:** Users who want to know how to implement app where errors are handled. +**Audience:** Users who want to make Lightning Apps more robust to potential issues. **Level:** Advanced ---- -************************************************* -Why should I care about handling app exceptions ? -************************************************* - -Imagine you are creating an application where your team can launch model training by providing their own Github Repo any time they want. - -As the application admin, you don't want the application to go down if their code has a bug and breaks. - -Instead, you would like the work to capture the exception and surface this to the users on failures. - -**************************************** -How can I configure exception handling ? -**************************************** - - -The LightningWork accepts an argument **raise_exception** which is **True** by default. This aligns with Python default behaviors. - -However, for the user case stated above, we want to capture the work exceptions. This is done by providing ``raise_exception=False`` to the work ``__init__`` method. - -.. code-block:: python - - MyCustomWork(raise_exception=False) # <== HERE: The exception is captured. - - # Default behavior - MyCustomWork(raise_exception=True) # <== HERE: The exception is raised within the flow and terminates the app - - -And you can customize this behavior by overriding the ``on_exception`` hook to the Lightning Work. - -.. code-block:: python - - import lightning as L - - - class MyCustomWork(L.LightningWork): - def on_exception(self, exception: Exception): - # do something when an exception is triggered. - pass - - -******************* -Application Example -******************* - -This is the pseudo-code for the application described above. - -.. code-block:: python - - import lightning_app as lapp - - - class RootFlow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.user_jobs = lapp.structures.Dict() - self.requested_jobs = [] - - def run(self): - for request in self.requested_jobs: - job_id = request["id"] - if job_id not in self.user_jobs: - # Note: The `GithubRepoLauncher` doesn't exist yet. - self.user_jobs[job_id] = GithubRepoLauncher( - **request, - raise_exception=False, # <== HERE: The exception is captured. - ) - self.user_jobs[job_id].run() - - if self.user_jobs[job_id].status.stage == "failed" and "printed" not in request: - print(self.user_jobs[job_id].status) # <== HERE: Print the user exception. - request["printed"] = True +.. include:: handling_app_exception_content.rst diff --git a/docs/source-app/core_api/lightning_work/handling_app_exception_content.rst b/docs/source-app/core_api/lightning_work/handling_app_exception_content.rst new file mode 100644 index 0000000000000..4840cf5fdf6f3 --- /dev/null +++ b/docs/source-app/core_api/lightning_work/handling_app_exception_content.rst @@ -0,0 +1,74 @@ + +*************************************************** +What handling Lightning App exceptions does for you +*************************************************** + +Imagine you are creating a Lightning App (App) where your team can launch model training by providing their own Github Repo any time they want. + +As the App admin, you don't want the App to go down if their code has a bug and breaks. + +Instead, you would like the LightningWork (Work) to capture the exception and present the issue to users. + +---- + +**************************** +Configure exception handling +**************************** + +The LightningWork (Work) accepts an argument **raise_exception** which is **True** by default. This aligns with Python default behaviors. + +However, for the user case stated in the previous section, we want to capture the Work exceptions. This is done by providing ``raise_exception=False`` to the work ``__init__`` method. + +.. code-block:: python + + import lightning as L + + MyCustomWork(raise_exception=False) # <== HERE: The exception is captured. + + # Default behavior + MyCustomWork(raise_exception=True) # <== HERE: The exception is raised within the flow and terminates the app + + +And you can customize this behavior by overriding the ``on_exception`` hook to the Work. + +.. code-block:: python + + import lightning as L + + class MyCustomWork(L.LightningWork): + + def on_exception(self, exception: Exception): + # do something when an exception is triggered. + +---- + +************************** +Exception handling example +************************** + +This is the pseudo-code for the application described above. + +.. code-block:: python + + import lightning as L + + class RootFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.user_jobs = L.structures.Dict() + self.requested_jobs = [] + + def run(self): + for request in self.requested_jobs: + job_id = request["id"] + if job_id not in self.user_jobs: + # Note: The `GithubRepoLauncher` doesn't exist yet. + self.user_jobs[job_id] = GithubRepoLauncher( + **request, + raise_exception=False, # <== HERE: The exception is captured. + ) + self.user_jobs[job_id].run() + + if self.user_jobs[job_id].status.stage == "failed" and "printed" not in request: + print(self.user_jobs[job_id].status) # <== HERE: Print the user exception. + request["printed"] = True diff --git a/docs/source-app/core_api/lightning_work/lightning_work.rst b/docs/source-app/core_api/lightning_work/lightning_work.rst index 8a1d3593d6bdb..fdf9ea2809d68 100644 --- a/docs/source-app/core_api/lightning_work/lightning_work.rst +++ b/docs/source-app/core_api/lightning_work/lightning_work.rst @@ -6,7 +6,5 @@ LightningWork ############# -The :class:`~lightning_app.core.work.LightningWork` component is a building block optimized for long-running jobs or integrating third-party services. LightningWork can be used for training large models, downloading a dataset, or any long-lasting operation. - .. autoclass:: lightning_app.core.work.LightningWork - :noindex: + :exclude-members: _aggregate_status_timeout, _is_state_attribute, _is_state_attribute, set_state diff --git a/docs/source-app/core_api/lightning_work/payload.rst b/docs/source-app/core_api/lightning_work/payload.rst index 5d6ab1f20de82..6c51efef980ae 100644 --- a/docs/source-app/core_api/lightning_work/payload.rst +++ b/docs/source-app/core_api/lightning_work/payload.rst @@ -1,87 +1,15 @@ :orphan: -############################# -Sharing Objects between Works -############################# +###################################### +Sharing Objects between LightningWorks +###################################### -**Audience:** Users who want to know how to transfer python objects between their works. +**Audience:** Users who want to know how to transfer Python objects between their LightningWorks. **Level:** Advanced -**Prerequisite**: Know about the pandas library and read the :ref:`access_app_state` guide. +**Prerequisite**: Level 16+ and know about the Pandas library and read the `Access app state guide <../../access_app_state.html>`_. ---- -************************************ -When do I need to transfer objects ? -************************************ - -Imagine your application is processing some data using `pandas DaFrame `_ and you want to pass those data to another work. This is when and what the **Payload API** is meant for. - - -************************************* -How can I use the Lightning Payload ? -************************************* - -The Payload enables non JSON-serializable attribute objects to be part of your work state and be communicated to other works. - -Here is an example how to use it: - -.. code-block:: python - - import lightning_app as la - import pandas as pd - - - class SourceWork(lapp.LightningWork): - def __init__(self): - super().__init__() - self.df = None - - def run(self): - # do some processing - - df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) - - # The object you care about needs to be wrapped into a Payload object. - self.df = lapp.storage.Payload(df) - - # You can access the original object from the payload using its value property. - print("src", self.df.value) - # src col1 col2 - # 0 1 3 - # 1 2 4 - -Once the Payload object is attached to your work state, it can be passed to another work via the flow as follows: - -.. code-block:: python - - import lightning_app as la - import pandas as pd - - - class DestinationWork(lapp.LightningWork): - def run(self, df: lapp.storage.Payload): - # You can access the original object from the payload using its value property. - print("dst", df.value) - # dst col1 col2 - # 0 1 3 - # 1 2 4 - - - class Flow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.src = SourceWork() - self.dst = DestinationWork() - - def run(self): - self.src.run() - # The pandas DataFrame created by the ``SourceWork`` - # is passed to the ``DestinationWork``. - # Internally, Lightning pickles and un-pickle the python object, - # so you receive a copy of the original object. - self.dst.run(df=self.src.df) - - - app = lapp.LightningApp(Flow()) +.. include:: payload_content.rst diff --git a/docs/source-app/core_api/lightning_work/payload_content.rst b/docs/source-app/core_api/lightning_work/payload_content.rst new file mode 100644 index 0000000000000..15adcd856f3ee --- /dev/null +++ b/docs/source-app/core_api/lightning_work/payload_content.rst @@ -0,0 +1,73 @@ + +************************************** +What transferring objects does for you +************************************** + +Imagine your application is processing some data using `pandas DaFrame `_ and you want to pass that data to another LightningWork (Work). This is what the **Payload API** is meant for. + +---- + +************************* +Use the Lightning Payload +************************* + +The Payload enables non JSON-serializable attribute objects to be part of your Work's state and to be communicated to other Works. + +Here is an example: + +.. code-block:: python + + import lightning as L + import pandas as pd + + class SourceWork(L.LightningWork): + def __init__(self): + super().__init__() + self.df = None + + def run(self): + # do some processing + + df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]}) + + # The object you care about needs to be wrapped into a Payload object. + self.df = L.storage.Payload(df) + + # You can access the original object from the payload using its value property. + print("src", self.df.value) + # src col1 col2 + # 0 1 3 + # 1 2 4 + +Once the Payload object is attached to your Work's state, it can be passed to another work using the LightningFlow (Flow) as follows: + +.. code-block:: python + + import lightning as L + import pandas as pd + + class DestinationWork(L.LightningWork): + + def run(self, df:L.storage.Payload): + # You can access the original object from the payload using its value property. + print("dst", df.value) + # dst col1 col2 + # 0 1 3 + # 1 2 4 + + class Flow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.src = SourceWork() + self.dst = DestinationWork() + + def run(self): + self.src.run() + # The pandas DataFrame created by the ``SourceWork`` + # is passed to the ``DestinationWork``. + # Internally, Lightning pickles and un-pickle the python object, + # so you receive a copy of the original object. + self.dst.run(df=self.src.df) + + app = L.LightningApp(Flow()) diff --git a/docs/source-app/core_api/lightning_work/status.rst b/docs/source-app/core_api/lightning_work/status.rst index ff7322b6c944c..af3a27ac4047e 100644 --- a/docs/source-app/core_api/lightning_work/status.rst +++ b/docs/source-app/core_api/lightning_work/status.rst @@ -1,9 +1,8 @@ :orphan: - -##################### -Lightning Work Status -##################### +#################### +LightningWork Status +#################### **Audience:** Users who want to understand ``LightningWork`` under the hood. @@ -11,199 +10,4 @@ Lightning Work Status ---- -******************* -What are statuses ? -******************* - -Statuses indicates transition points in the life of a Lightning Work and contain metadata. - -The different stages are: - -.. code-block:: python - - class WorkStageStatus: - NOT_STARTED = "not_started" - STOPPED = "stopped" - PENDING = "pending" - RUNNING = "running" - SUCCEEDED = "succeeded" - FAILED = "failed" - -And a single status is as follows: - -.. code-block:: python - - @dataclass - class WorkStatus: - stage: WorkStageStatus - timestamp: float - reason: Optional[str] = None - message: Optional[str] = None - count: int = 1 - - -On creation, the work's status flags all evaluate to ``False`` (in particular ``has_started``) and when calling ``work.run`` in your flow, -the work transition from ``is_pending`` to ``is_running`` and then to ``has_succeeded`` if everything when well or ``has_failed`` otherwise. - -.. code-block:: python - - from time import sleep - import lightning_app as la - - - class Work(lapp.LightningWork): - def run(self, value: int): - sleep(1) - if value == 0: - return - raise Exception(f"The provided value was {value}") - - - class Flow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.work = Work(raise_exception=False) - self.counter = 0 - - def run(self): - if not self.work.has_started: - print("NOT STARTED") - - elif self.work.is_pending: - print("PENDING") - - elif self.work.is_running: - print("RUNNING") - - elif self.work.has_succeeded: - print("SUCCESS") - - elif self.work.has_failed: - print("FAILED") - - elif self.work.has_stopped: - print("STOPPED") - self._exit() - - print(self.work.status) - self.work.run(self.counter) - self.counter += 1 - - - app = lapp.LightningApp(Flow()) - -Run this app as follows: - -.. code-block:: bash - - lightning run app test.py > app_log.txt - -And here is the expected output inside ``app_log.txt`` and as expected, -we are observing the following transition ``has_started``, ``is_pending``, ``is_running``, ``has_succeeded``, ``is_running`` and ``has_failed`` - -.. code-block:: console - - NOT STARTED - WorkStatus(stage='not_started', timestamp=1653498225.18468, reason=None, message=None, count=1) - PENDING - WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) - PENDING - WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) - PENDING - ... - PENDING - WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) - PENDING - WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) - RUNNING - WorkStatus(stage='running', timestamp=1653498228.825194, reason=None, message=None, count=1) - ... - SUCCESS - WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) - SUCCESS - WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) - SUCCESS - WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) - RUNNING - WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) - RUNNING - ... - WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) - RUNNING - WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) - FAILED - WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) - FAILED - WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) - FAILED - WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) - FAILED - WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) - ... - -In order to access all statuses, simply do: - -.. code-block:: python - - from time import sleep - import lightning_app as la - - - class Work(lapp.LightningWork): - def run(self, value: int): - sleep(1) - if value == 0: - return - raise Exception(f"The provided value was {value}") - - - class Flow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.work = Work(raise_exception=False) - self.counter = 0 - - def run(self): - print(self.statuses) - self.work.run(self.counter) - self.counter += 1 - - - app = lapp.LightningApp(Flow()) - - -Run this app as follows: - -.. code-block:: bash - - lightning run app test.py > app_log.txt - -And here is the expected output inside ``app_log.txt``: - - -.. code-block:: console - - # First execution with value = 0 - - [] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] - ... - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] - ... - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] - - # Second execution with value = 1 - - [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] - ... - [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] - [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='failed', timestamp=1653498628.210164, reason='user_exception', message='The provided value was 1', count=1)] - [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='failed', timestamp=1653498628.210164, reason='user_exception', message='The provided value was 1', count=1)] +.. include:: status_content.rst diff --git a/docs/source-app/core_api/lightning_work/status_content.rst b/docs/source-app/core_api/lightning_work/status_content.rst new file mode 100644 index 0000000000000..f593421346b2f --- /dev/null +++ b/docs/source-app/core_api/lightning_work/status_content.rst @@ -0,0 +1,195 @@ + +************************************* +Everything about LightningWork Status +************************************* + +Statuses indicate transition points in the life of a LightningWork (Work) and contain metadata. + +The different stages are: + +.. code-block:: python + + class WorkStageStatus: + NOT_STARTED = "not_started" + STOPPED = "stopped" + PENDING = "pending" + RUNNING = "running" + SUCCEEDED = "succeeded" + FAILED = "failed" + +And a single status is as follows: + +.. code-block:: python + + @dataclass + class WorkStatus: + stage: WorkStageStatus + timestamp: float + reason: Optional[str] = None + message: Optional[str] = None + count: int = 1 + + +On creation, the Work's status flags all evaluate to ``False`` (in particular ``has_started``) and when calling ``work.run`` in your Lightning Flow (Flow), +the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_succeeded`` if everything went well or ``has_failed`` otherwise. + +.. code-block:: python + + from time import sleep + import lightning as L + + class Work(L.LightningWork): + + def run(self, value: int): + sleep(1) + if value == 0: + return + raise Exception(f"The provided value was {value}") + + class Flow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.work = Work(raise_exception=False) + self.counter = 0 + + def run(self): + if not self.work.has_started: + print("NOT STARTED") + + elif self.work.is_pending: + print("PENDING") + + elif self.work.is_running: + print("RUNNING") + + elif self.work.has_succeeded: + print("SUCCESS") + + elif self.work.has_failed: + print("FAILED") + + elif self.work.has_stopped: + print("STOPPED") + self._exit() + + print(self.work.status) + self.work.run(self.counter) + self.counter += 1 + + app = L.LightningApp(Flow()) + +Run this app as follows: + +.. code-block:: bash + + lightning run app test.py > app_log.txt + +And here is the expected output inside ``app_log.txt`` and as expected, +we are observing the following transition ``has_started``, ``is_pending``, ``is_running``, ``has_succeeded``, ``is_running`` and ``has_failed`` + +.. code-block:: console + + NOT STARTED + WorkStatus(stage='not_started', timestamp=1653498225.18468, reason=None, message=None, count=1) + PENDING + WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) + PENDING + WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) + PENDING + ... + PENDING + WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) + PENDING + WorkStatus(stage='pending', timestamp=1653498225.217413, reason=None, message=None, count=1) + RUNNING + WorkStatus(stage='running', timestamp=1653498228.825194, reason=None, message=None, count=1) + ... + SUCCESS + WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) + SUCCESS + WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) + SUCCESS + WorkStatus(stage='succeeded', timestamp=1653498229.831793, reason=None, message=None, count=1) + RUNNING + WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) + RUNNING + ... + WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) + RUNNING + WorkStatus(stage='running', timestamp=1653498229.846451, reason=None, message=None, count=1) + FAILED + WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) + FAILED + WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) + FAILED + WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) + FAILED + WorkStatus(stage='failed', timestamp=1653498230.852565, reason='user_exception', message='The provided value was 1', count=1) + ... + +In order to access all statuses: + +.. code-block:: python + + from time import sleep + import lightning as L + + class Work(L.LightningWork): + + def run(self, value: int): + sleep(1) + if value == 0: + return + raise Exception(f"The provided value was {value}") + + class Flow(L.LightningFlow): + + def __init__(self): + super().__init__() + self.work = Work(raise_exception=False) + self.counter = 0 + + def run(self): + print(self.statuses) + self.work.run(self.counter) + self.counter += 1 + + app = L.LightningApp(Flow()) + + +Run this app as follows: + +.. code-block:: bash + + lightning run app test.py > app_log.txt + +And here is the expected output inside ``app_log.txt``: + + +.. code-block:: console + + # First execution with value = 0 + + [] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] + ... + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] + ... + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498622.252016, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498626.185683, reason=None, message=None, count=1), WorkStatus(stage='succeeded', timestamp=1653498627.191053, reason=None, message=None, count=1)] + + # Second execution with value = 1 + + [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] + ... + [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1)] + [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='failed', timestamp=1653498628.210164, reason='user_exception', message='The provided value was 1', count=1)] + [WorkStatus(stage='pending', timestamp=1653498627.204636, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='running', timestamp=1653498627.205509, reason=None, message=None, count=1), WorkStatus(stage='failed', timestamp=1653498628.210164, reason='user_exception', message='The provided value was 1', count=1)] diff --git a/docs/source-app/examples/dag/dag.rst b/docs/source-app/examples/dag/dag.rst index f9b304b736b3e..e3cc249ab334f 100644 --- a/docs/source-app/examples/dag/dag.rst +++ b/docs/source-app/examples/dag/dag.rst @@ -10,10 +10,10 @@ Below is a pseudo-code using the lightning framework that uses a LightningFlow t .. code-block:: python - import lightning_app as la + import lightning as L + class DAGFlow(L.LightningFlow): - class DAGFlow(lapp.LightningFlow): def __init__(self): super().__init__() self.processor = DataProcessorWork(...) @@ -29,21 +29,19 @@ Below is a pseudo-code to run several works in parallel using a built-in :class: .. code-block:: python - import lightning_app as la + import lightning as L + class DAGFlow(L.LightningFlow): - class DAGFlow(lapp.LightningFlow): def __init__(self): super().__init__() ... - self.train_works = lapp.structures.Dict( - **{ - "1": TrainingWork(..., parallel=True), - "2": TrainingWork(..., parallel=True), - "3": TrainingWork(..., parallel=True), - # ... - } - ) + self.train_works = L.structures.Dict(**{ + "1": TrainingWork(..., parallel=True), + "2": TrainingWork(..., parallel=True), + "3": TrainingWork(..., parallel=True), + ... + }) ... def run(self): @@ -59,6 +57,7 @@ Below is a pseudo-code to run several works in parallel using a built-in :class: self.serve_work.run(...) +---- ********** Next steps diff --git a/docs/source-app/examples/dag/dag_from_scratch.rst b/docs/source-app/examples/dag/dag_from_scratch.rst index 7c68c8ea87f33..cde46953328bd 100644 --- a/docs/source-app/examples/dag/dag_from_scratch.rst +++ b/docs/source-app/examples/dag/dag_from_scratch.rst @@ -15,7 +15,9 @@ In this example, you will learn how to create a simple DAG which: and learn how to schedule this entire process. -Find the complete example `here `_. +Find the complete example `here `_. + +---- ************************** Step 1: Implement your DAG @@ -33,19 +35,20 @@ First, let's define the component we need: * Processing is responsible to execute a ``processing.py`` script. * A collection of model work to train all models in parallel. -.. literalinclude:: ../../../../examples/dag/app.py +.. literalinclude:: ../../../examples/app_dag/app.py :lines: 55-79 And its run method executes the steps described above. Additionally, ``work.stop`` is used to reduce cost when running in the cloud. -.. literalinclude:: ../../../../examples/dag/app.py +.. literalinclude:: ../../../examples/app_dag/app.py :lines: 81-108 +---- ***************************** Step 2: Define the scheduling ***************************** -.. literalinclude:: ../../../../examples/dag/app.py +.. literalinclude:: ../../../examples/app_dag/app.py :lines: 109-137 diff --git a/docs/source-app/examples/data_explore_app.rst b/docs/source-app/examples/data_explore_app.rst index f66b3c2cb1236..cd7011a10e93c 100644 --- a/docs/source-app/examples/data_explore_app.rst +++ b/docs/source-app/examples/data_explore_app.rst @@ -1,3 +1,5 @@ +:orphan: + ########################## Build a Data Exploring App ########################## diff --git a/docs/source-app/examples/etl_app.rst b/docs/source-app/examples/etl_app.rst index 4cb581e399ce1..5b494e943e445 100644 --- a/docs/source-app/examples/etl_app.rst +++ b/docs/source-app/examples/etl_app.rst @@ -1,3 +1,5 @@ +:orphan: + ############### Build a ETL App ############### diff --git a/docs/source-app/examples/file_server/app.py b/docs/source-app/examples/file_server/app.py new file mode 100644 index 0000000000000..20308814ed7e9 --- /dev/null +++ b/docs/source-app/examples/file_server/app.py @@ -0,0 +1,236 @@ +import json +import os +import tarfile +import uuid +import zipfile +from dataclasses import dataclass +from typing import List +import lightning as L +from lightning.app.storage import Drive +from pathlib import Path + + +class FileServer(L.LightningWork): + def __init__( + self, + drive: Drive, + base_dir: str = "file_server", + chunk_size=10240, + **kwargs + ): + """This component uploads, downloads files to your application. + + Arguments: + drive: The drive can share data inside your application. + base_dir: The local directory where the data will be stored. + chunk_size: The quantity of bytes to download/upload at once. + """ + super().__init__( + cloud_build_config=L.BuildConfig(["flask, flask-cors"]), + parallel=True, + **kwargs, + ) + # 1: Attach the arguments to the state. + self.drive = drive + self.base_dir = base_dir + self.chunk_size = chunk_size + + # 2: Create a folder to store the data. + os.makedirs(self.base_dir, exist_ok=True) + + # 3: Keep a reference to the uploaded filenames. + self.uploaded_files = dict() + + def get_filepath(self, path: str) -> str: + """Returns file path stored on the file server.""" + return os.path.join(self.base_dir, path) + + def get_random_filename(self) -> str: + """Returns a random hash for the file name.""" + return uuid.uuid4().hex + + def upload_file(self, file): + """Upload a file while tracking its progress.""" + # 1: Track metadata about the file + filename = file.filename + uploaded_file = self.get_random_filename() + meta_file = uploaded_file + ".meta" + self.uploaded_files[filename] = { + "progress": (0, None), "done": False} + + # 2: Create a stream and write bytes of + # the file to the disk under `uploaded_file` path. + with open(self.get_filepath(uploaded_file), "wb") as out_file: + content = file.read(self.chunk_size) + while content: + # 2.1 Write the file bytes + size = out_file.write(content) + + # 2.2 Update the progress metadata + self.uploaded_files[filename]["progress"] = ( + self.uploaded_files[filename]["progress"][0] + size, + None, + ) + # 4: Read next chunk of data + content = file.read(self.chunk_size) + + # 3: Update metadata that the file has been uploaded. + full_size = self.uploaded_files[filename]["progress"][0] + self.drive.put(self.get_filepath(uploaded_file)) + self.uploaded_files[filename] = { + "progress": (full_size, full_size), + "done": True, + "uploaded_file": uploaded_file, + } + + # 4: Write down the metadata about the file to the disk + meta = { + "original_path": filename, + "display_name": os.path.splitext(filename)[0], + "size": full_size, + "drive_path": uploaded_file, + } + with open(self.get_filepath(meta_file), "wt") as f: + json.dump(meta, f) + + # 5: Put the file to the drive. + # It means other components can access get or list them. + self.drive.put(self.get_filepath(meta_file)) + return meta + + def list_files(self, file_path: str): + # 1: Get the local file path of the file server. + file_path = self.get_filepath(file_path) + + # 2: If the file exists in the drive, transfer it locally. + if not os.path.exists(file_path): + self.drive.get(file_path) + + if os.path.isdir(file_path): + result = set() + for _, _, f in os.walk(file_path): + for file in f: + if not file.endswith(".meta"): + for filename, meta in self.uploaded_files.items(): + if meta["uploaded_file"] == file: + result.add(filename) + return {"asset_names": [v for v in result]} + + # 3: If the filepath is a tar or zip file, list their contents + if zipfile.is_zipfile(file_path): + with zipfile.ZipFile(file_path, "r") as zf: + result = zf.namelist() + elif tarfile.is_tarfile(file_path): + with tarfile.TarFile(file_path, "r") as tf: + result = tf.getnames() + else: + raise ValueError("Cannot open archive file!") + + # 4: Returns the matching files. + return {"asset_names": result} + + def run(self): + # 1: Imports flask requirements. + from flask import Flask, request + from flask_cors import CORS + + # 2: Create a flask app + flask_app = Flask(__name__) + CORS(flask_app) + + # 3: Define the upload file endpoint + @flask_app.post("/upload_file/") + def upload_file(): + """Upload a file directly as form data.""" + f = request.files["file"] + return self.upload_file(f) + + @flask_app.get("/") + def list_files(): + return self.list_files(str(Path(self.base_dir).resolve())) + + # 5: Start the flask app while providing the `host` and `port`. + flask_app.run(host=self.host, port=self.port, load_dotenv=False) + + def alive(self): + """Hack: Returns whether the server is alive.""" + return self.url != "" + + +from lightning import LightningWork +import requests + +class TestFileServer(LightningWork): + + def __init__(self, drive: Drive): + super().__init__(cache_calls=True) + self.drive = drive + + def run(self, file_server_url: str, first = True): + if first: + with open("test.txt", "w") as f: + f.write("Some text.") + + response = requests.post( + file_server_url + "/upload_file/", + files={'file': open("test.txt", 'rb')} + ) + assert response.status_code == 200 + else: + response = requests.get(file_server_url) + assert response.status_code == 200 + assert response.json() == {"asset_names": ["test.txt"]} + + +from lightning import LightningApp, LightningFlow + +class Flow(LightningFlow): + + def __init__(self): + super().__init__() + # 1: Create a drive to share data between works + self.drive = Drive("lit://file_server") + # 2: Create the filer server + self.file_server = FileServer(self.drive) + # 3: Create the file ser + self.test_file_server = TestFileServer(self.drive) + + def run(self): + # 1: Start the file server. + self.file_server.run() + + # 2: Trigger the test file server work when ready. + if self.file_server.alive(): + # 3 Execute the test file server work. + self.test_file_server.run(self.file_server.url) + self.test_file_server.run(self.file_server.url, first=False) + + # 4 When both execution are successful, exit the app. + if self.test_file_server.num_successes == 2: + self._exit() + + def configure_layout(self): + # Expose the file_server component + # in the UI using its `/` endpoint. + return {"name": "File Server", "content": self.file_server} + +from lightning.app.runners import MultiProcessRuntime + +def test_file_server(): + app = LightningApp(Flow()) + MultiProcessRuntime(app).dispatch() + +from lightning.app.testing.testing import run_app_in_cloud + +def test_file_server_in_cloud(): + # You need to provide the directory containing the app file. + app_dir = "docs/source-app/examples/file_server" + with run_app_in_cloud(app_dir) as (admin_page, view_page, get_logs_fn): + """# 1. `admin_page` and `view_page` are playwright Page Objects. + + # Check out https://playwright.dev/python/ doc to learn more. + # You can click the UI and trigger actions. + + # 2. By calling logs = get_logs_fn(), + # you get all the logs currently on the admin page. + """ diff --git a/docs/source-app/examples/file_server/file_server.rst b/docs/source-app/examples/file_server/file_server.rst new file mode 100644 index 0000000000000..cd7297596ca43 --- /dev/null +++ b/docs/source-app/examples/file_server/file_server.rst @@ -0,0 +1,9 @@ +################### +Build a File Server +################### + +**Prerequisite**: Reach :ref:`level 16+ ` and read the `Drive article `_. + +---- + +.. include:: file_server_content.rst diff --git a/docs/source-app/examples/file_server/file_server_content.rst b/docs/source-app/examples/file_server/file_server_content.rst new file mode 100644 index 0000000000000..26603e04f817d --- /dev/null +++ b/docs/source-app/examples/file_server/file_server_content.rst @@ -0,0 +1,82 @@ +********* +Objective +********* + +Create a simple application where users can upload files and list the uploaded files. + +---- + +***************** +Final Application +***************** + +Here is a recording of the final application built in this example tested with pytest. + +.. raw:: html + + + +---- + +************* +System Design +************* + +In order to create such application, we need to build two components and an application: + +* A **File Server Component** that gives you the ability to download or list files shared with your application. This is particularly useful when you want to trigger an ML job but your users need to provide their own data or if the user wants to download the trained checkpoints. + +* A **Test File Server** Component to interact with the file server. + +* An application putting everything together and its associated pytest tests. + +---- + +******** +Tutorial +******** + +Let's dive in on how to create such application and component: + +.. raw:: html + +

+
+ +.. displayitem:: + :header: 1. Implement the File Server general structure + :description: Put together the shape of the component + :col_css: col-md-4 + :button_link: file_server_step_1.html + :height: 180 + :tag: Basic + +.. displayitem:: + :header: 2. Implement the File Server upload and list files methods + :description: Add the core functionalities to the component + :col_css: col-md-4 + :button_link: file_server_step_2.html + :height: 180 + :tag: Basic + +.. displayitem:: + :header: 3. Implement a File Server Testing Component + :description: Create a component to test the file server + :col_css: col-md-4 + :button_link: file_server_step_3.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 4. Implement tests for the File Server component with pytest + :description: Create an app to validate the upload and list files endpoints + :col_css: col-md-4 + :button_link: file_server_step_4.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/file_server/file_server_step_1.rst b/docs/source-app/examples/file_server/file_server_step_1.rst new file mode 100644 index 0000000000000..782e553e9fdd5 --- /dev/null +++ b/docs/source-app/examples/file_server/file_server_step_1.rst @@ -0,0 +1,11 @@ +:orphan: + +********************************************* +1. Implement the FileServer general structure +********************************************* + +Let's dive in on how to create such a component with the code below. + +.. literalinclude:: ./app.py + :lines: 1-44, 132-158 + :emphasize-lines: 16, 51- diff --git a/docs/source-app/examples/file_server/file_server_step_2.rst b/docs/source-app/examples/file_server/file_server_step_2.rst new file mode 100644 index 0000000000000..668b01b1771d1 --- /dev/null +++ b/docs/source-app/examples/file_server/file_server_step_2.rst @@ -0,0 +1,37 @@ +:orphan: + +********************************************************** +2. Implement the File Server upload and list_files methods +********************************************************** + +Let's dive in on how to implement such methods. + +*************************** +Implement the upload method +*************************** + +In this method, we are creating a stream between the uploaded file and the uploaded file stored on the file server disk. + +Once the file is uploaded, we are putting the file into the :class:`~lightning_app.storage.drive.Drive`, so it becomes persistent and accessible to all components. + +.. literalinclude:: ./app.py + :lines: 13, 52-100 + :emphasize-lines: 49 + +******************************* +Implement the fist_files method +******************************* + +First, in this method, we get the file in the file server filesystem, if available in the Drive. Once done, we list the the files under the provided paths and return the results. + +.. literalinclude:: ./app.py + :lines: 13, 101-131 + :emphasize-lines: 9 + + +******************* +Implement utilities +******************* + +.. literalinclude:: ./app.py + :lines: 13, 46-51 diff --git a/docs/source-app/examples/file_server/file_server_step_3.rst b/docs/source-app/examples/file_server/file_server_step_3.rst new file mode 100644 index 0000000000000..97b524a978ea3 --- /dev/null +++ b/docs/source-app/examples/file_server/file_server_step_3.rst @@ -0,0 +1,16 @@ +:orphan: + +******************************************** +3. Implement a File Server Testing Component +******************************************** + +Let's dive in on how to implement a testing component for a server. + +This component needs to test two things: + +* The **/upload_file/** endpoint by creating a file and sending its content to it. + +* The **/** endpoint listing files, by validating the that previously uploaded file is present in the response. + +.. literalinclude:: ./app.py + :lines: 161-183 diff --git a/docs/source-app/examples/file_server/file_server_step_4.rst b/docs/source-app/examples/file_server/file_server_step_4.rst new file mode 100644 index 0000000000000..06d9e051dc0cb --- /dev/null +++ b/docs/source-app/examples/file_server/file_server_step_4.rst @@ -0,0 +1,86 @@ +:orphan: + +************************************************************ +4. Implement tests for the File Server component with pytest +************************************************************ + +Let's create a simple Lightning App (App) with our **File Server** and the **File Server Test** components. + +Once the File Server is up and running, we'll execute the **test_file_server** LightningWork and when both calls are successful, we exit the App using ``self._exit``. + +.. literalinclude:: ./app.py + :lines: 186-216 + + +Simply create a ``test.py`` file with the following code and run ``pytest tests.py`` + +.. literalinclude:: ./app.py + :lines: 218-222 + +To test the App in the cloud, create a ``cloud_test.py`` file with the following code and run ``pytest cloud_test.py``. Under the hood, we are using the end-to-end testing `playwright `_ library so you can interact with the UI. + +.. literalinclude:: ./app.py + :lines: 224- + +---- + +******************** +Test the application +******************** + +Clone the lightning repo and run the following command: + +.. code-block:: bash + + pytest docs/source-app/examples/file_server/app.py --capture=no -v + +---- + +****************** +Find more examples +****************** + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Build a DAG + :description: Create a dag pipeline + :col_css: col-md-4 + :button_link: ../dag/dag.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a Github Repo Script Runner + :description: Run any script on github in the cloud + :col_css: col-md-4 + :button_link: ../github_repo_runner/github_repo_runner.html + :height: 150 + :tag: Intermediate + + +.. displayitem:: + :header: Build a HPO Sweeper + :description: Train multiple models with different parameters + :col_css: col-md-4 + :button_link: ../hpo/hpo.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a Model Server + :description: Serve multiple models with different parameters + :col_css: col-md-4 + :button_link: ../model_server/model_server.html + :height: 150 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/.lightning b/docs/source-app/examples/github_repo_runner/.lightning new file mode 100644 index 0000000000000..4bae28430e9a3 --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/.lightning @@ -0,0 +1 @@ +name: github_repo_runner diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py new file mode 100644 index 0000000000000..57aee800de59b --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -0,0 +1,299 @@ +import io +import os +import subprocess +import sys +from copy import deepcopy +from functools import partial +from subprocess import Popen +from typing import Dict, List, Optional + +from lightning import BuildConfig, CloudCompute, LightningApp, LightningFlow +from lightning.app import structures +from lightning.app.components.python import TracerPythonScript +from lightning.app.frontend import StreamlitFrontend +from lightning.app.storage.path import Path +from lightning.app.utilities.state import AppState + + +class GithubRepoRunner(TracerPythonScript): + def __init__( + self, + id: str, + github_repo: str, + script_path: str, + script_args: List[str], + requirements: List[str], + cloud_compute: Optional[CloudCompute] = None, + **kwargs, + ): + """The GithubRepoRunner Component clones a repo, runs a specific script with provided arguments and collect + logs. + + Arguments: + id: Identified of the component. + github_repo: The Github Repo URL to clone. + script_path: The path to the script to execute. + script_args: The arguments to be provided to the script. + requirements: The python requirements tp run the script. + cloud_compute: The object to select the cloud instance. + """ + super().__init__( + script_path=script_path, + script_args=script_args, + cloud_compute=cloud_compute, + cloud_build_config=BuildConfig(requirements=requirements), + ) + self.id = id + self.github_repo = github_repo + self.kwargs = kwargs + self.logs = [] + + def run(self, *args, **kwargs): + # 1. Hack: Patch stdout so we can capture the logs. + string_io = io.StringIO() + sys.stdout = string_io + + # 2: Use git command line to clone the repo. + repo_name = self.github_repo.split("/")[-1].replace(".git", "") + cwd = os.path.dirname(__file__) + subprocess.Popen( + f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() + + # 3: Execute the parent run method of the TracerPythonScript class. + os.chdir(os.path.join(cwd, repo_name)) + super().run(*args, **kwargs) + + # 4: Get all the collected logs and add them to the state. + # This isn't optimal as heavy, but works for this demo purpose. + self.logs = string_io.getvalue() + string_io.close() + + def configure_layout(self): + return {"name": self.id, "content": self} + + +class PyTorchLightningGithubRepoRunner(GithubRepoRunner): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.best_model_path = None + self.best_model_score = None + + def configure_tracer(self): + from pytorch_lightning import Trainer + from pytorch_lightning.callbacks import Callback + + tracer = super().configure_tracer() + + class TensorboardServerLauncher(Callback): + def __init__(self, work): + # The provided `work` is the + # current ``PyTorchLightningScript`` work. + self.w = work + + def on_train_start(self, trainer, *_): + # Add `host` and `port` for tensorboard to work in the cloud. + cmd = f"tensorboard --logdir='{trainer.logger.log_dir}'" + server_args = f"--host {self.w.host} --port {self.w.port}" + Popen(cmd + " " + server_args, shell=True) + + def trainer_pre_fn(self, *args, work=None, **kwargs): + # Intercept Trainer __init__ call + # and inject a ``TensorboardServerLauncher`` component. + kwargs["callbacks"].append(TensorboardServerLauncher(work)) + return {}, args, kwargs + + # 5. Patch the `__init__` method of the Trainer + # to inject our callback with a reference to the work. + tracer.add_traced( + Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) + return tracer + + def on_after_run(self, end_script_globals): + import torch + # 1. Once the script has finished to execute, + # we can collect its globals and access any objects. + trainer = end_script_globals["cli"].trainer + checkpoint_callback = trainer.checkpoint_callback + lightning_module = trainer.lightning_module + + # 2. From the checkpoint_callback, + # we are accessing the best model weights + checkpoint = torch.load(checkpoint_callback.best_model_path) + + # 3. Load the best weights and torchscript the model. + lightning_module.load_state_dict(checkpoint["state_dict"]) + lightning_module.to_torchscript(f"{self.name}.pt") + + # 4. Use lightning.app.storage.Pathto create a reference to the + # torch scripted model. In the cloud with multiple machines, + # by simply passing this reference to another work, + # it triggers automatically a file transfer. + self.best_model_path = Path(f"{self.name}.pt") + + # 5. Keep track of the metrics. + self.best_model_score = float(checkpoint_callback.best_model_score) + + +class KerasGithubRepoRunner(GithubRepoRunner): + """Left to the users to implement.""" + +class TensorflowGithubRepoRunner(GithubRepoRunner): + """Left to the users to implement.""" + +GITHUB_REPO_RUNNERS = { + "PyTorch Lightning": PyTorchLightningGithubRepoRunner, + "Keras": KerasGithubRepoRunner, + "Tensorflow": TensorflowGithubRepoRunner, +} + + +class Flow(LightningFlow): + def __init__(self): + super().__init__() + # 1: Keep track of the requests within the state + self.requests = [] + # 2: Create a dictionary of components. + self.ws = structures.Dict() + + def run(self): + # Iterate continuously over all requests + for request_id, request in enumerate(self.requests): + self._handle_request(request_id, deepcopy(request)) + + def _handle_request(self, request_id: int, request: Dict): + # 1: Create a name and find selected framework + name = f"w_{request_id}" + ml_framework = request["train"].pop("ml_framework") + + # 2: If the component hasn't been created yet, create it. + if name not in self.ws: + work_cls = GITHUB_REPO_RUNNERS[ml_framework] + work = work_cls(id=request["id"], **request["train"]) + self.ws[name] = work + + # 3: Run the component + self.ws[name].run() + + # 4: Once the component has finished, + # add metadata to the original request for the UI. + if self.ws[name].best_model_path: + request = self.requests[request_id] + request["best_model_score"] = self.ws[name].best_model_score + request["best_model_path"] = self.ws[name].best_model_path + + def configure_layout(self): + # Create a StreamLit UI for the user to run his Github Repo. + return StreamlitFrontend(render_fn=render_fn) + +def page_1__create_new_run(state): + import streamlit as st + + st.markdown("# Create a new Run 🎈") + + # 1: Collect arguments from the users + id = st.text_input("Name your run", value="my_first_run") + github_repo = st.text_input( + "Enter a Github Repo URL", value="https://github.com/Lightning-AI/lightning-quick-start.git" + ) + + default_script_args = "--trainer.max_epochs=5 --trainer.limit_train_batches=4 --trainer.limit_val_batches=4 --trainer.callbacks=ModelCheckpoint --trainer.callbacks.monitor=val_acc" + default_requirements = "torchvision, pytorch_lightning, jsonargparse[signatures]" + + script_path = st.text_input("Enter your script to run", value="train_script.py") + script_args = st.text_input("Enter your base script arguments", value=default_script_args) + requirements = st.text_input("Enter your requirements", value=default_requirements) + ml_framework = st.radio( + "Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"] + ) + + if ml_framework not in ("PyTorch Lightning"): + st.write(f"{ml_framework} isn't supported yet.") + return + + clicked = st.button("Submit") + + # 2: If clicked, create a new request. + if clicked: + new_request = { + "id": id, + "train": { + "github_repo": github_repo, + "script_path": script_path, + "script_args": script_args.split(" "), + "requirements": requirements.split(" "), + "ml_framework": ml_framework, + }, + } + # 3: IMPORTANT: Add a new request to the state in-place. + # The flow receives the UI request and dynamically create + # and run the associated work from the request information. + state.requests = state.requests + [new_request] + +def page_2__view_run_lists(state): + import streamlit as st + + st.markdown("# Run Lists 🎈") + # 1: Iterate through all the requests in the state. + for i, r in enumerate(state.requests): + i = str(i) + # 2: Display information such as request, logs, work state, model score. + work = state._state["structures"]["ws"]["works"][f"w_{i}"] + with st.expander(f"Expand to view Run {i}", expanded=False): + if st.checkbox(f"Expand to view your configuration", key=i): + st.json(r) + if st.checkbox(f"Expand to view logs", key=i): + st.code(body=work["vars"]["logs"]) + if st.checkbox(f"Expand to view your work state", key=i): + work["vars"].pop("logs") + st.json(work) + best_model_score = r.get("best_model_score", None) + if best_model_score: + if st.checkbox(f"Expand to view your run performance", key=i): + st.json( + {"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")} + ) + +def page_3__view_app_state(state): + import streamlit as st + + st.markdown("# App State 🎈") + st.write(state._state) + +def render_fn(state: AppState): + import streamlit as st + + + page_names_to_funcs = { + "Create a new Run": partial(page_1__create_new_run, state=state), + "View your Runs": partial(page_2__view_run_lists, state=state), + "View the App state": partial(page_3__view_app_state, state=state), + } + selected_page = st.sidebar.selectbox( + "Select a page", page_names_to_funcs.keys()) + page_names_to_funcs[selected_page]() + + +class RootFlow(LightningFlow): + def __init__(self): + super().__init__() + # Create the flow + self.flow = Flow() + + def run(self): + # Run the flow + self.flow.run() + + def configure_layout(self): + # 1: Add the main StreamLit UI + selection_tab = [{ + "name": "Run your Github Repo", + "content": self.flow, + }] + # 2: Add a new tab whenever a new work is dynamically created + run_tabs = [e.configure_layout() for e in self.flow.ws.values()] + # 3: Returns the list of tabs. + return selection_tab + run_tabs + + +app = LightningApp(RootFlow()) diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner.rst new file mode 100644 index 0000000000000..e8775a91a3d59 --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner.rst @@ -0,0 +1,12 @@ +################################# +Build a Github Repo Script Runner +################################# + + +**Audience:** Users that want to create interactive applications which runs Github Repo in the cloud at any scale for multiple users. + +**Prerequisite**: Reach :ref:`level 16+ ` and read the docstring of of :class:`~lightning_app.components.python.tracer.TracerPythonScript` component. + +---- + +.. include:: github_repo_runner_content.rst diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst new file mode 100644 index 0000000000000..9492d2af82a5b --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst @@ -0,0 +1,96 @@ + +********* +Objective +********* + +Create a simple application where users can enter information in a UI to run a given PyTorch Lightning Script from a given Github Repo with optionally some extra python requirements and arguments. + +Futhermore, the users should be able to monitor their training progress in real-time, view the logs and get the best monitored metric and associated checkpoint for their models. + +---- + +***************** +Final Application +***************** + +Here is a recording of the final application built in this example. The example is around 200 lines in total and should give you a great fundation to build your own Lightning App. + +.. raw:: html + + + +---- + +************* +System Design +************* + +In order to create such application, we need to build several components: + +* A GithubRepoRunner Component that clones a repo, runs a specific script with provided arguments and collect logs. + +* A PyTorch Lightning GithubRepoRunner Component that augments the GithubRepoRunner component to track PyTorch Lightning Trainer. + +* A UI for the users to provide to trigger dynamically a new execution. + +* A Flow to dynamically create GithubRepoRunner once a user submits information from the UI. + +Let's dive in on how to create such a component. + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: 1. Implement the GithubRepoRunner Component + :description: Clone and execute script from a GitHub Repo. + :col_css: col-md-4 + :button_link: github_repo_runner_step_1.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: 2. Implement the PyTorch Lightning GithubRepoRunner Component + :description: Automate PyTorch Lightning execution + :col_css: col-md-4 + :button_link: github_repo_runner_step_2.html + :height: 180 + :tag: Advanced + +.. displayitem:: + :header: 3. Implement the Flow to manage user requests + :description: Dynamically create GithubRepoRunner + :col_css: col-md-4 + :button_link: github_repo_runner_step_3.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 4. Implement the UI with StreamLit + :description: Several pages application + :col_css: col-md-4 + :button_link: github_repo_runner_step_4.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 5. Putting everything together + :description: + :col_css: col-md-4 + :button_link: github_repo_runner_step_5.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst new file mode 100644 index 0000000000000..3a683501fa3da --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst @@ -0,0 +1,62 @@ +:orphan: + +******************************************* +1. Implement the GithubRepoRunner Component +******************************************* + +The GithubRepoRunner Component clones a repo, runs a specific script with provided arguments and collect logs. + +Let's dive in on how to create such a component with the code below. + +.. literalinclude:: ./app.py + :lines: -72 + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: 2. Implement the PyTorch Lightning GithubRepoRunner Component + :description: Automate PyTorch Lightning execution + :col_css: col-md-4 + :button_link: github_repo_runner_step_2.html + :height: 180 + :tag: Advanced + +.. displayitem:: + :header: 3. Implement the Flow to manage user requests + :description: Dynamically create GithubRepoRunner + :col_css: col-md-4 + :button_link: github_repo_runner_step_3.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 4. Implement the UI with StreamLit + :description: Several pages application + :col_css: col-md-4 + :button_link: github_repo_runner_step_4.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 5. Putting everything together + :description: + :col_css: col-md-4 + :button_link: github_repo_runner_step_5.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst new file mode 100644 index 0000000000000..08858649df18e --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst @@ -0,0 +1,68 @@ +:orphan: + +************************************************************* +2. Implement the PyTorch Lightning GithubRepoRunner Component +************************************************************* + +The PyTorch Lightning GithubRepoRunner Component subclasses the GithubRepoRunner but tailor the execution experience to PyTorch Lightning. + +As a matter of fact, this component adds two primary tailored features for PyTorch Lightning users: + +* It injects dynamically a custom callback ``TensorboardServerLauncher`` in the PyTorch Lightning Trainer to start a tensorboard server so it can be exposed in Lightning App UI. + +* Once the script has runned, the ``on_after_run`` hook of the :class:`~lightning_app.components.python.tracer.TracerPythonScript` is invoked with the script globals, meaning we can collect anything we need. In particular, we are reloading the best model, torch scripting it and storing its path in the state along side the best metric score. + +Let's dive in on how to create such a component with the code below. + +.. literalinclude:: ./app.py + :lines: 75-136 + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: 1. Implement the GithubRepoRunner Component + :description: Clone and execute script from a GitHub Repo. + :col_css: col-md-4 + :button_link: github_repo_runner_step_1.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: 3. Implement the Flow to manage user requests + :description: Dynamically create GithubRepoRunner + :col_css: col-md-4 + :button_link: github_repo_runner_step_3.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 4. Implement the UI with StreamLit + :description: Several pages application + :col_css: col-md-4 + :button_link: github_repo_runner_step_4.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 5. Putting everything together + :description: + :col_css: col-md-4 + :button_link: github_repo_runner_step_5.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst new file mode 100644 index 0000000000000..fc1b3116beb8d --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst @@ -0,0 +1,62 @@ +:orphan: + +********************************************* +3. Implement the Flow to manage user requests +********************************************* + +In step 1 and 2, we have implemented ``GithubRepoRunner`` and ``PyTorchLightningGithubRepoRunner`` components. + +Now, we are going to create a component to dynamically handle user requests. +Let's dive in on how to create such a component with the code below. + +.. literalinclude:: ./app.py + :lines: 138-187 + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: 1. Implement the GithubRepoRunner Component + :description: Clone and execute script from a GitHub Repo. + :col_css: col-md-4 + :button_link: github_repo_runner_step_1.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: 2. Implement the PyTorch Lightning GithubRepoRunner Component + :description: Automate PyTorch Lightning execution + :col_css: col-md-4 + :button_link: github_repo_runner_step_2.html + :height: 180 + :tag: Advanced + +.. displayitem:: + :header: 4. Implement the UI with StreamLit + :description: Several pages application + :col_css: col-md-4 + :button_link: github_repo_runner_step_4.html + :height: 180 + :tag: Intermediate + + +.. displayitem:: + :header: 5. Putting everything together + :description: + :col_css: col-md-4 + :button_link: github_repo_runner_step_5.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst new file mode 100644 index 0000000000000..4c7a190b7bbc3 --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst @@ -0,0 +1,93 @@ +:orphan: + +********************************** +4. Implement the UI with StreamLit +********************************** + +In step 3, we have implemented a flow which dynamically create a work when a new request is added to the requests list. + +From the UI, we create 3 pages with `StreamLit `_: + +* **Page 1**: Create a form with add a new request to the flow state **requests**. + +* **Page 2**: Iterate through all the requests and display associated information. + +* **Page 3**: Display the entire App State. + +**************** +Render All Pages +**************** + +.. literalinclude:: ./app.py + :lines: 263-274 + + +****** +Page 1 +****** + +.. literalinclude:: ./app.py + :lines: 189-231 + :emphasize-lines: 43 + +****** +Page 2 +****** + +.. literalinclude:: ./app.py + :lines: 233-255 + +****** +Page 3 +****** + +.. literalinclude:: ./app.py + :lines: 257-261 + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: 1. Implement the GithubRepoRunner Component + :description: Clone and execute script from a GitHub Repo. + :col_css: col-md-4 + :button_link: github_repo_runner_step_1.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: 2. Implement the PyTorch Lightning GithubRepoRunner Component + :description: Automate PyTorch Lightning execution + :col_css: col-md-4 + :button_link: github_repo_runner_step_2.html + :height: 180 + :tag: Advanced + +.. displayitem:: + :header: 3. Implement the Flow to manage user requests + :description: Dynamically create GithubRepoRunner + :col_css: col-md-4 + :button_link: github_repo_runner_step_3.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: 5. Putting everything together + :description: + :col_css: col-md-4 + :button_link: github_repo_runner_step_5.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst new file mode 100644 index 0000000000000..bdad9523323d9 --- /dev/null +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst @@ -0,0 +1,77 @@ +:orphan: + +****************************** +5. Putting everything together +****************************** + +Let's dive in on how to create such a component with the code below. + +.. literalinclude:: ./app.py + :lines: 277- + + +******************* +Run the application +******************* + +Clone the lightning repo and run the following command: + +.. code-block:: bash + + lightning run app docs/source-app/examples/github_repo_runner/app.py + +Add **--cloud** to run this application in the cloud. + +.. code-block:: bash + + lightning run app docs/source-app/examples/github_repo_runner/app.py --cloud + +---- + +****************** +Find more examples +****************** + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Build a DAG + :description: Create a dag pipeline + :col_css: col-md-4 + :button_link: ../dag/dag.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a File Server + :description: Train multiple models with different parameters + :col_css: col-md-4 + :button_link: ../file_server/file_server.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a HPO Sweeper + :description: Train multiple models with different parameters + :col_css: col-md-4 + :button_link: ../hpo/hpo.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a Model Server + :description: Serve multiple models with different parameters + :col_css: col-md-4 + :button_link: ../model_server/model_server.html + :height: 150 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/hpo/build_from_scratch.rst b/docs/source-app/examples/hpo/build_from_scratch.rst new file mode 100644 index 0000000000000..cade8b7f6edc1 --- /dev/null +++ b/docs/source-app/examples/hpo/build_from_scratch.rst @@ -0,0 +1,41 @@ +:orphan: + +####################################### +Implement an HPO component from scratch +####################################### + +**Audience:** Users who want to understand how to implement sweep training from scratch. + +**Prereqs:** Finish Intermediate Level. + +---- + +******** +Examples +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Step 1: Implement an HPO component with the Lightning Works. + :description: Learn how it works under the hood + :col_css: col-md-4 + :button_link: hpo_wo.html + :height: 180 + :tag: Intermediate + +.. displayitem:: + :header: Step 2: Add the flow to your HPO component + :description: Learn how it works under the hood + :col_css: col-md-4 + :button_link: hpo_wi.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/tutorials/hpo/hpo.py b/docs/source-app/examples/hpo/hpo.py similarity index 74% rename from docs/source-app/tutorials/hpo/hpo.py rename to docs/source-app/examples/hpo/hpo.py index 69bccf67acfbd..dc4a2d62a7a9c 100644 --- a/docs/source-app/tutorials/hpo/hpo.py +++ b/docs/source-app/examples/hpo/hpo.py @@ -1,5 +1,6 @@ import optuna from objective import ObjectiveWork +from optuna.distributions import CategoricalDistribution, LogUniformDistribution TOTAL_TRIALS = 6 SIMULTANEOUS_TRIALS = 2 @@ -8,14 +9,16 @@ STUDY = optuna.create_study() DISTRIBUTIONS = { - "backbone": optuna.distributions.CategoricalDistribution(["resnet18", "resnet34"]), - "learning_rate": optuna.distributions.LogUniformDistribution(0.0001, 0.1), + "backbone": CategoricalDistribution(["resnet18", "resnet34"]), + "learning_rate": LogUniformDistribution(0.0001, 0.1), } TRIALS = [ObjectiveWork() for _ in range(TOTAL_TRIALS)] -while not DONE: # Lightning Infinite Loop +# Lightning Infinite Loop +while not DONE: - if NUM_TRIALS > TOTAL_TRIALS: # Finish the Hyperparameter Optimization + # Finish the Hyperparameter Optimization + if NUM_TRIALS >= TOTAL_TRIALS: DONE = True continue @@ -28,8 +31,10 @@ # If a work has already started, it won't be started again. if not objective_work.has_started: - trial = STUDY.ask(DISTRIBUTIONS) # Sample a new trial from the distributions - objective_work.run(trial_id=trial._trial_id, **trial.params) # Run the work + # Sample a new trial from the distributions + trial = STUDY.ask(DISTRIBUTIONS) + # Run the work + objective_work.run(trial_id=trial._trial_id, **trial.params) # With Lighting, the `objective_work` will run asynchronously # and the metric will be prodcued after X amount of time. diff --git a/docs/source-app/examples/hpo/hpo.rst b/docs/source-app/examples/hpo/hpo.rst new file mode 100644 index 0000000000000..c2db676c0731f --- /dev/null +++ b/docs/source-app/examples/hpo/hpo.rst @@ -0,0 +1,79 @@ +.. hpo: + +####################################################### +Build a Lightning Hyperparameter Optimization (HPO) App +####################################################### + + +******************* +A bit of background +******************* + +Traditionally, developing machine learning (ML) products requires choosing among a large space of +hyperparameters while creating and training the ML models. Hyperparameter optimization +(HPO) aims to find a well-performing hyperparameter configuration for a given ML model +on a dataset at hand, including the ML model, +its hyperparameters, and other data processing steps. + +HPOs free the human expert from a tedious and error-prone, manual hyperparameter tuning process. + +As an example, in the famous `scikit-learn `_ library, +hyperparameters are passed as arguments to the constructor of +the estimator classes such as ``C`` kernel for +`Support Vector Classifier `_, etc. + +It is possible and recommended to search the hyperparameter space for the best validation score. + +An HPO search consists of: + +* an objective method +* a defined parameter space +* a method for searching or sampling candidates + +A naive method for sampling candidates is grid search, which exhaustively considers all +hyperparameter combinations from a user-specified grid. + +Fortunately, HPO is an active area of research, and many methods have been developed to +optimize the time required to get strong candidates. + +In the following tutorial, you will learn how to use Lightning together with `Optuna `_. + +`Optuna `_ is an open source HPO framework to automate hyperparameter search. +Out-of-the-box, it provides efficient algorithms to search large spaces and prune unpromising trials for faster results. + +First, you will learn about the best practices on how to implement HPO without the Lightning Framework. +Secondly, we will dive into a working HPO application with Lightning, and finally create a neat +`HiPlot UI `_ +for our application. + +---- + +******** +Examples +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Re-use an existing HPO component + :description: Learn how to use Lightning HPO with your app. + :col_css: col-md-4 + :button_link: lightning_hpo.html + :height: 180 + :tag: Basic + +.. displayitem:: + :header: Implement an HPO component from scratch + :description: Learn how it works under the hood + :col_css: col-md-4 + :button_link: build_from_scratch.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/hpo/hpo_wi.rst b/docs/source-app/examples/hpo/hpo_wi.rst new file mode 100644 index 0000000000000..17dd971e9e926 --- /dev/null +++ b/docs/source-app/examples/hpo/hpo_wi.rst @@ -0,0 +1,57 @@ +:orphan: + +########################################## +Step 2: Add the flow to your HPO component +########################################## + +**Audience:** Users who want to understand how to implement HPO training from scratch with Lightning. + +**Prereqs:** Level 17+ + +---- + +Thanks to the simplified version, you should have a good grasp on how to implement HPO with Optuna. + +As the :class:`~lightning_app.core.app.LightningApp` handles the Infinite Loop, +it has been removed from within the run method of the HPORootFlow. + +However, the ``run`` method code is the same as the one defined above. + +.. literalinclude:: ../../../examples/app_hpo/app_wo_ui.py + :language: python + +The ``ObjectiveWork`` is sub-classing +the built-in :class:`~lightning_app.components.python.TracerPythonScript` +which enables launching scripts and more. + +.. literalinclude:: ../../../examples/app_hpo/objective.py + :language: python + +Finally, let's add the ``HiPlotFlow`` component to visualize our hyperparameter optimization. + +The metric and sampled parameters are added to the ``self.hi_plot.data`` list, enabling +updates to the dashboard in near-realtime. + +.. literalinclude:: ../../../examples/app_hpo/app_wi_ui.py + :diff: ../../../examples/app_hpo/app_wo_ui.py + +Here is the associated code with the ``HiPlotFlow`` component. + +In the ``render_fn`` method, the state of the ``HiPlotFlow`` is passed. +The ``state.data`` is accessed as it contains the metric and sampled parameters. + +.. literalinclude:: ../../../examples/app_hpo/hyperplot.py + +Run the HPO application with the following command: + +.. code-block:: console + + $ lightning run app examples/app_hpo/app_wi_ui.py + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + {0: ..., 1: ..., ..., 5: ...} + +Here is what the UI looks like when launched: + +.. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/hpo_ui_2.gif + :width: 100 % + :alt: Alternative text diff --git a/docs/source-app/examples/hpo/hpo_wo.rst b/docs/source-app/examples/hpo/hpo_wo.rst new file mode 100644 index 0000000000000..6a13ff253dbc2 --- /dev/null +++ b/docs/source-app/examples/hpo/hpo_wo.rst @@ -0,0 +1,57 @@ +:orphan: + +########################################################### +Step 1: Implement an HPO component with the Lightning Works +########################################################### + +**Audience:** Users who want to understand how to implement HPO training from scratch. + +**Prereqs:** Level 17+ + +---- + +In the example below, we are emulating the Lightning Infinite Loop. + +We are assuming we have already defined an ``ObjectiveWork`` component which is responsible to run the objective method and track the metric through its state. + +.. literalinclude:: ./hpo.py + :language: python + +We are running ``TOTAL_TRIALS`` trials by series of ``SIMULTANEOUS_TRIALS`` trials. +When starting, ``TOTAL_TRIALS`` ``ObjectiveWork`` are created. + +The entire code runs within an infinite loop as it would within Lightning. + +When iterating through the Works, if the current ``objective_work`` hasn't started, +some new parameters are sampled from the Optuna Study with our custom distributions +and then passed to run method of the ``objective_work``. + +The condition ``not objective_work.has_started`` will be ``False`` once ``objective_work.run()`` starts. + +Also, the second condition ``objective_work.has_told_study`` will be ``True`` when the metric +is defined within the state of the Work and has been shared with the study. + +Finally, once the current ``SIMULTANEOUS_TRIALS`` have both registered their +metric to the Optuna Study, simply increment ``NUM_TRIALS`` by ``SIMULTANEOUS_TRIALS`` to launch the next trials. + +Below, you can find the simplified version of the ``ObjectiveWork`` where the metric is randomly sampled using NumPy. + +In a realistic use case, the Work executes some user-defined code. + +.. literalinclude:: ./objective.py + :language: python + +Here are the logs produced when running the application above: + +.. code-block:: console + + $ python docs/source-app/tutorials/hpo/hpo.py + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + # After you have clicked `run` on the UI. + [I 2022-03-01 12:32:50,050] A new study created in memory with name: ... + {0: 13.994859806481264, 1: 59.866743330127825, ..., 5: 94.65919769609225} + +The following animation shows how this application works in the cloud: + +.. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/hpo.gif + :alt: Animation showing how to HPO works UI in a distributed manner. diff --git a/docs/source-app/examples/hpo/lightning_hpo.rst b/docs/source-app/examples/hpo/lightning_hpo.rst new file mode 100644 index 0000000000000..b1e2f11d3987d --- /dev/null +++ b/docs/source-app/examples/hpo/lightning_hpo.rst @@ -0,0 +1,99 @@ +:orphan: + +################################ +Re-use an existing HPO component +################################ + +**Audience:** Users who want to easily get started with HPO training. + +**Prereqs:** Level 8+ + +---- + +********************* +Install Lightning HPO +********************* + +Lightning HPO provides a Pythonic implementation for Scalable Hyperparameter Tuning +and relies on Optuna for providing state-of-the-art sampling hyper-parameters algorithms and efficient trial pruning strategies. + +Find the `Lightning Sweeper App `_ on `lightning.ai `_ and its associated `Github repo `_. + +.. code-block:: bash + + lightning install app lightning/hpo + +********************* +Lightning HPO Example +********************* + +In this tutorial, we are going to convert `Optuna Efficient Optimization Algorithms `_ into a Lightning App. + +The Optuna example optimizes the value (example: learning-rate) of a ``SGDClassifier`` from ``sklearn`` trained over the `Iris Dataset `_. + +.. literalinclude:: ./optuna_reference.py + :language: python + + +As you can see, several trials were pruned (stopped) before they finished all of the iterations. + +.. code-block:: console + + A new study created in memory with name: no-name-4423c12c-22e1-4eaf-ba60-caf0020403c6 + Trial 0 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.00020629773477269024}. Best is trial 0 with value: 0.07894736842105265. + Trial 1 finished with value: 0.368421052631579 and parameters: {'alpha': 0.0005250149151047217}. Best is trial 0 with value: 0.07894736842105265. + Trial 2 finished with value: 0.052631578947368474 and parameters: {'alpha': 5.9086862655635784e-05}. Best is trial 2 with value: 0.052631578947368474. + Trial 3 finished with value: 0.3421052631578947 and parameters: {'alpha': 0.07177263583415294}. Best is trial 2 with value: 0.052631578947368474. + Trial 4 finished with value: 0.23684210526315785 and parameters: {'alpha': 1.7451874636151302e-05}. Best is trial 2 with value: 0.052631578947368474. + Trial 5 pruned. + Trial 6 finished with value: 0.10526315789473684 and parameters: {'alpha': 1.4943994864178649e-05}. Best is trial 2 with value: 0.052631578947368474. + Trial 7 pruned. + Trial 8 pruned. + Trial 9 pruned. + Trial 10 pruned. + Trial 11 pruned. + Trial 12 pruned. + Trial 13 pruned. + Trial 14 pruned. + Trial 15 pruned. + Trial 16 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.006166329613687364}. Best is trial 2 with value: 0.052631578947368474. + Trial 17 pruned. + Trial 18 pruned. + Trial 19 pruned. + +The example above has been re-organized in order to run as Lightning App. + +.. literalinclude:: ./lightning_hpo_target.py + :language: python + +Now, your code can run at scale in the cloud, if needed, and it has a simple neat UI. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/lightning_hpo_optimizer.png + :alt: Lightning App UI + :width: 100 % + +As you can see, several trials were pruned (stopped) before they finished all of the iterations. Same as when using pure optuna. + +.. code-block:: console + + A new study created in memory with name: no-name-a93d848e-a225-4df3-a9c3-5f86680e295d + Trial 0 finished with value: 0.23684210526315785 and parameters: {'alpha': 0.006779437004523296}. Best is trial 0 with value: 0.23684210526315785. + Trial 1 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.008936151407006062}. Best is trial 1 with value: 0.07894736842105265. + Trial 2 finished with value: 0.052631578947368474 and parameters: {'alpha': 0.0035836511240528008}. Best is trial 2 with value: 0.052631578947368474. + Trial 3 finished with value: 0.052631578947368474 and parameters: {'alpha': 0.0005393218926409795}. Best is trial 2 with value: 0.052631578947368474. + Trial 4 finished with value: 0.1578947368421053 and parameters: {'alpha': 6.572557493358585e-05}. Best is trial 2 with value: 0.052631578947368474. + Trial 5 finished with value: 0.02631578947368418 and parameters: {'alpha': 0.0013953760106345603}. Best is trial 5 with value: 0.02631578947368418. + Trail 6 pruned. + Trail 7 pruned. + Trail 8 pruned. + Trail 9 pruned. + Trial 10 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.00555435554783454}. Best is trial 5 with value: 0.02631578947368418. + Trail 11 pruned. + Trial 12 finished with value: 0.052631578947368474 and parameters: {'alpha': 0.025624276147153992}. Best is trial 5 with value: 0.02631578947368418. + Trial 13 finished with value: 0.07894736842105265 and parameters: {'alpha': 0.014613957457075546}. Best is trial 5 with value: 0.02631578947368418. + Trail 14 pruned. + Trail 15 pruned. + Trail 16 pruned. + Trial 17 finished with value: 0.052631578947368474 and parameters: {'alpha': 0.01028208215647372}. Best is trial 5 with value: 0.02631578947368418. + Trail 18 pruned. + Trail 19 pruned. diff --git a/docs/source-app/examples/hpo/lightning_hpo_target.py b/docs/source-app/examples/hpo/lightning_hpo_target.py new file mode 100644 index 0000000000000..779f992554412 --- /dev/null +++ b/docs/source-app/examples/hpo/lightning_hpo_target.py @@ -0,0 +1,53 @@ +import optuna +from lightning_hpo import BaseObjective, Optimizer +from optuna.distributions import LogUniformDistribution +from sklearn import datasets +from sklearn.linear_model import SGDClassifier +from sklearn.model_selection import train_test_split + +from lightning import LightningApp, LightningFlow + + +class Objective(BaseObjective): + def run(self, params): + # WARNING: Don't forget to assign `params` to self, + # so they get tracked in the state. + self.params = params + + iris = datasets.load_iris() + classes = list(set(iris.target)) + train_x, valid_x, train_y, valid_y = train_test_split(iris.data, iris.target, test_size=0.25, random_state=0) + + clf = SGDClassifier(alpha=params["alpha"]) + + for step in range(100): + clf.partial_fit(train_x, train_y, classes=classes) + intermediate_value = 1.0 - clf.score(valid_x, valid_y) + + # WARNING: Assign to reports, + # so the state is instantly sent to the flow. + self.reports = self.reports + [[intermediate_value, step]] + + self.best_model_score = 1.0 - clf.score(valid_x, valid_y) + + def distributions(self): + return {"alpha": LogUniformDistribution(1e-5, 1e-1)} + + +class RootFlow(LightningFlow): + def __init__(self): + super().__init__() + self.optimizer = Optimizer( + objective_cls=Objective, + n_trials=20, + study=optuna.create_study(pruner=optuna.pruners.MedianPruner()), + ) + + def run(self): + self.optimizer.run() + + def configure_layout(self): + return {"name": "HyperPlot", "content": self.optimizer.hi_plot} + + +app = LightningApp(RootFlow()) diff --git a/docs/source-app/tutorials/hpo/objective.py b/docs/source-app/examples/hpo/objective.py similarity index 55% rename from docs/source-app/tutorials/hpo/objective.py rename to docs/source-app/examples/hpo/objective.py index 7e83551a14ca8..d20232fb10ab2 100644 --- a/docs/source-app/tutorials/hpo/objective.py +++ b/docs/source-app/examples/hpo/objective.py @@ -1,12 +1,11 @@ import numpy as np -from lightning_app import LightningWork +import lightning as L -class ObjectiveWork(LightningWork): +class ObjectiveWork(L.LightningWork): def __init__(self): super().__init__(parallel=True) - self.has_started = False self.metric = None self.trial_id = None self.params = None @@ -14,8 +13,9 @@ def __init__(self): def run(self, trial_id, **params): self.trial_id = trial_id - self.params = params # Received suggested `backbone` and `learning_rate` - self.has_started = True - # Emulate metric computation would be computed once a script has been completed. + # Received suggested `backbone` and `learning_rate` + self.params = params + # Emulate metric computation would be + # computed once a script has been completed. # In reality, this would excute a user defined script. self.metric = np.random.uniform(0, 100) diff --git a/docs/source-app/examples/hpo/optuna_reference.py b/docs/source-app/examples/hpo/optuna_reference.py new file mode 100644 index 0000000000000..46f76c8662244 --- /dev/null +++ b/docs/source-app/examples/hpo/optuna_reference.py @@ -0,0 +1,36 @@ +import logging +import sys + +import optuna +from sklearn import datasets +from sklearn.linear_model import SGDClassifier +from sklearn.model_selection import train_test_split + + +def objective(trial): + iris = datasets.load_iris() + classes = list(set(iris.target)) + train_x, valid_x, train_y, valid_y = train_test_split(iris.data, iris.target, test_size=0.25, random_state=0) + + alpha = trial.suggest_float("alpha", 1e-5, 1e-1, log=True) + clf = SGDClassifier(alpha=alpha) + + for step in range(100): + clf.partial_fit(train_x, train_y, classes=classes) + + # Report intermediate objective value. + intermediate_value = 1.0 - clf.score(valid_x, valid_y) + trial.report(intermediate_value, step) + + # Handle pruning based on the intermediate value. + if trial.should_prune(): + raise optuna.TrialPruned() + + return 1.0 - clf.score(valid_x, valid_y) + + +# Add stream handler of stdout to show the messages +logger = optuna.logging.get_logger("optuna") +logger.addHandler(logging.StreamHandler(sys.stdout)) +study = optuna.create_study(pruner=optuna.pruners.MedianPruner()) +study.optimize(objective, n_trials=20) diff --git a/docs/source-app/examples/model_deploy_app.rst b/docs/source-app/examples/model_deploy_app.rst deleted file mode 100644 index f4f564d77300a..0000000000000 --- a/docs/source-app/examples/model_deploy_app.rst +++ /dev/null @@ -1,3 +0,0 @@ -############################ -Build a Model Deployment App -############################ diff --git a/docs/source-app/examples/model_server_app/app.py b/docs/source-app/examples/model_server_app/app.py new file mode 100644 index 0000000000000..9985014b11912 --- /dev/null +++ b/docs/source-app/examples/model_server_app/app.py @@ -0,0 +1,34 @@ +from locust_component import Locust +from model_server import MLServer +from train import TrainModel + +from lightning import LightningApp, LightningFlow + + +class TrainAndServe(LightningFlow): + def __init__(self): + super().__init__() + self.train_model = TrainModel() + self.model_server = MLServer( + name="mnist-svm", + implementation="mlserver_sklearn.SKLearnModel", + workers=8, + ) + self.performance_tester = Locust(num_users=100) + + def run(self): + self.train_model.run() + self.model_server.run(self.train_model.best_model_path) + if self.model_server.alive(): + # The performance tester needs the model server to be up + # and running to be started, so the URL is added in the UI. + self.performance_tester.run(self.model_server.url) + + def configure_layout(self): + return [ + {"name": "Server", "content": self.model_server.url + "/docs"}, + {"name": "Server Testing", "content": self.performance_tester}, + ] + + +app = LightningApp(TrainAndServe()) diff --git a/docs/source-app/examples/model_server_app/load_testing.rst b/docs/source-app/examples/model_server_app/load_testing.rst new file mode 100644 index 0000000000000..97345dec4cfcb --- /dev/null +++ b/docs/source-app/examples/model_server_app/load_testing.rst @@ -0,0 +1,57 @@ +:orphan: + +*********************************** +3. Build the Load Testing Component +*********************************** + +Now, we are going to create a component to test the performance of your model server. + +We are going to use a python performance testing tool called `Locust `_. + +.. literalinclude:: ./locust_component.py + + +Finally, once the component is done, we need to crate a ``locustfile.py`` file which defines the format of the request to send to your model server. + +The endpoint to hit has the following format: ``/v2/models/{MODEL_NAME}/versions/{VERSION}/infer``. + +.. literalinclude:: ./locustfile.py + + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: 1. Build a Train Component + :description: Train a model and store its checkpoints with SKlearn + :col_css: col-md-4 + :button_link: train.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 2. Build a Model Server Component + :description: Use MLServer to server your models + :col_css: col-md-4 + :button_link: model_server.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 4. Putting everything together. + :description: Ensemble the components together and run the app + :col_css: col-md-4 + :button_link: putting_everything_together.html + :height: 150 + :tag: basic + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/model_server_app/locust_component.py b/docs/source-app/examples/model_server_app/locust_component.py new file mode 100644 index 0000000000000..70b342facb18a --- /dev/null +++ b/docs/source-app/examples/model_server_app/locust_component.py @@ -0,0 +1,43 @@ +import os +import subprocess + +from lightning import LightningWork +from lightning.app.utilities.packaging.build_config import BuildConfig + + +class Locust(LightningWork): + def __init__(self, num_users: int = 100): + """This component checks the performance of a server. The server url is passed to its run method. + + Arguments: + num_users: Number of users emulated by Locust + """ + # Note: Using the default port 8089 of Locust. + super().__init__( + port=8089, + parallel=True, + cloud_build_config=BuildConfig(requirements=["locust"]), + ) + self.num_users = num_users + + def run(self, load_tested_url: str): + # 1: Create the locust command line. + cmd = " ".join( + [ + "locust", + "--master-host", + str(self.host), + "--master-port", + str(self.port), + "--host", + str(load_tested_url), + "-u", + str(self.num_users), + ] + ) + # 2: Create another process with locust + process = subprocess.Popen(cmd, cwd=os.path.dirname(__file__), shell=True) + + # 3: Wait for the process to finish. As locust is a server, + # this waits infinitely or if killed. + process.wait() diff --git a/docs/source-app/examples/model_server_app/locustfile.py b/docs/source-app/examples/model_server_app/locustfile.py new file mode 100644 index 0000000000000..198d6de6cb553 --- /dev/null +++ b/docs/source-app/examples/model_server_app/locustfile.py @@ -0,0 +1,41 @@ +from locust import FastHttpUser, task +from sklearn import datasets +from sklearn.model_selection import train_test_split + + +class HelloWorldUser(FastHttpUser): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._prepare_inference_request() + + @task + def predict(self): + self.client.post( + "/v2/models/mnist-svm/versions/v0.0.1/infer", + json=self.inference_request, + ) + + def _prepare_inference_request(self): + # The digits dataset + digits = datasets.load_digits() + + # To apply a classifier on this data, + # we need to flatten the image, to + # turn the data in a (samples, feature) matrix: + n_samples = len(digits.images) + data = digits.images.reshape((n_samples, -1)) + + # Split data into train and test subsets + _, X_test, _, _ = train_test_split(data, digits.target, test_size=0.5, shuffle=False) + + x_0 = X_test[0:1] + self.inference_request = { + "inputs": [ + { + "name": "predict", + "shape": x_0.shape, + "datatype": "FP32", + "data": x_0.tolist(), + } + ] + } diff --git a/docs/source-app/examples/model_server_app/model_server.py b/docs/source-app/examples/model_server_app/model_server.py new file mode 100644 index 0000000000000..fa170a42b56da --- /dev/null +++ b/docs/source-app/examples/model_server_app/model_server.py @@ -0,0 +1,90 @@ +import json +import subprocess + +from lightning import LightningWork +from lightning.app.storage import Path +from lightning.app.utilities.packaging.build_config import BuildConfig + +# ML_SERVER_URL = https://github.com/SeldonIO/MLServer + + +class MLServer(LightningWork): + + """This components uses SeldonIO MLServer library. + + The model endpoint: /v2/models/{MODEL_NAME}/versions/{VERSION}/infer. + + Arguments: + name: The name of the model for the endpoint. + implementation: The model loader class. + Example: "mlserver_sklearn.SKLearnModel". + Learn more here: $ML_SERVER_URL/tree/master/runtimes + workers: Number of server worker. + """ + + def __init__( + self, + name: str, + implementation: str, + workers: int = 1, + **kw, + ): + super().__init__( + parallel=True, + cloud_build_config=BuildConfig( + requirements=["mlserver", "mlserver-sklearn"], + ), + **kw, + ) + # 1: Collect the config's. + self.settings = { + "debug": True, + "parallel_workers": workers, + } + self.model_settings = { + "name": name, + "implementation": implementation, + } + # 2: Keep track of latest version + self.version = 1 + + def run(self, model_path: Path): + """The model is downloaded when the run method is invoked. + + Arguments: + model_path: The path to the trained model. + """ + # 1: Use the host and port at runtime so it works in the cloud. + # $ML_SERVER_URL/blob/master/mlserver/settings.py#L50 + if self.version == 1: + # TODO: Reload the next version model of the model. + + self.settings.update({"host": self.host, "http_port": self.port}) + + with open("settings.json", "w") as f: + json.dump(self.settings, f) + + # 2. Store the model-settings + # $ML_SERVER_URL/blob/master/mlserver/settings.py#L120 + self.model_settings["parameters"] = { + "version": f"v0.0.{self.version}", + "uri": str(model_path.absolute()), + } + with open("model-settings.json", "w") as f: + json.dump(self.model_settings, f) + + # 3. Launch the Model Server + subprocess.Popen("mlserver start .", shell=True) + + # 4. Increment the version for the next time run is called. + self.version += 1 + + else: + # TODO: Load the next model and unload the previous one. + pass + + def alive(self): + # Current hack, when the url is available, + # the server is up and running. + # This would be cleaned out and automated. + return self.url != "" diff --git a/docs/source-app/examples/model_server_app/model_server.rst b/docs/source-app/examples/model_server_app/model_server.rst new file mode 100644 index 0000000000000..b1daa00d427d4 --- /dev/null +++ b/docs/source-app/examples/model_server_app/model_server.rst @@ -0,0 +1,48 @@ +:orphan: + +*********************************** +2. Build the Model Server Component +*********************************** + +In the code below, we use `MLServer `_ which aims to provide an easy way to start serving your machine learning models through a REST and gRPC interface, +fully compliant with KFServing's V2 Dataplane spec. + +.. literalinclude:: ./model_server.py + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: 1. Build a Train Component + :description: Train a model and store its checkpoints with SKlearn + :col_css: col-md-4 + :button_link: train.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 3. Build a Load Testing Component + :description: Use Locust to test your model servers + :col_css: col-md-4 + :button_link: load_testing.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 4. Putting everything together. + :description: Ensemble the components together and run the app + :col_css: col-md-4 + :button_link: putting_everything_together.html + :height: 150 + :tag: basic + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/model_server_app/model_server_app.rst b/docs/source-app/examples/model_server_app/model_server_app.rst new file mode 100644 index 0000000000000..65545e4e393df --- /dev/null +++ b/docs/source-app/examples/model_server_app/model_server_app.rst @@ -0,0 +1,13 @@ +:orphan: + +#################### +Build a Model Server +#################### + +**Audience:** Users who want to serve their trained models. + +**Prerequisite**: Reach :ref:`level 16+ `. + +---- + +.. include:: model_server_app_content.rst diff --git a/docs/source-app/examples/model_server_app/model_server_app_content.rst b/docs/source-app/examples/model_server_app/model_server_app_content.rst new file mode 100644 index 0000000000000..0b8007aafe159 --- /dev/null +++ b/docs/source-app/examples/model_server_app/model_server_app_content.rst @@ -0,0 +1,84 @@ + +********* +Objective +********* + +Create a simple application that trains and serves a `Sklearn `_ machine learning model with `MLServer from SeldonIO `_ + +---- + +***************** +Final Application +***************** + +Here is a gif of the final application built in this example. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/ml_server_2.gif + +---- + +************* +System Design +************* + +In order to create such application, we need to build several components: + +* A Model Train Component that trains a model and provide its trained weights + +* A Model Server Component that serves to an API endpoint the model generated by the **Model Train Component**. + +* A Load Testing Component that test the model server works as expected. This could be used to CI/CD the performance of newly generated models (left to the users). + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/model_server_app_2.png + +Let's dive into the tutorial. + +---- + +******** +Tutorial +******** + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: 1. Build a Train Component + :description: Train a model and store its checkpoints with SKlearn + :col_css: col-md-4 + :button_link: train.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 2. Build a Model Server Component + :description: Use MLServer to server your models + :col_css: col-md-4 + :button_link: model_server.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 3. Build a Load Testing Component + :description: Use Locust to test your model servers + :col_css: col-md-4 + :button_link: load_testing.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 4. Putting everything together. + :description: Ensemble the components together and run the app + :col_css: col-md-4 + :button_link: putting_everything_together.html + :height: 150 + :tag: basic + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/model_server_app/putting_everything_together.rst b/docs/source-app/examples/model_server_app/putting_everything_together.rst new file mode 100644 index 0000000000000..c11a5289d37ac --- /dev/null +++ b/docs/source-app/examples/model_server_app/putting_everything_together.rst @@ -0,0 +1,80 @@ +:orphan: + +****************************** +4. Putting everything together +****************************** + +In the code below, we put together the **TrainWork**, the **MLServer** and the **Locust** components in an ``app.py`` file. + +.. literalinclude:: ./app.py + + +*********** +Run the App +*********** + +To run the app, simply open a terminal and execute this command: + +.. code-block:: bash + + lightning run app docs/source-app/examples/model_deploy_app/app.py + +Here is a gif of the UI. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/ml_server_2.gif + +.. raw:: html + +
+ +Congrats, you have finished the **Build a Model Server** example ! + +---- + +****************** +Find more examples +****************** + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Build a DAG + :description: Create a dag pipeline + :col_css: col-md-4 + :button_link: ../dag/dag.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a File Server + :description: Train multiple models with different parameters + :col_css: col-md-4 + :button_link: ../file_server/file_server.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a Github Repo Script Runner + :description: Run code from the internet in the cloud + :col_css: col-md-4 + :button_link: ../github_repo_runner/github_repo_runner.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Build a HPO Sweeper + :description: Train multiple models with different parameters + :col_css: col-md-4 + :button_link: ../hpo/hpo.html + :height: 150 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/model_server_app/train.py b/docs/source-app/examples/model_server_app/train.py new file mode 100644 index 0000000000000..f3342b9f242ff --- /dev/null +++ b/docs/source-app/examples/model_server_app/train.py @@ -0,0 +1,42 @@ +import joblib +from sklearn import datasets, svm +from sklearn.model_selection import train_test_split + +from lightning import LightningWork +from lightning.app.storage import Path + + +class TrainModel(LightningWork): + + """This component trains a Sklearn SVC model on digits dataset.""" + + def __init__(self): + super().__init__() + # 1: Add element to the state. + self.best_model_path = None + + def run(self): + # 2: Load the Digits + digits = datasets.load_digits() + + # 3: To apply a classifier on this data, + # we need to flatten the image, to + # turn the data in a (samples, feature) matrix: + n_samples = len(digits.images) + data = digits.images.reshape((n_samples, -1)) + + # 4: Create a classifier: a support vector classifier + classifier = svm.SVC(gamma=0.001) + + # 5: Split data into train and test subsets + X_train, _, y_train, _ = train_test_split(data, digits.target, test_size=0.5, shuffle=False) + + # 6: We learn the digits on the first half of the digits + classifier.fit(X_train, y_train) + + # 7: Save the Sklearn model with `joblib`. + model_file_name = "mnist-svm.joblib" + joblib.dump(classifier, model_file_name) + + # 8: Keep a reference the the generated model. + self.best_model_path = Path("mnist-svm.joblib") diff --git a/docs/source-app/examples/model_server_app/train.rst b/docs/source-app/examples/model_server_app/train.rst new file mode 100644 index 0000000000000..4e828872f4278 --- /dev/null +++ b/docs/source-app/examples/model_server_app/train.rst @@ -0,0 +1,49 @@ +:orphan: + +**************************** +1. Build the Train Component +**************************** + +In the code below, we create a work which trains a simple `SVC `_ model on the digits dataset (classification). + +Once the model is trained, it is saved and a reference :class:`~lightning_app.storage.path.Path` with ``best_model_path`` state attribute. + +.. literalinclude:: ./train.py + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: 2. Build a Model Server Component + :description: Use MLServer to server your models + :col_css: col-md-4 + :button_link: model_server.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 3. Build a Load Testing Component + :description: Use Locust to test your model servers + :col_css: col-md-4 + :button_link: load_testing.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: 4. Putting everything together. + :description: Ensemble the components together and run the app + :col_css: col-md-4 + :button_link: putting_everything_together.html + :height: 150 + :tag: basic + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/research_demo_app.rst b/docs/source-app/examples/research_demo_app.rst index cb74ebcf3d325..90276f9b95f97 100644 --- a/docs/source-app/examples/research_demo_app.rst +++ b/docs/source-app/examples/research_demo_app.rst @@ -1,3 +1,5 @@ +:orphan: + ######################### Build a Research Demo App ######################### diff --git a/docs/source-app/glossary/app_tree.rst b/docs/source-app/glossary/app_tree.rst index 85a0cee21e16e..120d5d2e4ef69 100644 --- a/docs/source-app/glossary/app_tree.rst +++ b/docs/source-app/glossary/app_tree.rst @@ -1,3 +1,5 @@ +.. _app_component_tree: + ################### App Component Tree ################### @@ -6,6 +8,8 @@ App Component Tree **Level:** Basic +---- + **************************************** What is an Application Component Tree? **************************************** @@ -25,6 +29,8 @@ Here's a basic application with four flows and two works (associated tree struct A Lightning app runs all flows into a single process. Its flows coordinate the execution of the works each running in their own independent processes. +---- + *********************************************** How do I define my application component tree? *********************************************** @@ -35,22 +41,25 @@ You can attach your components in the **__init__** method of a flow. .. code-block:: python - import lightning_app as la + import lightning as L + class RootFlow(L.LightningFlow): - class RootFlow(lapp.LightningFlow): def __init__(self): super().__init__() - self.work = Work() # The `Work` component is attached here. + # The `Work` component is attached here. + self.work = Work() - self.nested_flow = NestedFlow() # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. + self.nested_flow = NestedFlow() Once done, simply add the root flow to a Lightning app as follows: .. code-block:: python - app = lapp.LightningApp(RootFlow()) + app = L.LightningApp(RootFlow()) +---- ****************************************** Is my application component tree static? @@ -62,16 +71,21 @@ You can simply attach your components in the **run** method of a flow using the .. code-block:: python - class RootFlow(lapp.LightningFlow): + class RootFlow(L.LightningFlow): + def run(self): if not hasattr(self, "work"): - setattr(self, "work", Work()) # The `Work` component is attached here. - getattr(self, "work").run() # Run the `Work` component. + # The `Work` component is attached here. + setattr(self, "work", Work()) + # Run the `Work` component. + getattr(self, "work").run() if not hasattr(self, "nested_flow"): - setattr(self, "nested_flow", NestedFlow()) # The `NestedFlow` component is attached here. - getattr(self, "wonested_flowrk").run() # Run the `NestedFlow` component. + # The `NestedFlow` component is attached here. + setattr(self, "nested_flow", NestedFlow()) + # Run the `NestedFlow` component. + getattr(self, "wonested_flowrk").run() But it is usually more readable to use Lightning built-in :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` as follows: @@ -80,17 +94,19 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app from lightning_app.structures import Dict + class RootFlow(L.LightningFlow): - class RootFlow(lapp.LightningFlow): def __init__(self): super().__init__() self.dict = Dict() def run(self): if "work" not in self.dict: - self.dict["work"] = Work() # The `Work` component is attached here. + # The `Work` component is attached here. + self.dict["work"] = Work() self.dict["work"].run() if "nested_flow" not in self.dict: - self.dict["nested_flow"] = NestedFlow() # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. + self.dict["nested_flow"] =NestedFlow() self.dict["nested_flow"].run() diff --git a/docs/source-app/glossary/build_config/build_config_advanced.rst b/docs/source-app/glossary/build_config/build_config_advanced.rst index c96ac93d079bc..f954bd3435c0f 100644 --- a/docs/source-app/glossary/build_config/build_config_advanced.rst +++ b/docs/source-app/glossary/build_config/build_config_advanced.rst @@ -23,7 +23,6 @@ Create a :class:`~lightning_app.utilities.packaging.build_config.BuildConfig` an from lightning_app import LightningWork, BuildConfig - class MyWork(LightningWork): def __init__(self): super().__init__() diff --git a/docs/source-app/glossary/build_config/build_config_basic.rst b/docs/source-app/glossary/build_config/build_config_basic.rst index 7fa87b5e14214..31b274e086db9 100644 --- a/docs/source-app/glossary/build_config/build_config_basic.rst +++ b/docs/source-app/glossary/build_config/build_config_basic.rst @@ -20,14 +20,14 @@ for more granular control. .. code-block:: bash ├── app.py - ├── requirements.txt # Global requirements for the entire app + ├── requirements.txt # Global requirements for the entire app └── works ├── serve - │ ├── requirements.txt # Requirements specific to the 'serve' work - │ └── serve.py # Source file for the LightningWork + │ ├── requirements.txt # Requirements specific to the 'serve' work + │ └── serve.py # Source file for the LightningWork └── train - ├── requirements.txt # Requirements specific to the 'train' work - └── train.py # Source file for the LightningWork + ├── requirements.txt # Requirements specific to the 'train' work + └── train.py # Source file for the LightningWork The requirements.txt file must be located in the same directry as the source file of the LightningWork. When the LightningWork starts up, it will pick up the requirements file if present and install all listed packages. @@ -46,14 +46,16 @@ Instead of listing the requirements in a file, you can also pass them to the Lig :class:`~lightning_app.utilities.packaging.build_config.BuildConfig`: .. code-block:: python + :emphasize-lines: 7 from lightning_app import LightningWork, BuildConfig - class MyWork(LightningWork): def __init__(self): super().__init__() - self.cloud_build_config = BuildConfig(requirements=["torch>=1.8", "torchmetrics"]) + self.cloud_build_config = BuildConfig( + requirements=["torch>=1.8", "torchmetrics"] + ) .. note:: The build config only applies when running in the cloud and gets ignored otherwise. A local build config is currently not supported. diff --git a/docs/source-app/glossary/build_config/build_config_intermediate.rst b/docs/source-app/glossary/build_config/build_config_intermediate.rst index 174f472facb8e..0a5839b1f3ea8 100644 --- a/docs/source-app/glossary/build_config/build_config_intermediate.rst +++ b/docs/source-app/glossary/build_config/build_config_intermediate.rst @@ -18,9 +18,9 @@ If you need to install additional system packages or run other configuration ste from lightning_app import BuildConfig - @dataclass class CustomBuildConfig(BuildConfig): + def build_commands(self): return ["sudo apt-get install libsparsehash-dev"] @@ -31,7 +31,6 @@ If you need to install additional system packages or run other configuration ste from lightning_app import LightningWork - class MyWork(LightningWork): def __init__(self): super().__init__() @@ -40,7 +39,9 @@ If you need to install additional system packages or run other configuration ste self.cloud_build_config = CustomBuildConfig() # Can also be combined with extra requirements - self.cloud_build_config = CustomBuildConfig(requirements=["torchmetrics"]) + self.cloud_build_config = CustomBuildConfig( + requirements=["torchmetrics"] + ) .. note:: diff --git a/docs/source-app/glossary/dag.rst b/docs/source-app/glossary/dag.rst index 6d843ffbd5b29..ef85d33cf338a 100644 --- a/docs/source-app/glossary/dag.rst +++ b/docs/source-app/glossary/dag.rst @@ -1,18 +1,8 @@ ###################### -Directed acyclic Graph +Directed Acyclic Graph ###################### **Audience:** Users coming from MLOps to Lightning Apps, looking for more flexibility. -.. warning:: documentation under development - ----- - -*************************************** -What is a Directed acyclic Graph (DAG)? -*************************************** - -.. note:: documentation under development - ---- ***************************** @@ -29,4 +19,28 @@ Can I Build a DAG with Lightning? ********************************* Yes! -DAGs are the (easiest) use-cases to build with Lightning Apps. For example, here's a full app that defines a DAG: +DAGs are one of the easiest Lightning Apps to build. For example, here's a `full app that defines a DAG <../examples/dag/dag.html>`_. + +---- + +******** +Examples +******** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Build a DAG + :description: Learn how to create a DAG with Lightning + :col_css: col-md-4 + :button_link: ../examples/dag/dag.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/glossary/debug_app.rst b/docs/source-app/glossary/debug_app.rst index 05c10914040dc..2d5c0d19903b0 100644 --- a/docs/source-app/glossary/debug_app.rst +++ b/docs/source-app/glossary/debug_app.rst @@ -1 +1,3 @@ +:orphan: + .. include:: ../workflows/debug_locally.rst diff --git a/docs/source-app/glossary/distributed_fe.rst b/docs/source-app/glossary/distributed_fe.rst index c0483158dfcb5..36d64b01436b6 100644 --- a/docs/source-app/glossary/distributed_fe.rst +++ b/docs/source-app/glossary/distributed_fe.rst @@ -1,3 +1,5 @@ +:orphan: + ##################### Distributed Front-End ##################### diff --git a/docs/source-app/glossary/distributed_hardware.rst b/docs/source-app/glossary/distributed_hardware.rst index 2dd51b3af2008..0a64f5f5c0720 100644 --- a/docs/source-app/glossary/distributed_hardware.rst +++ b/docs/source-app/glossary/distributed_hardware.rst @@ -1,3 +1,5 @@ +:orphan: + #################### Distributed Hardware #################### diff --git a/docs/source-app/glossary/environment_variables.rst b/docs/source-app/glossary/environment_variables.rst index fd41594656b0f..20ab1b09b6a0a 100644 --- a/docs/source-app/glossary/environment_variables.rst +++ b/docs/source-app/glossary/environment_variables.rst @@ -19,9 +19,8 @@ The environment variables are available in all flows and works, and can be acces .. code:: python import os - - print(os.environ["FOO"]) # BAR - print(os.environ["BAZ"]) # FAZ + print(os.environ["FOO"]) # BAR + print(os.environ["BAZ"]) # FAZ .. note:: Environment variables are currently not encrypted. diff --git a/docs/source-app/glossary/event_loop.rst b/docs/source-app/glossary/event_loop.rst index eb09e4f34c504..d0f97f50e708a 100644 --- a/docs/source-app/glossary/event_loop.rst +++ b/docs/source-app/glossary/event_loop.rst @@ -1,11 +1,11 @@ -.. _event_loop: - ########## Event loop ########## -Drawing inspiration from modern web frameworks like `React.js `_, the Lightning app runs all flows in an **event loop** (forever), which is triggered several times a second after collecting any works' state change. +.. _app_event_loop: + +Drawing inspiration from modern web frameworks like `React.js `_, the Lightning App runs all flows in an **event loop** (forever), which is triggered several times a second after collecting any works' state change. .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/lightning_loop.gif -When running an app in the cloud, the ``LightningWork`` run on different machines. LightningWork communicates any state changes to the **event loop** which re-executes the flow with the newly-collected works' state. +When running a Lightning App in the cloud, the ``LightningWork`` run on different machines. LightningWork communicates any state changes to the **event loop** which re-executes the flow with the newly-collected works' state. diff --git a/docs/source-app/glossary/fault_tolerance.rst b/docs/source-app/glossary/fault_tolerance.rst index 8f43962eb1744..b0ee6dfd21102 100644 --- a/docs/source-app/glossary/fault_tolerance.rst +++ b/docs/source-app/glossary/fault_tolerance.rst @@ -1,3 +1,5 @@ +:orphan: + ############### Fault tolerance ############### diff --git a/docs/source-app/glossary/lightning_app_overview/index.rst b/docs/source-app/glossary/lightning_app_overview/index.rst index 2dae01d9f0405..09de273affc50 100644 --- a/docs/source-app/glossary/lightning_app_overview/index.rst +++ b/docs/source-app/glossary/lightning_app_overview/index.rst @@ -3,6 +3,7 @@ ########################### Lightning Apps Key concepts ########################### + **Audience:** Users who want to know how the 🤯 magic works under the hood. ---- diff --git a/docs/source-app/glossary/scheduling.rst b/docs/source-app/glossary/scheduling.rst index 38f37a152bee2..5f9bfdea7a431 100644 --- a/docs/source-app/glossary/scheduling.rst +++ b/docs/source-app/glossary/scheduling.rst @@ -22,22 +22,19 @@ The LightningFlow has a ``schedule`` method which can be used to schedule your c class MyFlow(LightningFlow): + def run(self): if self.schedule("hourly"): # run some code once every hour. - pass if self.schedule("daily"): # run some code once day. - pass if self.schedule("daily") and anything_else: # run some code once day if the anything else is also True. - pass if self.schedule("2 4 * * mon,fri"): # defined with cron syntax, run some code at 04:02 on every Monday and Friday. - pass Learn more about the cron syntax `here `_ @@ -56,7 +53,6 @@ In the example above, the line ``self.schedule("hourly")`` will return ``True`` from lightning_app import LightningFlow from lightning_app.core.structures import List - class ScheduledDAG(LightningFlow): def __init__(self): super().__init__() @@ -65,10 +61,12 @@ In the example above, the line ``self.schedule("hourly")`` will return ``True`` def run(self): if self.schedule("hourly"): # dynamically instantiate - # don't forget to always attach your components to the flow !!! + # don't forget to always attach + # your components to the flow !!! self.list.append(MyDAGFlow(...)) - # run all dags, but the completed ones are cached and don't re-execute. + # run all dags, but the completed ones + # are cached and don't re-execute. for dag in self.list: dag.run() @@ -80,7 +78,6 @@ In the example above, the line ``self.schedule("hourly")`` will return ``True`` from lightning_app import LightningFlow from time import time - class ScheduledDAG(LightningFlow): def __init__(self): super().__init__() @@ -99,7 +96,6 @@ In the example above, the line ``self.schedule("hourly")`` will return ``True`` from lightning_app import LightningFlow from time import time - class ScheduledDAG(LightningFlow): def __init__(self): super().__init__() @@ -112,13 +108,18 @@ In the example above, the line ``self.schedule("hourly")`` will return ``True`` if self.schedule("hourly"): self.should_execute = True - if self.should_execute: # Runs in 10 min - self.data_processor.run(trigger_time=time()) # Runs in 5 min + # Runs in 10 min + if self.should_execute: + # Runs in 5 min + self.data_processor.run(trigger_time=time()) if self.data_processor.has_succeeded: - self.training_work.run(self.data_processor.data) # Runs in 5 min + # Runs in 5 min + self.training_work.run(self.data_processor.data) if self.training_work.has_succeeded: self.should_execute = False +---- + *********** Limitations *********** @@ -133,7 +134,6 @@ Here is an example of something which **WON'T** work: from lightning_app import LightningFlow from time import time - class ScheduledDAG(LightningFlow): def __init__(self): super().__init__() @@ -143,9 +143,11 @@ Here is an example of something which **WON'T** work: def run(self): ... if self.schedule("hourly"): - self.data_processor.run(trigger_time=time()) # This executes and finishes 5 min later + # This finishes 5 min later + self.data_processor.run(trigger_time=time()) if self.data_processor.has_succeeded: - # This will never be reached as the data processor will keep processing forever... + # This will never be reached as the + # data processor will keep processing forever... self.training_work.run(self.data_processor.data) ---- @@ -154,10 +156,30 @@ Here is an example of something which **WON'T** work: Frequently Asked Questions ************************** -- **Q: Can I use multiple nested schedule?** +- **Q: Can I use multiple nested scheduler?** No, as they might cancel themselves out, but you can capture the event of one to trigger the next one. + +- **Q: Can I use any arbitrary logic to schedule?** Yes, this design enables absolute flexibility, but you need to be careful to avoid bad practices. + +---- + +******** +Examples +******** + +.. raw:: html + +
+
- Not really as they might cancel themselves out, but you can capture the event of one to trigger the next one. +.. displayitem:: + :header: Build a DAG + :description: Learn how to schedule a DAG execution + :col_css: col-md-4 + :button_link: ../examples/dag/dag.html + :height: 180 + :tag: Intermediate -- **Q: Can I use any arbitrary logic to schedule?** +.. raw:: html - Yes, this design enables absolute flexibility, but you need to be careful to avoid bad practices. +
+
diff --git a/docs/source-app/glossary/sharing_components.rst b/docs/source-app/glossary/sharing_components.rst index c4df69f95c7a4..2699f5d3c1c47 100644 --- a/docs/source-app/glossary/sharing_components.rst +++ b/docs/source-app/glossary/sharing_components.rst @@ -6,6 +6,8 @@ Sharing my components **Level:** Basic +---- + ********************************************* Why should I consider sharing my components ? ********************************************* @@ -14,6 +16,8 @@ Lightning is community driven and its core objective is to make AI accessible to By creating components and sharing them with everyone else, the barrier to entry will be go down. +---- + ************************************* How should I organize my components ? ************************************* @@ -34,10 +38,12 @@ Here are the best practices steps before sharing the component: .. note:: As a Lightning user, it helps to implement your components thinking someone else is going use them. +---- + ****************************************** How should I proceed to share components ? ****************************************** Once your component is ready, create a PiPy package with your own library and then it can be re-used by anyone else. -Here is a `Component Template `_ from `William Falcon `_ to guide your component +Here is a `Component Template `_ from `William Falcon `_ to guide your component diff --git a/docs/source-app/glossary/storage/drive.rst b/docs/source-app/glossary/storage/drive.rst index 0888c80fe6bc5..e500e087cea8f 100644 --- a/docs/source-app/glossary/storage/drive.rst +++ b/docs/source-app/glossary/storage/drive.rst @@ -4,205 +4,8 @@ Drive Storage ############# -**Audience:** Users who want to put, list and get files from a shared disk space. - - -The Lightning Storage system makes it easy to share files between LightningWork so you can run your app both locally and in the cloud without changing the code. - ----- - -***************** -What is a Drive ? -***************** - -The Drive object provides a central place for your components to share data. - -The drive acts as an isolate folder and any component can access it by knowing its name. - -Your components can put, list, get, delete files from and to the Drive (except LightningFlow's). - ----- - -**************************** -Why should I use the Drive ? -**************************** - -Every instance of the Drive object acts as a Google Drive or Dropbox. - -By sharing the drive between components through the flow, -several components can have a shared place to read and write files from. +**Audience:** Users who want to put, list, and get files from a shared disk space. ---- -************************* -How do I create a Drive ? -************************* - -In order to create a Drive, you simply need to pass its name with the prefix ``lit://`` as follows: - -.. code-block:: python - - from lightning_app.storage import Drive - - # The identifier of this Drive is ``drive_1`` - # Note: You need to add Lightning protocol ``lit://`` as a prefix. - - drive_1 = Drive("lit://drive_1") - - # The identifier of this Drive is ``drive_2`` - drive_2 = Drive("lit://drive_2") - -Any components can create a drive object. - -.. code-block:: python - - from lightning_app import LightningFlow, LightningWork - from lightning_app.storage import Drive - - - class Flow(LightningFlow): - def __init__(self): - super().__init__() - self.drive_1 = Drive("lit://drive_1") - - def run(self): - ... - - - class Work(LightningWork): - def __init__(self): - super().__init__() - self.drive_1 = Drive("lit://drive_1") - - def run(self): - ... - ----- - -************************************* -What actions does the drive support ? -************************************* - -A drive supports put, list, get, delete actions. - -.. code-block:: python - - from lightning_app.storage import Drive - - drive = Drive("lit://drive") - - drive.list(".") # Returns [] as empty - - # Created file. - with open("a.txt", "w") as f: - f.write("Hello World !") - - drive.put("a.txt") - - drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. - - drive.get("a.txt") # Get the file into the current worker - - drive.delete("a.txt") - - drive.list(".") # Returns [] as empty - ----- - -****************************************** -How does component interacts with drives ? -****************************************** - -Here is an illustrated code example on how to create drives within works. - -.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/drive_2.png - -.. code-block:: python - - from lightning_app import LightningFlow, LightningWork - from lightning_app.core.app import LightningApp - from lightning_app.storage.drive import Drive - - - class Work_A(LightningWork): - def __init__(self): - super().__init__() - # The identifier of the Drive is ``drive_1`` - # Note: You need to add Lightning protocol ``lit://`` as a prefix. - self.drive_1 = Drive("lit://drive_1") - - def run(self): - # 1. Create a file. - with open("a.txt", "w") as f: - f.write("Hello World !") - - # 2. Put the file into the drive. - self.drive_1.put("a.txt") - - - class Work_B(LightningWork): - def __init__(self): - super().__init__() - - # Note: Work B has access 2 drives. - - # The identifier of this Drive is ``drive_1`` - self.drive_1 = Drive("lit://drive_1") - # The identifier of this Drive is ``drive_2`` - self.drive_2 = Drive("lit://drive_2") - - def run(self): - # 1. Create a file. - with open("b.txt", "w") as f: - f.write("Hello World !") - - # 2. Put the file into both drives. - self.drive_1.put("b.txt") - self.drive_2.put("b.txt") - - - class Work_C(LightningWork): - def __init__(self): - super().__init__() - self.drive_2 = Drive("lit://drive_2") - - def run(self): - # 1. Create a file. - with open("c.txt", "w") as f: - f.write("Hello World !") - - # 2. Put the file into the drive. - self.drive_2.put("c.txt") - ----- - -****************************************** -How can I transfer files with the Drive ? -****************************************** - -In the example below, the Drive is created by the flow and passed to its LightningWork's. - -The ``Work_1`` put a file **a.txt** in the **Drive("lit://this_drive_id")** and the ``Work_2`` can list and get the **a.txt** file from it. - -.. literalinclude:: ../../../../examples/drive/app.py - - ----- - -.. raw:: html - -
-
- -.. displayitem:: - :header: Learn about the Path Object. - :description: Transfer Files From One Component to Another by Reference. - :col_css: col-md-4 - :button_link: path.html - :height: 180 - :tag: Intermediate - -.. raw:: html - -
-
+.. include:: ../../glossary/storage/drive_content.rst diff --git a/docs/source-app/glossary/storage/drive_content.rst b/docs/source-app/glossary/storage/drive_content.rst new file mode 100644 index 0000000000000..263fab6093c1c --- /dev/null +++ b/docs/source-app/glossary/storage/drive_content.rst @@ -0,0 +1,199 @@ + + +************ +About Drives +************ + +Lightning Drive storage makes it easy to share files between LightningWorks so you can run your Lightning App both locally and in the cloud without changing the code. + +The Drive object provides a central place for your components to share data. + +The Drive acts as an isolate folder and any component can access it by knowing its name. + +Your components can put, list, get, and delete files from and to the Drive (except LightningFlows). + +---- + +*********************** +What Drive does for you +*********************** + +Think of every instance of the Drive object acting like a Google Drive or like Dropbox. + +By sharing the Drive between components through the LightningFlow, +several components can have a shared place to read and write files from. + +---- + +************** +Create a Drive +************** + +In order to create a Drive, you simply need to pass its name with the prefix ``lit://`` as follows: + +.. code-block:: python + + from lightning_app.storage import Drive + + # The identifier of this Drive is ``drive_1`` + # Note: You need to add Lightning protocol ``lit://`` as a prefix. + + drive_1 = Drive("lit://drive_1") + + # The identifier of this Drive is ``drive_2`` + drive_2 = Drive("lit://drive_2") + +Any components can create a drive object. + +.. code-block:: python + + from lightning_app import LightningFlow, LightningWork + from lightning_app.storage import Drive + + class Flow(LightningFlow): + def __init__(self): + super().__init__() + self.drive_1 = Drive("lit://drive_1") + + def run(self): + ... + + class Work(LightningWork): + def __init__(self): + super().__init__() + self.drive_1 = Drive("lit://drive_1") + + def run(self): + ... + +---- + +***************************** +Supported actions with Drives +***************************** + +A Drive supports put, list, get, and delete actions. + +.. code-block:: python + + from lightning_app.storage import Drive + + drive = Drive("lit://drive") + + drive.list(".") # Returns [] as empty + + # Created file. + with open("a.txt", "w") as f: + f.write("Hello World !") + + drive.put("a.txt") + + drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. + + drive.get("a.txt") # Get the file into the current worker + + drive.delete("a.txt") + + drive.list(".") # Returns [] as empty + +---- + +********************************** +Component interactions with Drives +********************************** + +Here is an illustrated code example on how to create drives within works. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/drive_2.png + +.. code-block:: python + + from lightning_app import LightningFlow, LightningWork + from lightning_app.core.app import LightningApp + from lightning_app.storage.drive import Drive + + + class Work_A(LightningWork): + + def __init__(self): + super().__init__() + # The identifier of the Drive is ``drive_1`` + # Note: You need to add Lightning protocol ``lit://`` as a prefix. + self.drive_1 = Drive("lit://drive_1") + + def run(self): + # 1. Create a file. + with open("a.txt", "w") as f: + f.write("Hello World !") + + # 2. Put the file into the drive. + self.drive_1.put("a.txt") + + + class Work_B(LightningWork): + + def __init__(self): + super().__init__() + + # Note: Work B has access 2 drives. + + # The identifier of this Drive is ``drive_1`` + self.drive_1 = Drive("lit://drive_1") + # The identifier of this Drive is ``drive_2`` + self.drive_2 = Drive("lit://drive_2") + + def run(self): + # 1. Create a file. + with open("b.txt", "w") as f: + f.write("Hello World !") + + # 2. Put the file into both drives. + self.drive_1.put("b.txt") + self.drive_2.put("b.txt") + + class Work_C(LightningWork): + + def __init__(self): + super().__init__() + self.drive_2 = Drive("lit://drive_2") + + def run(self): + # 1. Create a file. + with open("c.txt", "w") as f: + f.write("Hello World !") + + # 2. Put the file into the drive. + self.drive_2.put("c.txt") + +---- + +***************************** +Transfer files with Drive +***************************** + +In the example below, the Drive is created by the flow and passed to its LightningWork's. + +The ``Work_1`` put a file **a.txt** in the **Drive("lit://this_drive_id")** and the ``Work_2`` can list and get the **a.txt** file from it. + +.. literalinclude:: ../../../examples/app_drive/app.py + + +---- + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Learn about the Path Object. + :description: Transfer Files From One Component to Another by Reference. + :col_css: col-md-4 + :button_link: path.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/glossary/storage/path.rst b/docs/source-app/glossary/storage/path.rst index b46cdbfb1d4aa..1437bba577d80 100644 --- a/docs/source-app/glossary/storage/path.rst +++ b/docs/source-app/glossary/storage/path.rst @@ -81,7 +81,7 @@ Convert every filesystem path you want to share with other LightningWorks to by ... -Under the hood, we convert this string to a :class:`~lightning_app.storage.path.Path` object, which is a drop-in replacement for :class:`pathlib.Path` meaning it will work with :mod:`os`, :mod:`os.path` and :mod:`pathlib` filesystem operations out of the box! +Under the hood, we convert this string to a :class:`~lightning.app.storage.path.Path` object, which is a drop-in replacement for :class:`pathlib.Path` meaning it will work with :mod:`os`, :mod:`os.path` and :mod:`pathlib` filesystem operations out of the box! ---- @@ -99,7 +99,6 @@ For example, share a directory by passing it as an input to the run method of th from lightning_app import LightningFlow - class Flow(LightningFlow): def __init__(self): super().__init__() @@ -173,8 +172,8 @@ You can check if a path exists locally or remotely in the source Work using the # OR if checkpoint_dir.exists_local(): - # Do something with the file if it exists locally - files = os.listdir(checkpoint_dir) + # Do something with the file if it exists locally + files = os.listdir(checkpoint_dir) ---- @@ -191,7 +190,6 @@ Lightning makes sure all Paths that are part of the state get stored and made ac from lightning_app.storage import Path - class Work(LightningWork): def __init__(self): super().__init__() @@ -220,7 +218,6 @@ First, define a component that saves a checkpoint: import torch import os - class ModelTraining(LightningWork): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -267,8 +264,7 @@ Link both components via a parent component: self.train.run() self.deploy.run(checkpoint_dir=self.train.checkpoint_dir) - - app = lapp.LightningApp(Flow()) + app = L.LightningApp(Flow()) ---- diff --git a/docs/source-app/glossary/storage/storage.rst b/docs/source-app/glossary/storage/storage.rst index 5bfa9689c03eb..37bf992bf1af2 100644 --- a/docs/source-app/glossary/storage/storage.rst +++ b/docs/source-app/glossary/storage/storage.rst @@ -48,3 +48,30 @@ Lightning storage provides two solutions :class:`~lightning_app.storage.drive.Dr + + +---- + +******** +Examples +******** + + + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Build a File Server + :description: Learn how to use Drive to upload / download files to your app. + :col_css: col-md-4 + :button_link: ../../examples/file_server/file_server.html + :height: 180 + :tag: Intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/index.rst b/docs/source-app/index.rst index 80e287314319a..e6a1b5b61665e 100644 --- a/docs/source-app/index.rst +++ b/docs/source-app/index.rst @@ -3,16 +3,20 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to ⚡ Lightning AI -========================== +############################ +Welcome to ⚡ Lightning Apps +############################ .. twocolumns:: :left: - .. image:: https://pl-bolts-doc-images.s3.us-east-2.amazonaws.com/mov.gif + .. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/Lightning.gif :alt: Animation showing how to convert a standard training loop to a Lightning loop :right: - PyTorch Lightning is the deep learning framework for professional AI researchers and machine learning engineers who need maximal flexibility without sacrificing performance at scale. - Lightning evolves with you as your projects go from idea to paper/production. + Lightning is a distributed, modular, free, and open framework for + building all AI applications where the components you want, interact together. + The concept of Lightning apps was designed to help you focus on the what you care about, + and automate the rest. + The Lightning framework can be used for any type of AI app, from a simple demo to a production pipeline. .. raw:: html @@ -30,51 +34,22 @@ Welcome to ⚡ Lightning AI +---- -.. raw:: html - -
- - +****************** Install Lightning ------------------ - +****************** -.. - .. raw:: html - -
-
- -Make sure you use Python 3.8+ .. code-block:: bash - python -m pip install -U lightning - -.. - .. raw:: html - -
-
- - Conda users + pip install lightning - .. code-block:: bash - - Available after June 16th - - .. raw:: html - -
-
- -.. raw:: html - -
+---- +****************** Get Started ------------ +****************** .. raw:: html @@ -86,7 +61,7 @@ Get Started .. customcalloutitem:: :header: Build a Lightning App in 15 minutes :description: Learn the 4 key steps to build a Lightning app. - :button_link: pages/lightning_apps_intro.html + :button_link: lightning_apps_intro.html .. raw:: html @@ -107,37 +82,45 @@ Get Started :maxdepth: 1 :caption: Get Started - pages/lightning_apps_intro - pages/installation - + read_me_first + installation + lightning_apps_intro .. toctree:: :maxdepth: 1 - :caption: Core API Reference + :caption: App Building Skills - LightningApp - LightningFlow - LightningWork + Basic + Intermediate + Advanced .. toctree:: :maxdepth: 1 - :caption: Addons API Reference + :caption: Hands-on Examples - api_reference/components - api_reference/frontend - api_reference/runners - api_reference/storage + Build a DAG + Build a File Server + Build a Github Repo Script Runner + Build a HPO Sweeper + Build a Model Server + +.. + [Docs under construction] Build a data exploring app + [Docs under construction] Build a ETL app + [Docs under construction] Build a model deployment app + [Docs under construction] Build a research demo app .. toctree:: :maxdepth: 1 :caption: App development workflows - Access the app state + Access the app state Add a web user interface (UI) Add a web link Arrange app tabs Build a Lightning app Build a Lightning component + Cache Work run calls Extend an existing app Publish a Lightning component Run a server within a Lightning App @@ -145,42 +128,49 @@ Get Started Run work in parallel Share an app Share files between components + +.. [Docs under construction] Add a Lightning component [Docs under construction] Debug a distributed cloud app locally [Docs under construction] Enable fault tolerance [Docs under construction] Run components on different hardware - Cache Work run calls [Docs under construction] Schedule app runs [Docs under construction] Test an app - .. toctree:: :maxdepth: 1 - :caption: Hands-on Examples [Docs under construction] + :caption: Core API Reference - Build a sweeps app - Build a data exploring app - Build a DAG - Build a ETL app - Build a model deployment app - Build a research demo app + LightningApp + LightningFlow + LightningWork .. toctree:: :maxdepth: 1 - :caption: Glossary [Docs under construction] + :caption: Addons API Reference + api_reference/components + api_reference/frontend + api_reference/runners + api_reference/storage + +.. toctree:: + :maxdepth: 1 + :caption: Glossary App Components Tree Build Configuration DAG - Debug an app - Distributed front-ends - Distributed hardware Event loop Environment Variables - Fault tolerance Front-end Sharing Components Scheduling Storage UI + +.. + [Docs under construction] Debug an app + [Docs under construction] Distributed front-ends + [Docs under construction] Distributed hardware + [Docs under construction] Fault tolerance diff --git a/docs/source-app/pages/install_beginner.rst b/docs/source-app/install_beginner.rst similarity index 91% rename from docs/source-app/pages/install_beginner.rst rename to docs/source-app/install_beginner.rst index 71bec2704dcd0..150f188815945 100644 --- a/docs/source-app/pages/install_beginner.rst +++ b/docs/source-app/install_beginner.rst @@ -20,20 +20,16 @@ keeps your system Python installation clean. - ---- - We will describe two choices here, pick one: 1. :ref:`Python virtualenv `. 2. :ref:`Conda virtual environment `. - ---- - .. _python-virtualenv: ******************** @@ -50,9 +46,10 @@ If you can't run the command above or it returns a version older than 3.8, `install the latest version of Python `_. After installing it, make sure you can run the above command without errors. +---- Creating a Virtual Environment -============================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When starting with a new Python project, you typically want to create a new Python virtual environment. Navigate to the location of your project and run the following command: @@ -64,9 +61,10 @@ Navigate to the location of your project and run the following command: The name of the environment here is *lightning* but you can choose any other name you like. By running the above command, Python will create a new folder *lightning* in the current working directory. +---- Activating the Virtual Environment -================================== +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before you can install packages into the environment, you need to activate it: @@ -76,12 +74,10 @@ Before you can install packages into the environment, you need to activate it: You need to do this step every time you want to work on your project / open the terminal. With your virtual environment activated, you are now ready to -:doc:`install Lightning ` and get started with Apps! - +:doc:`install Lightning <../installation>` and get started with Apps! ---- - .. _conda: ******** @@ -97,9 +93,10 @@ To check that the installation was successful, open an new terminal and run: It should return a list of commands. +---- Creating a Conda Environment -============================ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When starting with a new Python project, you typically want to create a new Conda virtual environment. Navigate to the location of your project and run the following command: @@ -111,9 +108,10 @@ Navigate to the location of your project and run the following command: The name of the environment here is *lightning* but you can choose any other name you like. Note how we can also specify the Python version here. +---- Activating the Conda Environment -================================ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before you can install packages into the environment, you need to activate it: @@ -123,4 +121,4 @@ Before you can install packages into the environment, you need to activate it: You need to do this step every time you want to work on your project / open the terminal. With your virtual environment activated, you are now ready to -:doc:`install Lightning ` and get started with Apps! +:doc:`install Lightning <../installation>` and get started with Apps! diff --git a/docs/source-app/installation.rst b/docs/source-app/installation.rst new file mode 100644 index 0000000000000..1d4ebb6ed6268 --- /dev/null +++ b/docs/source-app/installation.rst @@ -0,0 +1,34 @@ + +.. _install: + + +############ +Installation +############ + +We strongly recommend to create a virtual environment first. +Don't know what this is? Follow our `beginner guide here `_. + +**Requirements** + +* Python 3.8.x or later (3.8.x, 3.9.x, 3.10.x) +* Pip (the latest versions of Python will already have this) +* Git +* PyTorch - https://pytorch.org/get-started/locally/ +* Setup an alias for Python: python=python3 +* Add the root folder of Lightning to the Environment Variables to PATH +* Install Z shell (zsh) (This is required for Windows to install the quickstart app) + +---- + +**************** +Install with pip +**************** + +0. Activate your virtual environment. + +1. Install the ``lightning`` package + + .. code:: bash + + python -m pip install -U lightning diff --git a/docs/source-app/pages/introduction.rst b/docs/source-app/intro.rst similarity index 95% rename from docs/source-app/pages/introduction.rst rename to docs/source-app/intro.rst index 5fff153dc8271..4b8f1317c185b 100644 --- a/docs/source-app/pages/introduction.rst +++ b/docs/source-app/intro.rst @@ -2,8 +2,9 @@ .. _what: +################### What is Lightning? -================== +################### Lightning is a free, modular, distributed, and open-source framework for building AI applications where the components you want to use interact together. @@ -19,10 +20,13 @@ regardless of their level of engineering expertise. :alt: What is Lightning gif. :width: 100 % +---- + .. _why: +*************** Why Lightning? -============== +*************** Easy to learn @@ -31,18 +35,24 @@ Easy to learn Lightning was built for creating AI apps, not for dev-ops. It offers an intuitive, pythonic and highly composable interface that allows you to focus on solving the problems that are important to you. +---- + Quick to deliver ^^^^^^^^^^^^^^^^ Lightning speeds the development process by offering testable templates you can build from, accelerating the process of moving from idea to prototype and finally to market. +---- + Easy to scale ^^^^^^^^^^^^^ Lightning provides a mirrored experience locally and in the cloud. The `lightning.ai `_. cloud platform abstracts the infrastructure, so you can run your apps at any scale. +---- + Easy to collaborate ^^^^^^^^^^^^^^^^^^^ @@ -50,8 +60,11 @@ Lightning was built for collaboration. By following the best MLOps practices provided through our documentation and example use cases, you can deploy state-of-the-art ML applications that are ready to be used by teams of all sizes. +---- + +***************************** What's Novel With Lightning? -============================ +***************************** Cloud Infra Made Simple and Pythonic diff --git a/docs/source-app/levels/advanced/index.rst b/docs/source-app/levels/advanced/index.rst new file mode 100644 index 0000000000000..9fa0b61eb11bb --- /dev/null +++ b/docs/source-app/levels/advanced/index.rst @@ -0,0 +1,74 @@ +.. _advanced_level: + +.. toctree:: + :maxdepth: 1 + :hidden: + + level_17 + level_18 + level_19 + level_20 + level_21 + +############### +Advanced skills +############### + +Learn to build Lightning Apps for enterprise workloads or advanced research. + +.. join_slack:: + :align: left + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Level 17: Check work status + :description: Learn to use work status to coordinate complex apps. + :button_link: level_17.html + :col_css: col-md-6 + :height: 150 + :tag: advanced + +.. displayitem:: + :header: Level 18: Cache calls into run + :description: Learn about caching calls in work.run. + :button_link: level_18.html + :col_css: col-md-6 + :height: 150 + :tag: advanced + +.. displayitem:: + :header: Level 19: Share objects between works + :description: Learn to build things like DAGs where values return from Work. + :button_link: level_19.html + :col_css: col-md-6 + :height: 150 + :tag: advanced + +.. displayitem:: + :header: Level 20: Handle Lightning App exceptions + :description: Learn to handle Lightning App exceptions. + :button_link: level_20.html + :col_css: col-md-6 + :height: 150 + :tag: advanced + +.. displayitem:: + :header: Level 21: Enable dynamic Works + :description: Learn to enable dynamic works for complex systems. + :button_link: level_21.html + :col_css: col-md-6 + :height: 150 + :tag: advanced + +.. raw:: html + +
+
diff --git a/docs/source-app/levels/advanced/level_17.rst b/docs/source-app/levels/advanced/level_17.rst new file mode 100644 index 0000000000000..29cef4a7bf3fc --- /dev/null +++ b/docs/source-app/levels/advanced/level_17.rst @@ -0,0 +1,10 @@ +########################### +Level 17: Check Work status +########################### +**Audience:** Users who want to stop/start Lightning Work based on a status. + +**Prereqs:** Level 16+ + +---- + +.. include:: ../../core_api/lightning_work/status_content.rst diff --git a/docs/source-app/levels/advanced/level_18.rst b/docs/source-app/levels/advanced/level_18.rst new file mode 100644 index 0000000000000..9b050b4452d57 --- /dev/null +++ b/docs/source-app/levels/advanced/level_18.rst @@ -0,0 +1,12 @@ +############################## +Level 18: Cache calls into run +############################## +**Audience:** Users who want Work.run() to activate multiple times in an app. + +**Prereqs:** Level 16+ and read the `Event Loop guide <../glossary/event_loop.html>`_. + + + +---- + +.. include:: ../../workflows/run_work_once_content.rst diff --git a/docs/source-app/levels/advanced/level_19.rst b/docs/source-app/levels/advanced/level_19.rst new file mode 100644 index 0000000000000..272b6fe282e3b --- /dev/null +++ b/docs/source-app/levels/advanced/level_19.rst @@ -0,0 +1,10 @@ +############################################## +Level 19: Share objects between LightningWorks +############################################## +**Audience:** Users moving DataFrames or outputs, between Lightning Works (usually data engineers). + +**Prereqs:** Level 16+ and know about the Pandas library and read the `Access app state guide <../../access_app_state.html>`_. + +---- + +.. include:: ../../core_api/lightning_work/payload_content.rst diff --git a/docs/source-app/levels/advanced/level_20.rst b/docs/source-app/levels/advanced/level_20.rst new file mode 100644 index 0000000000000..c83b696812a49 --- /dev/null +++ b/docs/source-app/levels/advanced/level_20.rst @@ -0,0 +1,11 @@ +######################################### +Level 20: Handle Lightning App exceptions +######################################### + +**Audience:** Users who want to make Lightning Apps more robust to potential issues. + +**Prereqs:** Level 16+ + +---- + +.. include:: ../../core_api/lightning_work/handling_app_exception_content.rst diff --git a/docs/source-app/levels/advanced/level_21.rst b/docs/source-app/levels/advanced/level_21.rst new file mode 100644 index 0000000000000..6d163fffa5609 --- /dev/null +++ b/docs/source-app/levels/advanced/level_21.rst @@ -0,0 +1,11 @@ +####################################### +Level 21: Enable dynamic LightningWorks +####################################### + +**Audience:** Users who want to start/stop multiple LightningWorks not defined in the beginning of the Lightning App. + +**Prereqs:** Level 16+ + +---- + +.. include:: ../../core_api/lightning_app/dynamic_work_content.rst diff --git a/docs/source-app/levels/basic/index.rst b/docs/source-app/levels/basic/index.rst new file mode 100644 index 0000000000000..24d34ae6795f4 --- /dev/null +++ b/docs/source-app/levels/basic/index.rst @@ -0,0 +1,92 @@ +.. _level_basic: + +.. toctree:: + :maxdepth: 1 + :hidden: + + level_1 + level_2 + level_3 + level_4 + level_5 + level_6 + level_7 + +############ +Basic skills +############ +Learn the basics of running Lightning Apps and modifying existing Lightning Apps. Researchers and machine learning engineers should start here. + +.. join_slack:: + :align: left + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Level 1: Clone and Run + :description: Learn how to get up and running in minutes + :button_link: level_1.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. displayitem:: + :header: Level 2: Run Lightning Apps locally + :description: Learn to run an Lightning Apps locally + :button_link: level_2.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. displayitem:: + :header: Level 3: Run Lightning Apps on the cloud + :description: Learn to run a Lightning Apps on the cloud + :button_link: level_3.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. displayitem:: + :header: Level 4: Modify existing Lightning Apps + :description: Modify a Lightning App from the Lightning App Gallery for your use case. + :button_link: level_4.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. displayitem:: + :header: Level 5: Share your Lightning App + :description: Learn how to share your Lightning App with colleagues + :button_link: level_5.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. displayitem:: + :header: Level 6: Publish your Lightning App + :description: Learn how to submit your Lightning App to the Lightning App Gallery + :button_link: level_6.html + :col_css: col-md-6 + :height: 150 + :tag: basic + + +.. displayitem:: + :header: Level 7: Run on your cloud account + :description: Learn how to run a Lightning App on your own cloud account. + :button_link: level_7.html + :col_css: col-md-6 + :height: 150 + :tag: basic + +.. raw:: html + +
+
diff --git a/docs/source-app/levels/basic/level_1.rst b/docs/source-app/levels/basic/level_1.rst new file mode 100644 index 0000000000000..cfb0045cfcf4f --- /dev/null +++ b/docs/source-app/levels/basic/level_1.rst @@ -0,0 +1,99 @@ +###################### +Level 1: Clone and Run +###################### +**Audience:** New users to the framework. + +**Prereqs:** None. + +---- + +********************** +Choose a Lightning App +********************** +The simplest way to get started with Lightning Apps, is to find a Lightning App similar to something you are trying +to build. Simply visit `lightning.ai `_ and find a Lightning App that is relevant to your work. + +If you need inspiration, check out some of these examples: + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Lightning Sweeper + :description: Use this Lightning App to run hyperparameter sweeps, or build your own version. + :button_link: https://lightning.ai/app/8FOWcOVsdf-Lightning%20Sweeper + :col_css: col-md-4 + :height: 200 + :tag: research workflows + +.. displayitem:: + :header: InVideo search + :description: Use this Lightning App to see a more end-to-end production workflow which can be extended with deployment strategies and more. + :button_link: https://lightning.ai/app/7pmQNIDxAE-InVideo%20Search + :col_css: col-md-4 + :height: 200 + :tag: production workflows + +.. displayitem:: + :header: Unsplash Image Search + :description: Example Lightning App if you want to build research demos. + :button_link: https://lightning.ai/app/vIDPvY3YAA-Unsplash%20Image%20Search + :col_css: col-md-4 + :height: 200 + :tag: demo workflows + +.. raw:: html + +
+
+ +---- + +************************ +Launch the Lightning App +************************ +The first thing you should try is to "launch" the Lightning App to see how a Lightning App works before you decide to dive into the code. +Some Lightning Apps cannot be launched because of the way they have been constructed, or for cost reasons for the author. + +For example, here's the page for the `Unsplash image search app `_. + +When you submit a Lightning App to the Lightning App Gallery, you can decide if a Lightning App can be launched. Otherwise, users +will have to clone & run the Lightning App to run on their accounts. + +---- + +*********** +Clone & Run +*********** +If you're happy with the Lightning App you found and want to try it out, the next action should be to **clone & run** the Lightning App. +To **clone & run** go to the details page of that Lightning App and press **Clone & Run**. + +For example, try the **Clone & Run** button `for this Lightning App `_. + +Clone and run starts the Lightning App on your account. If cloning and running is going to cost you money, you'll get +a prompt to verify that you want to do that. + +---- + +********************** +Open the Lightning App +********************** +Once the Lightning App has started, press "Launch" to see the Lightning App. You can send that link to any of your colleagues so +they can also use the Lightning App. + +For example, here's the page that appears for the `Unsplash Image Search Lightning App `_. + +.. Warning:: Remember, that if the Lightning App performs expensive actions, such as training a model, and you share with people, + they might accidentally incur costs on your account. Make sure this is what you are expecting. + +---- + +***************** +Download the code +***************** +Now that you verified that the Lightning App does indeed work end-to-end, go to the **Code** tab and click "Download zip", to get the full code +on your machine. diff --git a/docs/source-app/levels/basic/level_2.rst b/docs/source-app/levels/basic/level_2.rst new file mode 100644 index 0000000000000..0f75c33588ff3 --- /dev/null +++ b/docs/source-app/levels/basic/level_2.rst @@ -0,0 +1,36 @@ +################################### +Level 2: Run Lightning Apps locally +################################### +**Audience:** New users who want to run a Lightning App on their machines + +**Prereqs:** You already have the Lightning App code on your local machine. + +---- + +************ +Get the code +************ +If you followed the instructions for **Level 1: Clone and Run**, you already have the code for the Lightning App locally. Otherwise please go back to `Level 1 `_. + +---- + +*********** +Run locally +*********** +Run the Lightning App on your local machine by using the following command: + +.. code:: bash + + lightning run app app.py + +Now you'll see the Lightning App start up on your local machine. + +.. note:: At this time, you can only run one Lightning App locally at a time. **Submit a PR to unblock that!** + +---- + +************************* +Run on any remote machine +************************* +Remember you can always SSH into any of your cloud machines on your university or enterprise cluster and run +Lightning App from there. However, you will be responsible for the auto-scaling of those Lightning Apps. diff --git a/docs/source-app/levels/basic/level_3.rst b/docs/source-app/levels/basic/level_3.rst new file mode 100644 index 0000000000000..b2632509731eb --- /dev/null +++ b/docs/source-app/levels/basic/level_3.rst @@ -0,0 +1,56 @@ +############################# +Level 3: Run app on the cloud +############################# +**Audience:** Users who want to run an app on the Lightning Cloud. + +**Prereqs:** You have an app already running locally. + +---- + +**************************** +What is the Lightning Cloud? +**************************** +The Lightning Cloud is the platform that we've created to interface with the cloud providers. Today +the Lightning Cloud supports AWS. + +.. note:: Support for GCP and Azure is coming in the Fall of 2022! + +To use the Lightning Cloud, you buy credits that are used to pay the cloud providers. If you want to run +on your own AWS credentials, please contact us (support@lightning.ai) so we can get your clusters set up for you. + +---- + +**************** +Run on the cloud +**************** +To run the app on the cloud, simply add **--cloud** to the command + +.. code:: bash + + lightning run app app.py --cloud + +Lightning packages everything in that folder and uploads it to the cloud. Your code will be visible to everyone (just like Github). + +.. note:: To have private Lightning Apps, you'll need to upgrade your account. To upgrade, contact us ``_. + +---- + +************ +Ignore files +************ +Lightning sends everything in your Lightning App folder to the cloud. If you want to ignore certain files (such as datasets), +use the **.lightningignore** file which works just like the **.gitignore** + +.. code:: bash + + touch .lightningignore + +---- + +******************* +Manage requirements +******************* +A Lightning App is simply a Python file. However, when running on the cloud, it is encouraged that you add +a **requirements.txt** file so that the platform knows what requirements your Lightning App needs. + +If you require custom Docker images, each LightningWork has the ability to have a private Docker image. diff --git a/docs/source-app/levels/basic/level_4.rst b/docs/source-app/levels/basic/level_4.rst new file mode 100644 index 0000000000000..17fc8e90feb87 --- /dev/null +++ b/docs/source-app/levels/basic/level_4.rst @@ -0,0 +1,123 @@ +############################ +Level 4: Modify existing app +############################ +**Audience:** Users who have already run a Lightning App locally or remote and want to modify it. + +**Prereqs:** You've ran a Lightning App locally and the cloud. + +---- + +*************** +Change the code +*************** +A Lightning App is simply organized Python code. To modify an existing Lightning App, simply change the code! +There's nothing more you need to do. + +---- + +*************** +Add a component +*************** +A major superpower you get with Lightning Apps is modular workflows. This allows you to use our gallery +of opensource components to power your work. Find a component in the `component gallery `_ and add it to your +Lightning App. + +If you need inspiration, here are some components you might want to use to extend your Lightning App: + + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Slack Messenger + :description: Send a Slack notification when anything happens in your Lightning App. + :button_link: https://lightning.ai/components/LJFITYeNBZ-Slack%20Messenger + :col_css: col-md-4 + :height: 200 + :tag: monitoring + +.. displayitem:: + :header: Lightning Serve + :description: Make your model serving infrastructure resilient with over 7+ strategies such as canary, recreate, ramped and more. + :button_link: https://lightning.ai/components/BA2slXIke9-Lightning%20Serve + :col_css: col-md-4 + :height: 200 + :tag: production + +.. displayitem:: + :header: Lightning HPO + :description: Add a hyperparameter sweep to your Lightning App. + :button_link: https://lightning.ai/components/BA2slXI093-Lightning%20HPO + :col_css: col-md-4 + :height: 200 + :tag: research + +.. displayitem:: + :header: Jupyter Notebook + :description: Add a Jupyter Notebook to your Lightning App. + :button_link: https://lightning.ai/components/cRH1UHnvBx-Jupyter%20Notebook + :col_css: col-md-4 + :height: 200 + :tag: data science + +.. displayitem:: + :header: Google BigQuery + :description: Connect a big BigQuery dataset to your Lightning App. + :button_link: https://lightning.ai/components/Mtt4fnRlUE-Google%20BigQuery + :col_css: col-md-4 + :height: 200 + :tag: production + +.. raw:: html + +
+
+ +---- + +********************* +Install the component +********************* +To add a component, install the component first. + +We'll use the Slack messaging component as example: + +.. code:: bash + + lightning install component lightning/lit-slack-messenger + + +Now that the component is installed, make sure you add it to your requirements.txt + +.. code:: bash + + echo 'git+https://github.com/Lightning-AI/LAI-slack-messenger.git@4aa91554f51baf56fc14316365c67fcc67b61e7d' > requirements.txt + +---- + +***************** +Use the component +***************** +To use the component, simply import it and attach it to your Lightning App. + +.. code:: python + + import lightning as L + from lit_slack import SlackMessenger + + class YourComponent(L.LightningFlow): + def __init__(self): + super().__init__() + self.slack_messenger = SlackMessenger( + token='a-long-token', + channel_id='A03CB4A6AK7' + ) + + def run(self): + self.slack_messenger.send_message('hello from ⚡ lit slack ⚡') + + app = L.LightningApp(YourComponent()) diff --git a/docs/source-app/levels/basic/level_5.rst b/docs/source-app/levels/basic/level_5.rst new file mode 100644 index 0000000000000..c7ec29e78411d --- /dev/null +++ b/docs/source-app/levels/basic/level_5.rst @@ -0,0 +1,31 @@ +################################## +Level 5: Share your Lightning Apps +################################## +**Audience:** Users who work in teams. + +**Prereqs:** You've ran a Lightning App locally and the cloud. + +---- + +**************************************** +What sharing Lightning Apps does for you +**************************************** +When working on teams or to show your Lightning App to your users or collaborators, it's often +helpful to share a link to your Lightning App. + +Sharing a link to your Lightning App, allows your collaborators to see what you've built, instead of them having +to clone, install the repo, and very likely not get anything running. + +---- + +************************ +Share your Lightning App +************************ +The easiest way to share your Lightning Apps is to run them on Lightning Cloud: + +.. code:: bash + + lightning run app app.py --cloud + +Once your Lightning App starts running, press "Open", then copy the link from the browser and share the link with your +colleagues! diff --git a/docs/source-app/levels/basic/level_6.rst b/docs/source-app/levels/basic/level_6.rst new file mode 100644 index 0000000000000..8a228029ca2b9 --- /dev/null +++ b/docs/source-app/levels/basic/level_6.rst @@ -0,0 +1,78 @@ +################################### +Level 6: Publish your Lightning App +################################### +**Audience:** New users to the framework. + +**Prereqs:** None. + +---- + +******************************************* +What publishing Lightning Apps does for you +******************************************* +Publishing a Lightning App, allows you to share your work with the Lightning community so that others won't have +to repeat the work you've built. This is at the core of the opensource ethos, that allows us to build +on top of each other's work. + +If your Lightning App is for a company, or business you are trying to monetize, publishing your Lightning App allows your users +to visit a single place where they won't have to worry about installing your Lightning App or running it on their own +accounts. + +---- + +************************** +Publish your Lightning App +************************** +To publish a Lightning App, visit `lightning.ai `_, expand the "Apps & Components" menu, and hit "Submit yours". +This will take you to `this form `_. + +---- + +******************************************** +Guidelines for publishing your Lightning App +******************************************** +Only the very best apps get accepted to the Lightning Gallery. Here are some guidelines to help you with yours. + +---- + +Thumbnail +^^^^^^^^^ +Make sure you have a high-quality Thumbnail. The image should be 300x200 and smaller than 20kb. + +---- + +App name +^^^^^^^^ +App names can only be 1-3 words at most. Names should be unique/recognizable. Remember Uber, Snapchat, etc... + +---- + +Short Description +^^^^^^^^^^^^^^^^^ +The short description should be succinct and to the point. Avoid filler words like "this app allows you to". It should +describe what your users can do with the app NOT the tools that are being used in the app. + +---- + +Long Description +^^^^^^^^^^^^^^^^ +Use the long description to describe to the user what the app does in more detail, and why they should use it. +This is your time to shine! + +Don't use gimmiky sentences like "have you ever wanted to?", "wouldn't it be great if?". Be professional +and let the user know what value your app brings to them. + +---- + +Tags +^^^^ +Tags should allow users to know if the app is relevant for them. This is where it's helpful to let the +user know the domains (NLP, RL, CV) and even the key libraries used (PyTorch Lightning, Tensorboard, Timm, etc...). + +---- + +Code example +^^^^^^^^^^^^ +The code example should be minimal. Do not add anything the user doesn't need to know. Don't try to show +off all the features of your app or component, simply show the most minimal, critical piece of code that +can get your users started immediately. diff --git a/docs/source-app/levels/basic/level_7.rst b/docs/source-app/levels/basic/level_7.rst new file mode 100644 index 0000000000000..70f16e116c27c --- /dev/null +++ b/docs/source-app/levels/basic/level_7.rst @@ -0,0 +1,36 @@ +################################## +Level 7: Run on your cloud account +################################## +**Audience:** Users who want to run on their own cloud accounts + +**Prereqs:** Users ran a Lightning App locally and/or the cloud. + +---- + +**************************** +What is the Lightning Cloud? +**************************** +The Lightning Cloud is the platform that we've created to interface with the cloud providers. Today +the Lightning Cloud supports AWS. + +.. note:: Support for GCP and Azure is coming in the Fall of 2022! + +To use the Lightning Cloud, you buy credits that are used to pay the cloud providers. If you want to run +on your own AWS credentials, please contact us (support@lightning.ai) so we can get your clusters set up for you. + +---- + +**************************************** +Run Lightning Apps on your cloud account +**************************************** +To run Lightning Apps on your own account, you can simply SSH into the machines on your cloud account +and start the Lightning Apps there! This also works on any machine you have access to, such as your own +on-prem cluster, DGX machine, etc... However, you will have to manage your own auto-scaling +and distribition of work for complex Lightning Apps. + +If you want to automate all that complexity, we allow you to create as many clusters as you +want, on the cloud provider of your choice, using Lightning Cloud. Once you've configured your clusters, +you can run your Lightning Apps on those clusters. + +Free clusters on Lightning Cloud have limits of the number of machines you can run. To increase that limit, +please `contact us `_. diff --git a/docs/source-app/levels/intermediate/index.rst b/docs/source-app/levels/intermediate/index.rst new file mode 100644 index 0000000000000..a80da09959391 --- /dev/null +++ b/docs/source-app/levels/intermediate/index.rst @@ -0,0 +1,109 @@ +.. _intermediate_level: + +.. toctree:: + :maxdepth: 1 + :hidden: + + level_8 + level_9 + level_10 + level_11 + level_12 + level_13 + level_14 + level_15 + level_16 + +################### +Intermediate skills +################### +Learn to build your own Lightning Apps from scratch and the basics of the framework. + +.. join_slack:: + :align: left + +---- + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Level 8: Build a Lightning App + :description: Learn how to build a Lightning App from scratch. + :button_link: level_8.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 9: Event loop + :description: Learn about the event loop. + :button_link: level_9.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 10: Add web UIs + :description: Learn how to add web UIs to your Lightning App. + :button_link: level_10 + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 11: Customize cloud compute + :description: Learn to use different cloud machines. + :button_link: level_11.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 12: LightningFlow vs LightningWork + :description: Learn the difference between LightningFlow and LightningWork. + :button_link: level_12.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 13: Communicate with the Lightning App + :description: Learn to communicate across the Lightning App with the app state. + :button_link: level_13.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 14: Communicate between LightningFlow and LightningWork + :description: Learn about how LightningFlows communicate with LightningWorks. + :button_link: level_14.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 15: Share files between components + :description: Learn how Drives share files between components + :button_link: level_15.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. displayitem:: + :header: Level 16: Run LightningWorks in parallel + :description: Learn when to run LightningWorks in parallel + :button_link: level_16.html + :col_css: col-md-6 + :height: 150 + :tag: intermediate + +.. raw:: html + +
+
diff --git a/docs/source-app/levels/intermediate/level_10.rst b/docs/source-app/levels/intermediate/level_10.rst new file mode 100644 index 0000000000000..3e20771eab4ee --- /dev/null +++ b/docs/source-app/levels/intermediate/level_10.rst @@ -0,0 +1,14 @@ +########### +Add web UIs +########### + +**Audience:** Users who want to add a web user interface (UI) to their Lightning App + +**Prereqs:** You must have finished the `Basic levels `_. + + +Every component in a Lightning App can have its own web user interface (UI). + +---- + +.. include:: ../../workflows/add_web_ui/index_content.rst diff --git a/docs/source-app/levels/intermediate/level_11.rst b/docs/source-app/levels/intermediate/level_11.rst new file mode 100644 index 0000000000000..9dc8804743c9f --- /dev/null +++ b/docs/source-app/levels/intermediate/level_11.rst @@ -0,0 +1,12 @@ +################################# +Level 11: Customize cloud compute +################################# +**Audience:** Users who want to change their cloud compute + +**Prereqs:** + +**Level:** Intermediate + +---- + +.. include:: ../../core_api/lightning_work/compute_content.rst diff --git a/docs/source-app/levels/intermediate/level_12.rst b/docs/source-app/levels/intermediate/level_12.rst new file mode 100644 index 0000000000000..644ac59d98133 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_12.rst @@ -0,0 +1,10 @@ +###################### +Level 12: Flow vs Work +###################### +**Audience:** Users who need to do non trivial workloads in their apps. + +**Prereqs:** Level 8+ + +---- + +.. include:: ../../workflows/build_lightning_component/from_scratch_component_content.rst diff --git a/docs/source-app/levels/intermediate/level_13.rst b/docs/source-app/levels/intermediate/level_13.rst new file mode 100644 index 0000000000000..e11132eb3d806 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_13.rst @@ -0,0 +1,10 @@ +######################################## +Level 13: Communication in Lighting Apps +######################################## +**Audience:** Users that want to create complex reactive apps. + +**Prereqs:** Level 8+ + +---- + +.. include:: ../../workflows/access_app_state/access_app_state_content.rst diff --git a/docs/source-app/levels/intermediate/level_14.rst b/docs/source-app/levels/intermediate/level_14.rst new file mode 100644 index 0000000000000..c7b0d2c4abdd8 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_14.rst @@ -0,0 +1,10 @@ +############################################################### +Level 14: Communication between LightningFlow and LightningWork +############################################################### +**Audience:** Users who have multiple LightningWorks communicating with LightningFlows. + +**Prereqs:** Level 8+ and read the `Communication in Lighting Apps article <../../access_app_state.html>`_. + +---- + +.. include:: ../../core_api/lightning_app/communication_content.rst diff --git a/docs/source-app/levels/intermediate/level_15.rst b/docs/source-app/levels/intermediate/level_15.rst new file mode 100644 index 0000000000000..3f19aea603be1 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_15.rst @@ -0,0 +1,10 @@ +######################################## +Level 15: Share files between components +######################################## +**Audience:** Users who are moving large files such as artifacts or datasets. + +**Prereqs:** Level 8+ + +---- + +.. include:: ../../glossary/storage/drive_content.rst diff --git a/docs/source-app/levels/intermediate/level_16.rst b/docs/source-app/levels/intermediate/level_16.rst new file mode 100644 index 0000000000000..2921731b2605c --- /dev/null +++ b/docs/source-app/levels/intermediate/level_16.rst @@ -0,0 +1,10 @@ +######################################## +Level 16: Run LightningWorks in parallel +######################################## +**Audience:** Users who want to run multiple LightningWorks at once. + +**Prereqs:** Level 8+ + +---- + +.. include:: ../../workflows/run_work_in_parallel_content.rst diff --git a/docs/source-app/levels/intermediate/level_8.rst b/docs/source-app/levels/intermediate/level_8.rst new file mode 100644 index 0000000000000..a40fe75e82b28 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_8.rst @@ -0,0 +1,10 @@ +########################################### +Level 8: Build a Lightning App from Scratch +########################################### +**Audience:** Users who want to build an Lightning App from scratch + +**Prereqs:** You must have finished the `Basic levels `_. + +---- + +.. include:: ../../workflows/build_lightning_app/from_scratch_content.rst diff --git a/docs/source-app/levels/intermediate/level_9.rst b/docs/source-app/levels/intermediate/level_9.rst new file mode 100644 index 0000000000000..33a2e4f50c587 --- /dev/null +++ b/docs/source-app/levels/intermediate/level_9.rst @@ -0,0 +1,10 @@ +################### +Level 9: Event loop +################### +**Audience:** Users who want to build reactive Lightning Apps and move beyond DAGs. + +**Prereqs:** Level 8+ + +---- + +.. include:: ../../glossary/event_loop.rst diff --git a/docs/source-app/pages/lightning_apps_intro.rst b/docs/source-app/lightning_apps_intro.rst similarity index 73% rename from docs/source-app/pages/lightning_apps_intro.rst rename to docs/source-app/lightning_apps_intro.rst index ba3c962f165a8..b3b5d5498c023 100644 --- a/docs/source-app/pages/lightning_apps_intro.rst +++ b/docs/source-app/lightning_apps_intro.rst @@ -1,13 +1,3 @@ -.. toctree:: - :maxdepth: 1 - :hidden: - - ../workflows/extend_app - ../workflows/build_lightning_component/index - ../glossary/lightning_app_overview/index - ../workflows/run_on_private_cloud - - ############################ Lightning Apps in 15 minutes ############################ @@ -18,7 +8,7 @@ Lightning Apps in 15 minutes ---- -The app we build in this guide trains and deploys a model. +The App we build in this guide trains and deploys a model. .. (|qs_app|). @@ -28,16 +18,9 @@ The app we build in this guide trains and deploys a model.
see the app live here -A Lightning app is **Organized Python**, it enables AI researchers and ML engineers to build complex AI workflows without any of the **cloud** boilerplate. - -With Lightning apps your favorite components can work together on any machine at any scale. Here's an illustration: - -.. raw:: html - - +A Lightning App is **Organized Python**, it enables AI researchers and ML engineers to build complex AI workflows without any of the **cloud** boilerplate. -| +With Lightning Apps your favorite components can work together on any machine at any scale. Here's an illustration: Lightning Apps are: @@ -54,10 +37,10 @@ Lightning Apps are: ---- -******************* -Who can build apps? -******************* -Anyone who knows Python can build a Lightning app, even without ML experience. +***************************** +Who can build Lightning Apps? +***************************** +Anyone who knows Python can build a Lightning App, even without machine learning experience. ---- @@ -65,20 +48,20 @@ Anyone who knows Python can build a Lightning app, even without ML experience. Step 1: Install Lightning ************************* -First, you'll need to install Lightning (make sure you use Python 3.8+). +If you are using a :doc:`virtual environment`, don't forget to activate it before running commands. You must do so in every new shell. We highly recommend using virtual environments. -.. code:: bash +.. code:: python - python -m pip install -U lightning + pip install lightning -(pip and conda install coming soon) +(conda install coming soon) ---- ******************************** Step 2: Install Train Deploy App ******************************** -The first Lightning app we'll explore is an app to train and deploy a machine learning model. +The first Lightning App we'll explore is an app to train and deploy a machine learning model. .. [|qs_code|], [|qs_live_app|]. @@ -89,21 +72,20 @@ The first Lightning app we'll explore is an app to train and deploy a machine le .. |qs_code| raw:: html - code + code -Install this app by typing: +Install this App by typing: .. code-block:: bash lightning install app lightning/quick-start -Verify the app was succesfully installed: +Verify the App was succesfully installed: .. code-block:: bash cd lightning-quick-start - ls ---- @@ -140,19 +122,18 @@ Add the ``--cloud`` argument to run on the `Lightning.AI cloud .. displayitem:: - :header: Add components to your app - :description: Expand your app by adding components. + :header: Add components to your App + :description: Expand your App by adding components. :col_css: col-md-4 - :button_link: ../workflows/extend_app.html + :button_link: workflows/extend_app.html :height: 180 .. displayitem:: :header: Build a component :description: Learn to build your own component. :col_css: col-md-4 - :button_link: ../workflows/build_lightning_component/index.html + :button_link: workflows/build_lightning_component/index.html :height: 180 .. displayitem:: - :header: Explore more apps + :header: Explore more Apps :description: Explore more apps for inspiration. :col_css: col-md-4 :button_link: https://lightning.ai/apps @@ -450,10 +421,10 @@ Depending on your use case, you might want to check one of these out next. :button_link: core_api/lightning_app/index.html :height: 180 -.. displayitem::workflo +.. displayitem:: :header: Run on your private cloud - :description: Run lightning apps on your private VPC or on-prem. - :button_link: ../workflows/run_on_private_cloud.html + :description: Run Lightning Apps on your private VPC or on-prem. + :button_link: workflows/run_on_private_cloud.html :col_css: col-md-4 :height: 180 diff --git a/docs/source-app/pages/moving_to_the_cloud.rst b/docs/source-app/moving_to_the_cloud.rst similarity index 85% rename from docs/source-app/pages/moving_to_the_cloud.rst rename to docs/source-app/moving_to_the_cloud.rst index 27d01c11a1eea..210fe32db8b24 100644 --- a/docs/source-app/pages/moving_to_the_cloud.rst +++ b/docs/source-app/moving_to_the_cloud.rst @@ -2,9 +2,9 @@ .. _moving_to_the_cloud: -******************* +#################### Moving to the Cloud -******************* +#################### .. warning:: This is in progress and not yet fully supported. @@ -14,8 +14,12 @@ that trains an image classifier and serve it once trained. In this tutorial, you'll learn how to extend that application so that it works seamlessly both locally and in the cloud. +---- + +******************************** Step 1: Distributed Application -=============================== +******************************** + Distributed Storage ^^^^^^^^^^^^^^^^^^^ @@ -36,18 +40,17 @@ Without doing this conscientiously for every single path, your application will In the example below, a file written by **SourceFileWork** is being transferred by the flow to the **DestinationFileAndServeWork** work. The Path object is the reference to the file. -.. literalinclude:: ../../../examples/boring_app/app.py +.. literalinclude:: ../examples/app_boring/app.py :emphasize-lines: 5, 22, 28, 48 -In the ``../scripts/serve.py`` file, we are creating a **FastApi Service** running on port ``1111`` +In the ``scripts/serve.py`` file, we are creating a **FastApi Service** running on port ``1111`` that returns the content of the file received from **SourceFileWork** when a post request is sent to ``/file``. -.. literalinclude:: ../../../examples/boring_app/scripts/serve.py +.. literalinclude:: ../examples/app_boring/scripts/serve.py :emphasize-lines: 21, 23-26 - - +---- Distributed Frontend ^^^^^^^^^^^^^^^^^^^^ @@ -61,14 +64,14 @@ In order to assemble them, you need to do two things: Here's how to expose the port: -.. literalinclude:: ../../../examples/boring_app/app.py +.. literalinclude:: ../examples/app_boring/app.py :emphasize-lines: 8 :lines: 33-44 And here's how to expose your services within the ``configure_layout`` flow hook: -.. literalinclude:: ../../../examples/boring_app/app.py +.. literalinclude:: ../examples/app_boring/app.py :emphasize-lines: 5 :lines: 53-57 @@ -76,7 +79,7 @@ In this example, we're appending ``/file`` to our **FastApi Service** url. This means that our ``Boring Tab`` triggers the ``get_file_content`` from the **FastAPI Service** and embeds its content as an `IFrame `_. -.. literalinclude:: ../../../examples/boring_app/scripts/serve.py +.. literalinclude:: ../examples/app_boring/scripts/serve.py :lines: 23-26 @@ -86,9 +89,11 @@ Here's a visualization of the application described above: :alt: Storage API Animation :width: 100 % +---- +***************************** Step 2: Scalable Application -============================ +***************************** The benefit of defining long-running code inside a :class:`~lightning_app.core.work.LightningWork` @@ -101,8 +106,11 @@ By adapting the :ref:`quick_start` example as follows, you can easily run your c Without doing much, you’re now running a script on its own cluster of machines! 🤯 +---- + +***************************** Step 3: Resilient Application -============================= +***************************** We designed Lightning with a strong emphasis on supporting failure cases. The framework shines when the developer embraces our fault-tolerance best practices, diff --git a/docs/source-app/pages/installation.rst b/docs/source-app/pages/installation.rst deleted file mode 100644 index 6e704f2bda365..0000000000000 --- a/docs/source-app/pages/installation.rst +++ /dev/null @@ -1,26 +0,0 @@ - -.. _install: - - -############ -Installation -############ - -We strongly recommend to create a virtual environment first. -Don't know what this is? Follow our `beginner guide here `_. - -Python >= 3.8 is required. - -**************** -Install with pip -**************** - -0. Activate your virtual environment. - -1. Install the lightning package - - .. code:: bash - - python -m pip install -U lightning - - The ``--extra-index-url`` won't be required after June 16. diff --git a/docs/source-app/pages/quickstart.rst b/docs/source-app/quickstart.rst similarity index 92% rename from docs/source-app/pages/quickstart.rst rename to docs/source-app/quickstart.rst index 4ea358fa650ff..52591b3dc6c0e 100644 --- a/docs/source-app/pages/quickstart.rst +++ b/docs/source-app/quickstart.rst @@ -2,20 +2,23 @@ .. _quick_start: -*********** +############ Quick Start -*********** +############ In this guide, we'll run an application that trains an image classification model with the `MNIST Dataset `_, and uses `FastAPI `_ to serve it. +---- + +********************** Step 1 - Installation -===================== +********************** First, you'll need to install Lightning from source. You can find the complete guide here: :ref:`install`. -Then, you'll need to install the `Lightning Quick Start package `_. +Then, you'll need to install the `Lightning Quick Start package `_. .. code-block:: bash @@ -27,8 +30,11 @@ And download the training script used by the App: curl https://gist.githubusercontent.com/tchaton/b81c8d8ba0f4dd39a47bfa607d81d6d5/raw/8d9d70573a006d95bdcda8492e798d0771d7e61b/train_script.py > train_script.py +---- + +********************** Step 2 - Run the app -==================== +********************** To run your app, copy the following command to your local terminal: @@ -77,8 +83,10 @@ The build command will launch the app admin panel UI. In your app admin, you can :alt: Quick Start UI :width: 100 % +---- + This app behind the scenes --------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^ This application has one flow component which coordinates two works executing their own python script. Once the training is finished, the trained model weights are passed to the serve component. @@ -96,9 +104,11 @@ Here is the application timeline: :alt: Quick Start Timeline Application :width: 100 % +---- +************************************** Steps 3 - Build your app in the cloud -===================================== +************************************** Simply add **--cloud** to run this application in the cloud 🤯 @@ -115,9 +125,11 @@ And with just one line of code, run on cloud GPUs! Congratulations! You've now run your first application with Lightning. +---- +*********** Next steps -========== +*********** To learn how to build and modify apps, go to the :ref:`basics`. diff --git a/docs/source-app/read_me_first.rst b/docs/source-app/read_me_first.rst new file mode 100644 index 0000000000000..f55684ccd32bd --- /dev/null +++ b/docs/source-app/read_me_first.rst @@ -0,0 +1,61 @@ +.. toctree:: + :maxdepth: 1 + +################################ +New to Lightning? READ ME FIRST! +################################ + +Lightning is all about the APPS! THE LIGHTNING APPS! + +The Lightning framework is a distributed, modular, free, and open framework you can use to build all AI applications (Lightning Apps) where the components you want, interact together. + +Use the Lightning framework to build anything from production-ready, multi-cloud ML systems to simple research demos. + +With the framework you can run and even train your App models on premise or in the cloud on various CPU and GPU hardware. + +You can create your Apps from scratch, build them from a template, OR check out the `Apps Gallery `_ and +clone and then modify existing Apps for your own use. + +You can also build components for your Apps from scratch, from a template, OR use existing components from the `Components Gallery `_. + +---- + +************************** +I'm totally new (to AI/ML) +************************** + +No worries! Just `install Lightning `_ and then `clone and run Flashy `_! +Flashy will have you run your first ML models in no time, with no coding! + +While Flashy is the fastest way to get started with Machine Learning, eventually you'll want +to create your own Lightning Apps. When you're ready you can clone Apps and components +from the Apps and `App Gallery `_ (then modify them), build Apps or components +from templates, or build Apps and components from scratch. + +********************************************************* +I have some XP in AI/ML. LET'S BUILD SOME LIGHTNING APPS! +********************************************************* + +You have maybe built some demos or some enterprise production-ready ML Solutions, just not with Lightning. + +Let's get you started. Here's a few important things to keep in mind when building your Lightning Apps (App): + +* Always check the `Apps Gallery `_ and `Apps Components `_ first. Why build something from scratch when you can clone an App or add a component and then build off of someone else's work. +* You can run your Apps locally or in the cloud on CPUs or GPUs. To get started, you can only run one App locally. +* Lightning Apps consist of a `root LightningFlow component `_, that optionally contains a tree of 2 types of components: `LightningFlow `_ 🌊 and `LightningWork `_ ⚒️. Key functionality includes: + + * :ref:`A shared state between components. ` + * :ref:`A constantly running event loop for reactivity. ` + * :ref:`Dynamic attachment of components at runtime. ` + * Start and stop functionality of your works. + + +If you're ready to go, here's what you're gonna wanna do: + +#. `Install Lightning `_ +#. Check out the `Lightning in 15 minutes `_ topic. +#. Start learning those Lightning App Building Skills! + + * Start with Lightning's `Basic Skills `_. + * Move on to `Intermediate Skills `_. + * And when you're ready the `Advanced Skills `_. diff --git a/docs/source-app/pages/testing.rst b/docs/source-app/testing.rst similarity index 95% rename from docs/source-app/pages/testing.rst rename to docs/source-app/testing.rst index 8b38f21096330..51ec1841a4bbc 100644 --- a/docs/source-app/pages/testing.rst +++ b/docs/source-app/testing.rst @@ -10,8 +10,11 @@ TODO: Cleanup At the core of our system is an integration testing framework that will allow for a first-class experience creating integration tests for Lightning Apps. This document will explain how we can create a lightning app test, how we can execute it, and where to find more information. +---- + +*********** Philosophy ----------- +*********** Testing a Lightning app is unique. It is a superset of an application that converges machine learning, API development, and UI development. With that in mind, there are several philosophies (or "best practices") that you should adhere to: @@ -23,8 +26,11 @@ Testing a Lightning app is unique. It is a superset of an application that conve #. **Use your framework** - Testing apps should be framework agnostic. #. **Have fun!** - At the heart of testing is experimentation. Like any experiment, tests begin with a hypothesis of workability, but you can extend that to be more inclusive. Ask the question, write the test to answer your question, and make sure you have fun while doing it. +---- + +**************************************** Anatomy of a Lightning integration test ---------------------------------------- +**************************************** The following is a PyTest example of an integration test using the ``lightning_app.testing.testing`` module. @@ -48,7 +54,7 @@ The following is a PyTest example of an integration test using the ``lightning_a def test_v0_app_example(): command_line = [ - os.path.join(_PROJECT_ROOT, "examples/v0_app/app.py"), + os.path.join(_PROJECT_ROOT, "examples/app_v0/app.py"), "--blocking", "False", "--multiprocess", @@ -59,6 +65,8 @@ The following is a PyTest example of an integration test using the ``lightning_a assert "V0 App End" in str(result.stdout_bytes) assert result.exit_code == 0 +---- + Setting up the app ^^^^^^^^^^^^^^^^^^ @@ -72,8 +80,10 @@ To get started, you simply need to import the following: We will discuss ``application_testing`` in a bit, but first let's review the structure of ``TestLightningApp``. +---- + TestLightningApp -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^ The :class:`lightning_app.testing.testing.TestingLightningApp` class is available to use for provisioning and setting up your testing needs. Note that you do not need this class to move forward with testing. Any application that inherits ``LightningApp`` should suffice as long as you override the correct methods. Reviewing the TestLightnigApp we see some overrides that are already there. Please revuew the class for more information. @@ -97,6 +107,8 @@ Besides ``run_once`` there are a few other overrides available: These methods will skew your tests, so use them when needed. +---- + The Test ^^^^^^^^ @@ -105,7 +117,7 @@ We provide ``application_testing`` as a helper funtion to get your application u .. code-block:: command_line = [ - os.path.join(_PROJECT_ROOT, "examples/v0_app/app.py"), + os.path.join(_PROJECT_ROOT, "examples/app_v0/app.py"), "--blocking", "False", "--multiprocess", @@ -137,7 +149,10 @@ As mentioned earlier, ``application_testing`` is a helper method that allows you Since we injected "V0 App End" to the end of our test flow. The state was changed to ``AppStatus.STOPPING`` which means the process is done. Finally, we check the result's exit code to make sure that we did not throw an error during execution. +---- + +************ End to End ----------- +************ TODO diff --git a/docs/source-app/tutorials/hpo/hpo.rst b/docs/source-app/tutorials/hpo/hpo.rst deleted file mode 100644 index d6567194aee42..0000000000000 --- a/docs/source-app/tutorials/hpo/hpo.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. hpo: - -****************** -Build a Sweeps App -****************** - -Introduction -============ - -Traditionally, developing ML Products requires to choose among a large space of -hyperparameters while creating and training the ML models. Hyperparameter Optimization -(HPO) aims at finding a well-performing hyperparameter configuration of a given machine -learning model on a dataset at hand, including the machine learning model, -its hyperparameters and other data processing steps. - -Thus, HPO frees the human expert from a tedious and error-prone hyperparameter tuning process. - -As an example, in the famous `scikit-learn `_, hyperparameter are passed as arguments to the constructor of -the estimator classes such as ``C`` kernel for -`Support Vector Classifier `_, etc. - -It is possible and recommended to search the hyper-parameter space for the best validation score. - -An HPO search consists of: - -* an objective method -* a parameter space defined -* a method for searching or sampling candidates - -A naive method for sampling candidates is Grid Search which exhaustively considers all parameter combinations. - -Hopefully, the field of Hyperparameter Optimization is very active and many methods have been developed to -optimize the time required to get strong candidates. - -In the following tutorial, you will learn how to use Lightning together with the `Optuna `_. - -`Optuna `_ is an open source hyperparameter optimization framework to automate hyperparameter search. -Out-of-the-box, it provides efficient algorithms to search large spaces and prune unpromising trials for faster results. - -First, you will learn about the best practices on how to implement HPO without the Lightning Framework. -Secondly, we will dive into a working HPO application with Lightning and finally create a neat -`HiPlot UI `_ -for our application. - -HPO Example Without Lightning -============================= - -In the example below, we are emulating the Lightning Infinite Loop. - -We are assuming have already defined an ``ObjectiveWork`` component which is responsible to run the objective method and track the metric through its state. - -We are running ``TOTAL_TRIALS`` trials by series of ``SIMULTANEOUS_TRIALS`` trials. -When starting, ``TOTAL_TRIALS`` ``ObjectiveWork`` are created. - -The entire code runs within an infinite loop as it would within Lightning. - -When iterating through the works, if the current ``objective_work`` hasn't started, -some new parameters are sampled from the Optuna Study with our custom distributions -and then passed to run method of the ``objective_work``. - -The condition ``not objective_work.has_started`` will be ``False`` once ``objective_work.run()`` starts. - -Also, the second condition will be ``True`` when the metric finally is defined within the state of the work, -which happens after many iterations of the Lightning Infinite Loop. - -Finally, once the current ``SIMULTANEOUS_TRIALS`` have both registered their -metric to the Optuna Study, simply increments ``NUM_TRIALS`` by ``SIMULTANEOUS_TRIALS`` to launch the next trials. - -.. literalinclude:: ./hpo.py - :language: python - :emphasize-lines: 14, 16, 30-32, 37-40, 43, 46-47 - -Below, you can find the simplified version of the ``ObjectiveWork`` where the metric is randomly sampled using numpy. - -In realistic use case, the work executes some user defined code. - -.. literalinclude:: ./objective.py - :language: python - :emphasize-lines: 8, 19-21 - -Here are the logs produced when running the application above: - -.. code-block:: console - - $ python docs/source/tutorials/hpo/hpo.py - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - # After you have clicked `run` on the UI. - [I 2022-03-01 12:32:50,050] A new study created in memory with name: ... - {0: 13.994859806481264, 1: 59.866743330127825, ..., 5: 94.65919769609225} - -In the cloud, here is an animation how this application works: - -.. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/hpo.gif - :alt: Animation showing how to HPO works UI in a distributed manner. - - -HPO Example With Lightning -========================== - -Thanks the simplified version, you should have a good grasp on how to implement HPO with Optuna - -As the :class:`~lightning_app.core.app.LightningApp` handles the Infinite Loop, -it has been removed from within the run method of the HPORootFlow. - -However, the run method code is the same as the one defined above. - -.. literalinclude:: ../../../../examples/hpo/app_wo_ui.py - :language: python - :emphasize-lines: 5, 17-23, 52-59 - -The ``ObjectiveWork`` is sub-classing -the built-in :class:`~lightning_app.components.python.TracerPythonScript` -which enables to launch scripts and more. - -.. literalinclude:: ../../../../examples/hpo/objective.py - :language: python - :emphasize-lines: 9, 11, 15, 21-30, 40-46 - -Finally, let's add ``HiPlotFlow`` component to visualize our hyperparameter optimization. - -The metric and sampled parameters are added to the ``self.hi_plot.data`` list, enabling -to get the dashboard updated in near-realtime. - -.. literalinclude:: ../../../../examples/hpo/app_wi_ui.py - :diff: ../../../../examples/hpo/app_wo_ui.py - -Here is the associated code with the ``HiPlotFlow`` component. - -In the ``render_fn`` method, the state of the ``HiPlotFlow`` is passed. -The ``state.data`` is accessed as it contains the metric and sampled parameters. - -.. literalinclude:: ../../../../examples/hpo/hyperplot.py - -Run the HPO application with the following command: - -.. code-block:: console - - $ lightning run app examples/hpo/app_wi_ui.py - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - {0: ..., 1: ..., ..., 5: ...} - -Here is how the UI looks like when launched: - -.. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/hpo_ui_2.gif - :width: 100 % - :alt: Alternative text diff --git a/docs/source-app/pages/ui_and_frontends.rst b/docs/source-app/ui_and_frontends.rst similarity index 82% rename from docs/source-app/pages/ui_and_frontends.rst rename to docs/source-app/ui_and_frontends.rst index 0db452fdfb814..53e50cd534224 100644 --- a/docs/source-app/pages/ui_and_frontends.rst +++ b/docs/source-app/ui_and_frontends.rst @@ -14,8 +14,8 @@ You can easily embed other tools and services (like a GitHub repo, a `FastAPI Se To get started, you can use built-in templates for the following frameworks: -* `React.js `_ -* `StreamLit `_ +* `React.js `_ +* `StreamLit `_ diff --git a/docs/source-app/workflows/access_app_state.rst b/docs/source-app/workflows/access_app_state.rst deleted file mode 100644 index 881d2c684018d..0000000000000 --- a/docs/source-app/workflows/access_app_state.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. _access_app_state: - -################ -Access App State -################ - -**Audience:** Users who want to know how the app state can be accessed. - -**Level:** Basic - -*********************** -What is the App State ? -*********************** - -In Lightning, each component is stateful and their state are composed of all attributes defined within their **__init__** method. - -The **App State** is the collection of all the components state forming the application. - -***************************************** -What is the special about the App State ? -***************************************** - -The **App State** is always up-to-date, even running an application in the cloud on multiple machines. - -This means that every time an attribute is modified in a work, that information is automatically broadcasted to the flow. - -With this mechanism, any component can **react** to any other component **state changes** through the flow and complex system can be easily implemented. - -Lightning requires a state based driven mindset when implementing the flow. - -************************************ -When do I need to access the state ? -************************************ - -As a user, you are interacting with your component attributes, so most likely, -you won't need to access the component state directly, but it can be helpful to -understand how the state works under the hood. - -For example, here we define a **Flow** component and **Work** component, where the work increments a counter indefinitely and the flow prints its state which contains the work. - -You can easily check the state of your entire app as follows: - -.. literalinclude:: ../code_samples/quickstart/app_01.py - -Run the app with: - -.. code-block:: bash - - lightning run app docs/quickstart/app_01.py - -And here's the output you get when running the above application using **Lightning CLI**: - -.. code-block:: console - - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - State: {'works': {'w': {'vars': {'counter': 1}}}} - State: {'works': {'w': {'vars': {'counter': 2}}}} - State: {'works': {'w': {'vars': {'counter': 3}}}} - State: {'works': {'w': {'vars': {'counter': 3}}}} - State: {'works': {'w': {'vars': {'counter': 4}}}} - ... diff --git a/docs/source-app/workflows/access_app_state/access_app_state.rst b/docs/source-app/workflows/access_app_state/access_app_state.rst new file mode 100644 index 0000000000000..5f5be722d24aa --- /dev/null +++ b/docs/source-app/workflows/access_app_state/access_app_state.rst @@ -0,0 +1,11 @@ +############################## +Communication in Lighting Apps +############################## + +**Audience:** Users that want to create complex reactive apps. + +**Level:** Intermediate + +---- + +.. include:: access_app_state_content.rst diff --git a/docs/source-app/workflows/access_app_state/access_app_state_content.rst b/docs/source-app/workflows/access_app_state/access_app_state_content.rst new file mode 100644 index 0000000000000..f5a2c2eab03eb --- /dev/null +++ b/docs/source-app/workflows/access_app_state/access_app_state_content.rst @@ -0,0 +1,70 @@ +******************* +A bit of background +******************* + +Lightning allows you to create reactive distributed applications, where components can be distributed across different machines and even different clouds. + +To create reactive applications, components need to be able to communicate- share data, status, values, etc. For example, if you are creating an app that will retrain a model every time new data is added to a cloud dataset, you will need the dataset component to communicate to the training component. + +Lightning components can communicate via the app state. The app state is composed of all attributes defined within each components **__init__** method. + +All attributes of all LightningWork components are accessible in the LightningFlow components in real time. + +---- + +******************************* +What the App State does for you +******************************* + +Every time you update an attribute inside of a running work (separate process on local or remote machine), the attribute of the mirrored work in the flow side is updated with that same exact value automatically. + +Every time the app received a state update from a running work, the app applies the state change and re-executes the flow run method. This enables complex systems to be easily implemented through the state. + +The **App State** is the collection of all the components state forming the application and gets automatically up-to-date, even in distributed settings. + +---- + +******************** +Access the App State +******************** + +As a user, you are interacting with your component attributes, so most likely, +you won't need to access the component state directly, but it can be helpful to +understand how the state works under the hood. + +For example, here we define a **Flow** component and **Work** component, where the work increments a counter indefinitely and the flow prints its state which contains the work. + +You can easily check the state of your entire app as follows: + +.. literalinclude:: ../../workflows/access_app_state/app.py + +Run the app with: + +.. code-block:: bash + + lightning run app docs/source-app/workflows/access_app_state/app.py + +And here's the output you get when running the above application using **Lightning CLI**: + +.. code-block:: console + + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + State: {'works': {'w': {'vars': {'counter': 1}}}} + State: {'works': {'w': {'vars': {'counter': 2}}}} + State: {'works': {'w': {'vars': {'counter': 3}}}} + State: {'works': {'w': {'vars': {'counter': 3}}}} + State: {'works': {'w': {'vars': {'counter': 4}}}} + ... + + +*** +FAQ +*** + +* **How can a work update a flow ?** A work is a leaf in the component tree, therefore it can access / update only itself. + +* **How can a flow update a work ?** The flow can update the work state. However, once the run method is launched, the work is running isolated with a copy of state. If the flow keeps updating the work state, and the work does the same with different values, it creates a state divergence. In the future, we might support bi-directional state update between flow and works. + +* **How can a work update a work ?** No, the communication is simply between work and flow. The flow role is to coordinate and pass some state between works. + +* **How can a flow update a flow ?** Yes, the flows can update themselves as they are running in the same python process. diff --git a/docs/source-app/workflows/access_app_state/app.py b/docs/source-app/workflows/access_app_state/app.py new file mode 100644 index 0000000000000..0a435c61c9ed9 --- /dev/null +++ b/docs/source-app/workflows/access_app_state/app.py @@ -0,0 +1,27 @@ +import lightning as L +from lightning.app.utilities.app_helpers import pretty_state + + +class Work(L.LightningWork): + def __init__(self): + super().__init__(cache_calls=False) + # Attributes are registered automatically in the state. + self.counter = 0 + + def run(self): + # Incrementing an attribute gets reflected in the `Flow` state. + self.counter += 1 + + +class Flow(L.LightningFlow): + def __init__(self): + super().__init__() + self.w = Work() + + def run(self): + if self.w.has_started: + print(f"State: {pretty_state(self.state)} \n") + self.w.run() + + +app = L.LightningApp(Flow()) diff --git a/docs/source-app/workflows/add_components/index.rst b/docs/source-app/workflows/add_components/index.rst index c0656a81a69dc..ca95e44e1853d 100644 --- a/docs/source-app/workflows/add_components/index.rst +++ b/docs/source-app/workflows/add_components/index.rst @@ -1,3 +1,5 @@ +:orphan: + ########################### Add a component to your app ########################### @@ -11,7 +13,7 @@ Install a component Any Lightning component can be installed with: -.. code:: bash +.. code:: python lightning install component org/the-component-name diff --git a/docs/source-app/workflows/add_server/any_server.rst b/docs/source-app/workflows/add_server/any_server.rst index bdf09c67ee1e9..677478d70d740 100644 --- a/docs/source-app/workflows/add_server/any_server.rst +++ b/docs/source-app/workflows/add_server/any_server.rst @@ -21,39 +21,38 @@ Add a server to a component Any server that listens on a port, can be enabled via a work. For example, here's a plain python server: .. code:: python + :emphasize-lines: 11-12 import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) self.end_headers() - self.wfile.write(b"

Hello lit world ") - + html = "

Hello lit world " + self.wfile.write(html) - httpd = socketserver.TCPServer(("localhost", "3000"), PlainServer) + httpd = socketserver.TCPServer(('localhost', '3000'), PlainServer) httpd.serve_forever() To enable the server inside the component, start the server in the run method and use the ``self.host`` and ``self.port`` properties: .. code:: python - :emphasize-lines: 13, 14 + :emphasize-lines: 14-15 - import lightning_app as la + import lightning as L import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) self.end_headers() - self.wfile.write(b"

Hello lit world ") - + html = "

Hello lit world " + self.wfile.write(html) - class LitServer(lapp.LightningWork): + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) httpd.serve_forever() @@ -67,27 +66,25 @@ The final step, is to tell the Root component in which tab to render this compon In this case, we render the ``LitServer`` output in the ``home`` tab of the application. .. code:: python - :emphasize-lines: 19, 25 + :emphasize-lines: 20, 23, 28 - import lightning_app as la + import lightning as L import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) self.end_headers() - self.wfile.write(b"

Hello lit world ") - + html = "

Hello lit world " + self.wfile.write(html) - class LitServer(lapp.LightningWork): + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) httpd.serve_forever() - - class Root(lapp.LightningFlow): + class Root(L.LightningFlow): def __init__(self): super().__init__() self.lit_server = LitServer(parallel=True) @@ -96,11 +93,13 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl self.lit_server.run() def configure_layout(self): - tab1 = {"name": "home", "content": self.lit_server} + tab1 = { + 'name': 'home', + 'content': self.lit_server + } return tab1 - - app = lapp.LightningApp(Root()) + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in parallel while the rest of the Lightning App runs everything else. diff --git a/docs/source-app/workflows/add_server/flask_basic.rst b/docs/source-app/workflows/add_server/flask_basic.rst index dff64e12ca9b7..5ee2a232828c4 100644 --- a/docs/source-app/workflows/add_server/flask_basic.rst +++ b/docs/source-app/workflows/add_server/flask_basic.rst @@ -20,35 +20,33 @@ Add Flask to a component First, define your flask app as you normally would without Lightning: .. code:: python + :emphasize-lines: 9 from flask import Flask flask_app = Flask(__name__) - - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" - + return 'Hello, World!' - flask_app.run(host="0.0.0.0", port=80) + flask_app.run(host='0.0.0.0', port=80) To enable the server inside the component, start the Flask server in the run method and use the ``self.host`` and ``self.port`` properties: .. code:: python - :emphasize-lines: 11 + :emphasize-lines: 12 - import lightning_app as la + import lightning as L from flask import Flask - - class LitFlask(lapp.LightningWork): + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" + return 'Hello, World!' flask_app.run(host=self.host, port=self.port) @@ -61,24 +59,22 @@ The final step, is to tell the Root component in which tab to render this compon In this case, we render the ``LitFlask`` output in the ``home`` tab of the application. .. code:: python - :emphasize-lines: 16, 22 + :emphasize-lines: 17, 23 - import lightning_app as la + import lightning as L from flask import Flask - - class LitFlask(lapp.LightningWork): + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" + return 'Hello, World!' flask_app.run(host=self.host, port=self.port) - - class Root(lapp.LightningFlow): + class Root(L.LightningFlow): def __init__(self): super().__init__() self.lit_flask = LitFlask(parallel=True) @@ -87,11 +83,10 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli self.lit_flask.run() def configure_layout(self): - tab1 = {"name": "home", "content": self.lit_flask} + tab1 = {'name': 'home', 'content': self.lit_flask} return tab1 - - app = lapp.LightningApp(Root()) + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in the background while the rest of the Lightning App runs everything else. @@ -103,31 +98,16 @@ Run the app *********** Start the app to see your new UI! -.. code:: console +.. code:: bash lightning run app app.py To run the app on the cloud, use the ``--cloud`` argument. -.. code:: console +.. code:: bash lightning run app app.py --cloud -.. code:: python - - from flask import Flask - - - class LitFlask(lapp.LightningWork): - def run(self): - flask_app = Flask(__name__) - - @flask_app.route("/") - def hello(): - return "Hello, World!" - - flask_app.run(host=self.host, port=self.port) - ---- ******** diff --git a/docs/source-app/workflows/add_web_link.rst b/docs/source-app/workflows/add_web_link.rst index 4236359ef4b52..01ffdf62ef3e3 100644 --- a/docs/source-app/workflows/add_web_link.rst +++ b/docs/source-app/workflows/add_web_link.rst @@ -18,19 +18,23 @@ and connect the UIs. Create a file named **app.py** with this code: the app running here .. code:: python - :emphasize-lines: 5, 6, 7 + :emphasize-lines: 7,11 - import lightning_app as la + import lightning as L - - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def configure_layout(self): - tab_1 = {"name": "TB logs", "content": "https://bit.ly/tb-aasae"} - tab_2 = {"name": "Paper", "content": "https://arxiv.org/pdf/2107.12329.pdf"} + tab_1 = { + "name": "Logger", + "content": "https://bit.ly/tb-aasae" + } + tab_2 = { + "name": "Paper", + "content": "https://arxiv.org/pdf/2107.12329.pdf" + } return tab_1, tab_2 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) ---- @@ -39,12 +43,12 @@ Run the app *********** Run the app locally to see it! -.. code:: bash +.. code:: python lightning run app app.py Now run it on the cloud as well: -.. code:: bash +.. code:: python lightning run app app.py --cloud diff --git a/docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst b/docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst index 05b1731e4f253..095dee362bf48 100644 --- a/docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst @@ -1,3 +1,5 @@ +:orphan: + ########################################### Add a web UI with Angular.js (intermediate) ########################################### diff --git a/docs/source-app/workflows/add_web_ui/dash/basic.rst b/docs/source-app/workflows/add_web_ui/dash/basic.rst index 505a6535c161e..4316fc13a1992 100644 --- a/docs/source-app/workflows/add_web_ui/dash/basic.rst +++ b/docs/source-app/workflows/add_web_ui/dash/basic.rst @@ -39,26 +39,26 @@ First **create a file named app.py** with the app content: .. code:: bash - import lightning_app as la + import lightning as L import dash import plotly.express as px - class LitDash(lapp.LightningWork): + class LitDash(L.LightningWork): def run(self): - app = dash.Dash(__name__) + dash_app = dash.Dash(__name__) X = [1, 2, 3, 4, 5, 6] Y = [2, 4, 8, 16, 32, 64] fig = px.line(x=X, y=Y) - app.layout = dash.html.Div(children=[ + dash_app.layout = dash.html.Div(children=[ dash.html.H1(children='⚡ Hello Dash + Lightning⚡'), - dash.html.Div(children='''The Dash framework running inside a ⚡ Lightning App'''), + dash.html.Div(children='The Dash framework running inside a ⚡ Lightning App'), dash.dcc.Graph(id='example-graph', figure=fig) ]) - app.run_server(host=self.host, port=self.port) + dash_app.run_server(host=self.host, port=self.port) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_dash = LitDash(parallel=True) @@ -70,7 +70,7 @@ First **create a file named app.py** with the app content: tab1 = {"name": "home", "content": self.lit_dash} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) add 'dash' to a requirements.txt file: @@ -88,13 +88,13 @@ Run the app *********** Run the app locally to see it! -.. code:: bash +.. code:: python lightning run app app.py Now run it on the cloud as well: -.. code:: console +.. code:: python lightning run app app.py --cloud @@ -116,20 +116,18 @@ First, find the dash app you want to integrate. In this example, that app looks import dash import plotly.express as px - app = dash.Dash(__name__) + dash_app = dash.Dash(__name__) X = [1, 2, 3, 4, 5, 6] Y = [2, 4, 8, 16, 32, 64] fig = px.line(x=X, y=Y) - app.layout = dash.html.Div( - children=[ - dash.html.H1(children="⚡ Hello Dash + Lightning⚡"), - dash.html.Div(children="""The Dash framework running inside a ⚡ Lightning App"""), - dash.dcc.Graph(id="example-graph", figure=fig), - ] - ) + dash_app.layout = dash.html.Div(children=[ + dash.html.H1(children='⚡ Hello Dash + Lightning⚡'), + dash.html.Div(children='The Dash framework running inside a ⚡ Lightning App'), + dash.dcc.Graph(id='example-graph', figure=fig) + ]) - app.run_server(host="0.0.0.0", port=80) + dash_app.run_server(host='0.0.0.0', port=80) This dash app plots a simple line curve along with some HTMlapp. `Visit the Dash documentation for the full API `_. @@ -143,30 +141,26 @@ Add the dash app to the run method of a ``LightningWork`` component and run the .. code:: python :emphasize-lines: 6, 18 - import lightning_app as la + import lightning as L import dash import plotly.express as px - - class LitDash(lapp.LightningWork): + class LitDash(L.LightningWork): def run(self): - app = dash.Dash(__name__) + dash_app = dash.Dash(__name__) X = [1, 2, 3, 4, 5, 6] Y = [2, 4, 8, 16, 32, 64] fig = px.line(x=X, y=Y) - app.layout = dash.html.Div( - children=[ - dash.html.H1(children="⚡ Hello Dash + Lightning⚡"), - dash.html.Div(children="""The Dash framework running inside a ⚡ Lightning App"""), - dash.dcc.Graph(id="example-graph", figure=fig), - ] - ) - - app.run_server(host=self.host, port=self.port) + dash_app.layout = dash.html.Div(children=[ + dash.html.H1(children='⚡ Hello Dash + Lightning⚡'), + dash.html.Div(children='The Dash framework running inside a ⚡ Lightning App'), + dash.dcc.Graph(id='example-graph', figure=fig) + ]) + dash_app.run_server(host=self.host, port=self.port) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_dash = LitDash(parallel=True) @@ -178,8 +172,7 @@ Add the dash app to the run method of a ``LightningWork`` component and run the tab1 = {"name": "home", "content": self.lit_dash} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) ---- @@ -191,30 +184,26 @@ In this case, we render the ``LitDash`` UI in the ``home`` tab of the applicatio .. code:: python :emphasize-lines: 23, 29 - import lightning_app as la + import lightning as L import dash import plotly.express as px - - class LitDash(lapp.LightningWork): + class LitDash(L.LightningWork): def run(self): - app = dash.Dash(__name__) + dash_app = dash.Dash(__name__) X = [1, 2, 3, 4, 5, 6] Y = [2, 4, 8, 16, 32, 64] fig = px.line(x=X, y=Y) - app.layout = dash.html.Div( - children=[ - dash.html.H1(children="⚡ Hello Dash + Lightning⚡"), - dash.html.Div(children="""The Dash framework running inside a ⚡ Lightning App"""), - dash.dcc.Graph(id="example-graph", figure=fig), - ] - ) - - app.run_server(host=self.host, port=self.port) + dash_app.layout = dash.html.Div(children=[ + dash.html.H1(children='⚡ Hello Dash + Lightning⚡'), + dash.html.Div(children='The Dash framework running inside a ⚡ Lightning App'), + dash.dcc.Graph(id='example-graph', figure=fig) + ]) + dash_app.run_server(host=self.host, port=self.port) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_dash = LitDash(parallel=True) @@ -226,8 +215,7 @@ In this case, we render the ``LitDash`` UI in the ``home`` tab of the applicatio tab1 = {"name": "home", "content": self.lit_dash} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in the background while the rest of the Lightning App runs everything else. diff --git a/docs/source-app/workflows/add_web_ui/dash/index.rst b/docs/source-app/workflows/add_web_ui/dash/index.rst index 1364172543d5b..5abb444c8e9a9 100644 --- a/docs/source-app/workflows/add_web_ui/dash/index.rst +++ b/docs/source-app/workflows/add_web_ui/dash/index.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :maxdepth: 1 :hidden: diff --git a/docs/source-app/workflows/add_web_ui/dash/intermediate.rst b/docs/source-app/workflows/add_web_ui/dash/intermediate.rst index 1b1abd4625ed7..fac428238a5e0 100644 --- a/docs/source-app/workflows/add_web_ui/dash/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/dash/intermediate.rst @@ -10,11 +10,33 @@ Add a web UI with Dash (intermediate) ******************************* Interact with the App from Dash ******************************* -TODO: is this a thing? + +In the example below, every time you change the select year on the dashboard, this is directly communicated to the flow +and another work process the associated data frame with the provided year. + +.. literalinclude:: intermediate_plot.py + +Here is how the app looks like once running: + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/dash_plot.gif ---- *********************************** Interact with Dash from a component *********************************** -TODO: is this a thing? + +In the example below, when you click the toggle, the state of the work appears. + +Install the following libraries if you want to run the app. + +```bash +pip install dash_daq dash_renderjson +``` + +.. literalinclude:: intermediate_state.py + + +Here is how the app looks like once running: + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/dash_state.gif diff --git a/docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py b/docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py new file mode 100644 index 0000000000000..52860fe90d75b --- /dev/null +++ b/docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py @@ -0,0 +1,88 @@ +from typing import Optional + +import pandas as pd +import plotly.express as px +from dash import Dash, dcc, html, Input, Output + +import lightning as L +from lightning.app.storage.payload import Payload +from lightning_app.core.work import LightningWork + + +class LitDash(L.LightningWork): + def __init__(self): + super().__init__(parallel=True) + self.df = None + self.selected_year = None + + def run(self): + + df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv") + self.df = Payload(df) + + dash_app = Dash(__name__) + + dash_app.layout = html.Div( + [ + dcc.Graph(id="graph-with-slider"), + dcc.Slider( + df["year"].min(), + df["year"].max(), + step=None, + value=df["year"].min(), + marks={str(year): str(year) for year in df["year"].unique()}, + id="year-slider", + ), + ] + ) + + @dash_app.callback(Output("graph-with-slider", "figure"), Input("year-slider", "value")) + def update_figure(selected_year): + self.selected_year = selected_year + filtered_df = df[df.year == selected_year] + + fig = px.scatter( + filtered_df, + x="gdpPercap", + y="lifeExp", + size="pop", + color="continent", + hover_name="country", + log_x=True, + size_max=55, + ) + + fig.update_layout(transition_duration=500) + + return fig + + dash_app.run_server(host=self.host, port=self.port) + + +class Processor(LightningWork): + def run(self, df: Payload, selected_year: Optional[str]): + if selected_year: + df = df.value + filtered_df = df[df.year == selected_year] + print(f"[PROCESSOR|selected_year={selected_year}]") + print(filtered_df) + + +class LitApp(L.LightningFlow): + def __init__(self): + super().__init__() + self.lit_dash = LitDash() + self.processor = Processor(parallel=True) + + def run(self): + self.lit_dash.run() + + # Launch some processing based on the Dash Dashboard. + self.processor.run(self.lit_dash.df, self.lit_dash.selected_year) + + def configure_layout(self): + tab1 = {"name": "home", "content": self.lit_dash} + return tab1 + + +app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/add_web_ui/dash/intermediate_state.py b/docs/source-app/workflows/add_web_ui/dash/intermediate_state.py new file mode 100644 index 0000000000000..d2e37a5a37bc0 --- /dev/null +++ b/docs/source-app/workflows/add_web_ui/dash/intermediate_state.py @@ -0,0 +1,39 @@ +import dash +import dash_daq as daq +import dash_renderjson +from dash import html, Input, Output + +import lightning as L +from lightning.app.utilities.state import AppState + + +class LitDash(L.LightningWork): + def run(self): + dash_app = dash.Dash(__name__) + + dash_app.layout = html.Div([daq.ToggleSwitch(id="my-toggle-switch", value=False), html.Div(id="output")]) + + @dash_app.callback(Output("output", "children"), [Input("my-toggle-switch", "value")]) + def display_output(value): + if value: + state = AppState() + state._request_state() + return dash_renderjson.DashRenderjson(id="input", data=state._state, max_depth=-1, invert_theme=True) + + dash_app.run_server(host=self.host, port=self.port) + + +class LitApp(L.LightningFlow): + def __init__(self): + super().__init__() + self.lit_dash = LitDash(parallel=True) + + def run(self): + self.lit_dash.run() + + def configure_layout(self): + tab1 = {"name": "home", "content": self.lit_dash} + return tab1 + + +app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/add_web_ui/gradio/basic.rst b/docs/source-app/workflows/add_web_ui/gradio/basic.rst index f278ccf22e01c..fbfdd55553967 100644 --- a/docs/source-app/workflows/add_web_ui/gradio/basic.rst +++ b/docs/source-app/workflows/add_web_ui/gradio/basic.rst @@ -39,15 +39,14 @@ First **create a file named app.py** with the app content: .. code:: python - import lightning_app as la + import lightning as L from lightning_app.components.serve import ServeGradio import gradio as gr - class LitGradio(ServeGradio): - inputs = gr.inputs.Textbox(default="lightning", label="name input") - outputs = gr.outputs.Textbox(label="output") + inputs = gr.inputs.Textbox(default='lightning', label='name input') + outputs = gr.outputs.Textbox(label='output') examples = [["hello lightning"]] def predict(self, input_text): @@ -57,8 +56,7 @@ First **create a file named app.py** with the app content: fake_model = lambda x: f"hello {x}" return fake_model - - class RootFlow(lapp.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() self.lit_gradio = LitGradio() @@ -69,8 +67,7 @@ First **create a file named app.py** with the app content: def configure_layout(self): return [{"name": "home", "content": self.lit_gradio}] - - app = lapp.LightningApp(RootFlow()) + app = L.LightningApp(RootFlow()) add "gradio" to a requirements.txt file: @@ -87,13 +84,13 @@ Run the app *********** Run the app locally to see it! -.. code:: bash +.. code:: python lightning run app app.py Now run it on the cloud as well: -.. code:: bash +.. code:: python lightning run app app.py --cloud @@ -126,11 +123,10 @@ Here's an example: from lightning_app.components.serve import ServeGradio import gradio as gr - class LitGradio(ServeGradio): - inputs = gr.inputs.Textbox(default="lightning", label="name input") - outputs = gr.outputs.Textbox(label="output") + inputs = gr.inputs.Textbox(default='lightning', label='name input') + outputs = gr.outputs.Textbox(label='output') def predict(self, input_text): return self.model(input_text) @@ -151,15 +147,14 @@ In this case, we render the ``LitGradio`` UI in the ``home`` tab of the applicat .. code:: python :emphasize-lines: 21, 27 - import lightning_app as la + import lightning as L from lightning_app.components.serve import ServeGradio import gradio as gr - class LitGradio(ServeGradio): - inputs = gr.inputs.Textbox(default="lightning", label="name input") - outputs = gr.outputs.Textbox(label="output") + inputs = gr.inputs.Textbox(default='lightning', label='name input') + outputs = gr.outputs.Textbox(label='output') examples = [["hello lightning"]] def predict(self, input_text): @@ -169,8 +164,7 @@ In this case, we render the ``LitGradio`` UI in the ``home`` tab of the applicat fake_model = lambda x: f"hello {x}" return fake_model - - class RootFlow(lapp.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() self.lit_gradio = LitGradio() @@ -181,8 +175,7 @@ In this case, we render the ``LitGradio`` UI in the ``home`` tab of the applicat def configure_layout(self): return [{"name": "home", "content": self.lit_gradio}] - - app = lapp.LightningApp(RootFlow()) + app = L.LightningApp(RootFlow()) ---- @@ -193,15 +186,14 @@ Finally, don't forget to call run inside the Root Flow to serve the Gradio app. .. code:: python :emphasize-lines: 24 - import lightning_app as la + import lightning as L from lightning_app.components.serve import ServeGradio import gradio as gr - class LitGradio(ServeGradio): - inputs = gr.inputs.Textbox(default="lightning", label="name input") - outputs = gr.outputs.Textbox(label="output") + inputs = gr.inputs.Textbox(default='lightning', label='name input') + outputs = gr.outputs.Textbox(label='output') examples = [["hello lightning"]] def predict(self, input_text): @@ -211,8 +203,7 @@ Finally, don't forget to call run inside the Root Flow to serve the Gradio app. fake_model = lambda x: f"hello {x}" return fake_model - - class RootFlow(lapp.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() self.lit_gradio = LitGradio() @@ -223,5 +214,4 @@ Finally, don't forget to call run inside the Root Flow to serve the Gradio app. def configure_layout(self): return [{"name": "home", "content": self.lit_gradio}] - - app = lapp.LightningApp(RootFlow()) + app = L.LightningApp(RootFlow()) diff --git a/docs/source-app/workflows/add_web_ui/gradio/index.rst b/docs/source-app/workflows/add_web_ui/gradio/index.rst index a4d8d08eb9c50..740ae93aae0c4 100644 --- a/docs/source-app/workflows/add_web_ui/gradio/index.rst +++ b/docs/source-app/workflows/add_web_ui/gradio/index.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :maxdepth: 1 :hidden: diff --git a/docs/source-app/workflows/add_web_ui/gradio/intermediate.rst b/docs/source-app/workflows/add_web_ui/gradio/intermediate.rst index acbfa8e4a6dc0..bb20d566243b1 100644 --- a/docs/source-app/workflows/add_web_ui/gradio/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/gradio/intermediate.rst @@ -4,19 +4,18 @@ Add a web UI with Gradio (intermediate) .. note:: documentation coming soon. -.. - ************************************* - Interact with a component from the UI - ************************************* - .. warning:: is there such a thing for this with gradio? - -.. - ---- - -.. - ************************************* - Interact with the UI from a component - ************************************* - .. warning:: is there such a thing for this with gradio? - - ---- + +************************************* +Interact with a component from the UI +************************************* + +.. warning:: is there such a thing for this with gradio? + + +---- + +************************************* +Interact with the UI from a component +************************************* + +.. warning:: is there such a thing for this with gradio? diff --git a/docs/source-app/workflows/add_web_ui/html/basic.rst b/docs/source-app/workflows/add_web_ui/html/basic.rst index 685a495aff5cb..83b6fb7d13e21 100644 --- a/docs/source-app/workflows/add_web_ui/html/basic.rst +++ b/docs/source-app/workflows/add_web_ui/html/basic.rst @@ -36,7 +36,7 @@ The first step is to create an HTML file named **index.html**: ---- ************************ -Create the html demo app +Create the HTML demo app ************************ .. @@ -52,13 +52,13 @@ First **create a file named app.py** with the app content (in the same folder as .. code:: bash # app.py - import lightning_app as la + import lightning as L - class HelloComponent(lapp.LightningFlow): + class HelloComponent(L.LightningFlow): def configure_layout(self): - return lapp.frontend.web.StaticWebFrontend(serve_dir='.') + return L.frontend.web.StaticWebFrontend(serve_dir='.') - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.hello_component = HelloComponent() @@ -67,7 +67,7 @@ First **create a file named app.py** with the app content (in the same folder as tab1 = {"name": "home", "content": self.hello_component} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) ---- @@ -76,13 +76,13 @@ Run the app *********** Run the app locally to see it! -.. code:: bash +.. code:: python lightning run app app.py Now run it on the cloud as well: -.. code:: bash +.. code:: python lightning run app app.py --cloud @@ -103,13 +103,13 @@ Give the component an HTML UI, by returning a ``StaticWebFrontend`` object from :emphasize-lines: 5,6 # app.py - import lightning_app as la + import lightning as L - class HelloComponent(lapp.LightningFlow): + class HelloComponent(L.LightningFlow): def configure_layout(self): - return lapp.frontend.web.StaticWebFrontend(serve_dir='.') + return L.frontend.web.StaticWebFrontend(serve_dir='.') - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.hello_component = HelloComponent() @@ -118,7 +118,7 @@ Give the component an HTML UI, by returning a ``StaticWebFrontend`` object from tab1 = {"name": "home", "content": self.hello_component} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) The folder path given in ``StaticWebFrontend(serve_dir=)`` must point to a folder with an ``index.html`` page. @@ -133,13 +133,13 @@ In this case, we render the ``HelloComponent`` UI in the ``home`` tab of the app :emphasize-lines: 14, 15 # app.py - import lightning_app as la + import lightning as L - class HelloComponent(lapp.LightningFlow): + class HelloComponent(L.LightningFlow): def configure_layout(self): - return lapp.frontend.web.StaticWebFrontend(serve_dir='.') + return L.frontend.web.StaticWebFrontend(serve_dir='.') - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.hello_component = HelloComponent() @@ -148,4 +148,4 @@ In this case, we render the ``HelloComponent`` UI in the ``home`` tab of the app tab1 = {"name": "home", "content": self.hello_component} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/add_web_ui/html/index.rst b/docs/source-app/workflows/add_web_ui/html/index.rst index 0e4830d3a9e36..0eae9309877a7 100644 --- a/docs/source-app/workflows/add_web_ui/html/index.rst +++ b/docs/source-app/workflows/add_web_ui/html/index.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :maxdepth: 1 :hidden: diff --git a/docs/source-app/workflows/add_web_ui/index.rst b/docs/source-app/workflows/add_web_ui/index.rst index 54180d4df5573..79c0f16d66409 100644 --- a/docs/source-app/workflows/add_web_ui/index.rst +++ b/docs/source-app/workflows/add_web_ui/index.rst @@ -1,7 +1,9 @@ + ############################# Add a web user interface (UI) ############################# -Every component in a Lightning App can have its own web user interface (UI). + +**Audience:** Users who want to add a UI to their Lightning Apps ---- diff --git a/docs/source-app/workflows/add_web_ui/index_content.rst b/docs/source-app/workflows/add_web_ui/index_content.rst index 6995d0e9b2768..9602537a53574 100644 --- a/docs/source-app/workflows/add_web_ui/index_content.rst +++ b/docs/source-app/workflows/add_web_ui/index_content.rst @@ -1,21 +1,3 @@ -.. toctree:: - :maxdepth: 1 - :hidden: - - dash/index - gradio/index - streamlit/index - -.. toctree:: - :maxdepth: 1 - :hidden: - - integrate_any_javascript_framework - angular_js_intermediate - html/index - react/index - vue_js_intermediate - ************************************* Web UIs for non Javascript Developers ************************************* diff --git a/docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst b/docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst index 7856d3713a9ff..f1da660b09b66 100644 --- a/docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst +++ b/docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst @@ -1,3 +1,5 @@ +:orphan: + ################################## Integrate any javascript framework ################################## diff --git a/docs/source-app/workflows/add_web_ui/jupyter_basic.rst b/docs/source-app/workflows/add_web_ui/jupyter_basic.rst index ce1916e527834..61f58ab40670d 100644 --- a/docs/source-app/workflows/add_web_ui/jupyter_basic.rst +++ b/docs/source-app/workflows/add_web_ui/jupyter_basic.rst @@ -17,6 +17,8 @@ What is a Jupyter Notebook? TODO +---- + ******************* Install Jupyter Lab ******************* diff --git a/docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst b/docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst index b3e1c7a9f6f36..524bdd57538cf 100644 --- a/docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst +++ b/docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst @@ -15,11 +15,11 @@ Example code To illustrate how to communicate between a React app and a lightning App, we'll be using the `example_app.py` file which `lightning init react-ui `_ created: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py and the App.tsx file also created by `lightning init react-ui `_: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/ui/src/App.tsx +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/ui/src/App.tsx ---- @@ -31,13 +31,13 @@ To change the Lightning app from the React app, use `updateLightningState`. In this example, when you press **Start printing** in the React UI, it toggles the `react_ui.vars.should_print`: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/ui/src/App.tsx +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/ui/src/App.tsx :emphasize-lines: 20, 21, 23 By changing that variable in the Lightning app state, it sets **react_ui.should_print** to True, which enables the Lightning app to print: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py :emphasize-lines: 10, 22 ---- @@ -49,10 +49,10 @@ To change the React app from the Lightning app, use the values from the `lightni In this example, when the `react_ui.counter`` increaes in the Lightning app: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py :emphasize-lines: 18, 24 The React UI updates the text on the screen to reflect the count -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/ui/src/App.tsx +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/ui/src/App.tsx :emphasize-lines: 15 diff --git a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst index 7b4b67583eae5..fe72d09b27eec 100644 --- a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst +++ b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst @@ -15,11 +15,11 @@ Example code To illustrate how to connect a React app and a lightning App, we'll be using the `example_app.py` file which `lightning init react-ui `_ created: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py and the App.tsx file also created by `lightning init react-ui `_: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/ui/src/App.tsx +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/ui/src/App.tsx ---- @@ -28,7 +28,7 @@ Connect the component to the react UI ************************************* The first step is to connect the dist folder of the react app using `StaticWebFrontend`: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py :emphasize-lines: 13 the dist folder must contain an index.html file which is generated by the compilating command `yarn build` which @@ -42,7 +42,7 @@ Connect component to the root flow Next, connect your component to the root flow. Display the react app on the tab of your choice using `configure_layout`: -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py :emphasize-lines: 19, 27 ---- @@ -59,7 +59,7 @@ At this point, the React app will render in the Lightning app. Test it out! However, to make powerful React+Lightning apps, you must also connect the Lightning App state to the react app. These lines enable two-way communication between the react app and the Lightning app. -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/ui/src/App.tsx +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/ui/src/App.tsx :emphasize-lines: 10, 13 ---- @@ -69,7 +69,7 @@ Component vs App **************** Notice that in this guide, we connected a single react app to a single component. -.. literalinclude:: ../../../../../lightning_app/cli/react-ui-template/example_app.py +.. literalinclude:: ../../../../../src/lightning_app/cli/react-ui-template/example_app.py :emphasize-lines: 6-13 You can use this single react app for the FULL Lightning app, or you can specify a React app for EACH component. @@ -77,20 +77,17 @@ You can use this single react app for the FULL Lightning app, or you can specify .. code:: python :emphasize-lines: 5, 9, 18-20 - import lightning_app as la + import lightning as L - - class ComponentA(lapp.LightningFlow): + class ComponentA(L.LightningFlow): def configure_layout(self): - return lapp.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist") - + return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist") - class ComponentB(lapp.LightningFlow): + class ComponentB(L.LightningFlow): def configure_layout(self): - return lapp.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist") + return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist") - - class HelloLitReact(lapp.LightningFlow): + class HelloLitReact(L.LightningFlow): def __init__(self): super().__init__() self.react_app_1 = ComponentA() @@ -101,7 +98,6 @@ You can use this single react app for the FULL Lightning app, or you can specify tab_2 = {"name": "App 2", "content": self.react_app_2} return tab_1, tab_2 - - app = lapp.LightningApp(HelloLitReact()) + app = L.LightningApp(HelloLitReact()) This is a powerful idea that allows each Lightning component to have a self-contained web UI. diff --git a/docs/source-app/workflows/add_web_ui/react/index.rst b/docs/source-app/workflows/add_web_ui/react/index.rst index 1a0463e10521a..ba0f8d97d67d1 100644 --- a/docs/source-app/workflows/add_web_ui/react/index.rst +++ b/docs/source-app/workflows/add_web_ui/react/index.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :maxdepth: 1 :hidden: diff --git a/docs/source-app/workflows/add_web_ui/streamlit/basic.rst b/docs/source-app/workflows/add_web_ui/streamlit/basic.rst index 920bd8d84ab01..464b558032a69 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/basic.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/basic.rst @@ -38,20 +38,17 @@ First **create a file named app.py** with the app content: .. code:: python # app.py - import lightning_app as la + import lightning as L import streamlit as st - def your_streamlit_app(lightning_app_state): - st.write("hello world") - + st.write('hello world') - class LitStreamlit(lapp.LightningFlow): + class LitStreamlit(L.LightningFlow): def configure_layout(self): - return lapp.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - + return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_streamlit = LitStreamlit() @@ -60,8 +57,7 @@ First **create a file named app.py** with the app content: tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) add "streamlit" to a requirements.txt file: @@ -78,13 +74,13 @@ Run the app *********** Run the app locally to see it! -.. code:: bash +.. code:: python lightning run app app.py Now run it on the cloud as well: -.. code:: bash +.. code:: python lightning run app app.py --cloud @@ -105,9 +101,8 @@ First, find the streamlit app you want to integrate. In this example, that app l import streamlit as st - def your_streamlit_app(): - st.write("hello world") + st.write('hello world') Refer to the `Streamlit documentation `_ for more complex examples. @@ -122,20 +117,17 @@ the ``configure_layout`` method of the Lightning component you want to connect t :emphasize-lines: 8-10 # app.py - import lightning_app as la + import lightning as L import streamlit as st - def your_streamlit_app(lightning_app_state): - st.write("hello world") - + st.write('hello world') - class LitStreamlit(lapp.LightningFlow): + class LitStreamlit(L.LightningFlow): def configure_layout(self): - return lapp.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_streamlit = LitStreamlit() @@ -144,8 +136,7 @@ the ``configure_layout`` method of the Lightning component you want to connect t tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) The ``render_fn`` argument of the ``StreamlitFrontend`` class, points to a function that runs your Streamlit app. The first argument to the function is the lightning app state. Any changes to the app state update the app. @@ -161,20 +152,17 @@ In this case, we render the ``LitStreamlit`` UI in the ``home`` tab of the appli :emphasize-lines: 18 # app.py - import lightning_app as la + import lightning as L import streamlit as st - def your_streamlit_app(lightning_app_state): - st.write("hello world") - + st.write('hello world') - class LitStreamlit(lapp.LightningFlow): + class LitStreamlit(L.LightningFlow): def configure_layout(self): - return lapp.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_streamlit = LitStreamlit() @@ -183,5 +171,4 @@ In this case, we render the ``LitStreamlit`` UI in the ``home`` tab of the appli tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/add_web_ui/streamlit/index.rst b/docs/source-app/workflows/add_web_ui/streamlit/index.rst index 68d9b2011f308..2496729d45660 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/index.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/index.rst @@ -1,3 +1,5 @@ +:orphan: + .. toctree:: :maxdepth: 1 :hidden: diff --git a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst index 0926575ab1596..08ab0e874bcca 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst @@ -15,11 +15,11 @@ To modify the variables of a Lightning component, access the ``lightning_app_sta For example, here we increase the count variable of the Lightning Component every time a user presses a button: -.. code:: bash +.. code:: python :emphasize-lines: 7, 13 # app.py - import lightning_app as la + import lightning as L import streamlit as st def your_streamlit_app(lightning_app_state): @@ -27,15 +27,15 @@ For example, here we increase the count variable of the Lightning Component ever lightning_app_state.count += 1 st.write(f'current count: {lightning_app_state.count}') - class LitStreamlit(lapp.LightningFlow): + class LitStreamlit(L.LightningFlow): def __init__(self): super().__init__() self.count = 0 def configure_layout(self): - return lapp.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_streamlit = LitStreamlit() @@ -44,7 +44,7 @@ For example, here we increase the count variable of the Lightning Component ever tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) ---- @@ -56,17 +56,17 @@ parent component. In this example we update the value of the counter from the component: -.. code:: bash +.. code:: python :emphasize-lines: 6, 14 # app.py - import lightning_app as la + import lightning as L import streamlit as st def your_streamlit_app(lightning_app_state): st.write(f'current count: {lightning_app_state.count}') - class LitStreamlit(lapp.LightningFlow): + class LitStreamlit(L.LightningFlow): def __init__(self): super().__init__() self.count = 0 @@ -75,9 +75,9 @@ In this example we update the value of the counter from the component: self.count += 1 def configure_layout(self): - return lapp.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_streamlit = LitStreamlit() @@ -89,4 +89,4 @@ In this example we update the value of the counter from the component: tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst b/docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst index 7caf13be940ef..e8d9f3e843155 100644 --- a/docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst @@ -1,3 +1,5 @@ +:orphan: + ####################################### Add a web UI with Vue.js (intermediate) ####################################### diff --git a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst index 5780d8e7b04f9..988018cd795e8 100644 --- a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst +++ b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst @@ -5,49 +5,33 @@ Arrange app tabs (basic) ---- -******************* -Enable a single tab -******************* +***************************** +Enable a full-page single tab +***************************** + To enable a single tab on the app UI, return a single dictionary from the ``configure_layout`` method: .. code:: python :emphasize-lines: 9 - import lightning_app as la - + import lightning as L - class DemoComponent(lapp.demo.dumb_component): + class DemoComponent(L.demo.dumb_component): def configure_layout(self): - tab1 = {"name": "THE TAB NAME", "content": self.component_a} + tab1 = { + "name": "THE TAB NAME", + "content": self.component_a + } return tab1 + app = L.LightningApp(DemoComponent()) - app = lapp.LightningApp(DemoComponent()) -The "name" key defines the visible name of the tab on the UI. +The "name" key defines the visible name of the tab on the UI. It also shows up in the URL. The **"content"** key defines the target component to render in that tab. +When returning a single tab element like shown above, the UI will display it in full-page mode. ----- - -***************************** -Enable a full-page single tab -***************************** - -.. code:: python - :emphasize-lines: 6 - - import lightning_app as la - - - class DemoComponent(lapp.demo.dumb_component): - def configure_layout(self): - tab1 = {"name": None, "content": self.component_a} - return tab1 - - - app = lapp.LightningApp(DemoComponent()) - ---- ******************** @@ -57,19 +41,17 @@ Enable multiple tabs .. code:: python :emphasize-lines: 7 - import lightning_app as la - + import lightning as L - class DemoComponent(lapp.demo.dumb_component): + class DemoComponent(L.demo.dumb_component): def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 + app = L.LightningApp(DemoComponent()) - app = lapp.LightningApp(DemoComponent()) - -order matters! Try any of the following configurations: +The order matters! Try any of the following configurations: .. code:: python :emphasize-lines: 4, 9 @@ -79,7 +61,6 @@ order matters! Try any of the following configurations: tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 - def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} diff --git a/docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst b/docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst index 8136d907fbc11..798212bf7dc95 100644 --- a/docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst +++ b/docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst @@ -75,10 +75,10 @@ The command above generates an app file like this: from your_app_name import ComponentA, ComponentB - import lightning_app as la + import lightning as L - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self) -> None: super().__init__() self.component_a = ComponentA() @@ -89,7 +89,7 @@ The command above generates an app file like this: self.component_b.run() - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) Now you can add your own components as you wish! diff --git a/docs/source-app/workflows/build_lightning_app/from_scratch.rst b/docs/source-app/workflows/build_lightning_app/from_scratch.rst index 4f0a169d4bd41..38335cff9d386 100644 --- a/docs/source-app/workflows/build_lightning_app/from_scratch.rst +++ b/docs/source-app/workflows/build_lightning_app/from_scratch.rst @@ -1,124 +1,10 @@ -################### -Build Lightning app -################### -**Audience:** Users who want to build a lightning app from scratch. +################################## +Build a Lightning App from Scratch +################################## +**Audience:** Users who want to build a Lightning App from scratch. ----- - -************** -Fork and build -************** -Before you build a Lightning App from scratch, see if you can find an app that is similar to what you need -in the `Lightning App Gallery `_. - -Once you find the app you want, press "Get" to see it running on the cloud, then download the code -and change what you want! - ----- - -****************** -Build from scratch -****************** -If you didn't find an app similar to the one you need, simply create a file named **app.py** with these contents: - -.. code:: bash - - import lightning_app as la - - class WordComponent(lapp.LightningWork): - def __init__(self, word): - super().__init__() - self.word = word - def run(self): - print(self.word) - - - class LitApp(lapp.LightningFlow): - def __init__(self) -> None: - super().__init__() - self.hello = WordComponent('hello') - self.world = WordComponent('world') - - def run(self): - print('this is a simple Lightning app, make a better app!') - self.hello.run() - self.world.run() - - app = lapp.LightningApp(LitApp()) - ----- - -Run the app -^^^^^^^^^^^ -Run the app locally: - -.. code:: bash - - lightning run app app.py - -Run the app on the cloud: - -.. code:: bash - - lightning run app app.py --cloud +**Prereqs:** You must have finished the `Basic levels `_. ---- -********************* -Build from a template -********************* -If you didn't find an app similar to the one you need (in the `Lightning App gallery `_), another option is to start from a template. -The lightning CLI can generate a template with built-in testing that can be easily published to the -Lightning app gallery. - -Generate it with our template generator: - -.. code:: bash - - lightning init app your-app-name - -You'll see a print-out like this: - -.. code:: bash - - ➜ lightning init app your-app-name - - /Users/Your/Current/dir/your-app-name - INFO: laying out app template at /Users/Your/Current/dir/your-app-name - INFO: - Lightning app template created! - /Users/Your/Current/dir/your-app-name - - run your app with: - lightning run app your-app-name/your_app_name/app.py - - run it on the cloud to share with your collaborators: - lightning run app your-app-name/your_app_name/app.py --cloud - ----- - -Modify the template -^^^^^^^^^^^^^^^^^^^ -The command above generates an app file like this: - -.. code:: python - - from your_app_name import ComponentA, ComponentB - - import lightning_app as la - - - class LitApp(lapp.LightningFlow): - def __init__(self) -> None: - super().__init__() - self.component_a = ComponentA() - self.component_b = ComponentB() - - def run(self): - self.component_a.run() - self.component_b.run() - - - app = lapp.LightningApp(LitApp()) - -Now you can add your own components as you wish! +.. include:: from_scratch_content.rst diff --git a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst new file mode 100644 index 0000000000000..c43b1bbc2f749 --- /dev/null +++ b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst @@ -0,0 +1,118 @@ + +************** +WAIT! +************** +Before you build a Lightning App from scratch, see if you can find an app that is similar to what you need +in the `Lightning App Gallery `_. + +Once you find the Lightning App you want, press "Clone & Run" to see it running on the cloud, then download the code +and change what you want! + +---- + +****************** +Build from scratch +****************** +If you didn't find a Lightning App similar to the one you need, simply create a file named **app.py** with these contents: + +.. code:: python + + import lightning as L + + class WordComponent(L.LightningWork): + def __init__(self, word): + super().__init__() + self.word = word + def run(self): + print(self.word) + + + class LitApp(L.LightningFlow): + def __init__(self) -> None: + super().__init__() + self.hello = WordComponent('hello') + self.world = WordComponent('world') + + def run(self): + print('This is a simple Lightning app, make a better app!') + self.hello.run() + self.world.run() + + app = L.LightningApp(LitApp()) + +---- + +Run the Lightning App +^^^^^^^^^^^^^^^^^^^^^ +Run the Lightning App locally: + +.. code:: bash + + lightning run app app.py + +Run the Lightning App on the cloud: + +.. code:: bash + + lightning run app app.py --cloud + +---- + +************************************* +Build a Lightning App from a template +************************************* +If you didn't find an Lightning App similar to the one you need (in the `Lightning App gallery `_), another option is to start from a template. +The Lightning CLI can generate a template with built-in testing that can be easily published to the +Lightning App Gallery. + +Generate a Lightning App with our template generator: + +.. code:: bash + + lightning init app your-app-name + +You'll see a print-out like this: + +.. code:: bash + + ➜ lightning init app your-app-name + + /Users/Your/Current/dir/your-app-name + INFO: laying out app template at /Users/Your/Current/dir/your-app-name + INFO: + Lightning app template created! + /Users/Your/Current/dir/your-app-name + + run your app with: + lightning run app your-app-name/your_app_name/app.py + + run it on the cloud to share with your collaborators: + lightning run app your-app-name/your_app_name/app.py --cloud + +---- + +Modify the Lightning App template +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The command above generates a Lightning App file like this: + +.. code:: python + + from your_app_name import ComponentA, ComponentB + + import lightning as L + + + class LitApp(L.LightningFlow): + def __init__(self) -> None: + super().__init__() + self.component_a = ComponentA() + self.component_b = ComponentB() + + def run(self): + self.component_a.run() + self.component_b.run() + + + app = L.LightningApp(LitApp()) + +Now you can add your own components as you wish! diff --git a/docs/source-app/workflows/build_lightning_component/basic.rst b/docs/source-app/workflows/build_lightning_component/basic.rst index 37099a3a6fa0b..b72b9147e2f07 100644 --- a/docs/source-app/workflows/build_lightning_component/basic.rst +++ b/docs/source-app/workflows/build_lightning_component/basic.rst @@ -1,205 +1,8 @@ -################################### -Build a Lightning component (basic) -################################### +########################### +Build a Lightning component +########################### **Audience:** Users who want to build a Lightning component. ---- -******************************* -Why should I build a component? -******************************* -Lightning Components break up complex systems into modular components. The first obvious benefit is that components -can be reused across other apps. This means you can build once, test it and forget it. - -As a researcher it also means that your code can be taken to production without needing a team of engineers to help -productionize it. - -As a machine learning engineer, it means that your cloud system is: - -- fault tolerant -- cloud agnostic -- testable (unlike YAML/CI/CD code) -- version controlled -- enables cross-functional collaboration - ----- - -************** -Fork and build -************** -Before you build a Lightning component from scratch, see if you can find a component that is similar to what you need -in the `Lightning component Gallery `_. - -Once you find the component you want, download the code and change what you want! - ----- - -**************************** -Decide between Flow and Work -**************************** - -.. raw:: html - - Choosing between LightningFlow and LightningWork - -There are two types of components in Lightning, LightningFlow and LightningWork. - -Use a **LightningFlow** component for any programming logic that runs in less than 1 second. - -.. code:: python - - for i in range(10): - print(f"{i}: this kind of code belongs in a LightningFlow") - -Use a **LightningWork** component for any programming logic that takes more than 1 second or requires its own hardware. - -.. code:: python - - from time import sleep - - for i in range(100000): - sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") - ----- - -****************** -Build from scratch -****************** -The first option is if you want to build from scratch - ----- - -Option A: Build a LightningFlow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To implement a LightningFlow, simply subclass ``LightningFlow`` and define the run method: - -.. code:: python - :emphasize-lines: 5 - - # app.py - import lightning_app as la - - - class LitFlow(lapp.LightningFlow): - def run(self): - for i in range(10): - print(f"{i}: this kind of code belongs in a LightningFlow") - - - app = lapp.LightningApp(LitFlow()) - -run the app - -.. code:: bash - - lightning run app app.py - ----- - -Option B: build a LightningWork -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Only implement a LightningWork if this particular piece of code: - -- takes more than 1 second to execute -- or requires its own set of cloud resources -- or both - -To implement a LightningWork, simply subclass ``LightningWork`` and define the run method: - -.. code:: python - :emphasize-lines: 6 - - # app.py - from time import sleep - import lightning_app as la - - - class LitWork(lapp.LightningWork): - def run(self): - for i in range(100000): - sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") - -A LightningWork must always be attached to a LightningFlow and explicitely asked to ``run()``: - -.. code:: python - :emphasize-lines: 13, 16 - - from time import sleep - import lightning_app as la - - - class LitWork(lapp.LightningWork): - def run(self): - for i in range(100000): - sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") - - - class LitFlow(lapp.LightningFlow): - def __init__(self): - super().__init__() - self.lit_work = LitWork() - - def run(self): - self.lit_work.run() - - - app = lapp.LightningApp(LitFlow()) - -run the app - -.. code:: bash - - lightning run app app.py - ----- - -********************* -Build from a template -********************* -If you'd prefer a component template with built-in testing that can be easily published to the -Lightning component gallery, generate it with our template generator: - -.. code:: bash - - lightning init component your-component-name - -You'll see a print-out like this: - -.. code:: bash - - ➜ lightning init component your-component-name - INFO: laying out component template at /Users/williamfalcon/Developer/opensource/_/lightning/scratch/hello-world - INFO: - ⚡ Lightning component template created! ⚡ - /Users/williamfalcon/Developer/opensource/_/lightning/scratch/hello-world - - ... - ----- - -Modify the component template -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The command above generates a component file like this: - -.. code:: python - - import lightning_app as la - - - class TemplateComponent(lapp.LightningWork): - def __init__(self) -> None: - super().__init__() - self.value = 0 - - def run(self): - self.value += 1 - print("welcome to your work component") - print("this is running inside a work") - -Now you can modify the component as you wish! +.. include:: from_scratch_component_content.rst diff --git a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst new file mode 100644 index 0000000000000..0f7b0ff436eb0 --- /dev/null +++ b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst @@ -0,0 +1,193 @@ + +******************************* +LightningFlow vs. LightningWork +******************************* + +.. raw:: html + + Choosing between LightningFlow and LightningWork + +There are two types of components in Lightning, **LightningFlow** and **LightningWork**. + +Use a **LightningFlow** component for any programming logic that runs in less than 1 second. + +.. code:: python + + for i in range(10): + print(f'{i}: this kind of code belongs in a LightningFlow') + +Use a **LightningWork** component for any programming logic that takes more than 1 second or requires its own hardware. + +.. code:: python + + from time import sleep + + for i in range(100000): + sleep(2.0) + print(f'{i} LightningWork: work that is long running or may never end (like a server)') + +---- + +************************************************ +What building a Lightning component does for you +************************************************ +Lightning Components break up complex systems into modular components. The first obvious benefit is that components +can be reused across other apps. This means you can build once, test it and forget it. + +As a researcher it also means that your code can be taken to production without needing a team of engineers to help +productionize it. + +As a machine learning engineer, it means that your cloud system is: + +- fault tolerant +- cloud agnostic +- testable (unlike YAML/CI/CD code) +- version controlled +- enables cross-functional collaboration + +---- + +************** +WAIT! +************** +Before you build a Lightning component from scratch, see if you can find a component that is similar to what you need +in the `Lightning component Gallery `_. + +Once you find the component you want, download the code and change what you want! + +---- + +***************************************** +Build a Lighitning component from scratch +***************************************** +If you didn't find a Lightning component similar to the one you need, you can build one from scratch. + +---- + +Build a LightningFlow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +To implement a LightningFlow, simply subclass ``LightningFlow`` and define the run method: + +.. code:: python + :emphasize-lines: 5 + + # app.py + import lightning as L + + class LitFlow(L.LightningFlow): + def run(self): + for i in range(10): + print(f'{i}: this kind of code belongs in a LightningFlow') + + app = L.LightningApp(LitFlow()) + +run the app + +.. code:: bash + + lightning run app app.py + +---- + +Build a LightningWork +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Only implement a LightningWork if this particular piece of code: + +- takes more than 1 second to execute +- requires its own set of cloud resources +- or both + +To implement a LightningWork, simply subclass ``LightningWork`` and define the run method: + +.. code:: python + :emphasize-lines: 6 + + # app.py + from time import sleep + import lightning as L + + class LitWork(L.LightningWork): + def run(self): + for i in range(100000): + sleep(2.0) + print(f'{i} LightningWork: work that is long running or may never end (like a server)') + +A LightningWork must always be attached to a LightningFlow and explicitely asked to ``run()``: + +.. code:: python + :emphasize-lines: 13, 16 + + from time import sleep + import lightning as L + + class LitWork(L.LightningWork): + def run(self): + for i in range(100000): + sleep(2.0) + print(f'{i} LightningWork: work that is long running or may never end (like a server)') + + class LitFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.lit_work = LitWork() + + def run(self): + self.lit_work.run() + + app = L.LightningApp(LitFlow()) + +run the app + +.. code:: bash + + lightning run app app.py + +---- + +******************************************* +Build a Lightning component from a template +******************************************* +If you'd prefer a component template with built-in testing that can be easily published to the +Lightning component gallery, generate it with our template generator: + +.. code:: bash + + lightning init component your-component-name + +You'll see a print-out like this: + +.. code:: bash + + ➜ lightning init component your-component-name + INFO: laying out component template at /Users/williamfalcon/Developer/opensource/_/lightning/scratch/hello-world + INFO: + ⚡ Lightning component template created! ⚡ + /Users/williamfalcon/Developer/opensource/_/lightning/scratch/hello-world + + ... + +---- + +Modify the component template +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The command above generates a component file like this: + +.. code:: python + + import lightning as L + + + class TemplateComponent(L.LightningWork): + def __init__(self) -> None: + super().__init__() + self.value = 0 + + def run(self): + self.value += 1 + print("welcome to your work component") + print("this is running inside a work") + +Now you can modify the component as you wish! diff --git a/docs/source-app/workflows/build_lightning_component/index_content.rst b/docs/source-app/workflows/build_lightning_component/index_content.rst index 54dcad4d8950e..00b4e99ad1e5e 100644 --- a/docs/source-app/workflows/build_lightning_component/index_content.rst +++ b/docs/source-app/workflows/build_lightning_component/index_content.rst @@ -28,7 +28,7 @@ Basics
.. displayitem:: - :header: Build a component + :header: Build a Lightning component :description: Learn the basics of building a Lightning component :col_css: col-md-4 :button_link: basic.html @@ -36,8 +36,8 @@ Basics :tag: basic .. displayitem:: - :header: Explore community components - :description: Discover community-built components + :header: Explore community Lightning components + :description: Discover community-built Lightning components :col_css: col-md-4 :button_link: https://lightning.ai/components :height: 150 @@ -84,7 +84,7 @@ Intermediate :tag: intermediate .. displayitem:: - :header: Publish a component + :header: Publish a Lightning component :description: Learn the basics of publishing a Lightning component. :col_css: col-md-4 :button_link: publish_a_component.html diff --git a/docs/source-app/workflows/build_lightning_component/intermediate.rst b/docs/source-app/workflows/build_lightning_component/intermediate.rst index a1956b260df9b..27336424bf916 100644 --- a/docs/source-app/workflows/build_lightning_component/intermediate.rst +++ b/docs/source-app/workflows/build_lightning_component/intermediate.rst @@ -29,13 +29,12 @@ To *connect* this user interface to the component, define the configure_layout m .. code:: python :emphasize-lines: 5, 6 - import lightning_app as la + import lightning as L from lightning_app.frontend.web import StaticWebFrontend - - class LitHTMLComponent(lapp.LightningFlow): + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") + return StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') Finally, route the component's UI through the root component's **configure_layout** method: @@ -43,15 +42,13 @@ Finally, route the component's UI through the root component's **configure_layou :emphasize-lines: 14 # app.py - import lightning_app as la - + import lightning as L - class LitHTMLComponent(lapp.LightningFlow): + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return lapp.frontend.web.StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") + return L.frontend.web.StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') - - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self): super().__init__() self.lit_html_component = LitHTMLComponent() @@ -60,8 +57,7 @@ Finally, route the component's UI through the root component's **configure_layou tab1 = {"name": "home", "content": self.lit_html_component} return tab1 - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) Run your app and you'll see the UI on the Lightning App view: diff --git a/docs/source-app/workflows/build_lightning_component/publish_a_component.rst b/docs/source-app/workflows/build_lightning_component/publish_a_component.rst index f4ed73b2bd002..8364afd3d59ce 100644 --- a/docs/source-app/workflows/build_lightning_component/publish_a_component.rst +++ b/docs/source-app/workflows/build_lightning_component/publish_a_component.rst @@ -13,7 +13,7 @@ the default template. Generate your component template with this command: -.. code:: bash +.. code:: python lightning init component your-component-name @@ -36,20 +36,18 @@ Now import your component and use it in an app: # app.py from your_component import TemplateComponent - import lightning_app as la + import lightning as L - - class LitApp(lapp.LightningFlow): + class LitApp(L.LightningFlow): def __init__(self) -> None: super().__init__() self.your_component = TemplateComponent() def run(self): - print("this is a simple Lightning app to verify your component is working as expected") + print('this is a simple Lightning app to verify your component is working as expected') self.your_component.run() - - app = lapp.LightningApp(LitApp()) + app = L.LightningApp(LitApp()) and run the app: diff --git a/docs/source-app/workflows/debug_locally.rst b/docs/source-app/workflows/debug_locally.rst index 77ea163b4469b..cd5a5a80fde7c 100644 --- a/docs/source-app/workflows/debug_locally.rst +++ b/docs/source-app/workflows/debug_locally.rst @@ -1,3 +1,5 @@ +:orphan: + ##################################### Debug a Distributed Cloud App Locally ##################################### diff --git a/docs/source-app/workflows/enable_fault_tolerance.rst b/docs/source-app/workflows/enable_fault_tolerance.rst index c7af476d344b1..b1630d4d396ac 100644 --- a/docs/source-app/workflows/enable_fault_tolerance.rst +++ b/docs/source-app/workflows/enable_fault_tolerance.rst @@ -1,3 +1,5 @@ +:orphan: + ###################### Enable Fault Tolerance ###################### diff --git a/docs/source-app/workflows/run_app_on_cloud/cloud_files.rst b/docs/source-app/workflows/run_app_on_cloud/cloud_files.rst new file mode 100644 index 0000000000000..3130cd0f336b3 --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/cloud_files.rst @@ -0,0 +1,58 @@ +.. _ignore: + +################################## +Configure Your Lightning Cloud App +################################## + +**Audience:** Users who want to control Lightning App files on the cloud. + +---- + +************************************** +Ignore file uploads to Lightning cloud +************************************** +Running Lightning Apps on the cloud will upload the source code of your app to the cloud. You can use ``.lightningignore`` file(s) to ignore files or directories while uploading. The `.lightningignore` file follows the same format as a `.gitignore` +file. + +For example, the source code directory below with the ``.lightningignore`` file will ignore the file named +``model.pt`` and directory named ``data_dir``. + +.. code:: bash + + . + ├── README.md + ├── app.py + ├── data_dir + │ ├── image1.png + │ ├── image2.png + │ └── ... + ├── .lightningignore + ├── requirements.txt + └── model.pt + + +.. code:: bash + + ~/project/home ❯ cat .lightningignore + model.pt + data_dir + +A sample ``.lightningignore`` file can be found `here `_. + + +---- + +******************* +Structure app files +******************* + +We recommend your app contain the following files: + +.. code:: bash + + . + ├── .lightning (auto-generated- conatins Lightning configuration) + ├── .lightningignore (contains files not to upload to the cloud) + ├── app.py + ├── README.md (optional- a markdown description of your app) + └── requirements.txt (optional- conatins all your app dependencies) diff --git a/docs/source-app/workflows/run_app_on_cloud/index.rst b/docs/source-app/workflows/run_app_on_cloud/index.rst new file mode 100644 index 0000000000000..55bc3b6807809 --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/index.rst @@ -0,0 +1,5 @@ +##################### +Run apps on the cloud +##################### + +.. include:: index_content.rst diff --git a/docs/source-app/workflows/run_app_on_cloud/index_content.rst b/docs/source-app/workflows/run_app_on_cloud/index_content.rst new file mode 100644 index 0000000000000..b0f67570d7024 --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/index_content.rst @@ -0,0 +1,115 @@ +.. _run_app_in_cloud: + +.. toctree:: + :maxdepth: 1 + :hidden: + + cloud_files + lightning_cloud + on_prem + on_your_own_machine + +**Audience:** Users who want to share or scale Lightning Apps. + +---- + +***************************** +Run on Lightning Public Cloud +***************************** + +You can run Lightning Apps for free on the Public Lightning cloud with a single flag! + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Run on Lightning Cloud + :description: Learn how to run on the Lightning public cloud + :col_css: col-md-4 + :button_link: lightning_cloud.html + :height: 150 + :tag: basic + +.. displayitem:: + :header: Choose Hardware + :description: Configure you app cloud resources + :col_css: col-md-4 + :button_link: ../../core_api/lightning_work/compute.html + :height: 150 + :tag: Basic + +.. displayitem:: + :header: Set Environment Variables + :description: Manage your environment variables in the cloud + :col_css: col-md-4 + :button_link: ../../glossary/environment_variables.html + :height: 150 + :tag: Basic + +.. displayitem:: + :header: Configure Your Lightning Cloud App + :description: Customize your cloud apps files + :col_css: col-md-4 + :button_link: cloud_files.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Manage App Dependancies + :description: Configure your python requirements or use a custom docker image + :col_css: col-md-4 + :button_link: ../../glossary/build_config/build_config.html + :height: 150 + :tag: Intermediate + +.. displayitem:: + :header: Share Files Between Works + :description: Learn more about data transfering + :col_css: col-md-4 + :button_link: ../../glossary/storage/storage.html + :height: 150 + :tag: Intermediate + +.. raw:: html + +
+
+ +---- + +************ +Other Clouds +************ + +.. raw:: html + +
+
+ +.. Add callout items below this line + +.. displayitem:: + :header: Run On Your Own Machine + :description: Run Lightning Apps on any machine + :col_css: col-md-4 + :button_link: on_your_own_machine.html + :height: 150 + :tag: basic + +.. displayitem:: + :header: Run On Your Private Cloud + :description: Run Lightning Apps on your own cloud + :col_css: col-md-4 + :button_link: on_prem.html + :height: 150 + :tag: basic + + +.. raw:: html + +
+
diff --git a/docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst b/docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst new file mode 100644 index 0000000000000..aaa9d7bb088be --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst @@ -0,0 +1,67 @@ +####################### +Run an App on the Cloud +####################### + +**Audience:** Users who want to share their apps or run on specialized hardware (like GPUs). + +---- + +********************************* +Run on the public Lightning cloud +********************************* +To run any app on the public lightning cloud use the ``--cloud`` argument: + +.. code:: bash + + lightning run app app.py --cloud + + +.. note:: + By default, running your apps on the public Lightning cloud is free of charge using default CPUs, and any app uploaded to the Lightning cloud will be shared with the community (source code and app view will be public). If you would like to make your apps private please `contact us `_. + +If your app contains ``LightningWork`` components that require more compute resources, such as larger CPUs or **GPUs**, you'll need to add credits to your Lightning AI account. + + +---- + +************************** +Add dependencies to my app +************************** + + +Add all dependencies required to run your app to a `requirements.txt` file in your app's directory. Read :ref:`build_config` for more details. + + + +---- + + +******** +Name app +******** + +Simply use the ``--name`` flag when running your app, for example: + +.. code:: bash + + lightning run app app.py --cloud --name my-awesome-app + +Alternatively, you can change the name of the app in the ``.lightning`` file: + +.. code:: bash + + ~/project/home ❯ cat .lightning + name: my-awesome-app + +The ``.lightning`` file is a general configuration file. +To learn more about optional configuration file parameters, see :class:`~lightning.utilities.packaging.app_config.AppConfig`. + +------ + +******************** +Choose Cloud Compute +******************** + +You can configure the hardware your app is running on by setting a :class:`~lightning.utilities.packaging.cloud_compute.CloudCompute` object onto the ``cloud_compute`` property of your work's. + +Learn more with the :ref:`cloud_compute` guide diff --git a/docs/source-app/workflows/run_app_on_cloud/on_prem.rst b/docs/source-app/workflows/run_app_on_cloud/on_prem.rst new file mode 100644 index 0000000000000..be0a954f29b16 --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/on_prem.rst @@ -0,0 +1,6 @@ +########################### +Run an App on Private Cloud +########################### + + +To run Lightning apps on a private or on-prem cluster, `contact us `_. diff --git a/docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst b/docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst new file mode 100644 index 0000000000000..795205f552e70 --- /dev/null +++ b/docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst @@ -0,0 +1,23 @@ +####################### +Run on your own machine +####################### + +**Audience:** Users who want to run Lightning App on a remote machine. + +---- + +*********** +Run via ssh +*********** +To run a Lightning App on any machine, simply ssh to the machine and run the app directly + +.. code:: bash + + # log into your cloud machine + ssh your_name@your_cloud_machine + + # get your code on the machine and install deps + ... + + # start the app + lightning run app app.py diff --git a/docs/source-app/workflows/run_components_on_different_hardware.rst b/docs/source-app/workflows/run_components_on_different_hardware.rst index ef3aac100d398..9685c3461e511 100644 --- a/docs/source-app/workflows/run_components_on_different_hardware.rst +++ b/docs/source-app/workflows/run_components_on_different_hardware.rst @@ -1,3 +1,5 @@ +:orphan: + #################################### Run components on different hardware #################################### diff --git a/docs/source-app/workflows/run_work_in_parallel.rst b/docs/source-app/workflows/run_work_in_parallel.rst index 542b36b3b3f70..58089b6ed7f46 100644 --- a/docs/source-app/workflows/run_work_in_parallel.rst +++ b/docs/source-app/workflows/run_work_in_parallel.rst @@ -1,56 +1,11 @@ -#################### -Run work in parallel -#################### +############################## +Run LightningWorks in parallel +############################## -When there is a long-running workload such as a model training, or a deployment server, it may be desirable to run that work in parallel -while the rest of the app continues to execute. +**Audience:** Users who want to run multiple LightningWorks at once. ----- - -**************************** -Why do I need parallel work? -**************************** -The default behavior of the ``LightningWork`` is to wait for the ``run`` method to complete: - -.. code:: python - - import lightning_app as la - - - class Root(lapp.LightningFlow): - def __init__(self): - self.work_component_a = lapp.demo.InfinteWorkComponent() - - def run(self): - self.work_component_a.run() - print("this will never print") - -Since this Work component we created loops forever, the print statement will never execute. In practice -``LightningWork`` workloads are finite and don't run forever. - -When a ``LightningWork`` performs a heavy operation (longer than 1 second), or requires its own hardware, -work that is *not* done in parallel will slow down your app. +**Prereqs:** Level 8+ ---- -******************** -Enable parallel work -******************** -To run work in parallel while the rest of the app executes without delays, enable ``parallel=True``: - -.. code:: python - :emphasize-lines: 5 - - import lightning_app as la - - - class Root(lapp.LightningFlow): - def __init__(self): - self.work_component_a = lapp.demo.InfinteWorkComponent(parallel=True) - - def run(self): - self.work_component_a.run() - print("repeats while the infinite work runs ONCE (and forever) in parallel") - -Any work that will take more than **1 second** should be run in parallel -unless the rest of your app depends on the output of this work (for example, downloading a dataset). +.. include:: run_work_in_parallel_content.rst diff --git a/docs/source-app/workflows/run_work_in_parallel_content.rst b/docs/source-app/workflows/run_work_in_parallel_content.rst new file mode 100644 index 0000000000000..334f50e7e486a --- /dev/null +++ b/docs/source-app/workflows/run_work_in_parallel_content.rst @@ -0,0 +1,51 @@ + + +**************************************************** +What running LightningWorks in parallel does for you +**************************************************** + +When there is a long-running workload such as a model training, or a deployment server, you might want to run that LightningWork in parallel +while the rest of the Lightning App continues to execute. + +The default behavior of the ``LightningWork`` is to wait for the ``run`` method to complete: + +.. code:: python + + import lightning as L + + class Root(L.LightningFlow): + def __init__(self): + self.work_component_a = L.demo.InfinteWorkComponent() + + def run(self): + self.work_component_a.run() + print('this will never print') + +Since this LightningWork component we created loops forever, the print statement will never execute. In practice +``LightningWork`` workloads are finite and don't run forever. + +When a ``LightningWork`` performs a heavy operation (longer than 1 second), or requires its own hardware, +LightningWork that is *not* done in parallel will slow down your app. + +---- + +****************************** +Enable parallel LightningWorks +****************************** +To run LightningWorks in parallel, while the rest of the app executes without delays, enable ``parallel=True``: + +.. code:: python + :emphasize-lines: 5 + + import lightning as L + + class Root(L.LightningFlow): + def __init__(self): + self.work_component_a = L.demo.InfinteWorkComponent(parallel=True) + + def run(self): + self.work_component_a.run() + print('repeats while the infinite work runs ONCE (and forever) in parallel') + +Any LightningWorks that will take more than **1 second** should be run in parallel +unless the rest of your Lightning App depends on the output of this work (for example, downloading a dataset). diff --git a/docs/source-app/workflows/run_work_once.rst b/docs/source-app/workflows/run_work_once.rst index b98df496bc1c9..240cde3d08a8a 100644 --- a/docs/source-app/workflows/run_work_once.rst +++ b/docs/source-app/workflows/run_work_once.rst @@ -1,126 +1,13 @@ -.. _cache_calls: - -################# -Caching Work Runs -################# +########################## +Cache LightningWork Runs +########################## **Audience:** Users who want to know how ``LightningWork`` works. -**Level:** Basic +**Level:** Advanced -**Prerequisite**: Read the :ref:`event_loop` guide. +**Prereqs**: Level 16+ and read the `Event Loop guide <../glossary/event_loop.html>`_. ---- -********************************************************** -What does it mean to cache the calls of Work's run method? -********************************************************** - -By default, the run method in a LightningWork "remembers" (caches) the input arguments it is getting called with and does not execute again if called with the same arguments again. -In other words, the run method only executes when the input arguments have never been seen before. - -This behavior can be toggled on or off: - -.. code-block:: python - - # Run only when the input arguments change (default) - work = MyWork(cache_calls=True) - - # Run everytime regardless of whether input arguments change or not - work = MyWork(cache_calls=False) - - -To better understand this, imagine you want every day to sequentially download and process some data and then train a model on those data. -As explained in the pre-requisite, the Lightning App runs within an infinite while loop, so the pseudo-code of your application might looks like this: - -.. code-block:: python - - from datetime import datetime - - # Lightning code - while True: # This is the Lightning Event Loop - - # Your code - today = datetime.now().strftime("%D") # '05/25/22' - data_processor.run(today) - train_model.run(data_processor.data) - -In this scenario, you want your components to run ``once`` a day, no more! But your code is running within an infinite loop, how can this even work? -This is where the work's internal caching mechanism comes in. By default, Lightning caches a hash of the input provided to its run method and won't re-execute the method if the same input is provided again. -In the example above, the **data_processor** component run method receives the string **"05/25/22"**. It runs one time and any further execution during the day is skipped until tomorrow is reached and the work run method receives **06/25/22**. This logic applies everyday. -This caching mechanism is inspired from how `React.js Components and Props `_ renders website. Only changes to the inputs re-trigger execution. - -******************************* -How can I verify this behavior? -******************************* - -Here's an example of this behavior with LightningWork: - -.. literalinclude:: ../code_samples/basics/0.py - :language: python - :emphasize-lines: 10, 19 - -And you should see the following by running the code above: - -.. code-block:: console - - $ python example.py - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - # After you have clicked `run` on the UI. - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 10} - -As you can see, the intermediate run didn't execute, as we would expected when ``cache_calls=True``. - -************************************************ -What are the implications of turnin caching off? -************************************************ - -By setting ``cache_calls=False``, Lightning won't cache the return value and re-execute the run method on every call. - -.. literalinclude:: ../code_samples/basics/1.py - :diff: ../code_samples/basics/0.py - -.. code-block:: console - - $ python example.py - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - # After you have clicked `run` on the UI. - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 1} - I received the following props: args: () kwargs: {'value': 10} - - -Be aware than when setting both ``cache_calls=False`` and ``parallel=False`` to a work, the code after the ``self.work.run()`` is unreachable -as the work continuously execute in a blocking way. - -.. code-block:: python - - from lightning_app import LightningApp, LightningFlow, LightningWork - - - class Flow(LightningFlow): - def __init__(self): - super().__init__() - - self.work = Work(cache_calls=False, parallel=False) - - def run(self): - print("HERE BEFORE") - self.work.run() - print("HERE AFTER") - - - app = LightningApp(Flow()) - -.. code-block:: console - - $ lightning run app app.py - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - print("HERE BEFORE") - print("HERE BEFORE") - print("HERE BEFORE") - ... +.. include:: run_work_once_content.rst diff --git a/docs/source-app/workflows/run_work_once_content.rst b/docs/source-app/workflows/run_work_once_content.rst new file mode 100644 index 0000000000000..4388742358929 --- /dev/null +++ b/docs/source-app/workflows/run_work_once_content.rst @@ -0,0 +1,149 @@ + +******************************************************** +What caching the calls of Work's run method does for you +******************************************************** + +By default, the run method in a LightningWork (Work) "remembers" (caches) the input arguments it is getting called with and does not execute again if called with the same arguments again. +In other words, the run method only executes when the input arguments have never been seen before. + +You can turn caching on or off: + +.. code-block:: python + + # Run only when the input arguments change (default) + work = MyWork(cache_calls=True) + + # Run everytime regardless of whether input arguments change or not + work = MyWork(cache_calls=False) + +To better understand this, imagine that every day you want to sequentially download and process some data and then train a model on that data. +As explained in the `Event Loop guide <../glossary/event_loop.html>`_, the Lightning App runs within an infinite while loop, so the pseudo-code of your application might looks like this: + +.. code-block:: python + + from datetime import datetime + + # Lightning code + while True: # This is the Lightning Event Loop + + # Your code + today = datetime.now().strftime("%D") # '05/25/22' + data_processor.run(today) + train_model.run(data_processor.data) + +In this scenario, you want your components to run ``once`` a day, and no more than that! But your code is running within an infinite loop, how can this even work? +This is where the Work's internal caching mechanism comes in. By default, Lightning caches a hash of the input provided to its run method and won't re-execute the method if the same input is provided again. +In the example above, the **data_processor** component run method receives the string **"05/25/22"**. It runs one time and any further execution during the day is skipped until tomorrow is reached and the work run method receives **06/25/22**. This logic applies everyday. +This caching mechanism is inspired from how `React.js Components and Props `_ renders websites. Only changes to the inputs re-trigger execution. + +*************** +Caching Example +*************** + +Here's an example of this behavior with LightningWork: + +.. code:: python + :emphasize-lines: 11, 17 + + import lightning as L + + class ExampleWork( L.LightningWork): + def run(self, *args, **kwargs): + print(f"I received the following props: args: {args} kwargs: {kwargs}") + + work = ExampleWork() + work.run(value=1) + + # Providing the same value. This won't run as already cached. + work.run(value=1) + work.run(value=1) + work.run(value=1) + work.run(value=1) + + # Changing the provided value. This isn't cached and will run again. + work.run(value=10) + +And you should see the following by running the code above: + +.. code-block:: console + + $ python example.py + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + # After you have clicked `run` on the UI. + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 10} + +As you can see, the intermediate run didn't execute, as we would expected when ``cache_calls=True``. + +*********************************** +Implications of turning caching off +*********************************** + +By setting ``cache_calls=False``, Lightning won't cache the return value and re-execute the run method on every call. + +.. code:: python + :emphasize-lines: 7 + + from lightning_app import LightningWork + + class ExampleWork(LightningWork): + def run(self, *args, **kwargs): + print(f"I received the following props: args: {args} kwargs: {kwargs}") + + work = ExampleWork(cache_calls=False) + work.run(value=1) + + # Providing the same value. This won't run as already cached. + work.run(value=1) + work.run(value=1) + work.run(value=1) + work.run(value=1) + + # Changing the provided value. This isn't cached and will run again. + work.run(value=10) + +.. code-block:: console + + $ python example.py + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + # After you have clicked `run` on the UI. + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 1} + I received the following props: args: () kwargs: {'value': 10} + +Be aware than when setting both ``cache_calls=False`` and ``parallel=False`` to a work, the code after the ``self.work.run()`` is unreachable +as the work continuously execute in a blocking way. + +.. code-block:: python + :emphasize-lines: 9-10 + + from lightning_app import LightningApp, LightningFlow, LightningWork + + class Flow(LightningFlow): + + def __init__(self): + super().__init__() + + self.work = Work( + cache_calls=False, + parallel=False + ) + + def run(self): + print("HERE BEFORE") + self.work.run() + print("HERE AFTER") + + app = LightningApp(Flow()) + +.. code-block:: console + + $ lightning run app app.py + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + print("HERE BEFORE") + print("HERE BEFORE") + print("HERE BEFORE") + ... diff --git a/docs/source-app/workflows/schedule_apps.rst b/docs/source-app/workflows/schedule_apps.rst index e64c64872e565..7b596cd08b179 100644 --- a/docs/source-app/workflows/schedule_apps.rst +++ b/docs/source-app/workflows/schedule_apps.rst @@ -1,3 +1,5 @@ +:orphan: + ################# Schedule App Runs ################# diff --git a/docs/source-app/workflows/share_app.rst b/docs/source-app/workflows/share_app.rst index 7dd6be7c3715b..87045bde8da78 100644 --- a/docs/source-app/workflows/share_app.rst +++ b/docs/source-app/workflows/share_app.rst @@ -30,4 +30,4 @@ Run local: lightning run app app.py -then use one of the many guides to `expose a tunnel `. +And then, use one of the many guides to `expose a tunnel `_. diff --git a/docs/source-app/workflows/share_files_between_components.rst b/docs/source-app/workflows/share_files_between_components.rst index 7292236a18ba4..02c7d889a7122 100644 --- a/docs/source-app/workflows/share_files_between_components.rst +++ b/docs/source-app/workflows/share_files_between_components.rst @@ -34,8 +34,8 @@ To write a file, first create a reference to the file with the :class:`~lightnin boring_file_reference = Path("boring_file.txt") # write to that file - with open(self.boring_file_reference, "w") as f: - f.write("yolo") + with open(self.boring_file_reference, 'w') as f: + f.write('yolo') ---- @@ -62,7 +62,7 @@ To use a file, pass the reference to the file: ****************************** Use a directory - coming soon - *************** + ****************************** TODO ---- @@ -73,57 +73,32 @@ Example: Share a model checkpoint A common workflow in ML is to use a checkpoint created by another component. First, define a component that saves a checkpoint: -.. code:: python - - import lightning_app as lalit - from lightning_app.storage.path import Path - import torch - import os - - - class ModelTraining(lit.LightningWork): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.model_checkpoints_path = Path("/checkpoints") - - def run(self): - # make fake checkpoints - checkpoint_1 = torch.tensor([0, 1, 2, 3, 4]) - checkpoint_2 = torch.tensor([0, 1, 2, 3, 4]) - torch.save(checkpoint_1, self.model_checkpoints_path + "checkpoint_1.ckpt") - torch.save(checkpoint_2, self.model_checkpoints_path + "checkpoint_2.ckpt") - +.. literalinclude:: ./share_files_between_components/app.py + :lines: -19 Next, define a component that needs the checkpoints: -.. code:: python +.. literalinclude:: ./share_files_between_components/app.py + :lines: 20-31 - class ModelDeploy(lit.LightningWork): - def __init__(self, ckpt_path, *args, **kwargs): - super().__init__() - self.ckpt_path = ckpt_path +Link both components via a parent component: - def run(self): - ckpts = os.list_dir(self.ckpt_path) - checkpoint_1 = torch.load(ckpts[0]) - checkpoint_2 = torch.load(ckpts[1]) +.. literalinclude:: ./share_files_between_components/app.py + :lines: 32- -Link both components via a parent component: -.. code:: python +Run the app above with the following command: - class Root(lit.LightningFlow): - def __init__(self): - super().__init__() - self.train = ModelTraining() - self.deploy = ModelDeploy(ckpt_path=self.train.model_checkpoints_path) +.. code-block:: bash - def run(self): - self.train.run() - self.deploy.run() + lightning run app docs/source-app/workflows/share_files_between_components/app.py +.. code-block:: console - app = lit.LightningApp(Root()) + Your Lightning App is starting. This won't take long. + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + Loaded checkpoint_1: tensor([0, 1, 2, 3, 4]) + Loaded checkpoint_2: tensor([0, 1, 2, 3, 4]) For example, here we save a file on one component and use it in another component: @@ -132,11 +107,10 @@ For example, here we save a file on one component and use it in another componen from lightning_app.storage.path import Path - class ComponentA(LightningWork): def __init__(self): super().__init__() - self.boring_path = Path("boring_file.txt") + self.boring_path = None def run(self): # This should be used as a REFERENCE to the file. diff --git a/docs/source-app/workflows/share_files_between_components/app.py b/docs/source-app/workflows/share_files_between_components/app.py new file mode 100644 index 0000000000000..c087fc81175ad --- /dev/null +++ b/docs/source-app/workflows/share_files_between_components/app.py @@ -0,0 +1,48 @@ +import os + +import torch + +import lightning as L +from lightning.app.storage.path import Path + + +class ModelTraining(L.LightningWork): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.checkpoints_path = Path("./checkpoints") + + def run(self): + # make fake checkpoints + checkpoint_1 = torch.tensor([0, 1, 2, 3, 4]) + checkpoint_2 = torch.tensor([0, 1, 2, 3, 4]) + os.makedirs(self.checkpoints_path, exist_ok=True) + checkpoint_path = str(self.checkpoints_path / "checkpoint_{}.ckpt") + torch.save(checkpoint_1, str(checkpoint_path).format("1")) + torch.save(checkpoint_2, str(checkpoint_path).format("2")) + + +class ModelDeploy(L.LightningWork): + def __init__(self, ckpt_path, *args, **kwargs): + super().__init__() + self.ckpt_path = ckpt_path + + def run(self): + ckpts = os.listdir(self.ckpt_path) + checkpoint_1 = torch.load(os.path.join(self.ckpt_path, ckpts[0])) + checkpoint_2 = torch.load(os.path.join(self.ckpt_path, ckpts[1])) + print(f"Loaded checkpoint_1: {checkpoint_1}") + print(f"Loaded checkpoint_2: {checkpoint_2}") + + +class LitApp(L.LightningFlow): + def __init__(self): + super().__init__() + self.train = ModelTraining() + self.deploy = ModelDeploy(ckpt_path=self.train.checkpoints_path) + + def run(self): + self.train.run() + self.deploy.run() + + +app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/test_an_app.rst b/docs/source-app/workflows/test_an_app.rst index d8e48e2b58954..c51ae3aa8f652 100644 --- a/docs/source-app/workflows/test_an_app.rst +++ b/docs/source-app/workflows/test_an_app.rst @@ -1,3 +1,5 @@ +:orphan: + ########### Test an App ########### diff --git a/setup.py b/setup.py index a542b3c1e0291..6d271cc40b0aa 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") +_PACKAGE_NAME = "" _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From 08a80f53774a594d1b9a2d38656f18587bb4a426 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:03:06 +0100 Subject: [PATCH 002/119] update --- .gitignore | 1 + docs/examples/app_boring/.gitignore | 10 -- docs/examples/app_boring/.lightning | 1 - docs/examples/app_boring/__init__.py | 0 docs/examples/app_boring/app.py | 57 -------- docs/examples/app_boring/app_dynamic.py | 67 --------- docs/examples/app_boring/scripts/serve.py | 29 ---- docs/examples/app_components/__init__.py | 0 .../app_components/python/__init__.py | 0 docs/examples/app_components/python/app.py | 24 --- .../app_components/python/component_popen.py | 7 - .../app_components/python/component_tracer.py | 53 ------- .../app_components/python/pl_script.py | 65 --------- .../python/pytorch_lightning_script.py | 65 --------- .../app_components/serve/gradio/app.py | 53 ------- .../app_components/serve/gradio/beyonce.jpg | Bin 132520 -> 0 bytes .../serve/gradio/requirements.txt | 1 - docs/examples/app_dag/.gitignore | 6 - docs/examples/app_dag/.lightning | 1 - docs/examples/app_dag/.lightningignore | 8 - docs/examples/app_dag/app.py | 137 ------------------ docs/examples/app_dag/processing.py | 14 -- docs/examples/app_dag/requirements.txt | 2 - docs/examples/app_drive/.gitignore | 1 - docs/examples/app_drive/.lightning | 1 - docs/examples/app_drive/app.py | 51 ------- docs/examples/app_hpo/README.md | 64 -------- docs/examples/app_hpo/app_wi_ui.py | 61 -------- docs/examples/app_hpo/app_wo_ui.py | 58 -------- docs/examples/app_hpo/download_data.py | 5 - docs/examples/app_hpo/hyperplot.py | 34 ----- docs/examples/app_hpo/objective.py | 63 -------- docs/examples/app_hpo/pl_script.py | 43 ------ docs/examples/app_hpo/requirements.txt | 3 - docs/examples/app_hpo/utils.py | 54 ------- docs/examples/app_layout/.lightning | 1 - docs/examples/app_layout/__init__.py | 0 docs/examples/app_layout/app.py | 101 ------------- docs/examples/app_layout/ui1/index.html | 10 -- docs/examples/app_layout/ui2/index.html | 10 -- docs/examples/app_multi_node/.gitignore | 2 - docs/examples/app_multi_node/.lightning | 1 - docs/examples/app_multi_node/multi_node.py | 36 ----- docs/examples/app_multi_node/requirements.txt | 1 - docs/examples/app_payload/.lightning | 1 - docs/examples/app_payload/app.py | 31 ---- docs/examples/app_pickle_or_not/app.py | 55 ------- .../app_pickle_or_not/requirements.txt | 0 docs/examples/app_v0/.gitignore | 2 - docs/examples/app_v0/README.md | 18 --- docs/examples/app_v0/__init__.py | 0 docs/examples/app_v0/app.py | 49 ------- docs/examples/app_v0/emulate_ui.py | 19 --- docs/examples/app_v0/requirements.txt | 1 - docs/examples/app_v0/ui/a/index.html | 1 - docs/examples/app_v0/ui/b/index.html | 1 - 56 files changed, 1 insertion(+), 1378 deletions(-) delete mode 100644 docs/examples/app_boring/.gitignore delete mode 100644 docs/examples/app_boring/.lightning delete mode 100644 docs/examples/app_boring/__init__.py delete mode 100644 docs/examples/app_boring/app.py delete mode 100644 docs/examples/app_boring/app_dynamic.py delete mode 100644 docs/examples/app_boring/scripts/serve.py delete mode 100644 docs/examples/app_components/__init__.py delete mode 100644 docs/examples/app_components/python/__init__.py delete mode 100644 docs/examples/app_components/python/app.py delete mode 100644 docs/examples/app_components/python/component_popen.py delete mode 100644 docs/examples/app_components/python/component_tracer.py delete mode 100644 docs/examples/app_components/python/pl_script.py delete mode 100644 docs/examples/app_components/python/pytorch_lightning_script.py delete mode 100644 docs/examples/app_components/serve/gradio/app.py delete mode 100644 docs/examples/app_components/serve/gradio/beyonce.jpg delete mode 100644 docs/examples/app_components/serve/gradio/requirements.txt delete mode 100644 docs/examples/app_dag/.gitignore delete mode 100644 docs/examples/app_dag/.lightning delete mode 100644 docs/examples/app_dag/.lightningignore delete mode 100644 docs/examples/app_dag/app.py delete mode 100644 docs/examples/app_dag/processing.py delete mode 100644 docs/examples/app_dag/requirements.txt delete mode 100644 docs/examples/app_drive/.gitignore delete mode 100644 docs/examples/app_drive/.lightning delete mode 100644 docs/examples/app_drive/app.py delete mode 100644 docs/examples/app_hpo/README.md delete mode 100644 docs/examples/app_hpo/app_wi_ui.py delete mode 100644 docs/examples/app_hpo/app_wo_ui.py delete mode 100644 docs/examples/app_hpo/download_data.py delete mode 100644 docs/examples/app_hpo/hyperplot.py delete mode 100644 docs/examples/app_hpo/objective.py delete mode 100644 docs/examples/app_hpo/pl_script.py delete mode 100644 docs/examples/app_hpo/requirements.txt delete mode 100644 docs/examples/app_hpo/utils.py delete mode 100644 docs/examples/app_layout/.lightning delete mode 100644 docs/examples/app_layout/__init__.py delete mode 100644 docs/examples/app_layout/app.py delete mode 100644 docs/examples/app_layout/ui1/index.html delete mode 100644 docs/examples/app_layout/ui2/index.html delete mode 100644 docs/examples/app_multi_node/.gitignore delete mode 100644 docs/examples/app_multi_node/.lightning delete mode 100644 docs/examples/app_multi_node/multi_node.py delete mode 100644 docs/examples/app_multi_node/requirements.txt delete mode 100644 docs/examples/app_payload/.lightning delete mode 100644 docs/examples/app_payload/app.py delete mode 100644 docs/examples/app_pickle_or_not/app.py delete mode 100644 docs/examples/app_pickle_or_not/requirements.txt delete mode 100644 docs/examples/app_v0/.gitignore delete mode 100644 docs/examples/app_v0/README.md delete mode 100644 docs/examples/app_v0/__init__.py delete mode 100644 docs/examples/app_v0/app.py delete mode 100644 docs/examples/app_v0/emulate_ui.py delete mode 100644 docs/examples/app_v0/requirements.txt delete mode 100644 docs/examples/app_v0/ui/a/index.html delete mode 100644 docs/examples/app_v0/ui/b/index.html diff --git a/.gitignore b/.gitignore index 47b9bfff92523..b84da4a864e51 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cifar-10-batches-py # ctags tags .tags +docs/examples \ No newline at end of file diff --git a/docs/examples/app_boring/.gitignore b/docs/examples/app_boring/.gitignore deleted file mode 100644 index 94018704d9f90..0000000000000 --- a/docs/examples/app_boring/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -lightning_logs -*.pt -.storage/ -.shared/ -data -*.ckpt -redis-stable -node_modules -*.rdb -boring_file.txt diff --git a/docs/examples/app_boring/.lightning b/docs/examples/app_boring/.lightning deleted file mode 100644 index c85414d8c498a..0000000000000 --- a/docs/examples/app_boring/.lightning +++ /dev/null @@ -1 +0,0 @@ -name: boring-app diff --git a/docs/examples/app_boring/__init__.py b/docs/examples/app_boring/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_boring/app.py b/docs/examples/app_boring/app.py deleted file mode 100644 index 9ba11316c65a1..0000000000000 --- a/docs/examples/app_boring/app.py +++ /dev/null @@ -1,57 +0,0 @@ -import os - -import lightning as L -from lightning.app.components.python import TracerPythonScript -from lightning.app.storage.path import Path - -FILE_CONTENT = """ -Hello there! -This tab is currently an IFrame of the FastAPI Server running in `DestinationFileAndServeWork`. -Also, the content of this file was created in `SourceFileWork` and then transferred to `DestinationFileAndServeWork`. -Are you already 🤯 ? Stick with us, this is only the beginning. Lightning is 🚀. -""" - - -class SourceFileWork(L.LightningWork): - def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): - super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) - self.boring_path = None - - def run(self): - # This should be used as a REFERENCE to the file. - self.boring_path = "lit://boring_file.txt" - with open(self.boring_path, "w", encoding="utf-8") as f: - f.write(FILE_CONTENT) - - -class DestinationFileAndServeWork(TracerPythonScript): - def run(self, path: Path): - assert path.exists() - self.script_args += [f"--filepath={path}", f"--host={self.host}", f"--port={self.port}"] - super().run() - - -class BoringApp(L.LightningFlow): - def __init__(self): - super().__init__() - self.source_work = SourceFileWork() - self.dest_work = DestinationFileAndServeWork( - script_path=os.path.join(os.path.dirname(__file__), "scripts/serve.py"), - port=1111, - parallel=False, # runs until killed. - cloud_compute=L.CloudCompute(), - raise_exception=True, - ) - - def run(self): - self.source_work.run() - if self.source_work.has_succeeded: - # the flow passes the file from one work to another. - self.dest_work.run(self.source_work.boring_path) - self._exit("Boring App End") - - def configure_layout(self): - return {"name": "Boring Tab", "content": self.dest_work.url + "/file"} - - -app = L.LightningApp(BoringApp()) diff --git a/docs/examples/app_boring/app_dynamic.py b/docs/examples/app_boring/app_dynamic.py deleted file mode 100644 index 6e3fdfa3ccdee..0000000000000 --- a/docs/examples/app_boring/app_dynamic.py +++ /dev/null @@ -1,67 +0,0 @@ -import os - -import lightning as L -from lightning.app.components.python import TracerPythonScript -from lightning.app.storage.path import Path -from lightning.app.structures import Dict - -FILE_CONTENT = """ -Hello there! -This tab is currently an IFrame of the FastAPI Server running in `DestinationFileAndServeWork`. -Also, the content of this file was created in `SourceFileWork` and then transferred to `DestinationFileAndServeWork`. -Are you already 🤯 ? Stick with us, this is only the beginning. Lightning is 🚀. -""" - - -class SourceFileWork(L.LightningWork): - def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): - super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) - self.boring_path = None - - def run(self): - # This should be used as a REFERENCE to the file. - self.boring_path = "lit://boring_file.txt" - with open(self.boring_path, "w") as f: - f.write(FILE_CONTENT) - - -class DestinationFileAndServeWork(TracerPythonScript): - def run(self, path: Path): - assert path.exists() - self.script_args += [f"--filepath={path}", f"--host={self.host}", f"--port={self.port}"] - super().run() - - -class BoringApp(L.LightningFlow): - def __init__(self): - super().__init__() - self.dict = Dict() - - def run(self): - # create dynamically the source_work at runtime - if "src_w" not in self.dict: - self.dict["src_w"] = SourceFileWork() - - self.dict["src_w"].run() - - if self.dict["src_w"].has_succeeded: - - # create dynamically the dst_w at runtime - if "dst_w" not in self.dict: - self.dict["dst_w"] = DestinationFileAndServeWork( - script_path=os.path.join(os.path.dirname(__file__), "scripts/serve.py"), - port=1111, - parallel=False, # runs until killed. - cloud_compute=L.CloudCompute(), - raise_exception=True, - ) - - # the flow passes the file from one work to another. - self.dict["dst_w"].run(self.dict["src_w"].boring_path) - self._exit("Boring App End") - - def configure_layout(self): - return {"name": "Boring Tab", "content": self.dict["dst_w"].url + "/file" if "dst_w" in self.dict else ""} - - -app = L.LightningApp(BoringApp()) diff --git a/docs/examples/app_boring/scripts/serve.py b/docs/examples/app_boring/scripts/serve.py deleted file mode 100644 index 17c431ca378ac..0000000000000 --- a/docs/examples/app_boring/scripts/serve.py +++ /dev/null @@ -1,29 +0,0 @@ -import argparse -import os - -import uvicorn -from fastapi import FastAPI -from fastapi.requests import Request -from fastapi.responses import HTMLResponse - -if __name__ == "__main__": - - parser = argparse.ArgumentParser("Server Parser") - parser.add_argument("--filepath", type=str, help="Where to find the `filepath`") - parser.add_argument("--host", type=str, default="0.0.0.0", help="Server host`") - parser.add_argument("--port", type=int, default="8888", help="Server port`") - hparams = parser.parse_args() - - fastapi_service = FastAPI() - - if not os.path.exists(str(hparams.filepath)): - content = ["The file wasn't transferred"] - else: - content = open(hparams.filepath).readlines() # read the file received from SourceWork. - - @fastapi_service.get("/file") - async def get_file_content(request: Request, response_class=HTMLResponse): - lines = "\n".join(["

" + line + "

" for line in content]) - return HTMLResponse(f"
    {lines}
") - - uvicorn.run(app=fastapi_service, host=hparams.host, port=hparams.port) diff --git a/docs/examples/app_components/__init__.py b/docs/examples/app_components/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_components/python/__init__.py b/docs/examples/app_components/python/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_components/python/app.py b/docs/examples/app_components/python/app.py deleted file mode 100644 index 1386a699a09fb..0000000000000 --- a/docs/examples/app_components/python/app.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from pathlib import Path - -import lightning as L -from examples.components.python.component_tracer import PLTracerPythonScript - - -class RootFlow(L.LightningFlow): - def __init__(self): - super().__init__() - script_path = Path(__file__).parent / "pl_script.py" - self.tracer_python_script = PLTracerPythonScript(script_path) - - def run(self): - assert os.getenv("GLOBAL_RANK", "0") == "0" - if not self.tracer_python_script.has_started: - self.tracer_python_script.run() - if self.tracer_python_script.has_succeeded: - self._exit("tracer script succeed") - if self.tracer_python_script.has_failed: - self._exit("tracer script failed") - - -app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_components/python/component_popen.py b/docs/examples/app_components/python/component_popen.py deleted file mode 100644 index d3af5ee2d55c7..0000000000000 --- a/docs/examples/app_components/python/component_popen.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -from lightning.app.components.python import PopenPythonScript - -if __name__ == "__main__": - comp = PopenPythonScript(Path(__file__).parent / "pl_script.py") - comp.run() diff --git a/docs/examples/app_components/python/component_tracer.py b/docs/examples/app_components/python/component_tracer.py deleted file mode 100644 index 9edc48cf51a29..0000000000000 --- a/docs/examples/app_components/python/component_tracer.py +++ /dev/null @@ -1,53 +0,0 @@ -from lightning.app.components.python import TracerPythonScript -from lightning.app.storage.path import Path -from lightning.app.utilities.tracer import Tracer -from pytorch_lightning import Trainer - - -class PLTracerPythonScript(TracerPythonScript): - - """This component can be used for ANY PyTorch Lightning script to track its progress and extract its best model - path.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Define the component state. - self.global_step = None - self.best_model_path = None - - def configure_tracer(self) -> Tracer: - from pytorch_lightning.callbacks import Callback - - class MyInjectedCallback(Callback): - def __init__(self, lightning_work): - self.lightning_work = lightning_work - - def on_train_start(self, trainer, pl_module) -> None: - print("This code doesn't belong to the script but was injected.") - print("Even the Lightning Work is available and state transfer works !") - print(self.lightning_work) - - def on_batch_end(self, trainer, *_) -> None: - # On every batch end, collects some information. - # This is communicated automatically to the rest of the app, - # so you can track your training in real time in the Lightning App UI. - self.lightning_work.global_step = trainer.global_step - best_model_path = trainer.checkpoint_callback.best_model_path - if best_model_path: - self.lightning_work.best_model_path = Path(best_model_path) - - # This hook would be called every time - # before a Trainer `__init__` method is called. - - def trainer_pre_fn(trainer, *args, **kwargs): - kwargs["callbacks"] = kwargs.get("callbacks", []) + [MyInjectedCallback(self)] - return {}, args, kwargs - - tracer = super().configure_tracer() - tracer.add_traced(Trainer, "__init__", pre_fn=trainer_pre_fn) - return tracer - - -if __name__ == "__main__": - comp = PLTracerPythonScript(Path(__file__).parent / "pl_script.py") - res = comp.run() diff --git a/docs/examples/app_components/python/pl_script.py b/docs/examples/app_components/python/pl_script.py deleted file mode 100644 index 4ad17b459200c..0000000000000 --- a/docs/examples/app_components/python/pl_script.py +++ /dev/null @@ -1,65 +0,0 @@ -import torch -from torch.utils.data import DataLoader, Dataset - -from pytorch_lightning import LightningModule, Trainer - - -class RandomDataset(Dataset): - def __init__(self, size: int, length: int): - self.len = length - self.data = torch.randn(length, size) - - def __getitem__(self, index): - return self.data[index] - - def __len__(self): - return self.len - - -class BoringModel(LightningModule): - def __init__(self): - super().__init__() - self.layer = torch.nn.Linear(32, 2) - - def forward(self, x): - return self.layer(x) - - def loss(self, batch, prediction): - # An arbitrary loss to have a loss that updates the model weights during `Trainer.fit` calls - return torch.nn.functional.mse_loss(prediction, torch.ones_like(prediction)) - - def training_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"loss": loss} - - def validation_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"x": loss} - - def test_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"y": loss} - - def configure_optimizers(self): - optimizer = torch.optim.SGD(self.layer.parameters(), lr=0.1) - lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1) - return [optimizer], [lr_scheduler] - - def train_dataloader(self): - return DataLoader(RandomDataset(32, 64)) - - val_dataloader = train_dataloader - test_dataloader = train_dataloader - predict_dataloader = train_dataloader - - -if __name__ == "__main__": - model = BoringModel() - trainer = Trainer(max_epochs=1, accelerator="cpu", devices=2, strategy="ddp") - trainer.fit(model) - trainer.validate(model) - trainer.test(model) - trainer.predict(model) diff --git a/docs/examples/app_components/python/pytorch_lightning_script.py b/docs/examples/app_components/python/pytorch_lightning_script.py deleted file mode 100644 index 4ad17b459200c..0000000000000 --- a/docs/examples/app_components/python/pytorch_lightning_script.py +++ /dev/null @@ -1,65 +0,0 @@ -import torch -from torch.utils.data import DataLoader, Dataset - -from pytorch_lightning import LightningModule, Trainer - - -class RandomDataset(Dataset): - def __init__(self, size: int, length: int): - self.len = length - self.data = torch.randn(length, size) - - def __getitem__(self, index): - return self.data[index] - - def __len__(self): - return self.len - - -class BoringModel(LightningModule): - def __init__(self): - super().__init__() - self.layer = torch.nn.Linear(32, 2) - - def forward(self, x): - return self.layer(x) - - def loss(self, batch, prediction): - # An arbitrary loss to have a loss that updates the model weights during `Trainer.fit` calls - return torch.nn.functional.mse_loss(prediction, torch.ones_like(prediction)) - - def training_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"loss": loss} - - def validation_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"x": loss} - - def test_step(self, batch, batch_idx): - output = self(batch) - loss = self.loss(batch, output) - return {"y": loss} - - def configure_optimizers(self): - optimizer = torch.optim.SGD(self.layer.parameters(), lr=0.1) - lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1) - return [optimizer], [lr_scheduler] - - def train_dataloader(self): - return DataLoader(RandomDataset(32, 64)) - - val_dataloader = train_dataloader - test_dataloader = train_dataloader - predict_dataloader = train_dataloader - - -if __name__ == "__main__": - model = BoringModel() - trainer = Trainer(max_epochs=1, accelerator="cpu", devices=2, strategy="ddp") - trainer.fit(model) - trainer.validate(model) - trainer.test(model) - trainer.predict(model) diff --git a/docs/examples/app_components/serve/gradio/app.py b/docs/examples/app_components/serve/gradio/app.py deleted file mode 100644 index 7bb3e7bf790cb..0000000000000 --- a/docs/examples/app_components/serve/gradio/app.py +++ /dev/null @@ -1,53 +0,0 @@ -from functools import partial - -import gradio as gr -import requests -import torch -from PIL import Image - -import lightning as L -from lightning.app.components.serve import ServeGradio - - -# Credit to @akhaliq for his inspiring work. -# Find his original code there: https://huggingface.co/spaces/akhaliq/AnimeGANv2/blob/main/app.py -class AnimeGANv2UI(ServeGradio): - - inputs = gr.inputs.Image(type="pil") - outputs = gr.outputs.Image(type="pil") - elon = "https://upload.wikimedia.org/wikipedia/commons/3/34/Elon_Musk_Royal_Society_%28crop2%29.jpg" - img = Image.open(requests.get(elon, stream=True).raw) - img.save("elon.jpg") - examples = [["elon.jpg"]] - - def __init__(self): - super().__init__() - self.ready = False - - def predict(self, img): - return self.model(img=img) - - def build_model(self): - repo = "AK391/animegan2-pytorch:main" - model = torch.hub.load(repo, "generator", device="cpu") - face2paint = torch.hub.load(repo, "face2paint", size=512, device="cpu") - self.ready = True - return partial(face2paint, model=model) - - -class RootFlow(L.LightningFlow): - def __init__(self): - super().__init__() - self.demo = AnimeGANv2UI() - - def run(self): - self.demo.run() - - def configure_layout(self): - tabs = [] - if self.demo.ready: - tabs.append({"name": "Home", "content": self.demo}) - return tabs - - -app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_components/serve/gradio/beyonce.jpg b/docs/examples/app_components/serve/gradio/beyonce.jpg deleted file mode 100644 index 68b6084475b019bd37db953b87c37ec905b79b86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132520 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
t|(Tt)ka`yC-9-2TR)4O4mu-Wozl{ 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 self.total_trials: - self._exit() - - has_told_study = [] - - for trial_idx in range(self.num_trials): - work_name = f"objective_work_{trial_idx}" - if work_name not in self.ws: - objective_work = ObjectiveWork( - script_path=self.script_path, - data_dir=self.data_dir, - cloud_compute=L.CloudCompute("cpu"), - ) - self.ws[work_name] = objective_work - if not self.ws[work_name].has_started: - trial = self._study.ask(ObjectiveWork.distributions()) - self.ws[work_name].run(trial_id=trial._trial_id, **trial.params) - - if self.ws[work_name].metric and not self.ws[work_name].has_told_study: - self.hi_plot.data.append({"x": -1 * self.ws[work_name].metric, **self.ws[work_name].params}) - self._study.tell(self.ws[work_name].trial_id, self.ws[work_name].metric) - self.ws[work_name].has_told_study = True - - has_told_study.append(self.ws[work_name].has_told_study) - - if all(has_told_study): - self.num_trials += self.simultaneous_trials - - -if __name__ == "__main__": - app = L.LightningApp( - RootHPOFlow( - script_path=str(Path(__file__).parent / "pl_script.py"), - data_dir="data/hymenoptera_data_version_0", - total_trials=6, - simultaneous_trials=2, - ) - ) diff --git a/docs/examples/app_hpo/app_wo_ui.py b/docs/examples/app_hpo/app_wo_ui.py deleted file mode 100644 index b8a8448668573..0000000000000 --- a/docs/examples/app_hpo/app_wo_ui.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path - -import optuna -from objective import ObjectiveWork - -import lightning as L -from lightning.app.structures import Dict - - -class RootHPOFlow(L.LightningFlow): - def __init__(self, script_path, data_dir, total_trials, simultaneous_trials): - super().__init__() - self.script_path = script_path - self.data_dir = data_dir - self.total_trials = total_trials - self.simultaneous_trials = simultaneous_trials - self.num_trials = simultaneous_trials - self._study = optuna.create_study() - self.ws = Dict() - - def run(self): - if self.num_trials >= self.total_trials: - self._exit() - - has_told_study = [] - - for trial_idx in range(self.num_trials): - work_name = f"objective_work_{trial_idx}" - if work_name not in self.ws: - objective_work = ObjectiveWork( - script_path=self.script_path, - data_dir=self.data_dir, - cloud_compute=L.CloudCompute("cpu"), - ) - self.ws[work_name] = objective_work - if not self.ws[work_name].has_started: - trial = self._study.ask(ObjectiveWork.distributions()) - self.ws[work_name].run(trial_id=trial._trial_id, **trial.params) - - if self.ws[work_name].metric and not self.ws[work_name].has_told_study: - self._study.tell(self.ws[work_name].trial_id, self.ws[work_name].metric) - self.ws[work_name].has_told_study = True - - has_told_study.append(self.ws[work_name].has_told_study) - - if all(has_told_study): - self.num_trials += self.simultaneous_trials - - -if __name__ == "__main__": - app = L.LightningApp( - RootHPOFlow( - script_path=str(Path(__file__).parent / "pl_script.py"), - data_dir="data/hymenoptera_data_version_0", - total_trials=6, - simultaneous_trials=2, - ) - ) diff --git a/docs/examples/app_hpo/download_data.py b/docs/examples/app_hpo/download_data.py deleted file mode 100644 index d82b86a9dee95..0000000000000 --- a/docs/examples/app_hpo/download_data.py +++ /dev/null @@ -1,5 +0,0 @@ -from utils import download_data - -data_dir = "hymenoptera_data_version_0" -download_url = f"https://pl-flash-data.s3.amazonaws.com/{data_dir}.zip" -download_data(download_url, "./data") diff --git a/docs/examples/app_hpo/hyperplot.py b/docs/examples/app_hpo/hyperplot.py deleted file mode 100644 index 105285822705c..0000000000000 --- a/docs/examples/app_hpo/hyperplot.py +++ /dev/null @@ -1,34 +0,0 @@ -import lightning as L -from lightning.app.frontend.stream_lit import StreamlitFrontend -from lightning.app.utilities.state import AppState - - -class HiPlotFlow(L.LightningFlow): - def __init__(self): - super().__init__() - self.data = [] - - def run(self): - pass - - def configure_layout(self): - return StreamlitFrontend(render_fn=render_fn) - - -def render_fn(state: AppState): - import json - - import hiplot as hip - import streamlit as st - from streamlit_autorefresh import st_autorefresh - - st.set_page_config(layout="wide") - st_autorefresh(interval=1000, limit=None, key="refresh") - - if not state.data: - st.write("No data available yet ! Stay tuned") - return - - xp = hip.Experiment.from_iterable(state.data) - ret_val = xp.to_streamlit(ret="selected_uids", key="hip").display() - st.markdown("hiplot returned " + json.dumps(ret_val)) diff --git a/docs/examples/app_hpo/objective.py b/docs/examples/app_hpo/objective.py deleted file mode 100644 index f2d1ebb6a747f..0000000000000 --- a/docs/examples/app_hpo/objective.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -import tempfile -from datetime import datetime -from typing import Optional - -import pandas as pd -import torch -from optuna.distributions import CategoricalDistribution, LogUniformDistribution -from torchmetrics import Accuracy - -import lightning as L -from lightning.app.components.python import TracerPythonScript - - -class ObjectiveWork(TracerPythonScript): - def __init__(self, script_path: str, data_dir: str, cloud_compute: Optional[L.CloudCompute]): - timestamp = datetime.now().strftime("%H:%M:%S") - tmpdir = tempfile.TemporaryDirectory().name - submission_path = os.path.join(tmpdir, f"{timestamp}.csv") - best_model_path = os.path.join(tmpdir, f"{timestamp}.model.pt") - super().__init__( - script_path, - script_args=[ - f"--train_data_path={data_dir}/train", - f"--test_data_path={data_dir}/test", - f"--submission_path={submission_path}", - f"--best_model_path={best_model_path}", - ], - cloud_compute=cloud_compute, - ) - self.data_dir = data_dir - self.best_model_path = best_model_path - self.submission_path = submission_path - self.metric = None - self.trial_id = None - self.metric = None - self.params = None - self.has_told_study = False - - def run(self, trial_id: int, **params): - self.trial_id = trial_id - self.params = params - self.script_args.extend([f"--{k}={v}" for k, v in params.items()]) - super().run() - self.compute_metric() - - def _to_labels(self, path: str): - return torch.from_numpy(pd.read_csv(path).label.values) - - def compute_metric(self): - self.metric = -1 * float( - Accuracy()( - self._to_labels(self.submission_path), - self._to_labels(f"{self.data_dir}/ground_truth.csv"), - ) - ) - - @staticmethod - def distributions(): - return { - "backbone": CategoricalDistribution(["resnet18", "resnet34"]), - "learning_rate": LogUniformDistribution(0.0001, 0.1), - } diff --git a/docs/examples/app_hpo/pl_script.py b/docs/examples/app_hpo/pl_script.py deleted file mode 100644 index bbc453798431a..0000000000000 --- a/docs/examples/app_hpo/pl_script.py +++ /dev/null @@ -1,43 +0,0 @@ -import argparse -import os - -import pandas as pd -import torch -from flash import Trainer -from flash.image import ImageClassificationData, ImageClassifier - -# Parse arguments provided by the Work. -parser = argparse.ArgumentParser() -parser.add_argument("--train_data_path", type=str, required=True) -parser.add_argument("--submission_path", type=str, required=True) -parser.add_argument("--test_data_path", type=str, required=True) -parser.add_argument("--best_model_path", type=str, required=True) -# Optional -parser.add_argument("--backbone", type=str, default="resnet18") -parser.add_argument("--learning_rate", type=float, default=0.01) -args = parser.parse_args() - - -datamodule = ImageClassificationData.from_folders( - train_folder=args.train_data_path, - batch_size=8, -) - -model = ImageClassifier(datamodule.num_classes, backbone=args.backbone) -trainer = Trainer(fast_dev_run=True) -trainer.fit(model, datamodule=datamodule) -trainer.save_checkpoint(args.best_model_path) - -datamodule = ImageClassificationData.from_folders( - predict_folder=args.test_data_path, - batch_size=8, -) - -predictions = Trainer().predict(model, datamodule=datamodule) -submission_data = [ - {"filename": os.path.basename(p["metadata"]["filepath"]), "label": torch.argmax(p["preds"]).item()} - for batch in predictions - for p in batch -] -df = pd.DataFrame(submission_data) -df.to_csv(args.submission_path, index=False) diff --git a/docs/examples/app_hpo/requirements.txt b/docs/examples/app_hpo/requirements.txt deleted file mode 100644 index bd85880da2237..0000000000000 --- a/docs/examples/app_hpo/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -optuna -lightning-flash[image,serve] == 0.7.0 -hiplot diff --git a/docs/examples/app_hpo/utils.py b/docs/examples/app_hpo/utils.py deleted file mode 100644 index 3e8960ea893fc..0000000000000 --- a/docs/examples/app_hpo/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -import os.path -import tarfile -import zipfile - -import requests - - -def download_data(url: str, path: str = "data/", verbose: bool = False) -> None: - """Download file with progressbar. - - # Code taken from: https://gist.github.com/ruxi/5d6803c116ec1130d484a4ab8c00c603 - # __author__ = "github.com/ruxi" - # __license__ = "MIT" - - Usage: - download_file('http://web4host.net/5MB.zip') - """ - if url == "NEED_TO_BE_CREATED": - raise NotImplementedError - - if not os.path.exists(path): - os.makedirs(path) - local_filename = os.path.join(path, url.split("/")[-1]) - r = requests.get(url, stream=True, verify=False) - file_size = int(r.headers["Content-Length"]) if "Content-Length" in r.headers else 0 - chunk_size = 1024 - num_bars = int(file_size / chunk_size) - if verbose: - print(dict(file_size=file_size)) - print(dict(num_bars=num_bars)) - - if not os.path.exists(local_filename): - with open(local_filename, "wb") as fp: - for chunk in r.iter_content(chunk_size=chunk_size): - fp.write(chunk) # type: ignore - - def extract_tarfile(file_path: str, extract_path: str, mode: str): - if os.path.exists(file_path): - with tarfile.open(file_path, mode=mode) as tar_ref: - for member in tar_ref.getmembers(): - try: - tar_ref.extract(member, path=extract_path, set_attrs=False) - except PermissionError: - raise PermissionError(f"Could not extract tar file {file_path}") - - if ".zip" in local_filename: - if os.path.exists(local_filename): - with zipfile.ZipFile(local_filename, "r") as zip_ref: - zip_ref.extractall(path) - elif local_filename.endswith(".tar.gz") or local_filename.endswith(".tgz"): - extract_tarfile(local_filename, path, "r:gz") - elif local_filename.endswith(".tar.bz2") or local_filename.endswith(".tbz"): - extract_tarfile(local_filename, path, "r:bz2") diff --git a/docs/examples/app_layout/.lightning b/docs/examples/app_layout/.lightning deleted file mode 100644 index 48e8408f9e81e..0000000000000 --- a/docs/examples/app_layout/.lightning +++ /dev/null @@ -1 +0,0 @@ -name: layout-example diff --git a/docs/examples/app_layout/__init__.py b/docs/examples/app_layout/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_layout/app.py b/docs/examples/app_layout/app.py deleted file mode 100644 index b7feb3e6d07be..0000000000000 --- a/docs/examples/app_layout/app.py +++ /dev/null @@ -1,101 +0,0 @@ -"""An example showcasing how `configure_layout` can be used to nest user interfaces of different flows. - -Run the app: - -lightning run app examples/layout/demo.py - -This starts one server for each flow that returns a UI. Access the UI at the link printed in the terminal. -""" - -import os -from time import sleep - -import lightning as L -from lightning.app.frontend.stream_lit import StreamlitFrontend -from lightning.app.frontend.web import StaticWebFrontend - - -class C11(L.LightningFlow): - def __init__(self): - super().__init__() - self.message = "Hello Streamlit!" - - def run(self): - pass - - def configure_layout(self): - return StreamlitFrontend(render_fn=render_c11) - - -def render_c11(state): - import streamlit as st - - st.write(state.message) - - -class C21(L.LightningFlow): - def __init__(self): - super().__init__() - - def run(self): - pass - - def configure_layout(self): - return StaticWebFrontend(os.path.join(os.path.dirname(__file__), "ui1")) - - -class C22(L.LightningFlow): - def __init__(self): - super().__init__() - - def run(self): - pass - - def configure_layout(self): - return StaticWebFrontend(os.path.join(os.path.dirname(__file__), "ui2")) - - -class C1(L.LightningFlow): - def __init__(self): - super().__init__() - self.c11 = C11() - - def run(self): - pass - - -class C2(L.LightningFlow): - def __init__(self): - super().__init__() - self.c21 = C21() - self.c22 = C22() - - def run(self): - pass - - def configure_layout(self): - return [ - dict(name="one", content=self.c21), - dict(name="two", content=self.c22), - ] - - -class Root(L.LightningFlow): - def __init__(self): - super().__init__() - self.c1 = C1() - self.c2 = C2() - - def run(self): - sleep(10) - self._exit("Layout End") - - def configure_layout(self): - return [ - dict(name="one", content=self.c1.c11), - dict(name="two", content=self.c2), - dict(name="three", content="https://lightning.ai"), - ] - - -app = L.LightningApp(Root()) diff --git a/docs/examples/app_layout/ui1/index.html b/docs/examples/app_layout/ui1/index.html deleted file mode 100644 index 7019634b87fd5..0000000000000 --- a/docs/examples/app_layout/ui1/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - One - - -One - - diff --git a/docs/examples/app_layout/ui2/index.html b/docs/examples/app_layout/ui2/index.html deleted file mode 100644 index f9b6432e4963d..0000000000000 --- a/docs/examples/app_layout/ui2/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Two - - -Two - - diff --git a/docs/examples/app_multi_node/.gitignore b/docs/examples/app_multi_node/.gitignore deleted file mode 100644 index 33eb0ef33c61c..0000000000000 --- a/docs/examples/app_multi_node/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.storage/ -.shared/ diff --git a/docs/examples/app_multi_node/.lightning b/docs/examples/app_multi_node/.lightning deleted file mode 100644 index 7befcc74ea6d3..0000000000000 --- a/docs/examples/app_multi_node/.lightning +++ /dev/null @@ -1 +0,0 @@ -name: multi-node-demo diff --git a/docs/examples/app_multi_node/multi_node.py b/docs/examples/app_multi_node/multi_node.py deleted file mode 100644 index adc8df1c74815..0000000000000 --- a/docs/examples/app_multi_node/multi_node.py +++ /dev/null @@ -1,36 +0,0 @@ -import lightning as L - - -class Work(L.LightningWork): - def __init__(self, cloud_compute: L.CloudCompute = L.CloudCompute(), **kwargs): - super().__init__(parallel=True, **kwargs, cloud_compute=cloud_compute) - - def run(self, main_address="localhost", main_port=1111, world_size=1, rank=0, init=False): - if init: - return - - import torch.distributed - - print(f"Initializing process group: {main_address=}, {main_port=}, {world_size=}, {rank=}") - torch.distributed.init_process_group( - backend="gloo", init_method=f"tcp://{main_address}:{main_port}", world_size=world_size, rank=rank - ) - gathered = [torch.zeros(1) for _ in range(world_size)] - torch.distributed.all_gather(gathered, torch.tensor([rank]).float()) - print(gathered) - - -class MultiNodeDemo(L.LightningFlow): - def __init__(self): - super().__init__() - self.work0 = Work() - self.work1 = Work() - - def run(self): - self.work0.run(init=True) - if self.work0.internal_ip: - self.work0.run(main_address=self.work0.internal_ip, main_port=self.work0.port, world_size=2, rank=0) - self.work1.run(main_address=self.work0.internal_ip, main_port=self.work0.port, world_size=2, rank=1) - - -app = L.LightningApp(MultiNodeDemo()) diff --git a/docs/examples/app_multi_node/requirements.txt b/docs/examples/app_multi_node/requirements.txt deleted file mode 100644 index 12c6d5d5eac2a..0000000000000 --- a/docs/examples/app_multi_node/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -torch diff --git a/docs/examples/app_payload/.lightning b/docs/examples/app_payload/.lightning deleted file mode 100644 index 933d6ed9a73e1..0000000000000 --- a/docs/examples/app_payload/.lightning +++ /dev/null @@ -1 +0,0 @@ -name: payload diff --git a/docs/examples/app_payload/app.py b/docs/examples/app_payload/app.py deleted file mode 100644 index 66de76d964adc..0000000000000 --- a/docs/examples/app_payload/app.py +++ /dev/null @@ -1,31 +0,0 @@ -import lightning as L -from lightning.app.storage.payload import Payload - - -class SourceFileWriterWork(L.LightningWork): - def __init__(self): - super().__init__() - self.value = None - - def run(self): - self.value = Payload(42) - - -class DestinationWork(L.LightningWork): - def run(self, payload): - assert payload.value == 42 - - -class RootFlow(L.LightningFlow): - def __init__(self): - super().__init__() - self.src = SourceFileWriterWork() - self.dst = DestinationWork() - - def run(self): - self.src.run() - self.dst.run(self.src.value) - self._exit("Application End!") - - -app = L.LightningApp(RootFlow()) diff --git a/docs/examples/app_pickle_or_not/app.py b/docs/examples/app_pickle_or_not/app.py deleted file mode 100644 index bda24cb5b7967..0000000000000 --- a/docs/examples/app_pickle_or_not/app.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging - -import lightning as L - -logger = logging.getLogger(__name__) - - -class PickleChecker(L.LightningWork): - def run(self, pickle_image: bytes): - parsed = self.parse_image(pickle_image) - if parsed == b"it is a pickle": - return True - elif parsed == b"it is not a pickle": - return False - else: - raise Exception("Couldn't parse the image") - - @staticmethod - def parse_image(image_str: bytes): - return image_str - - -class Slack(L.LightningFlow): - def __init__(self): - super().__init__() - - @staticmethod - def send_message(message): - logger.info(f"Sending message: {message}") - - def run(self): - pass - - -class RootComponent(L.LightningFlow): - def __init__(self): - super().__init__() - self.pickle_checker = PickleChecker() - self.slack = Slack() - self.counter = 3 - - def run(self): - if self.counter > 0: - logger.info(f"Running the app {self.counter}") - image_str = b"it is not a pickle" - if self.pickle_checker.run(image_str): - self.slack.send_message("It's a pickle!") - else: - self.slack.send_message("It's not a pickle!") - self.counter -= 1 - else: - self._exit("Pickle or Not End") - - -app = L.LightningApp(RootComponent()) diff --git a/docs/examples/app_pickle_or_not/requirements.txt b/docs/examples/app_pickle_or_not/requirements.txt deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_v0/.gitignore b/docs/examples/app_v0/.gitignore deleted file mode 100644 index 186149fa056fe..0000000000000 --- a/docs/examples/app_v0/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.storage -.lightning diff --git a/docs/examples/app_v0/README.md b/docs/examples/app_v0/README.md deleted file mode 100644 index 516283ae9cedd..0000000000000 --- a/docs/examples/app_v0/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# v0 app - -This app is a flow-only app with nothing fancy. -This is meant to present the basic functionalities of the lightning framework. - -## Starting it - -Local - -```bash -lightning run app app.py -``` - -Cloud - -```bash -lightning run app app.py --cloud -``` diff --git a/docs/examples/app_v0/__init__.py b/docs/examples/app_v0/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/docs/examples/app_v0/app.py b/docs/examples/app_v0/app.py deleted file mode 100644 index 26345f5b43e46..0000000000000 --- a/docs/examples/app_v0/app.py +++ /dev/null @@ -1,49 +0,0 @@ -# v0_app.py -import os -from datetime import datetime -from time import sleep - -import lightning as L -from lightning.app.frontend.web import StaticWebFrontend - - -class Word(L.LightningFlow): - def __init__(self, letter): - super().__init__() - self.letter = letter - self.repeats = letter - - def run(self): - self.repeats += self.letter - - def configure_layout(self): - return StaticWebFrontend(os.path.join(os.path.dirname(__file__), f"ui/{self.letter}")) - - -class V0App(L.LightningFlow): - def __init__(self): - super().__init__() - self.aas = Word("a") - self.bbs = Word("b") - self.counter = 0 - - def run(self): - now = datetime.now() - now = now.strftime("%H:%M:%S") - log = {"time": now, "a": self.aas.repeats, "b": self.bbs.repeats} - print(log) - self.aas.run() - self.bbs.run() - - sleep(2.0) - self.counter += 1 - - def configure_layout(self): - tab1 = {"name": "Tab_1", "content": self.aas} - tab2 = {"name": "Tab_2", "content": self.bbs} - tab3 = {"name": "Tab_3", "content": "https://tensorboard.dev/experiment/8m1aX0gcQ7aEmH0J7kbBtg/#scalars"} - - return [tab1, tab2, tab3] - - -app = L.LightningApp(V0App()) diff --git a/docs/examples/app_v0/emulate_ui.py b/docs/examples/app_v0/emulate_ui.py deleted file mode 100644 index 8a5b45c1c3904..0000000000000 --- a/docs/examples/app_v0/emulate_ui.py +++ /dev/null @@ -1,19 +0,0 @@ -from time import sleep - -import requests - -from lightning.app.utilities.state import headers_for - -headers = headers_for({}) -headers["X-Lightning-Type"] = "DEFAULT" - -res = requests.get("http://127.0.0.1:7501/state", headers=headers) - - -res = requests.post("http://127.0.0.1:7501/state", json={"stage": "running"}, headers=headers) -print(res) - -sleep(10) - -res = requests.post("http://127.0.0.1:7501/state", json={"stage": "stopping"}, headers=headers) -print(res) diff --git a/docs/examples/app_v0/requirements.txt b/docs/examples/app_v0/requirements.txt deleted file mode 100644 index edfce786a4d18..0000000000000 --- a/docs/examples/app_v0/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -py diff --git a/docs/examples/app_v0/ui/a/index.html b/docs/examples/app_v0/ui/a/index.html deleted file mode 100644 index 6ddb9a5a1323c..0000000000000 --- a/docs/examples/app_v0/ui/a/index.html +++ /dev/null @@ -1 +0,0 @@ -
Hello from component A
diff --git a/docs/examples/app_v0/ui/b/index.html b/docs/examples/app_v0/ui/b/index.html deleted file mode 100644 index 3bfd9e24cb7f7..0000000000000 --- a/docs/examples/app_v0/ui/b/index.html +++ /dev/null @@ -1 +0,0 @@ -
Hello from component B
From 7a422a1a77c216c1ce71f0e4baf60f8d40835119 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 15:05:25 +0000 Subject: [PATCH 003/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- .../code_samples/quickstart/app/app_1.py | 2 +- .../lightning_app/communication_content.rst | 10 ++++-- .../lightning_work/payload_content.rst | 10 +++--- .../lightning_work/status_content.rst | 10 +++--- docs/source-app/examples/file_server/app.py | 32 ++++++++--------- .../examples/github_repo_runner/app.py | 36 +++++++++---------- docs/source-app/glossary/app_tree.rst | 17 +++++---- .../build_config/build_config_advanced.rst | 1 + .../build_config/build_config_basic.rst | 5 ++- .../build_config_intermediate.rst | 7 ++-- .../glossary/environment_variables.rst | 5 +-- .../glossary/storage/drive_content.rst | 14 ++++---- docs/source-app/glossary/storage/path.rst | 8 +++-- docs/source-app/levels/basic/level_4.rst | 9 +++-- .../workflows/add_server/any_server.rst | 15 +++++--- .../workflows/add_server/flask_basic.rst | 22 +++++++----- .../react/connect_react_and_lightning.rst | 4 +++ .../add_web_ui/streamlit/intermediate.rst | 14 ++++++-- .../arrange_tabs/arrange_app_basic.rst | 10 +++--- .../from_scratch_content.rst | 9 +++-- .../from_scratch_component_content.rst | 16 ++++++--- .../intermediate.rst | 8 +++-- .../run_work_in_parallel_content.rst | 6 ++-- .../workflows/run_work_once_content.rst | 18 +++++----- .../share_files_between_components.rst | 5 +-- 26 files changed, 172 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index b84da4a864e51..18f7c448f2717 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,4 @@ cifar-10-batches-py # ctags tags .tags -docs/examples \ No newline at end of file +docs/examples diff --git a/docs/source-app/code_samples/quickstart/app/app_1.py b/docs/source-app/code_samples/quickstart/app/app_1.py index dc7a789728463..ac41c5ef83fa1 100644 --- a/docs/source-app/code_samples/quickstart/app/app_1.py +++ b/docs/source-app/code_samples/quickstart/app/app_1.py @@ -1,9 +1,9 @@ import flash from flash.core.data.utils import download_data from flash.image import ImageClassificationData, ImageClassifier -from pytorch_lightning.callbacks import ModelCheckpoint import lightning as L +from pytorch_lightning.callbacks import ModelCheckpoint # Step 1: Create a training LightningWork component that gets a backbone as input diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index edad7b5dc74ad..ba00f3e4beb30 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -28,6 +28,7 @@ As the LightningWork is running into its own process, its state changes is sent import lightning as L + class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -37,8 +38,8 @@ As the LightningWork is running into its own process, its state changes is sent for _ in range(int(10e6)): self.counter += 1 - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.w = WorkCounter() @@ -47,6 +48,7 @@ As the LightningWork is running into its own process, its state changes is sent self.w.run() print(self.w.counter) + app = L.LightningApp(Flow()) @@ -54,7 +56,7 @@ A delta sent from the LightningWork to the LightningFlow looks like this: .. code-block:: python - {'values_changed': {"root['works']['w']['vars']['counter']": {'new_value': 425}}} + {"values_changed": {"root['works']['w']['vars']['counter']": {"new_value": 425}}} Here is the associated illustration: @@ -75,6 +77,7 @@ Communication from the LightningFlow to the LightningWork while running **isn't import lightning as L from time import sleep + class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -85,8 +88,8 @@ Communication from the LightningFlow to the LightningWork while running **isn't sleep(1) print(f"Work {self.counter}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.w = WorkCounter() @@ -97,6 +100,7 @@ Communication from the LightningFlow to the LightningWork while running **isn't print(f"Flow {self.w.counter}") self.w.counter += 1 + app = L.LightningApp(Flow()) As you can observe, there is a divergence between the value within the LightningWork and the LightningFlow. diff --git a/docs/source-app/core_api/lightning_work/payload_content.rst b/docs/source-app/core_api/lightning_work/payload_content.rst index 15adcd856f3ee..780f3985e30ea 100644 --- a/docs/source-app/core_api/lightning_work/payload_content.rst +++ b/docs/source-app/core_api/lightning_work/payload_content.rst @@ -20,6 +20,7 @@ Here is an example: import lightning as L import pandas as pd + class SourceWork(L.LightningWork): def __init__(self): super().__init__() @@ -28,7 +29,7 @@ Here is an example: def run(self): # do some processing - df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]}) + df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) # The object you care about needs to be wrapped into a Payload object. self.df = L.storage.Payload(df) @@ -46,17 +47,17 @@ Once the Payload object is attached to your Work's state, it can be passed to an import lightning as L import pandas as pd - class DestinationWork(L.LightningWork): - def run(self, df:L.storage.Payload): + class DestinationWork(L.LightningWork): + def run(self, df: L.storage.Payload): # You can access the original object from the payload using its value property. print("dst", df.value) # dst col1 col2 # 0 1 3 # 1 2 4 - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.src = SourceWork() @@ -70,4 +71,5 @@ Once the Payload object is attached to your Work's state, it can be passed to an # so you receive a copy of the original object. self.dst.run(df=self.src.df) + app = L.LightningApp(Flow()) diff --git a/docs/source-app/core_api/lightning_work/status_content.rst b/docs/source-app/core_api/lightning_work/status_content.rst index f593421346b2f..7ee8b65ac852b 100644 --- a/docs/source-app/core_api/lightning_work/status_content.rst +++ b/docs/source-app/core_api/lightning_work/status_content.rst @@ -38,16 +38,16 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc from time import sleep import lightning as L - class Work(L.LightningWork): + class Work(L.LightningWork): def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -77,6 +77,7 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc self.work.run(self.counter) self.counter += 1 + app = L.LightningApp(Flow()) Run this app as follows: @@ -135,16 +136,16 @@ In order to access all statuses: from time import sleep import lightning as L - class Work(L.LightningWork): + class Work(L.LightningWork): def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -155,6 +156,7 @@ In order to access all statuses: self.work.run(self.counter) self.counter += 1 + app = L.LightningApp(Flow()) diff --git a/docs/source-app/examples/file_server/app.py b/docs/source-app/examples/file_server/app.py index 20308814ed7e9..5de9f3720a351 100644 --- a/docs/source-app/examples/file_server/app.py +++ b/docs/source-app/examples/file_server/app.py @@ -4,20 +4,15 @@ import uuid import zipfile from dataclasses import dataclass +from pathlib import Path from typing import List + import lightning as L from lightning.app.storage import Drive -from pathlib import Path class FileServer(L.LightningWork): - def __init__( - self, - drive: Drive, - base_dir: str = "file_server", - chunk_size=10240, - **kwargs - ): + def __init__(self, drive: Drive, base_dir: str = "file_server", chunk_size=10240, **kwargs): """This component uploads, downloads files to your application. Arguments: @@ -55,8 +50,7 @@ def upload_file(self, file): filename = file.filename uploaded_file = self.get_random_filename() meta_file = uploaded_file + ".meta" - self.uploaded_files[filename] = { - "progress": (0, None), "done": False} + self.uploaded_files[filename] = {"progress": (0, None), "done": False} # 2: Create a stream and write bytes of # the file to the disk under `uploaded_file` path. @@ -157,24 +151,22 @@ def alive(self): return self.url != "" -from lightning import LightningWork import requests -class TestFileServer(LightningWork): +from lightning import LightningWork + +class TestFileServer(LightningWork): def __init__(self, drive: Drive): super().__init__(cache_calls=True) self.drive = drive - def run(self, file_server_url: str, first = True): + def run(self, file_server_url: str, first=True): if first: with open("test.txt", "w") as f: f.write("Some text.") - response = requests.post( - file_server_url + "/upload_file/", - files={'file': open("test.txt", 'rb')} - ) + response = requests.post(file_server_url + "/upload_file/", files={"file": open("test.txt", "rb")}) assert response.status_code == 200 else: response = requests.get(file_server_url) @@ -184,8 +176,8 @@ def run(self, file_server_url: str, first = True): from lightning import LightningApp, LightningFlow -class Flow(LightningFlow): +class Flow(LightningFlow): def __init__(self): super().__init__() # 1: Create a drive to share data between works @@ -214,14 +206,18 @@ def configure_layout(self): # in the UI using its `/` endpoint. return {"name": "File Server", "content": self.file_server} + from lightning.app.runners import MultiProcessRuntime + def test_file_server(): app = LightningApp(Flow()) MultiProcessRuntime(app).dispatch() + from lightning.app.testing.testing import run_app_in_cloud + def test_file_server_in_cloud(): # You need to provide the directory containing the app file. app_dir = "docs/source-app/examples/file_server" diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index 57aee800de59b..0efc0e02b3839 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -56,8 +56,7 @@ def run(self, *args, **kwargs): # 2: Use git command line to clone the repo. repo_name = self.github_repo.split("/")[-1].replace(".git", "") cwd = os.path.dirname(__file__) - subprocess.Popen( - f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() + subprocess.Popen(f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() # 3: Execute the parent run method of the TracerPythonScript class. os.chdir(os.path.join(cwd, repo_name)) @@ -73,7 +72,6 @@ def configure_layout(self): class PyTorchLightningGithubRepoRunner(GithubRepoRunner): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.best_model_path = None @@ -105,12 +103,12 @@ def trainer_pre_fn(self, *args, work=None, **kwargs): # 5. Patch the `__init__` method of the Trainer # to inject our callback with a reference to the work. - tracer.add_traced( - Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) + tracer.add_traced(Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) return tracer def on_after_run(self, end_script_globals): import torch + # 1. Once the script has finished to execute, # we can collect its globals and access any objects. trainer = end_script_globals["cli"].trainer @@ -138,9 +136,11 @@ def on_after_run(self, end_script_globals): class KerasGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" + class TensorflowGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" + GITHUB_REPO_RUNNERS = { "PyTorch Lightning": PyTorchLightningGithubRepoRunner, "Keras": KerasGithubRepoRunner, @@ -186,6 +186,7 @@ def configure_layout(self): # Create a StreamLit UI for the user to run his Github Repo. return StreamlitFrontend(render_fn=render_fn) + def page_1__create_new_run(state): import streamlit as st @@ -203,9 +204,7 @@ def page_1__create_new_run(state): script_path = st.text_input("Enter your script to run", value="train_script.py") script_args = st.text_input("Enter your base script arguments", value=default_script_args) requirements = st.text_input("Enter your requirements", value=default_requirements) - ml_framework = st.radio( - "Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"] - ) + ml_framework = st.radio("Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"]) if ml_framework not in ("PyTorch Lightning"): st.write(f"{ml_framework} isn't supported yet.") @@ -230,6 +229,7 @@ def page_1__create_new_run(state): # and run the associated work from the request information. state.requests = state.requests + [new_request] + def page_2__view_run_lists(state): import streamlit as st @@ -250,9 +250,8 @@ def page_2__view_run_lists(state): best_model_score = r.get("best_model_score", None) if best_model_score: if st.checkbox(f"Expand to view your run performance", key=i): - st.json( - {"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")} - ) + st.json({"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")}) + def page_3__view_app_state(state): import streamlit as st @@ -260,17 +259,16 @@ def page_3__view_app_state(state): st.markdown("# App State 🎈") st.write(state._state) + def render_fn(state: AppState): import streamlit as st - page_names_to_funcs = { "Create a new Run": partial(page_1__create_new_run, state=state), "View your Runs": partial(page_2__view_run_lists, state=state), "View the App state": partial(page_3__view_app_state, state=state), } - selected_page = st.sidebar.selectbox( - "Select a page", page_names_to_funcs.keys()) + selected_page = st.sidebar.selectbox("Select a page", page_names_to_funcs.keys()) page_names_to_funcs[selected_page]() @@ -286,10 +284,12 @@ def run(self): def configure_layout(self): # 1: Add the main StreamLit UI - selection_tab = [{ - "name": "Run your Github Repo", - "content": self.flow, - }] + selection_tab = [ + { + "name": "Run your Github Repo", + "content": self.flow, + } + ] # 2: Add a new tab whenever a new work is dynamically created run_tabs = [e.configure_layout() for e in self.flow.ws.values()] # 3: Returns the list of tabs. diff --git a/docs/source-app/glossary/app_tree.rst b/docs/source-app/glossary/app_tree.rst index 120d5d2e4ef69..860560bdcedf2 100644 --- a/docs/source-app/glossary/app_tree.rst +++ b/docs/source-app/glossary/app_tree.rst @@ -43,14 +43,14 @@ You can attach your components in the **__init__** method of a flow. import lightning as L - class RootFlow(L.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() # The `Work` component is attached here. self.work = Work() - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. self.nested_flow = NestedFlow() Once done, simply add the root flow to a Lightning app as follows: @@ -72,20 +72,19 @@ You can simply attach your components in the **run** method of a flow using the .. code-block:: python class RootFlow(L.LightningFlow): - def run(self): if not hasattr(self, "work"): - # The `Work` component is attached here. + # The `Work` component is attached here. setattr(self, "work", Work()) # Run the `Work` component. - getattr(self, "work").run() + getattr(self, "work").run() if not hasattr(self, "nested_flow"): - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. setattr(self, "nested_flow", NestedFlow()) # Run the `NestedFlow` component. - getattr(self, "wonested_flowrk").run() + getattr(self, "wonested_flowrk").run() But it is usually more readable to use Lightning built-in :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` as follows: @@ -94,8 +93,8 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app from lightning_app.structures import Dict - class RootFlow(L.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() self.dict = Dict() @@ -108,5 +107,5 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app if "nested_flow" not in self.dict: # The `NestedFlow` component is attached here. - self.dict["nested_flow"] =NestedFlow() + self.dict["nested_flow"] = NestedFlow() self.dict["nested_flow"].run() diff --git a/docs/source-app/glossary/build_config/build_config_advanced.rst b/docs/source-app/glossary/build_config/build_config_advanced.rst index f954bd3435c0f..c96ac93d079bc 100644 --- a/docs/source-app/glossary/build_config/build_config_advanced.rst +++ b/docs/source-app/glossary/build_config/build_config_advanced.rst @@ -23,6 +23,7 @@ Create a :class:`~lightning_app.utilities.packaging.build_config.BuildConfig` an from lightning_app import LightningWork, BuildConfig + class MyWork(LightningWork): def __init__(self): super().__init__() diff --git a/docs/source-app/glossary/build_config/build_config_basic.rst b/docs/source-app/glossary/build_config/build_config_basic.rst index 31b274e086db9..c3e3ae8c6ffe2 100644 --- a/docs/source-app/glossary/build_config/build_config_basic.rst +++ b/docs/source-app/glossary/build_config/build_config_basic.rst @@ -50,12 +50,11 @@ Instead of listing the requirements in a file, you can also pass them to the Lig from lightning_app import LightningWork, BuildConfig + class MyWork(LightningWork): def __init__(self): super().__init__() - self.cloud_build_config = BuildConfig( - requirements=["torch>=1.8", "torchmetrics"] - ) + self.cloud_build_config = BuildConfig(requirements=["torch>=1.8", "torchmetrics"]) .. note:: The build config only applies when running in the cloud and gets ignored otherwise. A local build config is currently not supported. diff --git a/docs/source-app/glossary/build_config/build_config_intermediate.rst b/docs/source-app/glossary/build_config/build_config_intermediate.rst index 0a5839b1f3ea8..174f472facb8e 100644 --- a/docs/source-app/glossary/build_config/build_config_intermediate.rst +++ b/docs/source-app/glossary/build_config/build_config_intermediate.rst @@ -18,9 +18,9 @@ If you need to install additional system packages or run other configuration ste from lightning_app import BuildConfig + @dataclass class CustomBuildConfig(BuildConfig): - def build_commands(self): return ["sudo apt-get install libsparsehash-dev"] @@ -31,6 +31,7 @@ If you need to install additional system packages or run other configuration ste from lightning_app import LightningWork + class MyWork(LightningWork): def __init__(self): super().__init__() @@ -39,9 +40,7 @@ If you need to install additional system packages or run other configuration ste self.cloud_build_config = CustomBuildConfig() # Can also be combined with extra requirements - self.cloud_build_config = CustomBuildConfig( - requirements=["torchmetrics"] - ) + self.cloud_build_config = CustomBuildConfig(requirements=["torchmetrics"]) .. note:: diff --git a/docs/source-app/glossary/environment_variables.rst b/docs/source-app/glossary/environment_variables.rst index 20ab1b09b6a0a..fd41594656b0f 100644 --- a/docs/source-app/glossary/environment_variables.rst +++ b/docs/source-app/glossary/environment_variables.rst @@ -19,8 +19,9 @@ The environment variables are available in all flows and works, and can be acces .. code:: python import os - print(os.environ["FOO"]) # BAR - print(os.environ["BAZ"]) # FAZ + + print(os.environ["FOO"]) # BAR + print(os.environ["BAZ"]) # FAZ .. note:: Environment variables are currently not encrypted. diff --git a/docs/source-app/glossary/storage/drive_content.rst b/docs/source-app/glossary/storage/drive_content.rst index 263fab6093c1c..c2c1357d0f6df 100644 --- a/docs/source-app/glossary/storage/drive_content.rst +++ b/docs/source-app/glossary/storage/drive_content.rst @@ -50,6 +50,7 @@ Any components can create a drive object. from lightning_app import LightningFlow, LightningWork from lightning_app.storage import Drive + class Flow(LightningFlow): def __init__(self): super().__init__() @@ -58,6 +59,7 @@ Any components can create a drive object. def run(self): ... + class Work(LightningWork): def __init__(self): super().__init__() @@ -80,7 +82,7 @@ A Drive supports put, list, get, and delete actions. drive = Drive("lit://drive") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty # Created file. with open("a.txt", "w") as f: @@ -88,13 +90,13 @@ A Drive supports put, list, get, and delete actions. drive.put("a.txt") - drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. + drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. - drive.get("a.txt") # Get the file into the current worker + drive.get("a.txt") # Get the file into the current worker drive.delete("a.txt") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty ---- @@ -114,7 +116,6 @@ Here is an illustrated code example on how to create drives within works. class Work_A(LightningWork): - def __init__(self): super().__init__() # The identifier of the Drive is ``drive_1`` @@ -131,7 +132,6 @@ Here is an illustrated code example on how to create drives within works. class Work_B(LightningWork): - def __init__(self): super().__init__() @@ -151,8 +151,8 @@ Here is an illustrated code example on how to create drives within works. self.drive_1.put("b.txt") self.drive_2.put("b.txt") - class Work_C(LightningWork): + class Work_C(LightningWork): def __init__(self): super().__init__() self.drive_2 = Drive("lit://drive_2") diff --git a/docs/source-app/glossary/storage/path.rst b/docs/source-app/glossary/storage/path.rst index 1437bba577d80..6da6459cf1195 100644 --- a/docs/source-app/glossary/storage/path.rst +++ b/docs/source-app/glossary/storage/path.rst @@ -99,6 +99,7 @@ For example, share a directory by passing it as an input to the run method of th from lightning_app import LightningFlow + class Flow(LightningFlow): def __init__(self): super().__init__() @@ -172,8 +173,8 @@ You can check if a path exists locally or remotely in the source Work using the # OR if checkpoint_dir.exists_local(): - # Do something with the file if it exists locally - files = os.listdir(checkpoint_dir) + # Do something with the file if it exists locally + files = os.listdir(checkpoint_dir) ---- @@ -190,6 +191,7 @@ Lightning makes sure all Paths that are part of the state get stored and made ac from lightning_app.storage import Path + class Work(LightningWork): def __init__(self): super().__init__() @@ -218,6 +220,7 @@ First, define a component that saves a checkpoint: import torch import os + class ModelTraining(LightningWork): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -264,6 +267,7 @@ Link both components via a parent component: self.train.run() self.deploy.run(checkpoint_dir=self.train.checkpoint_dir) + app = L.LightningApp(Flow()) diff --git a/docs/source-app/levels/basic/level_4.rst b/docs/source-app/levels/basic/level_4.rst index 17fc8e90feb87..c5ce24ff42e2f 100644 --- a/docs/source-app/levels/basic/level_4.rst +++ b/docs/source-app/levels/basic/level_4.rst @@ -109,15 +109,14 @@ To use the component, simply import it and attach it to your Lightning App. import lightning as L from lit_slack import SlackMessenger + class YourComponent(L.LightningFlow): def __init__(self): super().__init__() - self.slack_messenger = SlackMessenger( - token='a-long-token', - channel_id='A03CB4A6AK7' - ) + self.slack_messenger = SlackMessenger(token="a-long-token", channel_id="A03CB4A6AK7") def run(self): - self.slack_messenger.send_message('hello from ⚡ lit slack ⚡') + self.slack_messenger.send_message("hello from ⚡ lit slack ⚡") + app = L.LightningApp(YourComponent()) diff --git a/docs/source-app/workflows/add_server/any_server.rst b/docs/source-app/workflows/add_server/any_server.rst index 677478d70d740..398951276c0a5 100644 --- a/docs/source-app/workflows/add_server/any_server.rst +++ b/docs/source-app/workflows/add_server/any_server.rst @@ -26,6 +26,7 @@ Any server that listens on a port, can be enabled via a work. For example, here' import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -33,7 +34,8 @@ Any server that listens on a port, can be enabled via a work. For example, here' html = "

Hello lit world

" self.wfile.write(html) - httpd = socketserver.TCPServer(('localhost', '3000'), PlainServer) + + httpd = socketserver.TCPServer(("localhost", "3000"), PlainServer) httpd.serve_forever() To enable the server inside the component, start the server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -45,6 +47,7 @@ To enable the server inside the component, start the server in the run method an import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -52,6 +55,7 @@ To enable the server inside the component, start the server in the run method an html = "

Hello lit world " self.wfile.write(html) + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) @@ -72,6 +76,7 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -79,11 +84,13 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl html = "

Hello lit world " self.wfile.write(html) + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) httpd.serve_forever() + class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -93,12 +100,10 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl self.lit_server.run() def configure_layout(self): - tab1 = { - 'name': 'home', - 'content': self.lit_server - } + tab1 = {"name": "home", "content": self.lit_server} return tab1 + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in parallel diff --git a/docs/source-app/workflows/add_server/flask_basic.rst b/docs/source-app/workflows/add_server/flask_basic.rst index 5ee2a232828c4..38ca282346248 100644 --- a/docs/source-app/workflows/add_server/flask_basic.rst +++ b/docs/source-app/workflows/add_server/flask_basic.rst @@ -26,11 +26,13 @@ First, define your flask app as you normally would without Lightning: flask_app = Flask(__name__) - @flask_app.route('/') + + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" + - flask_app.run(host='0.0.0.0', port=80) + flask_app.run(host="0.0.0.0", port=80) To enable the server inside the component, start the Flask server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -40,13 +42,14 @@ To enable the server inside the component, start the Flask server in the run met import lightning as L from flask import Flask + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route('/') + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" flask_app.run(host=self.host, port=self.port) @@ -64,16 +67,18 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli import lightning as L from flask import Flask + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route('/') + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" flask_app.run(host=self.host, port=self.port) + class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -83,9 +88,10 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli self.lit_flask.run() def configure_layout(self): - tab1 = {'name': 'home', 'content': self.lit_flask} + tab1 = {"name": "home", "content": self.lit_flask} return tab1 + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in the background diff --git a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst index fe72d09b27eec..c1c0c5e2017c8 100644 --- a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst +++ b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst @@ -79,14 +79,17 @@ You can use this single react app for the FULL Lightning app, or you can specify import lightning as L + class ComponentA(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist") + class ComponentB(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist") + class HelloLitReact(L.LightningFlow): def __init__(self): super().__init__() @@ -98,6 +101,7 @@ You can use this single react app for the FULL Lightning app, or you can specify tab_2 = {"name": "App 2", "content": self.react_app_2} return tab_1, tab_2 + app = L.LightningApp(HelloLitReact()) This is a powerful idea that allows each Lightning component to have a self-contained web UI. diff --git a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst index 08ab0e874bcca..ac289c2eb27e1 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst @@ -22,10 +22,12 @@ For example, here we increase the count variable of the Lightning Component ever import lightning as L import streamlit as st + def your_streamlit_app(lightning_app_state): - if st.button('press to increase count'): + if st.button("press to increase count"): lightning_app_state.count += 1 - st.write(f'current count: {lightning_app_state.count}') + st.write(f"current count: {lightning_app_state.count}") + class LitStreamlit(L.LightningFlow): def __init__(self): @@ -35,6 +37,7 @@ For example, here we increase the count variable of the Lightning Component ever def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -44,6 +47,7 @@ For example, here we increase the count variable of the Lightning Component ever tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 + app = L.LightningApp(LitApp()) ---- @@ -63,8 +67,10 @@ In this example we update the value of the counter from the component: import lightning as L import streamlit as st + def your_streamlit_app(lightning_app_state): - st.write(f'current count: {lightning_app_state.count}') + st.write(f"current count: {lightning_app_state.count}") + class LitStreamlit(L.LightningFlow): def __init__(self): @@ -77,6 +83,7 @@ In this example we update the value of the counter from the component: def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -89,4 +96,5 @@ In this example we update the value of the counter from the component: tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 + app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst index 988018cd795e8..91c0e53854760 100644 --- a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst +++ b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst @@ -16,14 +16,13 @@ To enable a single tab on the app UI, return a single dictionary from the ``conf import lightning as L + class DemoComponent(L.demo.dumb_component): def configure_layout(self): - tab1 = { - "name": "THE TAB NAME", - "content": self.component_a - } + tab1 = {"name": "THE TAB NAME", "content": self.component_a} return tab1 + app = L.LightningApp(DemoComponent()) @@ -43,12 +42,14 @@ Enable multiple tabs import lightning as L + class DemoComponent(L.demo.dumb_component): def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 + app = L.LightningApp(DemoComponent()) The order matters! Try any of the following configurations: @@ -61,6 +62,7 @@ The order matters! Try any of the following configurations: tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 + def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} diff --git a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst index c43b1bbc2f749..91e7fea93e28c 100644 --- a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst +++ b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst @@ -19,10 +19,12 @@ If you didn't find a Lightning App similar to the one you need, simply create a import lightning as L + class WordComponent(L.LightningWork): def __init__(self, word): super().__init__() self.word = word + def run(self): print(self.word) @@ -30,14 +32,15 @@ If you didn't find a Lightning App similar to the one you need, simply create a class LitApp(L.LightningFlow): def __init__(self) -> None: super().__init__() - self.hello = WordComponent('hello') - self.world = WordComponent('world') + self.hello = WordComponent("hello") + self.world = WordComponent("world") def run(self): - print('This is a simple Lightning app, make a better app!') + print("This is a simple Lightning app, make a better app!") self.hello.run() self.world.run() + app = L.LightningApp(LitApp()) ---- diff --git a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst index 0f7b0ff436eb0..7faef8ee03df8 100644 --- a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst +++ b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst @@ -17,7 +17,7 @@ Use a **LightningFlow** component for any programming logic that runs in less th .. code:: python for i in range(10): - print(f'{i}: this kind of code belongs in a LightningFlow') + print(f"{i}: this kind of code belongs in a LightningFlow") Use a **LightningWork** component for any programming logic that takes more than 1 second or requires its own hardware. @@ -27,7 +27,7 @@ Use a **LightningWork** component for any programming logic that takes more than for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") ---- @@ -77,10 +77,12 @@ To implement a LightningFlow, simply subclass ``LightningFlow`` and define the r # app.py import lightning as L + class LitFlow(L.LightningFlow): def run(self): for i in range(10): - print(f'{i}: this kind of code belongs in a LightningFlow') + print(f"{i}: this kind of code belongs in a LightningFlow") + app = L.LightningApp(LitFlow()) @@ -109,11 +111,12 @@ To implement a LightningWork, simply subclass ``LightningWork`` and define the r from time import sleep import lightning as L + class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") A LightningWork must always be attached to a LightningFlow and explicitely asked to ``run()``: @@ -123,11 +126,13 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked from time import sleep import lightning as L + class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") + class LitFlow(L.LightningFlow): def __init__(self): @@ -137,6 +142,7 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked def run(self): self.lit_work.run() + app = L.LightningApp(LitFlow()) run the app diff --git a/docs/source-app/workflows/build_lightning_component/intermediate.rst b/docs/source-app/workflows/build_lightning_component/intermediate.rst index 27336424bf916..070d3aa0caf1a 100644 --- a/docs/source-app/workflows/build_lightning_component/intermediate.rst +++ b/docs/source-app/workflows/build_lightning_component/intermediate.rst @@ -32,9 +32,10 @@ To *connect* this user interface to the component, define the configure_layout m import lightning as L from lightning_app.frontend.web import StaticWebFrontend + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') + return StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") Finally, route the component's UI through the root component's **configure_layout** method: @@ -44,9 +45,11 @@ Finally, route the component's UI through the root component's **configure_layou # app.py import lightning as L + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return L.frontend.web.StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') + return L.frontend.web.StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") + class LitApp(L.LightningFlow): def __init__(self): @@ -57,6 +60,7 @@ Finally, route the component's UI through the root component's **configure_layou tab1 = {"name": "home", "content": self.lit_html_component} return tab1 + app = L.LightningApp(LitApp()) Run your app and you'll see the UI on the Lightning App view: diff --git a/docs/source-app/workflows/run_work_in_parallel_content.rst b/docs/source-app/workflows/run_work_in_parallel_content.rst index 334f50e7e486a..ecb87c5cea7ff 100644 --- a/docs/source-app/workflows/run_work_in_parallel_content.rst +++ b/docs/source-app/workflows/run_work_in_parallel_content.rst @@ -13,13 +13,14 @@ The default behavior of the ``LightningWork`` is to wait for the ``run`` method import lightning as L + class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent() def run(self): self.work_component_a.run() - print('this will never print') + print("this will never print") Since this LightningWork component we created loops forever, the print statement will never execute. In practice ``LightningWork`` workloads are finite and don't run forever. @@ -39,13 +40,14 @@ To run LightningWorks in parallel, while the rest of the app executes without de import lightning as L + class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent(parallel=True) def run(self): self.work_component_a.run() - print('repeats while the infinite work runs ONCE (and forever) in parallel') + print("repeats while the infinite work runs ONCE (and forever) in parallel") Any LightningWorks that will take more than **1 second** should be run in parallel unless the rest of your Lightning App depends on the output of this work (for example, downloading a dataset). diff --git a/docs/source-app/workflows/run_work_once_content.rst b/docs/source-app/workflows/run_work_once_content.rst index 4388742358929..dbef37813572a 100644 --- a/docs/source-app/workflows/run_work_once_content.rst +++ b/docs/source-app/workflows/run_work_once_content.rst @@ -24,10 +24,10 @@ As explained in the `Event Loop guide <../glossary/event_loop.html>`_, the Light from datetime import datetime # Lightning code - while True: # This is the Lightning Event Loop + while True: # This is the Lightning Event Loop # Your code - today = datetime.now().strftime("%D") # '05/25/22' + today = datetime.now().strftime("%D") # '05/25/22' data_processor.run(today) train_model.run(data_processor.data) @@ -47,10 +47,12 @@ Here's an example of this behavior with LightningWork: import lightning as L - class ExampleWork( L.LightningWork): + + class ExampleWork(L.LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") + work = ExampleWork() work.run(value=1) @@ -86,10 +88,12 @@ By setting ``cache_calls=False``, Lightning won't cache the return value and re- from lightning_app import LightningWork + class ExampleWork(LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") + work = ExampleWork(cache_calls=False) work.run(value=1) @@ -122,21 +126,19 @@ as the work continuously execute in a blocking way. from lightning_app import LightningApp, LightningFlow, LightningWork - class Flow(LightningFlow): + class Flow(LightningFlow): def __init__(self): super().__init__() - self.work = Work( - cache_calls=False, - parallel=False - ) + self.work = Work(cache_calls=False, parallel=False) def run(self): print("HERE BEFORE") self.work.run() print("HERE AFTER") + app = LightningApp(Flow()) .. code-block:: console diff --git a/docs/source-app/workflows/share_files_between_components.rst b/docs/source-app/workflows/share_files_between_components.rst index 02c7d889a7122..15108515b3947 100644 --- a/docs/source-app/workflows/share_files_between_components.rst +++ b/docs/source-app/workflows/share_files_between_components.rst @@ -34,8 +34,8 @@ To write a file, first create a reference to the file with the :class:`~lightnin boring_file_reference = Path("boring_file.txt") # write to that file - with open(self.boring_file_reference, 'w') as f: - f.write('yolo') + with open(self.boring_file_reference, "w") as f: + f.write("yolo") ---- @@ -107,6 +107,7 @@ For example, here we save a file on one component and use it in another componen from lightning_app.storage.path import Path + class ComponentA(LightningWork): def __init__(self): super().__init__() From 490df41d4a63b3ef4c016709bc7caa315db3798e Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:13:11 +0100 Subject: [PATCH 004/119] update --- .github/workflows/ci-app_block.yml | 1 + MANIFEST.in | 6 ++++++ requirements/app/base.txt | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-app_block.yml b/.github/workflows/ci-app_block.yml index 8c2dd772aa1ad..88d9e38795619 100644 --- a/.github/workflows/ci-app_block.yml +++ b/.github/workflows/ci-app_block.yml @@ -13,6 +13,7 @@ jobs: - name: Get changed files using defaults id: changed-files uses: tj-actions/changed-files@v23 + - name: List all added files run: | for file in ${{ steps.changed-files.outputs.all_changed_and_modified_files }}; do diff --git a/MANIFEST.in b/MANIFEST.in index 37c72c103b69c..ef7d418eef976 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,3 +9,9 @@ recursive-include src *.md recursive-include requirements *.txt recursive-include src *.md recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/requirements/app/base.txt b/requirements/app/base.txt index 07c460362e533..97763d48522fe 100644 --- a/requirements/app/base.txt +++ b/requirements/app/base.txt @@ -8,7 +8,7 @@ py starsessions requests urllib3 -fsspec==2022.01.0 +fsspec[http]>=2021.05.0, !=2021.06.0 s3fs==2022.1.0 croniter # for now until we found something more robust. traitlets < 5.2.0 # Traitlets 5.2.X fails: https://github.com/ipython/traitlets/issues/741 From 0a483a76ff9b493235845e7d6525964adbdbd7fd Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:22:43 +0100 Subject: [PATCH 005/119] update --- .github/workflows/ci-pytorch_test-full.yml | 2 +- MANIFEST.in | 10 ------- docs/source-app/conf.py | 35 ++++++++++------------ docs/source-pytorch/conf.py | 10 +++++++ setup.py | 2 +- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 42ec2b71fd0b6..03adf155bd080 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -88,7 +88,7 @@ jobs: run: | flag=$(python -c "print('--pre' if '${{matrix.release}}' == 'pre' else '')" 2>&1) url=$(python -c "print('test/cpu/torch_test.html' if '${{matrix.release}}' == 'pre' else 'cpu/torch_stable.html')" 2>&1) - pip install -e .[test] --upgrade $flag --find-links "https://download.pytorch.org/whl/${url}" + pip install -e '.[test]' --upgrade $flag --find-links "https://download.pytorch.org/whl/${url}" pip list shell: bash diff --git a/MANIFEST.in b/MANIFEST.in index ef7d418eef976..ddde80aef9ed8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,13 +5,3 @@ include .actions/setup_tools.py include *.cff # citation info recursive-include src *.md recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt diff --git a/docs/source-app/conf.py b/docs/source-app/conf.py index 18b5dcf0ff708..be95b0d2b349c 100644 --- a/docs/source-app/conf.py +++ b/docs/source-app/conf.py @@ -15,28 +15,32 @@ import os import shutil import sys +from importlib.util import module_from_spec, spec_from_file_location import pt_lightning_sphinx_theme -import lightning_app - _PATH_HERE = os.path.abspath(os.path.dirname(__file__)) _PATH_ROOT = os.path.realpath(os.path.join(_PATH_HERE, "..", "..")) sys.path.insert(0, os.path.abspath(_PATH_ROOT)) SPHINX_MOCK_REQUIREMENTS = int(os.environ.get("SPHINX_MOCK_REQUIREMENTS", True)) +# alternative https://stackoverflow.com/a/67692/4521646 +spec = spec_from_file_location("lightning_app/__about__.py", os.path.join(_PATH_ROOT, "lightning_app", "__about__.py")) +about = module_from_spec(spec) +spec.loader.exec_module(about) + # -- Project information ----------------------------------------------------- # this name shall match the project name in Github as it is used for linking to code project = "lightning" -copyright = lightning_app.__copyright__ -author = lightning_app.__author__ +copyright = about.__copyright__ +author = about.__author__ # The short X.Y version -version = lightning_app.__version__ +version = about.__version__ # The full version, including alpha/beta/rc tags -release = lightning_app.__version__ +release = about.__version__ # Options for the linkcode extension # ---------------------------------- @@ -156,8 +160,8 @@ # documentation. html_theme_options = { - "pytorch_project": lightning_app.__homepage__, - "canonical_url": lightning_app.__homepage__, + "pytorch_project": about.__homepage__, + "canonical_url": about.__homepage__, "collapse_navigation": False, "display_version": True, "logo_only": False, @@ -223,7 +227,7 @@ project + " Documentation", author, project, - lightning_app.__docs__, + about.__docs__, "Miscellaneous", ), ] @@ -277,15 +281,6 @@ def setup(app): path_ipynb2 = os.path.join(path_nbs, os.path.basename(path_ipynb)) shutil.copy(path_ipynb, path_ipynb2) -# copy all examples to local folder -path_examples = os.path.join(_PATH_HERE, "..", "examples") -if not os.path.isdir(path_examples): - os.mkdir(path_examples) -for path_app_example in glob.glob(os.path.join(_PATH_ROOT, "examples", "app_*")): - path_app_example2 = os.path.join(path_examples, os.path.basename(path_app_example)) - if not os.path.isdir(path_app_example2): - shutil.copytree(path_app_example, path_app_example2, dirs_exist_ok=True) - # Ignoring Third-party packages # https://stackoverflow.com/questions/15889621/sphinx-how-to-exclude-imports-in-automodule @@ -319,7 +314,7 @@ def _package_list_from_file(file): def linkcode_resolve(domain, info): def find_source(): # try to find the file and line number, based on code from numpy: - # https://github.com/numpy/numpy/blob/master/doc/source-app/conf.py#L286 + # https://github.com/numpy/numpy/blob/master/doc/source/conf.py#L286 obj = sys.modules[info["module"]] for part in info["fullname"].split("."): obj = getattr(obj, part) @@ -386,6 +381,6 @@ def find_source(): doctest_global_setup = """ import importlib import os -import lightning as L +import lightning_app """ coverage_skip_undoc_in_source = True diff --git a/docs/source-pytorch/conf.py b/docs/source-pytorch/conf.py index 6ebb32ce870dc..c004ad2827e87 100644 --- a/docs/source-pytorch/conf.py +++ b/docs/source-pytorch/conf.py @@ -312,6 +312,16 @@ def setup(app): # shutil.copy(path_ipynb, path_ipynb2) +# copy all examples to local folder +path_examples = os.path.join(_PATH_HERE, "..", "examples") +if not os.path.isdir(path_examples): + os.mkdir(path_examples) +for path_app_example in glob.glob(os.path.join(_PATH_ROOT, "examples", "app_*")): + path_app_example2 = os.path.join(path_examples, os.path.basename(path_app_example)) + if not os.path.isdir(path_app_example2): + shutil.copytree(path_app_example, path_app_example2, dirs_exist_ok=True) + + # Ignoring Third-party packages # https://stackoverflow.com/questions/15889621/sphinx-how-to-exclude-imports-in-automodule def package_list_from_file(file): diff --git a/setup.py b/setup.py index 6d271cc40b0aa..a542b3c1e0291 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = "" +_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From 5ba1b6191bd665b88ba43adb226c88280a65abd1 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:26:06 +0100 Subject: [PATCH 006/119] update --- requirements/app/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/app/base.txt b/requirements/app/base.txt index 97763d48522fe..07c460362e533 100644 --- a/requirements/app/base.txt +++ b/requirements/app/base.txt @@ -8,7 +8,7 @@ py starsessions requests urllib3 -fsspec[http]>=2021.05.0, !=2021.06.0 +fsspec==2022.01.0 s3fs==2022.1.0 croniter # for now until we found something more robust. traitlets < 5.2.0 # Traitlets 5.2.X fails: https://github.com/ipython/traitlets/issues/741 From 9417c300926e1aaf20a440516f796305dfc56206 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:30:20 +0100 Subject: [PATCH 007/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 1 + .github/workflows/ci-pytorch_test-full.yml | 1 + .github/workflows/ci-pytorch_test-slow.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index c062e6e02acb1..728eae2b685b4 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -11,6 +11,7 @@ on: # Trigger the workflow on push or pull request, but only for the master bra - "tests/tests_app/**" # todo: implement job skip - "tests/tests_app_examples/**" # todo: implement job skip - "examples/app_*" # todo: implement job skip + - "docs/source-app/**" # todo: implement job skip concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 03adf155bd080..5d122f8aafb8a 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -12,6 +12,7 @@ on: # Trigger the workflow on push or pull request, but only for the master bra - "tests/tests_app/**" # todo: implement job skip - "tests/tests_app_examples/**" # todo: implement job skip - "examples/app_*" # todo: implement job skip + - "docs/source-app/**" # todo: implement job skip concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index 279c4ffe772a8..b831766a1d597 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -12,6 +12,7 @@ on: # Trigger the workflow on push or pull request, but only for the master bra - "tests/tests_app/**" # todo: implement job skip - "tests/tests_app_examples/**" # todo: implement job skip - "examples/app_*" # todo: implement job skip + - "docs/source-app/**" # todo: implement job skip concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} From 6b360ec13d4d1196f8bd17349df95ffa723479ae Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:31:48 +0100 Subject: [PATCH 008/119] update --- .github/workflows/ci-app_block.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-app_block.yml b/.github/workflows/ci-app_block.yml index 88d9e38795619..7b3ae57d43253 100644 --- a/.github/workflows/ci-app_block.yml +++ b/.github/workflows/ci-app_block.yml @@ -1,6 +1,16 @@ name: Block app edits -on: ["pull_request"] +on: # Trigger the workflow on push or pull request, but only for the master branch + pull_request: + branches: [master, "release/*"] + types: [opened, reopened, ready_for_review, synchronize] + paths-ignore: + - "src/lightning_app/**" # todo: implement job skip + - "tests/tests_app/**" # todo: implement job skip + - "tests/tests_app_examples/**" # todo: implement job skip + - "examples/app_*" # todo: implement job skip + - "docs/source-app/**" # todo: implement job skip + jobs: block: From 923914f8723dbb51b3051c9b37a8719980b2b1d6 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:33:26 +0100 Subject: [PATCH 009/119] update --- .gitignore | 1 - MANIFEST.in | 2 -- 2 files changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 18f7c448f2717..47b9bfff92523 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,3 @@ cifar-10-batches-py # ctags tags .tags -docs/examples diff --git a/MANIFEST.in b/MANIFEST.in index ddde80aef9ed8..a8dbcff69b631 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,3 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info -recursive-include src *.md -recursive-include requirements *.txt From 295d14d00eed3236af44b8fa058d7a898ab9bc59 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 16:37:35 +0100 Subject: [PATCH 010/119] update --- docs/source-pytorch/conf.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/source-pytorch/conf.py b/docs/source-pytorch/conf.py index c004ad2827e87..6ebb32ce870dc 100644 --- a/docs/source-pytorch/conf.py +++ b/docs/source-pytorch/conf.py @@ -312,16 +312,6 @@ def setup(app): # shutil.copy(path_ipynb, path_ipynb2) -# copy all examples to local folder -path_examples = os.path.join(_PATH_HERE, "..", "examples") -if not os.path.isdir(path_examples): - os.mkdir(path_examples) -for path_app_example in glob.glob(os.path.join(_PATH_ROOT, "examples", "app_*")): - path_app_example2 = os.path.join(path_examples, os.path.basename(path_app_example)) - if not os.path.isdir(path_app_example2): - shutil.copytree(path_app_example, path_app_example2, dirs_exist_ok=True) - - # Ignoring Third-party packages # https://stackoverflow.com/questions/15889621/sphinx-how-to-exclude-imports-in-automodule def package_list_from_file(file): From 948436bb8c4f32992c8ca0f328fdd15513110dcd Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 17:56:14 +0100 Subject: [PATCH 011/119] update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 47b9bfff92523..7c91383869542 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cifar-10-batches-py # ctags tags .tags +src/lightning_app/ui/* \ No newline at end of file From 2f05948ddf85c72c6af66f51f6536eec40b9d5c3 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 17:56:49 +0100 Subject: [PATCH 012/119] update --- .../code_samples/quickstart/app/app_0.py | 3 +- .../code_samples/quickstart/app/app_1.py | 2 +- docs/source-app/conf.py | 35 ++++---- .../lightning_app}/app.py | 0 .../core_api/lightning_app/communication.rst | 2 +- .../lightning_app/communication_content.rst | 81 ++++++++++++++----- .../lightning_work/payload_content.rst | 10 +-- .../lightning_work/status_content.rst | 10 +-- docs/source-app/examples/file_server/app.py | 32 ++++---- .../examples/github_repo_runner/app.py | 36 ++++----- docs/source-app/glossary/app_tree.rst | 17 ++-- .../build_config/build_config_advanced.rst | 1 - .../build_config/build_config_basic.rst | 5 +- .../build_config_intermediate.rst | 7 +- .../glossary/environment_variables.rst | 5 +- .../glossary/storage/drive_content.rst | 14 ++-- docs/source-app/glossary/storage/path.rst | 8 +- docs/source-app/index.rst | 9 +-- docs/source-app/levels/advanced/index.rst | 22 ++--- docs/source-app/levels/advanced/level_16.rst | 10 +++ docs/source-app/levels/advanced/level_17.rst | 12 +-- docs/source-app/levels/advanced/level_18.rst | 14 ++-- docs/source-app/levels/advanced/level_19.rst | 13 +-- docs/source-app/levels/advanced/level_20.rst | 10 +-- docs/source-app/levels/advanced/level_21.rst | 11 --- docs/source-app/levels/basic/level_4.rst | 9 ++- docs/source-app/levels/intermediate/index.rst | 21 ++--- .../levels/intermediate/level_13.rst | 10 +-- .../levels/intermediate/level_14.rst | 12 +-- .../levels/intermediate/level_15.rst | 6 +- .../levels/intermediate/level_16.rst | 10 --- .../access_app_state/access_app_state.rst | 11 --- .../access_app_state_content.rst | 70 ---------------- .../workflows/add_server/any_server.rst | 15 ++-- .../workflows/add_server/flask_basic.rst | 22 ++--- .../react/connect_react_and_lightning.rst | 4 - .../add_web_ui/streamlit/intermediate.rst | 14 +--- .../arrange_tabs/arrange_app_basic.rst | 10 +-- .../from_scratch_content.rst | 9 +-- .../from_scratch_component_content.rst | 16 ++-- .../intermediate.rst | 8 +- .../run_work_in_parallel_content.rst | 6 +- .../workflows/run_work_once_content.rst | 18 ++--- .../share_files_between_components.rst | 5 +- 44 files changed, 272 insertions(+), 373 deletions(-) rename docs/source-app/{workflows/access_app_state => core_api/lightning_app}/app.py (100%) create mode 100644 docs/source-app/levels/advanced/level_16.rst delete mode 100644 docs/source-app/levels/advanced/level_21.rst delete mode 100644 docs/source-app/levels/intermediate/level_16.rst delete mode 100644 docs/source-app/workflows/access_app_state/access_app_state.rst delete mode 100644 docs/source-app/workflows/access_app_state/access_app_state_content.rst diff --git a/docs/source-app/code_samples/quickstart/app/app_0.py b/docs/source-app/code_samples/quickstart/app/app_0.py index 3952cafc957e0..82b687b0f258b 100644 --- a/docs/source-app/code_samples/quickstart/app/app_0.py +++ b/docs/source-app/code_samples/quickstart/app/app_0.py @@ -1,6 +1,5 @@ -from docs.quickstart.app_02 import HourLongWork - import lightning as L +from docs.quickstart.app_02 import HourLongWork class RootFlow(L.LightningFlow): diff --git a/docs/source-app/code_samples/quickstart/app/app_1.py b/docs/source-app/code_samples/quickstart/app/app_1.py index ac41c5ef83fa1..dc7a789728463 100644 --- a/docs/source-app/code_samples/quickstart/app/app_1.py +++ b/docs/source-app/code_samples/quickstart/app/app_1.py @@ -1,9 +1,9 @@ import flash from flash.core.data.utils import download_data from flash.image import ImageClassificationData, ImageClassifier +from pytorch_lightning.callbacks import ModelCheckpoint import lightning as L -from pytorch_lightning.callbacks import ModelCheckpoint # Step 1: Create a training LightningWork component that gets a backbone as input diff --git a/docs/source-app/conf.py b/docs/source-app/conf.py index be95b0d2b349c..18b5dcf0ff708 100644 --- a/docs/source-app/conf.py +++ b/docs/source-app/conf.py @@ -15,32 +15,28 @@ import os import shutil import sys -from importlib.util import module_from_spec, spec_from_file_location import pt_lightning_sphinx_theme +import lightning_app + _PATH_HERE = os.path.abspath(os.path.dirname(__file__)) _PATH_ROOT = os.path.realpath(os.path.join(_PATH_HERE, "..", "..")) sys.path.insert(0, os.path.abspath(_PATH_ROOT)) SPHINX_MOCK_REQUIREMENTS = int(os.environ.get("SPHINX_MOCK_REQUIREMENTS", True)) -# alternative https://stackoverflow.com/a/67692/4521646 -spec = spec_from_file_location("lightning_app/__about__.py", os.path.join(_PATH_ROOT, "lightning_app", "__about__.py")) -about = module_from_spec(spec) -spec.loader.exec_module(about) - # -- Project information ----------------------------------------------------- # this name shall match the project name in Github as it is used for linking to code project = "lightning" -copyright = about.__copyright__ -author = about.__author__ +copyright = lightning_app.__copyright__ +author = lightning_app.__author__ # The short X.Y version -version = about.__version__ +version = lightning_app.__version__ # The full version, including alpha/beta/rc tags -release = about.__version__ +release = lightning_app.__version__ # Options for the linkcode extension # ---------------------------------- @@ -160,8 +156,8 @@ # documentation. html_theme_options = { - "pytorch_project": about.__homepage__, - "canonical_url": about.__homepage__, + "pytorch_project": lightning_app.__homepage__, + "canonical_url": lightning_app.__homepage__, "collapse_navigation": False, "display_version": True, "logo_only": False, @@ -227,7 +223,7 @@ project + " Documentation", author, project, - about.__docs__, + lightning_app.__docs__, "Miscellaneous", ), ] @@ -281,6 +277,15 @@ def setup(app): path_ipynb2 = os.path.join(path_nbs, os.path.basename(path_ipynb)) shutil.copy(path_ipynb, path_ipynb2) +# copy all examples to local folder +path_examples = os.path.join(_PATH_HERE, "..", "examples") +if not os.path.isdir(path_examples): + os.mkdir(path_examples) +for path_app_example in glob.glob(os.path.join(_PATH_ROOT, "examples", "app_*")): + path_app_example2 = os.path.join(path_examples, os.path.basename(path_app_example)) + if not os.path.isdir(path_app_example2): + shutil.copytree(path_app_example, path_app_example2, dirs_exist_ok=True) + # Ignoring Third-party packages # https://stackoverflow.com/questions/15889621/sphinx-how-to-exclude-imports-in-automodule @@ -314,7 +319,7 @@ def _package_list_from_file(file): def linkcode_resolve(domain, info): def find_source(): # try to find the file and line number, based on code from numpy: - # https://github.com/numpy/numpy/blob/master/doc/source/conf.py#L286 + # https://github.com/numpy/numpy/blob/master/doc/source-app/conf.py#L286 obj = sys.modules[info["module"]] for part in info["fullname"].split("."): obj = getattr(obj, part) @@ -381,6 +386,6 @@ def find_source(): doctest_global_setup = """ import importlib import os -import lightning_app +import lightning as L """ coverage_skip_undoc_in_source = True diff --git a/docs/source-app/workflows/access_app_state/app.py b/docs/source-app/core_api/lightning_app/app.py similarity index 100% rename from docs/source-app/workflows/access_app_state/app.py rename to docs/source-app/core_api/lightning_app/app.py diff --git a/docs/source-app/core_api/lightning_app/communication.rst b/docs/source-app/core_api/lightning_app/communication.rst index cf7d14470f37e..9a823b0844915 100644 --- a/docs/source-app/core_api/lightning_app/communication.rst +++ b/docs/source-app/core_api/lightning_app/communication.rst @@ -8,7 +8,7 @@ Communication between Lightning Components **Level:** Intermediate -**Prerequisite**: Read the `Communication in Lighting Apps article <../../access_app_state.html>`_. +**Prerequisite**: Read the `Communication in Lightning Apps article <../../access_app_state.html>`_. ---- diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index ba00f3e4beb30..36ed886e794ce 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -1,16 +1,19 @@ +******************************** +Communication between components +******************************** -************************************************** -What communication between components does for you -************************************************** +When creating interactive Lightning Apps (App) with multiple components, you may need your components to share information with each other and rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc. -When creating interactive apps with multiple components, you might want your components to share information with each other. You might to rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc. +To accomplish that, Lightning components can communicate using the App State. The App State is composed of all attributes defined within each component's **__init__** method e.g anything attached to the component with **self.x = y**. -By design, the :class:`~lightning_app.core.flow.LightningFlow` communicates to all :class:`~lightning_app.core.flow.LightningWork` within the application, but :class:`~lightning_app.core.flow.LightningWork` can't communicate between each other directly, they need the flow as a proxy to do so. +All attributes of all **LightningWork (Work)** components are accessible in the **LightningFlow (Flow)** components in real-time. -Once a ``LightningWork`` is running, any updates to its state is automatically communicated to the flow as a delta (using `DeepDiff `_). The state communication isn't bi-directional, it is only done from work to flow. +By design, the Flows communicate to all **Works** within the application. However, Works can't communicate between each other directly, they must use Flows as a proxy to communicate. -Internally, the Lightning App is alternatively collecting deltas sent from all the registered ``LightningWorks`` and/or UI, and running the root flow run method of the app. +Once a Work is running, any updates to the Work's state is automatically communicated to the Flow, as a delta (using `DeepDiff `_). The state communication isn't bi-directional, communication is only done from Work to Flow. + +Internally, the App is alternatively collecting deltas sent from all the registered Works and/or UI, and running the root Flow run method of the App. ---- @@ -18,17 +21,20 @@ Internally, the Lightning App is alternatively collecting deltas sent from all t Communication from LightningWork to LightningFlow ************************************************* -Here's an example to better understand communication from LightningWork to LightningFlow. +LightningFlow (Flow) can access their children's LightningWork (Work) state. + +When a running Work attribute gets updated inside its method (separate process locally or remote machine), the app re-executes Flow's run method once it receives the state update from the Work. + +Here's an example to better understand communication from Work to Flow. The ``WorkCounter`` increments a counter until 1 million and the ``Flow`` prints the work counter. -As the LightningWork is running into its own process, its state changes is sent to the LightningFlow which contains the latest value of the counter. +As the Work is running its own process, its state changes are sent to the Flow which contains the latest value of the counter. .. code-block:: python import lightning as L - class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -38,8 +44,8 @@ As the LightningWork is running into its own process, its state changes is sent for _ in range(int(10e6)): self.counter += 1 - class Flow(L.LightningFlow): + def __init__(self): super().__init__() self.w = WorkCounter() @@ -48,15 +54,14 @@ As the LightningWork is running into its own process, its state changes is sent self.w.run() print(self.w.counter) - app = L.LightningApp(Flow()) -A delta sent from the LightningWork to the LightningFlow looks like this: +A delta sent from the Work to the Flow looks like this: .. code-block:: python - {"values_changed": {"root['works']['w']['vars']['counter']": {"new_value": 425}}} + {'values_changed': {"root['works']['w']['vars']['counter']": {'new_value': 425}}} Here is the associated illustration: @@ -64,20 +69,59 @@ Here is the associated illustration: :alt: Mechanism showing how delta are sent. :width: 100 % +Here's another example that is slightly different. Here we define a Flow and Work, where the Work increments a counter indefinitely and the Flow prints its state which contains the Work. + +You can easily check the state of your entire app as follows: + +.. literalinclude:: ../../core_api/lightning_app/app.py + +Run the app with: + +.. code-block:: bash + + lightning run app docs/source-app/core_api/lightning_app/app.py + +And here's the output you get when running the App using the **Lightning CLI**: + +.. code-block:: console + + INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view + State: {'works': {'w': {'vars': {'counter': 1}}}} + State: {'works': {'w': {'vars': {'counter': 2}}}} + State: {'works': {'w': {'vars': {'counter': 3}}}} + State: {'works': {'w': {'vars': {'counter': 3}}}} + State: {'works': {'w': {'vars': {'counter': 4}}}} + ... + +---- + +************************************************* +Communication from LightningFlow to LightningFlow +************************************************* + +Every LightningFlow (Flow) can access the state of any of its children Flow's. + +Here's an example to better understand communication from Flow to Flow. + +.. code-block:: python + + import lightning as L + ---- ************************************************* Communication from LightningFlow to LightningWork ************************************************* -Communication from the LightningFlow to the LightningWork while running **isn't support yet**. If your application requires this feature, please open an issue on Github. +Communication from the LightningFlow (Flow) to the LightningWork (Work) while running **isn't supported yet**. If your application requires this feature, please open an issue on Github. + +Here's an example of what would happen if you try to have the Flow communicate with the Work: .. code-block:: python import lightning as L from time import sleep - class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -88,8 +132,8 @@ Communication from the LightningFlow to the LightningWork while running **isn't sleep(1) print(f"Work {self.counter}") - class Flow(L.LightningFlow): + def __init__(self): super().__init__() self.w = WorkCounter() @@ -100,10 +144,9 @@ Communication from the LightningFlow to the LightningWork while running **isn't print(f"Flow {self.w.counter}") self.w.counter += 1 - app = L.LightningApp(Flow()) -As you can observe, there is a divergence between the value within the LightningWork and the LightningFlow. +As you can see, there is a divergence between the values within the Work and the Flow. .. code-block:: console diff --git a/docs/source-app/core_api/lightning_work/payload_content.rst b/docs/source-app/core_api/lightning_work/payload_content.rst index 780f3985e30ea..15adcd856f3ee 100644 --- a/docs/source-app/core_api/lightning_work/payload_content.rst +++ b/docs/source-app/core_api/lightning_work/payload_content.rst @@ -20,7 +20,6 @@ Here is an example: import lightning as L import pandas as pd - class SourceWork(L.LightningWork): def __init__(self): super().__init__() @@ -29,7 +28,7 @@ Here is an example: def run(self): # do some processing - df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) + df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]}) # The object you care about needs to be wrapped into a Payload object. self.df = L.storage.Payload(df) @@ -47,17 +46,17 @@ Once the Payload object is attached to your Work's state, it can be passed to an import lightning as L import pandas as pd - class DestinationWork(L.LightningWork): - def run(self, df: L.storage.Payload): + + def run(self, df:L.storage.Payload): # You can access the original object from the payload using its value property. print("dst", df.value) # dst col1 col2 # 0 1 3 # 1 2 4 - class Flow(L.LightningFlow): + def __init__(self): super().__init__() self.src = SourceWork() @@ -71,5 +70,4 @@ Once the Payload object is attached to your Work's state, it can be passed to an # so you receive a copy of the original object. self.dst.run(df=self.src.df) - app = L.LightningApp(Flow()) diff --git a/docs/source-app/core_api/lightning_work/status_content.rst b/docs/source-app/core_api/lightning_work/status_content.rst index 7ee8b65ac852b..f593421346b2f 100644 --- a/docs/source-app/core_api/lightning_work/status_content.rst +++ b/docs/source-app/core_api/lightning_work/status_content.rst @@ -38,16 +38,16 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc from time import sleep import lightning as L - class Work(L.LightningWork): + def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -77,7 +77,6 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc self.work.run(self.counter) self.counter += 1 - app = L.LightningApp(Flow()) Run this app as follows: @@ -136,16 +135,16 @@ In order to access all statuses: from time import sleep import lightning as L - class Work(L.LightningWork): + def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -156,7 +155,6 @@ In order to access all statuses: self.work.run(self.counter) self.counter += 1 - app = L.LightningApp(Flow()) diff --git a/docs/source-app/examples/file_server/app.py b/docs/source-app/examples/file_server/app.py index 5de9f3720a351..20308814ed7e9 100644 --- a/docs/source-app/examples/file_server/app.py +++ b/docs/source-app/examples/file_server/app.py @@ -4,15 +4,20 @@ import uuid import zipfile from dataclasses import dataclass -from pathlib import Path from typing import List - import lightning as L from lightning.app.storage import Drive +from pathlib import Path class FileServer(L.LightningWork): - def __init__(self, drive: Drive, base_dir: str = "file_server", chunk_size=10240, **kwargs): + def __init__( + self, + drive: Drive, + base_dir: str = "file_server", + chunk_size=10240, + **kwargs + ): """This component uploads, downloads files to your application. Arguments: @@ -50,7 +55,8 @@ def upload_file(self, file): filename = file.filename uploaded_file = self.get_random_filename() meta_file = uploaded_file + ".meta" - self.uploaded_files[filename] = {"progress": (0, None), "done": False} + self.uploaded_files[filename] = { + "progress": (0, None), "done": False} # 2: Create a stream and write bytes of # the file to the disk under `uploaded_file` path. @@ -151,22 +157,24 @@ def alive(self): return self.url != "" -import requests - from lightning import LightningWork - +import requests class TestFileServer(LightningWork): + def __init__(self, drive: Drive): super().__init__(cache_calls=True) self.drive = drive - def run(self, file_server_url: str, first=True): + def run(self, file_server_url: str, first = True): if first: with open("test.txt", "w") as f: f.write("Some text.") - response = requests.post(file_server_url + "/upload_file/", files={"file": open("test.txt", "rb")}) + response = requests.post( + file_server_url + "/upload_file/", + files={'file': open("test.txt", 'rb')} + ) assert response.status_code == 200 else: response = requests.get(file_server_url) @@ -176,8 +184,8 @@ def run(self, file_server_url: str, first=True): from lightning import LightningApp, LightningFlow - class Flow(LightningFlow): + def __init__(self): super().__init__() # 1: Create a drive to share data between works @@ -206,18 +214,14 @@ def configure_layout(self): # in the UI using its `/` endpoint. return {"name": "File Server", "content": self.file_server} - from lightning.app.runners import MultiProcessRuntime - def test_file_server(): app = LightningApp(Flow()) MultiProcessRuntime(app).dispatch() - from lightning.app.testing.testing import run_app_in_cloud - def test_file_server_in_cloud(): # You need to provide the directory containing the app file. app_dir = "docs/source-app/examples/file_server" diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index 0efc0e02b3839..57aee800de59b 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -56,7 +56,8 @@ def run(self, *args, **kwargs): # 2: Use git command line to clone the repo. repo_name = self.github_repo.split("/")[-1].replace(".git", "") cwd = os.path.dirname(__file__) - subprocess.Popen(f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() + subprocess.Popen( + f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() # 3: Execute the parent run method of the TracerPythonScript class. os.chdir(os.path.join(cwd, repo_name)) @@ -72,6 +73,7 @@ def configure_layout(self): class PyTorchLightningGithubRepoRunner(GithubRepoRunner): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.best_model_path = None @@ -103,12 +105,12 @@ def trainer_pre_fn(self, *args, work=None, **kwargs): # 5. Patch the `__init__` method of the Trainer # to inject our callback with a reference to the work. - tracer.add_traced(Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) + tracer.add_traced( + Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) return tracer def on_after_run(self, end_script_globals): import torch - # 1. Once the script has finished to execute, # we can collect its globals and access any objects. trainer = end_script_globals["cli"].trainer @@ -136,11 +138,9 @@ def on_after_run(self, end_script_globals): class KerasGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" - class TensorflowGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" - GITHUB_REPO_RUNNERS = { "PyTorch Lightning": PyTorchLightningGithubRepoRunner, "Keras": KerasGithubRepoRunner, @@ -186,7 +186,6 @@ def configure_layout(self): # Create a StreamLit UI for the user to run his Github Repo. return StreamlitFrontend(render_fn=render_fn) - def page_1__create_new_run(state): import streamlit as st @@ -204,7 +203,9 @@ def page_1__create_new_run(state): script_path = st.text_input("Enter your script to run", value="train_script.py") script_args = st.text_input("Enter your base script arguments", value=default_script_args) requirements = st.text_input("Enter your requirements", value=default_requirements) - ml_framework = st.radio("Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"]) + ml_framework = st.radio( + "Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"] + ) if ml_framework not in ("PyTorch Lightning"): st.write(f"{ml_framework} isn't supported yet.") @@ -229,7 +230,6 @@ def page_1__create_new_run(state): # and run the associated work from the request information. state.requests = state.requests + [new_request] - def page_2__view_run_lists(state): import streamlit as st @@ -250,8 +250,9 @@ def page_2__view_run_lists(state): best_model_score = r.get("best_model_score", None) if best_model_score: if st.checkbox(f"Expand to view your run performance", key=i): - st.json({"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")}) - + st.json( + {"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")} + ) def page_3__view_app_state(state): import streamlit as st @@ -259,16 +260,17 @@ def page_3__view_app_state(state): st.markdown("# App State 🎈") st.write(state._state) - def render_fn(state: AppState): import streamlit as st + page_names_to_funcs = { "Create a new Run": partial(page_1__create_new_run, state=state), "View your Runs": partial(page_2__view_run_lists, state=state), "View the App state": partial(page_3__view_app_state, state=state), } - selected_page = st.sidebar.selectbox("Select a page", page_names_to_funcs.keys()) + selected_page = st.sidebar.selectbox( + "Select a page", page_names_to_funcs.keys()) page_names_to_funcs[selected_page]() @@ -284,12 +286,10 @@ def run(self): def configure_layout(self): # 1: Add the main StreamLit UI - selection_tab = [ - { - "name": "Run your Github Repo", - "content": self.flow, - } - ] + selection_tab = [{ + "name": "Run your Github Repo", + "content": self.flow, + }] # 2: Add a new tab whenever a new work is dynamically created run_tabs = [e.configure_layout() for e in self.flow.ws.values()] # 3: Returns the list of tabs. diff --git a/docs/source-app/glossary/app_tree.rst b/docs/source-app/glossary/app_tree.rst index 860560bdcedf2..120d5d2e4ef69 100644 --- a/docs/source-app/glossary/app_tree.rst +++ b/docs/source-app/glossary/app_tree.rst @@ -43,14 +43,14 @@ You can attach your components in the **__init__** method of a flow. import lightning as L - class RootFlow(L.LightningFlow): + def __init__(self): super().__init__() # The `Work` component is attached here. self.work = Work() - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. self.nested_flow = NestedFlow() Once done, simply add the root flow to a Lightning app as follows: @@ -72,19 +72,20 @@ You can simply attach your components in the **run** method of a flow using the .. code-block:: python class RootFlow(L.LightningFlow): + def run(self): if not hasattr(self, "work"): - # The `Work` component is attached here. + # The `Work` component is attached here. setattr(self, "work", Work()) # Run the `Work` component. - getattr(self, "work").run() + getattr(self, "work").run() if not hasattr(self, "nested_flow"): - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. setattr(self, "nested_flow", NestedFlow()) # Run the `NestedFlow` component. - getattr(self, "wonested_flowrk").run() + getattr(self, "wonested_flowrk").run() But it is usually more readable to use Lightning built-in :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` as follows: @@ -93,8 +94,8 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app from lightning_app.structures import Dict - class RootFlow(L.LightningFlow): + def __init__(self): super().__init__() self.dict = Dict() @@ -107,5 +108,5 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app if "nested_flow" not in self.dict: # The `NestedFlow` component is attached here. - self.dict["nested_flow"] = NestedFlow() + self.dict["nested_flow"] =NestedFlow() self.dict["nested_flow"].run() diff --git a/docs/source-app/glossary/build_config/build_config_advanced.rst b/docs/source-app/glossary/build_config/build_config_advanced.rst index c96ac93d079bc..f954bd3435c0f 100644 --- a/docs/source-app/glossary/build_config/build_config_advanced.rst +++ b/docs/source-app/glossary/build_config/build_config_advanced.rst @@ -23,7 +23,6 @@ Create a :class:`~lightning_app.utilities.packaging.build_config.BuildConfig` an from lightning_app import LightningWork, BuildConfig - class MyWork(LightningWork): def __init__(self): super().__init__() diff --git a/docs/source-app/glossary/build_config/build_config_basic.rst b/docs/source-app/glossary/build_config/build_config_basic.rst index c3e3ae8c6ffe2..31b274e086db9 100644 --- a/docs/source-app/glossary/build_config/build_config_basic.rst +++ b/docs/source-app/glossary/build_config/build_config_basic.rst @@ -50,11 +50,12 @@ Instead of listing the requirements in a file, you can also pass them to the Lig from lightning_app import LightningWork, BuildConfig - class MyWork(LightningWork): def __init__(self): super().__init__() - self.cloud_build_config = BuildConfig(requirements=["torch>=1.8", "torchmetrics"]) + self.cloud_build_config = BuildConfig( + requirements=["torch>=1.8", "torchmetrics"] + ) .. note:: The build config only applies when running in the cloud and gets ignored otherwise. A local build config is currently not supported. diff --git a/docs/source-app/glossary/build_config/build_config_intermediate.rst b/docs/source-app/glossary/build_config/build_config_intermediate.rst index 174f472facb8e..0a5839b1f3ea8 100644 --- a/docs/source-app/glossary/build_config/build_config_intermediate.rst +++ b/docs/source-app/glossary/build_config/build_config_intermediate.rst @@ -18,9 +18,9 @@ If you need to install additional system packages or run other configuration ste from lightning_app import BuildConfig - @dataclass class CustomBuildConfig(BuildConfig): + def build_commands(self): return ["sudo apt-get install libsparsehash-dev"] @@ -31,7 +31,6 @@ If you need to install additional system packages or run other configuration ste from lightning_app import LightningWork - class MyWork(LightningWork): def __init__(self): super().__init__() @@ -40,7 +39,9 @@ If you need to install additional system packages or run other configuration ste self.cloud_build_config = CustomBuildConfig() # Can also be combined with extra requirements - self.cloud_build_config = CustomBuildConfig(requirements=["torchmetrics"]) + self.cloud_build_config = CustomBuildConfig( + requirements=["torchmetrics"] + ) .. note:: diff --git a/docs/source-app/glossary/environment_variables.rst b/docs/source-app/glossary/environment_variables.rst index fd41594656b0f..20ab1b09b6a0a 100644 --- a/docs/source-app/glossary/environment_variables.rst +++ b/docs/source-app/glossary/environment_variables.rst @@ -19,9 +19,8 @@ The environment variables are available in all flows and works, and can be acces .. code:: python import os - - print(os.environ["FOO"]) # BAR - print(os.environ["BAZ"]) # FAZ + print(os.environ["FOO"]) # BAR + print(os.environ["BAZ"]) # FAZ .. note:: Environment variables are currently not encrypted. diff --git a/docs/source-app/glossary/storage/drive_content.rst b/docs/source-app/glossary/storage/drive_content.rst index c2c1357d0f6df..263fab6093c1c 100644 --- a/docs/source-app/glossary/storage/drive_content.rst +++ b/docs/source-app/glossary/storage/drive_content.rst @@ -50,7 +50,6 @@ Any components can create a drive object. from lightning_app import LightningFlow, LightningWork from lightning_app.storage import Drive - class Flow(LightningFlow): def __init__(self): super().__init__() @@ -59,7 +58,6 @@ Any components can create a drive object. def run(self): ... - class Work(LightningWork): def __init__(self): super().__init__() @@ -82,7 +80,7 @@ A Drive supports put, list, get, and delete actions. drive = Drive("lit://drive") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty # Created file. with open("a.txt", "w") as f: @@ -90,13 +88,13 @@ A Drive supports put, list, get, and delete actions. drive.put("a.txt") - drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. + drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. - drive.get("a.txt") # Get the file into the current worker + drive.get("a.txt") # Get the file into the current worker drive.delete("a.txt") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty ---- @@ -116,6 +114,7 @@ Here is an illustrated code example on how to create drives within works. class Work_A(LightningWork): + def __init__(self): super().__init__() # The identifier of the Drive is ``drive_1`` @@ -132,6 +131,7 @@ Here is an illustrated code example on how to create drives within works. class Work_B(LightningWork): + def __init__(self): super().__init__() @@ -151,8 +151,8 @@ Here is an illustrated code example on how to create drives within works. self.drive_1.put("b.txt") self.drive_2.put("b.txt") - class Work_C(LightningWork): + def __init__(self): super().__init__() self.drive_2 = Drive("lit://drive_2") diff --git a/docs/source-app/glossary/storage/path.rst b/docs/source-app/glossary/storage/path.rst index 6da6459cf1195..1437bba577d80 100644 --- a/docs/source-app/glossary/storage/path.rst +++ b/docs/source-app/glossary/storage/path.rst @@ -99,7 +99,6 @@ For example, share a directory by passing it as an input to the run method of th from lightning_app import LightningFlow - class Flow(LightningFlow): def __init__(self): super().__init__() @@ -173,8 +172,8 @@ You can check if a path exists locally or remotely in the source Work using the # OR if checkpoint_dir.exists_local(): - # Do something with the file if it exists locally - files = os.listdir(checkpoint_dir) + # Do something with the file if it exists locally + files = os.listdir(checkpoint_dir) ---- @@ -191,7 +190,6 @@ Lightning makes sure all Paths that are part of the state get stored and made ac from lightning_app.storage import Path - class Work(LightningWork): def __init__(self): super().__init__() @@ -220,7 +218,6 @@ First, define a component that saves a checkpoint: import torch import os - class ModelTraining(LightningWork): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -267,7 +264,6 @@ Link both components via a parent component: self.train.run() self.deploy.run(checkpoint_dir=self.train.checkpoint_dir) - app = L.LightningApp(Flow()) diff --git a/docs/source-app/index.rst b/docs/source-app/index.rst index e6a1b5b61665e..9de5b2b1b1756 100644 --- a/docs/source-app/index.rst +++ b/docs/source-app/index.rst @@ -40,16 +40,15 @@ Welcome to ⚡ Lightning Apps Install Lightning ****************** - .. code-block:: bash pip install lightning ---- -****************** +*********** Get Started -****************** +*********** .. raw:: html @@ -112,15 +111,15 @@ Get Started .. toctree:: :maxdepth: 1 - :caption: App development workflows + :caption: How to... - Access the app state Add a web user interface (UI) Add a web link Arrange app tabs Build a Lightning app Build a Lightning component Cache Work run calls + Customize your cloud compute Extend an existing app Publish a Lightning component Run a server within a Lightning App diff --git a/docs/source-app/levels/advanced/index.rst b/docs/source-app/levels/advanced/index.rst index 9fa0b61eb11bb..265a8ce961690 100644 --- a/docs/source-app/levels/advanced/index.rst +++ b/docs/source-app/levels/advanced/index.rst @@ -4,11 +4,11 @@ :maxdepth: 1 :hidden: + level_16 level_17 level_18 level_19 level_20 - level_21 ############### Advanced skills @@ -29,41 +29,41 @@ Learn to build Lightning Apps for enterprise workloads or advanced research. .. Add callout items below this line .. displayitem:: - :header: Level 17: Check work status + :header: Level 16: Check work status :description: Learn to use work status to coordinate complex apps. - :button_link: level_17.html + :button_link: level_16.html :col_css: col-md-6 :height: 150 :tag: advanced .. displayitem:: - :header: Level 18: Cache calls into run + :header: Level 17: Cache calls into run :description: Learn about caching calls in work.run. - :button_link: level_18.html + :button_link: level_17.html :col_css: col-md-6 :height: 150 :tag: advanced .. displayitem:: - :header: Level 19: Share objects between works + :header: Level 18: Share objects between works :description: Learn to build things like DAGs where values return from Work. - :button_link: level_19.html + :button_link: level_18.html :col_css: col-md-6 :height: 150 :tag: advanced .. displayitem:: - :header: Level 20: Handle Lightning App exceptions + :header: Level 19: Handle Lightning App exceptions :description: Learn to handle Lightning App exceptions. - :button_link: level_20.html + :button_link: level_19.html :col_css: col-md-6 :height: 150 :tag: advanced .. displayitem:: - :header: Level 21: Enable dynamic Works + :header: Level 20: Enable dynamic Works :description: Learn to enable dynamic works for complex systems. - :button_link: level_21.html + :button_link: level_20.html :col_css: col-md-6 :height: 150 :tag: advanced diff --git a/docs/source-app/levels/advanced/level_16.rst b/docs/source-app/levels/advanced/level_16.rst new file mode 100644 index 0000000000000..58c1e1fc7e0ad --- /dev/null +++ b/docs/source-app/levels/advanced/level_16.rst @@ -0,0 +1,10 @@ +########################### +Level 16: Check Work status +########################### +**Audience:** Users who want to stop/start Lightning Work based on a status. + +**Prereqs:** Level 16+ + +---- + +.. include:: ../../core_api/lightning_work/status_content.rst diff --git a/docs/source-app/levels/advanced/level_17.rst b/docs/source-app/levels/advanced/level_17.rst index 29cef4a7bf3fc..7bc8dda7036cc 100644 --- a/docs/source-app/levels/advanced/level_17.rst +++ b/docs/source-app/levels/advanced/level_17.rst @@ -1,10 +1,10 @@ -########################### -Level 17: Check Work status -########################### -**Audience:** Users who want to stop/start Lightning Work based on a status. +############################## +Level 17: Cache calls into run +############################## +**Audience:** Users who want Work.run() to activate multiple times in an app. -**Prereqs:** Level 16+ +**Prereqs:** Level 16+ and read the `Event Loop guide <../glossary/event_loop.html>`_. ---- -.. include:: ../../core_api/lightning_work/status_content.rst +.. include:: ../../workflows/run_work_once_content.rst diff --git a/docs/source-app/levels/advanced/level_18.rst b/docs/source-app/levels/advanced/level_18.rst index 9b050b4452d57..c934d2fd08160 100644 --- a/docs/source-app/levels/advanced/level_18.rst +++ b/docs/source-app/levels/advanced/level_18.rst @@ -1,12 +1,10 @@ -############################## -Level 18: Cache calls into run -############################## -**Audience:** Users who want Work.run() to activate multiple times in an app. - -**Prereqs:** Level 16+ and read the `Event Loop guide <../glossary/event_loop.html>`_. - +############################################## +Level 18: Share objects between LightningWorks +############################################## +**Audience:** Users moving DataFrames or outputs, between Lightning Works (usually data engineers). +**Prereqs:** Level 16+ and know about the Pandas library and read the `Access app state guide <../../access_app_state.html>`_. ---- -.. include:: ../../workflows/run_work_once_content.rst +.. include:: ../../core_api/lightning_work/payload_content.rst diff --git a/docs/source-app/levels/advanced/level_19.rst b/docs/source-app/levels/advanced/level_19.rst index 272b6fe282e3b..99a859e1ad2ca 100644 --- a/docs/source-app/levels/advanced/level_19.rst +++ b/docs/source-app/levels/advanced/level_19.rst @@ -1,10 +1,11 @@ -############################################## -Level 19: Share objects between LightningWorks -############################################## -**Audience:** Users moving DataFrames or outputs, between Lightning Works (usually data engineers). +######################################### +Level 19: Handle Lightning App exceptions +######################################### -**Prereqs:** Level 16+ and know about the Pandas library and read the `Access app state guide <../../access_app_state.html>`_. +**Audience:** Users who want to make Lightning Apps more robust to potential issues. + +**Prereqs:** Level 16+ ---- -.. include:: ../../core_api/lightning_work/payload_content.rst +.. include:: ../../core_api/lightning_work/handling_app_exception_content.rst diff --git a/docs/source-app/levels/advanced/level_20.rst b/docs/source-app/levels/advanced/level_20.rst index c83b696812a49..1d045e85fc692 100644 --- a/docs/source-app/levels/advanced/level_20.rst +++ b/docs/source-app/levels/advanced/level_20.rst @@ -1,11 +1,11 @@ -######################################### -Level 20: Handle Lightning App exceptions -######################################### +####################################### +Level 20: Enable dynamic LightningWorks +####################################### -**Audience:** Users who want to make Lightning Apps more robust to potential issues. +**Audience:** Users who want to create/run/stop multiple LightningWorks not defined at app instantiation. **Prereqs:** Level 16+ ---- -.. include:: ../../core_api/lightning_work/handling_app_exception_content.rst +.. include:: ../../core_api/lightning_app/dynamic_work_content.rst diff --git a/docs/source-app/levels/advanced/level_21.rst b/docs/source-app/levels/advanced/level_21.rst deleted file mode 100644 index 6d163fffa5609..0000000000000 --- a/docs/source-app/levels/advanced/level_21.rst +++ /dev/null @@ -1,11 +0,0 @@ -####################################### -Level 21: Enable dynamic LightningWorks -####################################### - -**Audience:** Users who want to start/stop multiple LightningWorks not defined in the beginning of the Lightning App. - -**Prereqs:** Level 16+ - ----- - -.. include:: ../../core_api/lightning_app/dynamic_work_content.rst diff --git a/docs/source-app/levels/basic/level_4.rst b/docs/source-app/levels/basic/level_4.rst index c5ce24ff42e2f..17fc8e90feb87 100644 --- a/docs/source-app/levels/basic/level_4.rst +++ b/docs/source-app/levels/basic/level_4.rst @@ -109,14 +109,15 @@ To use the component, simply import it and attach it to your Lightning App. import lightning as L from lit_slack import SlackMessenger - class YourComponent(L.LightningFlow): def __init__(self): super().__init__() - self.slack_messenger = SlackMessenger(token="a-long-token", channel_id="A03CB4A6AK7") + self.slack_messenger = SlackMessenger( + token='a-long-token', + channel_id='A03CB4A6AK7' + ) def run(self): - self.slack_messenger.send_message("hello from ⚡ lit slack ⚡") - + self.slack_messenger.send_message('hello from ⚡ lit slack ⚡') app = L.LightningApp(YourComponent()) diff --git a/docs/source-app/levels/intermediate/index.rst b/docs/source-app/levels/intermediate/index.rst index a80da09959391..f712743350b41 100644 --- a/docs/source-app/levels/intermediate/index.rst +++ b/docs/source-app/levels/intermediate/index.rst @@ -12,7 +12,6 @@ level_13 level_14 level_15 - level_16 ################### Intermediate skills @@ -72,33 +71,25 @@ Learn to build your own Lightning Apps from scratch and the basics of the framew :tag: intermediate .. displayitem:: - :header: Level 13: Communicate with the Lightning App - :description: Learn to communicate across the Lightning App with the app state. - :button_link: level_13.html - :col_css: col-md-6 - :height: 150 - :tag: intermediate - -.. displayitem:: - :header: Level 14: Communicate between LightningFlow and LightningWork + :header: Level 13: Communicate between Lightning components :description: Learn about how LightningFlows communicate with LightningWorks. - :button_link: level_14.html + :button_link: level_13.html :col_css: col-md-6 :height: 150 :tag: intermediate .. displayitem:: - :header: Level 15: Share files between components + :header: Level 14: Share files between components :description: Learn how Drives share files between components - :button_link: level_15.html + :button_link: level_14.html :col_css: col-md-6 :height: 150 :tag: intermediate .. displayitem:: - :header: Level 16: Run LightningWorks in parallel + :header: Level 15: Run LightningWorks in parallel :description: Learn when to run LightningWorks in parallel - :button_link: level_16.html + :button_link: level_15.html :col_css: col-md-6 :height: 150 :tag: intermediate diff --git a/docs/source-app/levels/intermediate/level_13.rst b/docs/source-app/levels/intermediate/level_13.rst index e11132eb3d806..f5ce87eda3537 100644 --- a/docs/source-app/levels/intermediate/level_13.rst +++ b/docs/source-app/levels/intermediate/level_13.rst @@ -1,10 +1,10 @@ -######################################## -Level 13: Communication in Lighting Apps -######################################## -**Audience:** Users that want to create complex reactive apps. +#################################################### +Level 13: Communication between Lightning Components +#################################################### +**Audience:** Users who have multiple LightningWorks communicating with LightningFlows. **Prereqs:** Level 8+ ---- -.. include:: ../../workflows/access_app_state/access_app_state_content.rst +.. include:: ../../core_api/lightning_app/communication_content.rst diff --git a/docs/source-app/levels/intermediate/level_14.rst b/docs/source-app/levels/intermediate/level_14.rst index c7b0d2c4abdd8..5fbdb371c26ac 100644 --- a/docs/source-app/levels/intermediate/level_14.rst +++ b/docs/source-app/levels/intermediate/level_14.rst @@ -1,10 +1,10 @@ -############################################################### -Level 14: Communication between LightningFlow and LightningWork -############################################################### -**Audience:** Users who have multiple LightningWorks communicating with LightningFlows. +######################################## +Level 14: Share files between components +######################################## +**Audience:** Users who are moving large files such as artifacts or datasets. -**Prereqs:** Level 8+ and read the `Communication in Lighting Apps article <../../access_app_state.html>`_. +**Prereqs:** Level 8+ ---- -.. include:: ../../core_api/lightning_app/communication_content.rst +.. include:: ../../glossary/storage/drive_content.rst diff --git a/docs/source-app/levels/intermediate/level_15.rst b/docs/source-app/levels/intermediate/level_15.rst index 3f19aea603be1..255e573a71d69 100644 --- a/docs/source-app/levels/intermediate/level_15.rst +++ b/docs/source-app/levels/intermediate/level_15.rst @@ -1,10 +1,10 @@ ######################################## -Level 15: Share files between components +Level 15: Run LightningWorks in parallel ######################################## -**Audience:** Users who are moving large files such as artifacts or datasets. +**Audience:** Users who want to run multiple LightningWorks at once. **Prereqs:** Level 8+ ---- -.. include:: ../../glossary/storage/drive_content.rst +.. include:: ../../workflows/run_work_in_parallel_content.rst diff --git a/docs/source-app/levels/intermediate/level_16.rst b/docs/source-app/levels/intermediate/level_16.rst deleted file mode 100644 index 2921731b2605c..0000000000000 --- a/docs/source-app/levels/intermediate/level_16.rst +++ /dev/null @@ -1,10 +0,0 @@ -######################################## -Level 16: Run LightningWorks in parallel -######################################## -**Audience:** Users who want to run multiple LightningWorks at once. - -**Prereqs:** Level 8+ - ----- - -.. include:: ../../workflows/run_work_in_parallel_content.rst diff --git a/docs/source-app/workflows/access_app_state/access_app_state.rst b/docs/source-app/workflows/access_app_state/access_app_state.rst deleted file mode 100644 index 5f5be722d24aa..0000000000000 --- a/docs/source-app/workflows/access_app_state/access_app_state.rst +++ /dev/null @@ -1,11 +0,0 @@ -############################## -Communication in Lighting Apps -############################## - -**Audience:** Users that want to create complex reactive apps. - -**Level:** Intermediate - ----- - -.. include:: access_app_state_content.rst diff --git a/docs/source-app/workflows/access_app_state/access_app_state_content.rst b/docs/source-app/workflows/access_app_state/access_app_state_content.rst deleted file mode 100644 index f5a2c2eab03eb..0000000000000 --- a/docs/source-app/workflows/access_app_state/access_app_state_content.rst +++ /dev/null @@ -1,70 +0,0 @@ -******************* -A bit of background -******************* - -Lightning allows you to create reactive distributed applications, where components can be distributed across different machines and even different clouds. - -To create reactive applications, components need to be able to communicate- share data, status, values, etc. For example, if you are creating an app that will retrain a model every time new data is added to a cloud dataset, you will need the dataset component to communicate to the training component. - -Lightning components can communicate via the app state. The app state is composed of all attributes defined within each components **__init__** method. - -All attributes of all LightningWork components are accessible in the LightningFlow components in real time. - ----- - -******************************* -What the App State does for you -******************************* - -Every time you update an attribute inside of a running work (separate process on local or remote machine), the attribute of the mirrored work in the flow side is updated with that same exact value automatically. - -Every time the app received a state update from a running work, the app applies the state change and re-executes the flow run method. This enables complex systems to be easily implemented through the state. - -The **App State** is the collection of all the components state forming the application and gets automatically up-to-date, even in distributed settings. - ----- - -******************** -Access the App State -******************** - -As a user, you are interacting with your component attributes, so most likely, -you won't need to access the component state directly, but it can be helpful to -understand how the state works under the hood. - -For example, here we define a **Flow** component and **Work** component, where the work increments a counter indefinitely and the flow prints its state which contains the work. - -You can easily check the state of your entire app as follows: - -.. literalinclude:: ../../workflows/access_app_state/app.py - -Run the app with: - -.. code-block:: bash - - lightning run app docs/source-app/workflows/access_app_state/app.py - -And here's the output you get when running the above application using **Lightning CLI**: - -.. code-block:: console - - INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view - State: {'works': {'w': {'vars': {'counter': 1}}}} - State: {'works': {'w': {'vars': {'counter': 2}}}} - State: {'works': {'w': {'vars': {'counter': 3}}}} - State: {'works': {'w': {'vars': {'counter': 3}}}} - State: {'works': {'w': {'vars': {'counter': 4}}}} - ... - - -*** -FAQ -*** - -* **How can a work update a flow ?** A work is a leaf in the component tree, therefore it can access / update only itself. - -* **How can a flow update a work ?** The flow can update the work state. However, once the run method is launched, the work is running isolated with a copy of state. If the flow keeps updating the work state, and the work does the same with different values, it creates a state divergence. In the future, we might support bi-directional state update between flow and works. - -* **How can a work update a work ?** No, the communication is simply between work and flow. The flow role is to coordinate and pass some state between works. - -* **How can a flow update a flow ?** Yes, the flows can update themselves as they are running in the same python process. diff --git a/docs/source-app/workflows/add_server/any_server.rst b/docs/source-app/workflows/add_server/any_server.rst index 398951276c0a5..677478d70d740 100644 --- a/docs/source-app/workflows/add_server/any_server.rst +++ b/docs/source-app/workflows/add_server/any_server.rst @@ -26,7 +26,6 @@ Any server that listens on a port, can be enabled via a work. For example, here' import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -34,8 +33,7 @@ Any server that listens on a port, can be enabled via a work. For example, here' html = "

Hello lit world " self.wfile.write(html) - - httpd = socketserver.TCPServer(("localhost", "3000"), PlainServer) + httpd = socketserver.TCPServer(('localhost', '3000'), PlainServer) httpd.serve_forever() To enable the server inside the component, start the server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -47,7 +45,6 @@ To enable the server inside the component, start the server in the run method an import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -55,7 +52,6 @@ To enable the server inside the component, start the server in the run method an html = "

Hello lit world " self.wfile.write(html) - class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) @@ -76,7 +72,6 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl import socketserver from http import HTTPStatus, server - class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -84,13 +79,11 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl html = "

Hello lit world " self.wfile.write(html) - class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) httpd.serve_forever() - class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -100,10 +93,12 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl self.lit_server.run() def configure_layout(self): - tab1 = {"name": "home", "content": self.lit_server} + tab1 = { + 'name': 'home', + 'content': self.lit_server + } return tab1 - app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in parallel diff --git a/docs/source-app/workflows/add_server/flask_basic.rst b/docs/source-app/workflows/add_server/flask_basic.rst index 38ca282346248..5ee2a232828c4 100644 --- a/docs/source-app/workflows/add_server/flask_basic.rst +++ b/docs/source-app/workflows/add_server/flask_basic.rst @@ -26,13 +26,11 @@ First, define your flask app as you normally would without Lightning: flask_app = Flask(__name__) - - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" - + return 'Hello, World!' - flask_app.run(host="0.0.0.0", port=80) + flask_app.run(host='0.0.0.0', port=80) To enable the server inside the component, start the Flask server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -42,14 +40,13 @@ To enable the server inside the component, start the Flask server in the run met import lightning as L from flask import Flask - class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" + return 'Hello, World!' flask_app.run(host=self.host, port=self.port) @@ -67,18 +64,16 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli import lightning as L from flask import Flask - class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route("/") + @flask_app.route('/') def hello(): - return "Hello, World!" + return 'Hello, World!' flask_app.run(host=self.host, port=self.port) - class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -88,10 +83,9 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli self.lit_flask.run() def configure_layout(self): - tab1 = {"name": "home", "content": self.lit_flask} + tab1 = {'name': 'home', 'content': self.lit_flask} return tab1 - app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in the background diff --git a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst index c1c0c5e2017c8..fe72d09b27eec 100644 --- a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst +++ b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst @@ -79,17 +79,14 @@ You can use this single react app for the FULL Lightning app, or you can specify import lightning as L - class ComponentA(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist") - class ComponentB(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist") - class HelloLitReact(L.LightningFlow): def __init__(self): super().__init__() @@ -101,7 +98,6 @@ You can use this single react app for the FULL Lightning app, or you can specify tab_2 = {"name": "App 2", "content": self.react_app_2} return tab_1, tab_2 - app = L.LightningApp(HelloLitReact()) This is a powerful idea that allows each Lightning component to have a self-contained web UI. diff --git a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst index ac289c2eb27e1..08ab0e874bcca 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst @@ -22,12 +22,10 @@ For example, here we increase the count variable of the Lightning Component ever import lightning as L import streamlit as st - def your_streamlit_app(lightning_app_state): - if st.button("press to increase count"): + if st.button('press to increase count'): lightning_app_state.count += 1 - st.write(f"current count: {lightning_app_state.count}") - + st.write(f'current count: {lightning_app_state.count}') class LitStreamlit(L.LightningFlow): def __init__(self): @@ -37,7 +35,6 @@ For example, here we increase the count variable of the Lightning Component ever def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -47,7 +44,6 @@ For example, here we increase the count variable of the Lightning Component ever tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - app = L.LightningApp(LitApp()) ---- @@ -67,10 +63,8 @@ In this example we update the value of the counter from the component: import lightning as L import streamlit as st - def your_streamlit_app(lightning_app_state): - st.write(f"current count: {lightning_app_state.count}") - + st.write(f'current count: {lightning_app_state.count}') class LitStreamlit(L.LightningFlow): def __init__(self): @@ -83,7 +77,6 @@ In this example we update the value of the counter from the component: def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) - class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -96,5 +89,4 @@ In this example we update the value of the counter from the component: tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 - app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst index 91c0e53854760..988018cd795e8 100644 --- a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst +++ b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst @@ -16,13 +16,14 @@ To enable a single tab on the app UI, return a single dictionary from the ``conf import lightning as L - class DemoComponent(L.demo.dumb_component): def configure_layout(self): - tab1 = {"name": "THE TAB NAME", "content": self.component_a} + tab1 = { + "name": "THE TAB NAME", + "content": self.component_a + } return tab1 - app = L.LightningApp(DemoComponent()) @@ -42,14 +43,12 @@ Enable multiple tabs import lightning as L - class DemoComponent(L.demo.dumb_component): def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 - app = L.LightningApp(DemoComponent()) The order matters! Try any of the following configurations: @@ -62,7 +61,6 @@ The order matters! Try any of the following configurations: tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 - def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} diff --git a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst index 91e7fea93e28c..c43b1bbc2f749 100644 --- a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst +++ b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst @@ -19,12 +19,10 @@ If you didn't find a Lightning App similar to the one you need, simply create a import lightning as L - class WordComponent(L.LightningWork): def __init__(self, word): super().__init__() self.word = word - def run(self): print(self.word) @@ -32,15 +30,14 @@ If you didn't find a Lightning App similar to the one you need, simply create a class LitApp(L.LightningFlow): def __init__(self) -> None: super().__init__() - self.hello = WordComponent("hello") - self.world = WordComponent("world") + self.hello = WordComponent('hello') + self.world = WordComponent('world') def run(self): - print("This is a simple Lightning app, make a better app!") + print('This is a simple Lightning app, make a better app!') self.hello.run() self.world.run() - app = L.LightningApp(LitApp()) ---- diff --git a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst index 7faef8ee03df8..0f7b0ff436eb0 100644 --- a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst +++ b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst @@ -17,7 +17,7 @@ Use a **LightningFlow** component for any programming logic that runs in less th .. code:: python for i in range(10): - print(f"{i}: this kind of code belongs in a LightningFlow") + print(f'{i}: this kind of code belongs in a LightningFlow') Use a **LightningWork** component for any programming logic that takes more than 1 second or requires its own hardware. @@ -27,7 +27,7 @@ Use a **LightningWork** component for any programming logic that takes more than for i in range(100000): sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") + print(f'{i} LightningWork: work that is long running or may never end (like a server)') ---- @@ -77,12 +77,10 @@ To implement a LightningFlow, simply subclass ``LightningFlow`` and define the r # app.py import lightning as L - class LitFlow(L.LightningFlow): def run(self): for i in range(10): - print(f"{i}: this kind of code belongs in a LightningFlow") - + print(f'{i}: this kind of code belongs in a LightningFlow') app = L.LightningApp(LitFlow()) @@ -111,12 +109,11 @@ To implement a LightningWork, simply subclass ``LightningWork`` and define the r from time import sleep import lightning as L - class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") + print(f'{i} LightningWork: work that is long running or may never end (like a server)') A LightningWork must always be attached to a LightningFlow and explicitely asked to ``run()``: @@ -126,13 +123,11 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked from time import sleep import lightning as L - class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f"{i} LightningWork: work that is long running or may never end (like a server)") - + print(f'{i} LightningWork: work that is long running or may never end (like a server)') class LitFlow(L.LightningFlow): def __init__(self): @@ -142,7 +137,6 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked def run(self): self.lit_work.run() - app = L.LightningApp(LitFlow()) run the app diff --git a/docs/source-app/workflows/build_lightning_component/intermediate.rst b/docs/source-app/workflows/build_lightning_component/intermediate.rst index 070d3aa0caf1a..27336424bf916 100644 --- a/docs/source-app/workflows/build_lightning_component/intermediate.rst +++ b/docs/source-app/workflows/build_lightning_component/intermediate.rst @@ -32,10 +32,9 @@ To *connect* this user interface to the component, define the configure_layout m import lightning as L from lightning_app.frontend.web import StaticWebFrontend - class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") + return StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') Finally, route the component's UI through the root component's **configure_layout** method: @@ -45,11 +44,9 @@ Finally, route the component's UI through the root component's **configure_layou # app.py import lightning as L - class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return L.frontend.web.StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") - + return L.frontend.web.StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') class LitApp(L.LightningFlow): def __init__(self): @@ -60,7 +57,6 @@ Finally, route the component's UI through the root component's **configure_layou tab1 = {"name": "home", "content": self.lit_html_component} return tab1 - app = L.LightningApp(LitApp()) Run your app and you'll see the UI on the Lightning App view: diff --git a/docs/source-app/workflows/run_work_in_parallel_content.rst b/docs/source-app/workflows/run_work_in_parallel_content.rst index ecb87c5cea7ff..334f50e7e486a 100644 --- a/docs/source-app/workflows/run_work_in_parallel_content.rst +++ b/docs/source-app/workflows/run_work_in_parallel_content.rst @@ -13,14 +13,13 @@ The default behavior of the ``LightningWork`` is to wait for the ``run`` method import lightning as L - class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent() def run(self): self.work_component_a.run() - print("this will never print") + print('this will never print') Since this LightningWork component we created loops forever, the print statement will never execute. In practice ``LightningWork`` workloads are finite and don't run forever. @@ -40,14 +39,13 @@ To run LightningWorks in parallel, while the rest of the app executes without de import lightning as L - class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent(parallel=True) def run(self): self.work_component_a.run() - print("repeats while the infinite work runs ONCE (and forever) in parallel") + print('repeats while the infinite work runs ONCE (and forever) in parallel') Any LightningWorks that will take more than **1 second** should be run in parallel unless the rest of your Lightning App depends on the output of this work (for example, downloading a dataset). diff --git a/docs/source-app/workflows/run_work_once_content.rst b/docs/source-app/workflows/run_work_once_content.rst index dbef37813572a..4388742358929 100644 --- a/docs/source-app/workflows/run_work_once_content.rst +++ b/docs/source-app/workflows/run_work_once_content.rst @@ -24,10 +24,10 @@ As explained in the `Event Loop guide <../glossary/event_loop.html>`_, the Light from datetime import datetime # Lightning code - while True: # This is the Lightning Event Loop + while True: # This is the Lightning Event Loop # Your code - today = datetime.now().strftime("%D") # '05/25/22' + today = datetime.now().strftime("%D") # '05/25/22' data_processor.run(today) train_model.run(data_processor.data) @@ -47,12 +47,10 @@ Here's an example of this behavior with LightningWork: import lightning as L - - class ExampleWork(L.LightningWork): + class ExampleWork( L.LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") - work = ExampleWork() work.run(value=1) @@ -88,12 +86,10 @@ By setting ``cache_calls=False``, Lightning won't cache the return value and re- from lightning_app import LightningWork - class ExampleWork(LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") - work = ExampleWork(cache_calls=False) work.run(value=1) @@ -126,19 +122,21 @@ as the work continuously execute in a blocking way. from lightning_app import LightningApp, LightningFlow, LightningWork - class Flow(LightningFlow): + def __init__(self): super().__init__() - self.work = Work(cache_calls=False, parallel=False) + self.work = Work( + cache_calls=False, + parallel=False + ) def run(self): print("HERE BEFORE") self.work.run() print("HERE AFTER") - app = LightningApp(Flow()) .. code-block:: console diff --git a/docs/source-app/workflows/share_files_between_components.rst b/docs/source-app/workflows/share_files_between_components.rst index 15108515b3947..02c7d889a7122 100644 --- a/docs/source-app/workflows/share_files_between_components.rst +++ b/docs/source-app/workflows/share_files_between_components.rst @@ -34,8 +34,8 @@ To write a file, first create a reference to the file with the :class:`~lightnin boring_file_reference = Path("boring_file.txt") # write to that file - with open(self.boring_file_reference, "w") as f: - f.write("yolo") + with open(self.boring_file_reference, 'w') as f: + f.write('yolo') ---- @@ -107,7 +107,6 @@ For example, here we save a file on one component and use it in another componen from lightning_app.storage.path import Path - class ComponentA(LightningWork): def __init__(self): super().__init__() From a2ee2ba06ee755a3a131759e8be942497ab003bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:58:03 +0000 Subject: [PATCH 013/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7c91383869542..c27a9e898de68 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,4 @@ cifar-10-batches-py # ctags tags .tags -src/lightning_app/ui/* \ No newline at end of file +src/lightning_app/ui/* From 722847c860585f77b51c1885f599dcc38a35d4c9 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 4 Jul 2022 17:59:10 +0100 Subject: [PATCH 014/119] update --- docs/source-app/_static/images/brandmark.png | Bin 60816 -> 0 bytes src/lightning_app/README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 docs/source-app/_static/images/brandmark.png diff --git a/docs/source-app/_static/images/brandmark.png b/docs/source-app/_static/images/brandmark.png deleted file mode 100644 index 76648a7ed99e019dff43a641562fc9b238fc7951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60816 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

|YPrMQ(76*=X&6xdryZZGOjzUA253wEM$}f}5 zX?7;)jugB3dxWM=an*6E=D#-|B|*DCK$K;6-sOM$(0RyznSq(EyhR@Iudiz-c*-oQ zuJ|eVUC_@xd{?a%o_Fe<9j`j*|YHRDX;p2PGi)k)TQ{uLz$ zPQphMqgn4T<2hqg^OCl4Qadz|`9XfLY@Ubb>}HgDHBJBPI4YaH0DUq?YRsa{4y?lJ6g^hu z$KvId8jSHV#ew4yet-o1ZT}mz&#rH8KsruLcvJY4YzoRk=>0x%gKFvQ9zAiT;lSTl zQQ1-j=>5;6#?IsMy;sIYNo+);BrW-lYm7V3Kp%JoS zeIXhD-ux~2z9zdOqbNjFtnTW!j*1ZSP(3=>u4I6*NTBO}pKvVG^_KscvVy)s$AdOr zwr)OZ)RrSk6m4+0Hn58;F94mGeN)~)-WE->p=2-&{|yiQ#3b$wX{8%xf4i}N8>gk1 z@(y}|;<(3u^Bt!t{q^E?JwN@@xi{#jeAM-*;7ks7#o zke{J~*+H*T954OPM7+EcMm}xSc5TQhurv2>FT(EQG{mF^2o%dX|0DmJRgE*@uOh)> z#FQ?!Wj`^eS$V&dyN%0?Bj_JeS`G;OHNDEtfL}Bwz`JSCg)%c-U^cQ8y*oq5*lS7# zXX^vo2|g3dt-j2re{Y5^2a1 z;lIU~cD-}iD_}8dH-Dt=_fj$a%(v)&J^c;xvPKl1w4Nw){aWWCurW=lJ>Q>^FJN#3 zgt2bb`BVR1M%!Gr&iWDSDFY_#%-^-7p@&}b!rx@HGfD>j84e=QHwp9$bSWbl~c8#sJ^H7vnez(5g=}ifS<@FmD~M9VZrcY1lI?2g>yhm3S8h zwobqJ_bU3rW$U=$e_GC5Xx3@4$o#t`w>!`2s2ThdEQ&)*no|y$SrEZ=TkG>E++#U@ zb$w_vz@^DeD^q2Fza*wy>K%aoU!~%UG~rLyHEN4-SLAYT z{yNKz=^O z3?>BeOD8aq*J0e1Z&L=`Shwf#e5_3V=e{qFMfEjYywd2El3 zjsCTj55mqz#gn-4v7!kIz`5i!aK(Wuyl2USGfIItXZ1OM+k~I*m{D623>KDsi<$tV zp3&r+f?!%PM&;GV)X!_d{s+z>hLA-Tc&kWHU zQ#r+z*kmF-z*R8xb+m^qVjl%$V06Jxum5;y~acwOJe n0e|m6T1r%*#(ygq9kI+J8x^wnv-?H_@_Wqj)5mg;THX48Rfu|9 diff --git a/src/lightning_app/README.md b/src/lightning_app/README.md index 5871813a111ba..996dce559b03a 100644 --- a/src/lightning_app/README.md +++ b/src/lightning_app/README.md @@ -1,6 +1,6 @@

- + **With Lightning Apps, you build exactly what you need: from production-ready, multi-cloud ML systems to simple research demos.** From 7013b92b2050de04bc6a7408c6775a718c641541 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:59:32 +0000 Subject: [PATCH 015/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../code_samples/quickstart/app/app_0.py | 3 +- .../code_samples/quickstart/app/app_1.py | 2 +- .../lightning_app/communication_content.rst | 10 ++++-- .../lightning_work/payload_content.rst | 10 +++--- .../lightning_work/status_content.rst | 10 +++--- docs/source-app/examples/file_server/app.py | 32 ++++++++--------- .../examples/github_repo_runner/app.py | 36 +++++++++---------- docs/source-app/glossary/app_tree.rst | 17 +++++---- .../build_config/build_config_advanced.rst | 1 + .../build_config/build_config_basic.rst | 5 ++- .../build_config_intermediate.rst | 7 ++-- .../glossary/environment_variables.rst | 5 +-- .../glossary/storage/drive_content.rst | 14 ++++---- docs/source-app/glossary/storage/path.rst | 8 +++-- docs/source-app/levels/basic/level_4.rst | 9 +++-- .../workflows/add_server/any_server.rst | 15 +++++--- .../workflows/add_server/flask_basic.rst | 22 +++++++----- .../react/connect_react_and_lightning.rst | 4 +++ .../add_web_ui/streamlit/intermediate.rst | 14 ++++++-- .../arrange_tabs/arrange_app_basic.rst | 10 +++--- .../from_scratch_content.rst | 9 +++-- .../from_scratch_component_content.rst | 16 ++++++--- .../intermediate.rst | 8 +++-- .../run_work_in_parallel_content.rst | 6 ++-- .../workflows/run_work_once_content.rst | 18 +++++----- .../share_files_between_components.rst | 5 +-- 26 files changed, 173 insertions(+), 123 deletions(-) diff --git a/docs/source-app/code_samples/quickstart/app/app_0.py b/docs/source-app/code_samples/quickstart/app/app_0.py index 82b687b0f258b..3952cafc957e0 100644 --- a/docs/source-app/code_samples/quickstart/app/app_0.py +++ b/docs/source-app/code_samples/quickstart/app/app_0.py @@ -1,6 +1,7 @@ -import lightning as L from docs.quickstart.app_02 import HourLongWork +import lightning as L + class RootFlow(L.LightningFlow): def __init__(self, child_work_1: L.LightningWork, child_work_2: L.LightningWork): diff --git a/docs/source-app/code_samples/quickstart/app/app_1.py b/docs/source-app/code_samples/quickstart/app/app_1.py index dc7a789728463..ac41c5ef83fa1 100644 --- a/docs/source-app/code_samples/quickstart/app/app_1.py +++ b/docs/source-app/code_samples/quickstart/app/app_1.py @@ -1,9 +1,9 @@ import flash from flash.core.data.utils import download_data from flash.image import ImageClassificationData, ImageClassifier -from pytorch_lightning.callbacks import ModelCheckpoint import lightning as L +from pytorch_lightning.callbacks import ModelCheckpoint # Step 1: Create a training LightningWork component that gets a backbone as input diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index 36ed886e794ce..1191d61b9cbdd 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -35,6 +35,7 @@ As the Work is running its own process, its state changes are sent to the Flow w import lightning as L + class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -44,8 +45,8 @@ As the Work is running its own process, its state changes are sent to the Flow w for _ in range(int(10e6)): self.counter += 1 - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.w = WorkCounter() @@ -54,6 +55,7 @@ As the Work is running its own process, its state changes are sent to the Flow w self.w.run() print(self.w.counter) + app = L.LightningApp(Flow()) @@ -61,7 +63,7 @@ A delta sent from the Work to the Flow looks like this: .. code-block:: python - {'values_changed': {"root['works']['w']['vars']['counter']": {'new_value': 425}}} + {"values_changed": {"root['works']['w']['vars']['counter']": {"new_value": 425}}} Here is the associated illustration: @@ -122,6 +124,7 @@ Here's an example of what would happen if you try to have the Flow communicate w import lightning as L from time import sleep + class WorkCounter(L.LightningWork): def __init__(self): super().__init__(parallel=True) @@ -132,8 +135,8 @@ Here's an example of what would happen if you try to have the Flow communicate w sleep(1) print(f"Work {self.counter}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.w = WorkCounter() @@ -144,6 +147,7 @@ Here's an example of what would happen if you try to have the Flow communicate w print(f"Flow {self.w.counter}") self.w.counter += 1 + app = L.LightningApp(Flow()) As you can see, there is a divergence between the values within the Work and the Flow. diff --git a/docs/source-app/core_api/lightning_work/payload_content.rst b/docs/source-app/core_api/lightning_work/payload_content.rst index 15adcd856f3ee..780f3985e30ea 100644 --- a/docs/source-app/core_api/lightning_work/payload_content.rst +++ b/docs/source-app/core_api/lightning_work/payload_content.rst @@ -20,6 +20,7 @@ Here is an example: import lightning as L import pandas as pd + class SourceWork(L.LightningWork): def __init__(self): super().__init__() @@ -28,7 +29,7 @@ Here is an example: def run(self): # do some processing - df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]}) + df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}) # The object you care about needs to be wrapped into a Payload object. self.df = L.storage.Payload(df) @@ -46,17 +47,17 @@ Once the Payload object is attached to your Work's state, it can be passed to an import lightning as L import pandas as pd - class DestinationWork(L.LightningWork): - def run(self, df:L.storage.Payload): + class DestinationWork(L.LightningWork): + def run(self, df: L.storage.Payload): # You can access the original object from the payload using its value property. print("dst", df.value) # dst col1 col2 # 0 1 3 # 1 2 4 - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.src = SourceWork() @@ -70,4 +71,5 @@ Once the Payload object is attached to your Work's state, it can be passed to an # so you receive a copy of the original object. self.dst.run(df=self.src.df) + app = L.LightningApp(Flow()) diff --git a/docs/source-app/core_api/lightning_work/status_content.rst b/docs/source-app/core_api/lightning_work/status_content.rst index f593421346b2f..7ee8b65ac852b 100644 --- a/docs/source-app/core_api/lightning_work/status_content.rst +++ b/docs/source-app/core_api/lightning_work/status_content.rst @@ -38,16 +38,16 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc from time import sleep import lightning as L - class Work(L.LightningWork): + class Work(L.LightningWork): def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -77,6 +77,7 @@ the Work transitions from ``is_pending`` to ``is_running`` and then to ``has_suc self.work.run(self.counter) self.counter += 1 + app = L.LightningApp(Flow()) Run this app as follows: @@ -135,16 +136,16 @@ In order to access all statuses: from time import sleep import lightning as L - class Work(L.LightningWork): + class Work(L.LightningWork): def run(self, value: int): sleep(1) if value == 0: return raise Exception(f"The provided value was {value}") - class Flow(L.LightningFlow): + class Flow(L.LightningFlow): def __init__(self): super().__init__() self.work = Work(raise_exception=False) @@ -155,6 +156,7 @@ In order to access all statuses: self.work.run(self.counter) self.counter += 1 + app = L.LightningApp(Flow()) diff --git a/docs/source-app/examples/file_server/app.py b/docs/source-app/examples/file_server/app.py index 20308814ed7e9..5de9f3720a351 100644 --- a/docs/source-app/examples/file_server/app.py +++ b/docs/source-app/examples/file_server/app.py @@ -4,20 +4,15 @@ import uuid import zipfile from dataclasses import dataclass +from pathlib import Path from typing import List + import lightning as L from lightning.app.storage import Drive -from pathlib import Path class FileServer(L.LightningWork): - def __init__( - self, - drive: Drive, - base_dir: str = "file_server", - chunk_size=10240, - **kwargs - ): + def __init__(self, drive: Drive, base_dir: str = "file_server", chunk_size=10240, **kwargs): """This component uploads, downloads files to your application. Arguments: @@ -55,8 +50,7 @@ def upload_file(self, file): filename = file.filename uploaded_file = self.get_random_filename() meta_file = uploaded_file + ".meta" - self.uploaded_files[filename] = { - "progress": (0, None), "done": False} + self.uploaded_files[filename] = {"progress": (0, None), "done": False} # 2: Create a stream and write bytes of # the file to the disk under `uploaded_file` path. @@ -157,24 +151,22 @@ def alive(self): return self.url != "" -from lightning import LightningWork import requests -class TestFileServer(LightningWork): +from lightning import LightningWork + +class TestFileServer(LightningWork): def __init__(self, drive: Drive): super().__init__(cache_calls=True) self.drive = drive - def run(self, file_server_url: str, first = True): + def run(self, file_server_url: str, first=True): if first: with open("test.txt", "w") as f: f.write("Some text.") - response = requests.post( - file_server_url + "/upload_file/", - files={'file': open("test.txt", 'rb')} - ) + response = requests.post(file_server_url + "/upload_file/", files={"file": open("test.txt", "rb")}) assert response.status_code == 200 else: response = requests.get(file_server_url) @@ -184,8 +176,8 @@ def run(self, file_server_url: str, first = True): from lightning import LightningApp, LightningFlow -class Flow(LightningFlow): +class Flow(LightningFlow): def __init__(self): super().__init__() # 1: Create a drive to share data between works @@ -214,14 +206,18 @@ def configure_layout(self): # in the UI using its `/` endpoint. return {"name": "File Server", "content": self.file_server} + from lightning.app.runners import MultiProcessRuntime + def test_file_server(): app = LightningApp(Flow()) MultiProcessRuntime(app).dispatch() + from lightning.app.testing.testing import run_app_in_cloud + def test_file_server_in_cloud(): # You need to provide the directory containing the app file. app_dir = "docs/source-app/examples/file_server" diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index 57aee800de59b..0efc0e02b3839 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -56,8 +56,7 @@ def run(self, *args, **kwargs): # 2: Use git command line to clone the repo. repo_name = self.github_repo.split("/")[-1].replace(".git", "") cwd = os.path.dirname(__file__) - subprocess.Popen( - f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() + subprocess.Popen(f"git clone {self.github_repo}", cwd=cwd, shell=True).wait() # 3: Execute the parent run method of the TracerPythonScript class. os.chdir(os.path.join(cwd, repo_name)) @@ -73,7 +72,6 @@ def configure_layout(self): class PyTorchLightningGithubRepoRunner(GithubRepoRunner): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.best_model_path = None @@ -105,12 +103,12 @@ def trainer_pre_fn(self, *args, work=None, **kwargs): # 5. Patch the `__init__` method of the Trainer # to inject our callback with a reference to the work. - tracer.add_traced( - Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) + tracer.add_traced(Trainer, "__init__", pre_fn=partial(trainer_pre_fn, work=self)) return tracer def on_after_run(self, end_script_globals): import torch + # 1. Once the script has finished to execute, # we can collect its globals and access any objects. trainer = end_script_globals["cli"].trainer @@ -138,9 +136,11 @@ def on_after_run(self, end_script_globals): class KerasGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" + class TensorflowGithubRepoRunner(GithubRepoRunner): """Left to the users to implement.""" + GITHUB_REPO_RUNNERS = { "PyTorch Lightning": PyTorchLightningGithubRepoRunner, "Keras": KerasGithubRepoRunner, @@ -186,6 +186,7 @@ def configure_layout(self): # Create a StreamLit UI for the user to run his Github Repo. return StreamlitFrontend(render_fn=render_fn) + def page_1__create_new_run(state): import streamlit as st @@ -203,9 +204,7 @@ def page_1__create_new_run(state): script_path = st.text_input("Enter your script to run", value="train_script.py") script_args = st.text_input("Enter your base script arguments", value=default_script_args) requirements = st.text_input("Enter your requirements", value=default_requirements) - ml_framework = st.radio( - "Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"] - ) + ml_framework = st.radio("Select your ML Training Frameworks", options=["PyTorch Lightning", "Keras", "Tensorflow"]) if ml_framework not in ("PyTorch Lightning"): st.write(f"{ml_framework} isn't supported yet.") @@ -230,6 +229,7 @@ def page_1__create_new_run(state): # and run the associated work from the request information. state.requests = state.requests + [new_request] + def page_2__view_run_lists(state): import streamlit as st @@ -250,9 +250,8 @@ def page_2__view_run_lists(state): best_model_score = r.get("best_model_score", None) if best_model_score: if st.checkbox(f"Expand to view your run performance", key=i): - st.json( - {"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")} - ) + st.json({"best_model_score": best_model_score, "best_model_path": r.get("best_model_path")}) + def page_3__view_app_state(state): import streamlit as st @@ -260,17 +259,16 @@ def page_3__view_app_state(state): st.markdown("# App State 🎈") st.write(state._state) + def render_fn(state: AppState): import streamlit as st - page_names_to_funcs = { "Create a new Run": partial(page_1__create_new_run, state=state), "View your Runs": partial(page_2__view_run_lists, state=state), "View the App state": partial(page_3__view_app_state, state=state), } - selected_page = st.sidebar.selectbox( - "Select a page", page_names_to_funcs.keys()) + selected_page = st.sidebar.selectbox("Select a page", page_names_to_funcs.keys()) page_names_to_funcs[selected_page]() @@ -286,10 +284,12 @@ def run(self): def configure_layout(self): # 1: Add the main StreamLit UI - selection_tab = [{ - "name": "Run your Github Repo", - "content": self.flow, - }] + selection_tab = [ + { + "name": "Run your Github Repo", + "content": self.flow, + } + ] # 2: Add a new tab whenever a new work is dynamically created run_tabs = [e.configure_layout() for e in self.flow.ws.values()] # 3: Returns the list of tabs. diff --git a/docs/source-app/glossary/app_tree.rst b/docs/source-app/glossary/app_tree.rst index 120d5d2e4ef69..860560bdcedf2 100644 --- a/docs/source-app/glossary/app_tree.rst +++ b/docs/source-app/glossary/app_tree.rst @@ -43,14 +43,14 @@ You can attach your components in the **__init__** method of a flow. import lightning as L - class RootFlow(L.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() # The `Work` component is attached here. self.work = Work() - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. self.nested_flow = NestedFlow() Once done, simply add the root flow to a Lightning app as follows: @@ -72,20 +72,19 @@ You can simply attach your components in the **run** method of a flow using the .. code-block:: python class RootFlow(L.LightningFlow): - def run(self): if not hasattr(self, "work"): - # The `Work` component is attached here. + # The `Work` component is attached here. setattr(self, "work", Work()) # Run the `Work` component. - getattr(self, "work").run() + getattr(self, "work").run() if not hasattr(self, "nested_flow"): - # The `NestedFlow` component is attached here. + # The `NestedFlow` component is attached here. setattr(self, "nested_flow", NestedFlow()) # Run the `NestedFlow` component. - getattr(self, "wonested_flowrk").run() + getattr(self, "wonested_flowrk").run() But it is usually more readable to use Lightning built-in :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` as follows: @@ -94,8 +93,8 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app from lightning_app.structures import Dict - class RootFlow(L.LightningFlow): + class RootFlow(L.LightningFlow): def __init__(self): super().__init__() self.dict = Dict() @@ -108,5 +107,5 @@ But it is usually more readable to use Lightning built-in :class:`~lightning_app if "nested_flow" not in self.dict: # The `NestedFlow` component is attached here. - self.dict["nested_flow"] =NestedFlow() + self.dict["nested_flow"] = NestedFlow() self.dict["nested_flow"].run() diff --git a/docs/source-app/glossary/build_config/build_config_advanced.rst b/docs/source-app/glossary/build_config/build_config_advanced.rst index f954bd3435c0f..c96ac93d079bc 100644 --- a/docs/source-app/glossary/build_config/build_config_advanced.rst +++ b/docs/source-app/glossary/build_config/build_config_advanced.rst @@ -23,6 +23,7 @@ Create a :class:`~lightning_app.utilities.packaging.build_config.BuildConfig` an from lightning_app import LightningWork, BuildConfig + class MyWork(LightningWork): def __init__(self): super().__init__() diff --git a/docs/source-app/glossary/build_config/build_config_basic.rst b/docs/source-app/glossary/build_config/build_config_basic.rst index 31b274e086db9..c3e3ae8c6ffe2 100644 --- a/docs/source-app/glossary/build_config/build_config_basic.rst +++ b/docs/source-app/glossary/build_config/build_config_basic.rst @@ -50,12 +50,11 @@ Instead of listing the requirements in a file, you can also pass them to the Lig from lightning_app import LightningWork, BuildConfig + class MyWork(LightningWork): def __init__(self): super().__init__() - self.cloud_build_config = BuildConfig( - requirements=["torch>=1.8", "torchmetrics"] - ) + self.cloud_build_config = BuildConfig(requirements=["torch>=1.8", "torchmetrics"]) .. note:: The build config only applies when running in the cloud and gets ignored otherwise. A local build config is currently not supported. diff --git a/docs/source-app/glossary/build_config/build_config_intermediate.rst b/docs/source-app/glossary/build_config/build_config_intermediate.rst index 0a5839b1f3ea8..174f472facb8e 100644 --- a/docs/source-app/glossary/build_config/build_config_intermediate.rst +++ b/docs/source-app/glossary/build_config/build_config_intermediate.rst @@ -18,9 +18,9 @@ If you need to install additional system packages or run other configuration ste from lightning_app import BuildConfig + @dataclass class CustomBuildConfig(BuildConfig): - def build_commands(self): return ["sudo apt-get install libsparsehash-dev"] @@ -31,6 +31,7 @@ If you need to install additional system packages or run other configuration ste from lightning_app import LightningWork + class MyWork(LightningWork): def __init__(self): super().__init__() @@ -39,9 +40,7 @@ If you need to install additional system packages or run other configuration ste self.cloud_build_config = CustomBuildConfig() # Can also be combined with extra requirements - self.cloud_build_config = CustomBuildConfig( - requirements=["torchmetrics"] - ) + self.cloud_build_config = CustomBuildConfig(requirements=["torchmetrics"]) .. note:: diff --git a/docs/source-app/glossary/environment_variables.rst b/docs/source-app/glossary/environment_variables.rst index 20ab1b09b6a0a..fd41594656b0f 100644 --- a/docs/source-app/glossary/environment_variables.rst +++ b/docs/source-app/glossary/environment_variables.rst @@ -19,8 +19,9 @@ The environment variables are available in all flows and works, and can be acces .. code:: python import os - print(os.environ["FOO"]) # BAR - print(os.environ["BAZ"]) # FAZ + + print(os.environ["FOO"]) # BAR + print(os.environ["BAZ"]) # FAZ .. note:: Environment variables are currently not encrypted. diff --git a/docs/source-app/glossary/storage/drive_content.rst b/docs/source-app/glossary/storage/drive_content.rst index 263fab6093c1c..c2c1357d0f6df 100644 --- a/docs/source-app/glossary/storage/drive_content.rst +++ b/docs/source-app/glossary/storage/drive_content.rst @@ -50,6 +50,7 @@ Any components can create a drive object. from lightning_app import LightningFlow, LightningWork from lightning_app.storage import Drive + class Flow(LightningFlow): def __init__(self): super().__init__() @@ -58,6 +59,7 @@ Any components can create a drive object. def run(self): ... + class Work(LightningWork): def __init__(self): super().__init__() @@ -80,7 +82,7 @@ A Drive supports put, list, get, and delete actions. drive = Drive("lit://drive") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty # Created file. with open("a.txt", "w") as f: @@ -88,13 +90,13 @@ A Drive supports put, list, get, and delete actions. drive.put("a.txt") - drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. + drive.list(".") # Returns ["a.txt"] as the file copied in the Drive during the put action. - drive.get("a.txt") # Get the file into the current worker + drive.get("a.txt") # Get the file into the current worker drive.delete("a.txt") - drive.list(".") # Returns [] as empty + drive.list(".") # Returns [] as empty ---- @@ -114,7 +116,6 @@ Here is an illustrated code example on how to create drives within works. class Work_A(LightningWork): - def __init__(self): super().__init__() # The identifier of the Drive is ``drive_1`` @@ -131,7 +132,6 @@ Here is an illustrated code example on how to create drives within works. class Work_B(LightningWork): - def __init__(self): super().__init__() @@ -151,8 +151,8 @@ Here is an illustrated code example on how to create drives within works. self.drive_1.put("b.txt") self.drive_2.put("b.txt") - class Work_C(LightningWork): + class Work_C(LightningWork): def __init__(self): super().__init__() self.drive_2 = Drive("lit://drive_2") diff --git a/docs/source-app/glossary/storage/path.rst b/docs/source-app/glossary/storage/path.rst index 1437bba577d80..6da6459cf1195 100644 --- a/docs/source-app/glossary/storage/path.rst +++ b/docs/source-app/glossary/storage/path.rst @@ -99,6 +99,7 @@ For example, share a directory by passing it as an input to the run method of th from lightning_app import LightningFlow + class Flow(LightningFlow): def __init__(self): super().__init__() @@ -172,8 +173,8 @@ You can check if a path exists locally or remotely in the source Work using the # OR if checkpoint_dir.exists_local(): - # Do something with the file if it exists locally - files = os.listdir(checkpoint_dir) + # Do something with the file if it exists locally + files = os.listdir(checkpoint_dir) ---- @@ -190,6 +191,7 @@ Lightning makes sure all Paths that are part of the state get stored and made ac from lightning_app.storage import Path + class Work(LightningWork): def __init__(self): super().__init__() @@ -218,6 +220,7 @@ First, define a component that saves a checkpoint: import torch import os + class ModelTraining(LightningWork): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -264,6 +267,7 @@ Link both components via a parent component: self.train.run() self.deploy.run(checkpoint_dir=self.train.checkpoint_dir) + app = L.LightningApp(Flow()) diff --git a/docs/source-app/levels/basic/level_4.rst b/docs/source-app/levels/basic/level_4.rst index 17fc8e90feb87..c5ce24ff42e2f 100644 --- a/docs/source-app/levels/basic/level_4.rst +++ b/docs/source-app/levels/basic/level_4.rst @@ -109,15 +109,14 @@ To use the component, simply import it and attach it to your Lightning App. import lightning as L from lit_slack import SlackMessenger + class YourComponent(L.LightningFlow): def __init__(self): super().__init__() - self.slack_messenger = SlackMessenger( - token='a-long-token', - channel_id='A03CB4A6AK7' - ) + self.slack_messenger = SlackMessenger(token="a-long-token", channel_id="A03CB4A6AK7") def run(self): - self.slack_messenger.send_message('hello from ⚡ lit slack ⚡') + self.slack_messenger.send_message("hello from ⚡ lit slack ⚡") + app = L.LightningApp(YourComponent()) diff --git a/docs/source-app/workflows/add_server/any_server.rst b/docs/source-app/workflows/add_server/any_server.rst index 677478d70d740..398951276c0a5 100644 --- a/docs/source-app/workflows/add_server/any_server.rst +++ b/docs/source-app/workflows/add_server/any_server.rst @@ -26,6 +26,7 @@ Any server that listens on a port, can be enabled via a work. For example, here' import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -33,7 +34,8 @@ Any server that listens on a port, can be enabled via a work. For example, here' html = "

Hello lit world

" self.wfile.write(html) - httpd = socketserver.TCPServer(('localhost', '3000'), PlainServer) + + httpd = socketserver.TCPServer(("localhost", "3000"), PlainServer) httpd.serve_forever() To enable the server inside the component, start the server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -45,6 +47,7 @@ To enable the server inside the component, start the server in the run method an import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -52,6 +55,7 @@ To enable the server inside the component, start the server in the run method an html = "

Hello lit world " self.wfile.write(html) + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) @@ -72,6 +76,7 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl import socketserver from http import HTTPStatus, server + class PlainServer(server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(HTTPStatus.OK) @@ -79,11 +84,13 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl html = "

Hello lit world " self.wfile.write(html) + class LitServer(L.LightningWork): def run(self): httpd = socketserver.TCPServer((self.host, self.port), PlainServer) httpd.serve_forever() + class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -93,12 +100,10 @@ In this case, we render the ``LitServer`` output in the ``home`` tab of the appl self.lit_server.run() def configure_layout(self): - tab1 = { - 'name': 'home', - 'content': self.lit_server - } + tab1 = {"name": "home", "content": self.lit_server} return tab1 + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in parallel diff --git a/docs/source-app/workflows/add_server/flask_basic.rst b/docs/source-app/workflows/add_server/flask_basic.rst index 5ee2a232828c4..38ca282346248 100644 --- a/docs/source-app/workflows/add_server/flask_basic.rst +++ b/docs/source-app/workflows/add_server/flask_basic.rst @@ -26,11 +26,13 @@ First, define your flask app as you normally would without Lightning: flask_app = Flask(__name__) - @flask_app.route('/') + + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" + - flask_app.run(host='0.0.0.0', port=80) + flask_app.run(host="0.0.0.0", port=80) To enable the server inside the component, start the Flask server in the run method and use the ``self.host`` and ``self.port`` properties: @@ -40,13 +42,14 @@ To enable the server inside the component, start the Flask server in the run met import lightning as L from flask import Flask + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route('/') + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" flask_app.run(host=self.host, port=self.port) @@ -64,16 +67,18 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli import lightning as L from flask import Flask + class LitFlask(L.LightningWork): def run(self): flask_app = Flask(__name__) - @flask_app.route('/') + @flask_app.route("/") def hello(): - return 'Hello, World!' + return "Hello, World!" flask_app.run(host=self.host, port=self.port) + class Root(L.LightningFlow): def __init__(self): super().__init__() @@ -83,9 +88,10 @@ In this case, we render the ``LitFlask`` output in the ``home`` tab of the appli self.lit_flask.run() def configure_layout(self): - tab1 = {'name': 'home', 'content': self.lit_flask} + tab1 = {"name": "home", "content": self.lit_flask} return tab1 + app = L.LightningApp(Root()) We use the ``parallel=True`` argument of ``LightningWork`` to run the server in the background diff --git a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst index fe72d09b27eec..c1c0c5e2017c8 100644 --- a/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst +++ b/docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst @@ -79,14 +79,17 @@ You can use this single react app for the FULL Lightning app, or you can specify import lightning as L + class ComponentA(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist") + class ComponentB(L.LightningFlow): def configure_layout(self): return L.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist") + class HelloLitReact(L.LightningFlow): def __init__(self): super().__init__() @@ -98,6 +101,7 @@ You can use this single react app for the FULL Lightning app, or you can specify tab_2 = {"name": "App 2", "content": self.react_app_2} return tab_1, tab_2 + app = L.LightningApp(HelloLitReact()) This is a powerful idea that allows each Lightning component to have a self-contained web UI. diff --git a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst index 08ab0e874bcca..ac289c2eb27e1 100644 --- a/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst +++ b/docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst @@ -22,10 +22,12 @@ For example, here we increase the count variable of the Lightning Component ever import lightning as L import streamlit as st + def your_streamlit_app(lightning_app_state): - if st.button('press to increase count'): + if st.button("press to increase count"): lightning_app_state.count += 1 - st.write(f'current count: {lightning_app_state.count}') + st.write(f"current count: {lightning_app_state.count}") + class LitStreamlit(L.LightningFlow): def __init__(self): @@ -35,6 +37,7 @@ For example, here we increase the count variable of the Lightning Component ever def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -44,6 +47,7 @@ For example, here we increase the count variable of the Lightning Component ever tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 + app = L.LightningApp(LitApp()) ---- @@ -63,8 +67,10 @@ In this example we update the value of the counter from the component: import lightning as L import streamlit as st + def your_streamlit_app(lightning_app_state): - st.write(f'current count: {lightning_app_state.count}') + st.write(f"current count: {lightning_app_state.count}") + class LitStreamlit(L.LightningFlow): def __init__(self): @@ -77,6 +83,7 @@ In this example we update the value of the counter from the component: def configure_layout(self): return L.frontend.StreamlitFrontend(render_fn=your_streamlit_app) + class LitApp(L.LightningFlow): def __init__(self): super().__init__() @@ -89,4 +96,5 @@ In this example we update the value of the counter from the component: tab1 = {"name": "home", "content": self.lit_streamlit} return tab1 + app = L.LightningApp(LitApp()) diff --git a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst index 988018cd795e8..91c0e53854760 100644 --- a/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst +++ b/docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst @@ -16,14 +16,13 @@ To enable a single tab on the app UI, return a single dictionary from the ``conf import lightning as L + class DemoComponent(L.demo.dumb_component): def configure_layout(self): - tab1 = { - "name": "THE TAB NAME", - "content": self.component_a - } + tab1 = {"name": "THE TAB NAME", "content": self.component_a} return tab1 + app = L.LightningApp(DemoComponent()) @@ -43,12 +42,14 @@ Enable multiple tabs import lightning as L + class DemoComponent(L.demo.dumb_component): def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 + app = L.LightningApp(DemoComponent()) The order matters! Try any of the following configurations: @@ -61,6 +62,7 @@ The order matters! Try any of the following configurations: tab2 = {"name": "Tab B", "content": self.component_b} return tab1, tab2 + def configure_layout(self): tab1 = {"name": "Tab A", "content": self.component_a} tab2 = {"name": "Tab B", "content": self.component_b} diff --git a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst index c43b1bbc2f749..91e7fea93e28c 100644 --- a/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst +++ b/docs/source-app/workflows/build_lightning_app/from_scratch_content.rst @@ -19,10 +19,12 @@ If you didn't find a Lightning App similar to the one you need, simply create a import lightning as L + class WordComponent(L.LightningWork): def __init__(self, word): super().__init__() self.word = word + def run(self): print(self.word) @@ -30,14 +32,15 @@ If you didn't find a Lightning App similar to the one you need, simply create a class LitApp(L.LightningFlow): def __init__(self) -> None: super().__init__() - self.hello = WordComponent('hello') - self.world = WordComponent('world') + self.hello = WordComponent("hello") + self.world = WordComponent("world") def run(self): - print('This is a simple Lightning app, make a better app!') + print("This is a simple Lightning app, make a better app!") self.hello.run() self.world.run() + app = L.LightningApp(LitApp()) ---- diff --git a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst index 0f7b0ff436eb0..7faef8ee03df8 100644 --- a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst +++ b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst @@ -17,7 +17,7 @@ Use a **LightningFlow** component for any programming logic that runs in less th .. code:: python for i in range(10): - print(f'{i}: this kind of code belongs in a LightningFlow') + print(f"{i}: this kind of code belongs in a LightningFlow") Use a **LightningWork** component for any programming logic that takes more than 1 second or requires its own hardware. @@ -27,7 +27,7 @@ Use a **LightningWork** component for any programming logic that takes more than for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") ---- @@ -77,10 +77,12 @@ To implement a LightningFlow, simply subclass ``LightningFlow`` and define the r # app.py import lightning as L + class LitFlow(L.LightningFlow): def run(self): for i in range(10): - print(f'{i}: this kind of code belongs in a LightningFlow') + print(f"{i}: this kind of code belongs in a LightningFlow") + app = L.LightningApp(LitFlow()) @@ -109,11 +111,12 @@ To implement a LightningWork, simply subclass ``LightningWork`` and define the r from time import sleep import lightning as L + class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") A LightningWork must always be attached to a LightningFlow and explicitely asked to ``run()``: @@ -123,11 +126,13 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked from time import sleep import lightning as L + class LitWork(L.LightningWork): def run(self): for i in range(100000): sleep(2.0) - print(f'{i} LightningWork: work that is long running or may never end (like a server)') + print(f"{i} LightningWork: work that is long running or may never end (like a server)") + class LitFlow(L.LightningFlow): def __init__(self): @@ -137,6 +142,7 @@ A LightningWork must always be attached to a LightningFlow and explicitely asked def run(self): self.lit_work.run() + app = L.LightningApp(LitFlow()) run the app diff --git a/docs/source-app/workflows/build_lightning_component/intermediate.rst b/docs/source-app/workflows/build_lightning_component/intermediate.rst index 27336424bf916..070d3aa0caf1a 100644 --- a/docs/source-app/workflows/build_lightning_component/intermediate.rst +++ b/docs/source-app/workflows/build_lightning_component/intermediate.rst @@ -32,9 +32,10 @@ To *connect* this user interface to the component, define the configure_layout m import lightning as L from lightning_app.frontend.web import StaticWebFrontend + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') + return StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") Finally, route the component's UI through the root component's **configure_layout** method: @@ -44,9 +45,11 @@ Finally, route the component's UI through the root component's **configure_layou # app.py import lightning as L + class LitHTMLComponent(L.LightningFlow): def configure_layout(self): - return L.frontend.web.StaticWebFrontend(serve_dir='path/to/folder/with/index.html/inside') + return L.frontend.web.StaticWebFrontend(serve_dir="path/to/folder/with/index.html/inside") + class LitApp(L.LightningFlow): def __init__(self): @@ -57,6 +60,7 @@ Finally, route the component's UI through the root component's **configure_layou tab1 = {"name": "home", "content": self.lit_html_component} return tab1 + app = L.LightningApp(LitApp()) Run your app and you'll see the UI on the Lightning App view: diff --git a/docs/source-app/workflows/run_work_in_parallel_content.rst b/docs/source-app/workflows/run_work_in_parallel_content.rst index 334f50e7e486a..ecb87c5cea7ff 100644 --- a/docs/source-app/workflows/run_work_in_parallel_content.rst +++ b/docs/source-app/workflows/run_work_in_parallel_content.rst @@ -13,13 +13,14 @@ The default behavior of the ``LightningWork`` is to wait for the ``run`` method import lightning as L + class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent() def run(self): self.work_component_a.run() - print('this will never print') + print("this will never print") Since this LightningWork component we created loops forever, the print statement will never execute. In practice ``LightningWork`` workloads are finite and don't run forever. @@ -39,13 +40,14 @@ To run LightningWorks in parallel, while the rest of the app executes without de import lightning as L + class Root(L.LightningFlow): def __init__(self): self.work_component_a = L.demo.InfinteWorkComponent(parallel=True) def run(self): self.work_component_a.run() - print('repeats while the infinite work runs ONCE (and forever) in parallel') + print("repeats while the infinite work runs ONCE (and forever) in parallel") Any LightningWorks that will take more than **1 second** should be run in parallel unless the rest of your Lightning App depends on the output of this work (for example, downloading a dataset). diff --git a/docs/source-app/workflows/run_work_once_content.rst b/docs/source-app/workflows/run_work_once_content.rst index 4388742358929..dbef37813572a 100644 --- a/docs/source-app/workflows/run_work_once_content.rst +++ b/docs/source-app/workflows/run_work_once_content.rst @@ -24,10 +24,10 @@ As explained in the `Event Loop guide <../glossary/event_loop.html>`_, the Light from datetime import datetime # Lightning code - while True: # This is the Lightning Event Loop + while True: # This is the Lightning Event Loop # Your code - today = datetime.now().strftime("%D") # '05/25/22' + today = datetime.now().strftime("%D") # '05/25/22' data_processor.run(today) train_model.run(data_processor.data) @@ -47,10 +47,12 @@ Here's an example of this behavior with LightningWork: import lightning as L - class ExampleWork( L.LightningWork): + + class ExampleWork(L.LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") + work = ExampleWork() work.run(value=1) @@ -86,10 +88,12 @@ By setting ``cache_calls=False``, Lightning won't cache the return value and re- from lightning_app import LightningWork + class ExampleWork(LightningWork): def run(self, *args, **kwargs): print(f"I received the following props: args: {args} kwargs: {kwargs}") + work = ExampleWork(cache_calls=False) work.run(value=1) @@ -122,21 +126,19 @@ as the work continuously execute in a blocking way. from lightning_app import LightningApp, LightningFlow, LightningWork - class Flow(LightningFlow): + class Flow(LightningFlow): def __init__(self): super().__init__() - self.work = Work( - cache_calls=False, - parallel=False - ) + self.work = Work(cache_calls=False, parallel=False) def run(self): print("HERE BEFORE") self.work.run() print("HERE AFTER") + app = LightningApp(Flow()) .. code-block:: console diff --git a/docs/source-app/workflows/share_files_between_components.rst b/docs/source-app/workflows/share_files_between_components.rst index 02c7d889a7122..15108515b3947 100644 --- a/docs/source-app/workflows/share_files_between_components.rst +++ b/docs/source-app/workflows/share_files_between_components.rst @@ -34,8 +34,8 @@ To write a file, first create a reference to the file with the :class:`~lightnin boring_file_reference = Path("boring_file.txt") # write to that file - with open(self.boring_file_reference, 'w') as f: - f.write('yolo') + with open(self.boring_file_reference, "w") as f: + f.write("yolo") ---- @@ -107,6 +107,7 @@ For example, here we save a file on one component and use it in another componen from lightning_app.storage.path import Path + class ComponentA(LightningWork): def __init__(self): super().__init__() From fd759da8a925147b4fb940a04483837ee0c8dca5 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 08:10:57 +0100 Subject: [PATCH 016/119] update latest docs --- docs/source-app/_static/images/brandmark.png | Bin 0 -> 60816 bytes .../_templates/classtemplate_no_index.rst | 16 ++ docs/source-app/_templates/layout.html | 10 + .../api/lightning_app.core.LightningApp.rst | 17 ++ .../api/lightning_app.core.LightningFlow.rst | 17 ++ .../api/lightning_app.core.LightningWork.rst | 17 ++ .../api_reference/api_references.rst | 90 ++++++++ docs/source-app/basics.rst | 2 +- .../code_samples/convert_pl_to_app/app.py | 17 ++ .../convert_pl_to_app/requirements.py | 3 + .../code_samples/convert_pl_to_app/train.py | 47 +++++ docs/source-app/core_api/core_api.rst | 40 ++++ .../core_api/lightning_app/lightning_app.rst | 1 + .../lightning_work/compute_content.rst | 4 +- .../lightning_work/lightning_work.rst | 1 + docs/source-app/examples/dag/dag.rst | 6 +- .../examples/file_server/file_server.rst | 3 + .../github_repo_runner/github_repo_runner.rst | 3 +- .../github_repo_runner_content.rst | 4 +- docs/source-app/examples/hands_on_example.rst | 50 +++++ docs/source-app/examples/hpo/hpo.rst | 3 +- .../model_server_app/model_server_app.rst | 2 + .../get_started/add_an_interactive_demo.rst | 18 ++ docs/source-app/get_started/build_model.rst | 76 +++++++ .../get_started/go_beyond_training.rst | 18 ++ .../go_beyond_training_content.rst} | 106 ++++------ .../jumpstart_from_app_gallery.rst | 123 +++++++++++ .../jumpstart_from_component_gallery.rst | 152 ++++++++++++++ .../get_started/lightning_apps_intro.rst | 14 ++ .../get_started/training_with_apps.rst | 136 ++++++++++++ .../get_started/what_app_can_do.rst | 196 ++++++++++++++++++ docs/source-app/glossary/app_tree.rst | 4 +- docs/source-app/glossary/event_loop.rst | 4 +- docs/source-app/glossary/index.rst | 80 +++++++ docs/source-app/index.rst | 190 ++++++++++++++--- docs/source-app/install_beginner.rst | 2 - docs/source-app/installation.rst | 9 +- docs/source-app/levels/intermediate/index.rst | 2 +- docs/source-app/quickstart.rst | 4 +- docs/source-app/read_me_first.rst | 61 ------ .../from_scratch_component_content.rst | 3 +- docs/source-app/workflows/extend_app.rst | 18 +- docs/source-app/workflows/index.rst | 114 ++++++++++ 43 files changed, 1494 insertions(+), 189 deletions(-) create mode 100644 docs/source-app/_static/images/brandmark.png create mode 100644 docs/source-app/_templates/classtemplate_no_index.rst create mode 100644 docs/source-app/_templates/layout.html create mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst create mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst create mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst create mode 100644 docs/source-app/api_reference/api_references.rst create mode 100644 docs/source-app/code_samples/convert_pl_to_app/app.py create mode 100644 docs/source-app/code_samples/convert_pl_to_app/requirements.py create mode 100644 docs/source-app/code_samples/convert_pl_to_app/train.py create mode 100644 docs/source-app/core_api/core_api.rst create mode 100644 docs/source-app/examples/hands_on_example.rst create mode 100644 docs/source-app/get_started/add_an_interactive_demo.rst create mode 100644 docs/source-app/get_started/build_model.rst create mode 100644 docs/source-app/get_started/go_beyond_training.rst rename docs/source-app/{lightning_apps_intro.rst => get_started/go_beyond_training_content.rst} (81%) create mode 100644 docs/source-app/get_started/jumpstart_from_app_gallery.rst create mode 100644 docs/source-app/get_started/jumpstart_from_component_gallery.rst create mode 100644 docs/source-app/get_started/lightning_apps_intro.rst create mode 100644 docs/source-app/get_started/training_with_apps.rst create mode 100644 docs/source-app/get_started/what_app_can_do.rst create mode 100644 docs/source-app/glossary/index.rst delete mode 100644 docs/source-app/read_me_first.rst create mode 100644 docs/source-app/workflows/index.rst diff --git a/docs/source-app/_static/images/brandmark.png b/docs/source-app/_static/images/brandmark.png new file mode 100644 index 0000000000000000000000000000000000000000..76648a7ed99e019dff43a641562fc9b238fc7951 GIT binary patch literal 60816 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

|YPrMQ(76*=X&6xdryZZGOjzUA253wEM$}f}5 zX?7;)jugB3dxWM=an*6E=D#-|B|*DCK$K;6-sOM$(0RyznSq(EyhR@Iudiz-c*-oQ zuJ|eVUC_@xd{?a%o_Fe<9j`j*|YHRDX;p2PGi)k)TQ{uLz$ zPQphMqgn4T<2hqg^OCl4Qadz|`9XfLY@Ubb>}HgDHBJBPI4YaH0DUq?YRsa{4y?lJ6g^hu z$KvId8jSHV#ew4yet-o1ZT}mz&#rH8KsruLcvJY4YzoRk=>0x%gKFvQ9zAiT;lSTl zQQ1-j=>5;6#?IsMy;sIYNo+);BrW-lYm7V3Kp%JoS zeIXhD-ux~2z9zdOqbNjFtnTW!j*1ZSP(3=>u4I6*NTBO}pKvVG^_KscvVy)s$AdOr zwr)OZ)RrSk6m4+0Hn58;F94mGeN)~)-WE->p=2-&{|yiQ#3b$wX{8%xf4i}N8>gk1 z@(y}|;<(3u^Bt!t{q^E?JwN@@xi{#jeAM-*;7ks7#o zke{J~*+H*T954OPM7+EcMm}xSc5TQhurv2>FT(EQG{mF^2o%dX|0DmJRgE*@uOh)> z#FQ?!Wj`^eS$V&dyN%0?Bj_JeS`G;OHNDEtfL}Bwz`JSCg)%c-U^cQ8y*oq5*lS7# zXX^vo2|g3dt-j2re{Y5^2a1 z;lIU~cD-}iD_}8dH-Dt=_fj$a%(v)&J^c;xvPKl1w4Nw){aWWCurW=lJ>Q>^FJN#3 zgt2bb`BVR1M%!Gr&iWDSDFY_#%-^-7p@&}b!rx@HGfD>j84e=QHwp9$bSWbl~c8#sJ^H7vnez(5g=}ifS<@FmD~M9VZrcY1lI?2g>yhm3S8h zwobqJ_bU3rW$U=$e_GC5Xx3@4$o#t`w>!`2s2ThdEQ&)*no|y$SrEZ=TkG>E++#U@ zb$w_vz@^DeD^q2Fza*wy>K%aoU!~%UG~rLyHEN4-SLAYT z{yNKz=^O z3?>BeOD8aq*J0e1Z&L=`Shwf#e5_3V=e{qFMfEjYywd2El3 zjsCTj55mqz#gn-4v7!kIz`5i!aK(Wuyl2USGfIItXZ1OM+k~I*m{D623>KDsi<$tV zp3&r+f?!%PM&;GV)X!_d{s+z>hLA-Tc&kWHU zQ#r+z*kmF-z*R8xb+m^qVjl%$V06Jxum5;y~acwOJe n0e|m6T1r%*#(ygq9kI+J8x^wnv-?H_@_Wqj)5mg;THX48Rfu|9 literal 0 HcmV?d00001 diff --git a/docs/source-app/_templates/classtemplate_no_index.rst b/docs/source-app/_templates/classtemplate_no_index.rst new file mode 100644 index 0000000000000..1455912e1d04f --- /dev/null +++ b/docs/source-app/_templates/classtemplate_no_index.rst @@ -0,0 +1,16 @@ +:orphan: + +.. role:: hidden + :class: hidden-section +.. currentmodule:: {{ module }} + + +{{ name | underline }} + +.. autoclass:: {{ name }} + :members: + :noindex: + +.. + autogenerated from source-app/_templates/classtemplate.rst + note it does not have :inherited-members: diff --git a/docs/source-app/_templates/layout.html b/docs/source-app/_templates/layout.html new file mode 100644 index 0000000000000..dfb2c269edbbb --- /dev/null +++ b/docs/source-app/_templates/layout.html @@ -0,0 +1,10 @@ +{% extends "!layout.html" %} + + +{% block footer %} +{{ super() }} + + +{% endblock %} diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst new file mode 100644 index 0000000000000..573c5527a1fc7 --- /dev/null +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -0,0 +1,17 @@ +:orphan: + +.. role:: hidden + :class: hidden-section +.. currentmodule:: lightning_app.core + + +LightningApp +============ + +.. autoclass:: LightningApp + :members: + :noindex: + +.. + autogenerated from source-app/_templates/classtemplate.rst + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst new file mode 100644 index 0000000000000..707f3a12487c8 --- /dev/null +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -0,0 +1,17 @@ +:orphan: + +.. role:: hidden + :class: hidden-section +.. currentmodule:: lightning_app.core + + +LightningFlow +============= + +.. autoclass:: LightningFlow + :members: + :noindex: + +.. + autogenerated from source-app/_templates/classtemplate.rst + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst new file mode 100644 index 0000000000000..df73cf2f2c4ea --- /dev/null +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -0,0 +1,17 @@ +:orphan: + +.. role:: hidden + :class: hidden-section +.. currentmodule:: lightning_app.core + + +LightningWork +============= + +.. autoclass:: LightningWork + :members: + :noindex: + +.. + autogenerated from source-app/_templates/classtemplate.rst + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api_references.rst b/docs/source-app/api_reference/api_references.rst new file mode 100644 index 0000000000000..c6e9e30358ed5 --- /dev/null +++ b/docs/source-app/api_reference/api_references.rst @@ -0,0 +1,90 @@ +:orphan: + +############################## +Lightning App - API References +############################## + +Core +---- + +.. currentmodule:: lightning_app.core + +.. autosummary:: + :toctree: api + :nosignatures: + :template: classtemplate_no_index.rst + + LightningApp + LightningFlow + LightningWork + +Learn more about :ref:`Lightning Core `. + +---- + +Built-in Components +___________________ + +.. currentmodule:: lightning_app.components + +.. autosummary:: + :toctree: generated/ + :nosignatures: + :template: classtemplate_no_index.rst + + ~python.popen.PopenPythonScript + ~python.tracer.TracerPythonScript + ~serve.gradio.ServeGradio + ~serve.serve.ModelInferenceAPI + +---- + +Frontend's +__________ + +.. currentmodule:: lightning_app.frontend + +.. autosummary:: + :toctree: generated/ + :nosignatures: + :template: classtemplate_no_index.rst + + ~frontend.Frontend + ~web.StaticWebFrontend + ~stream_lit.StreamlitFrontend + +Learn more about :ref:`Frontend's `. + +---- + +Storage +_______ + +.. currentmodule:: lightning_app.storage + +.. autosummary:: + :toctree: generated/ + :nosignatures: + :template: classtemplate_no_index.rst + + ~path.Path + ~drive.Drive + ~payload.Payload + +Learn more about :ref:`Storage `. + +---- + +Runners +_______ + +.. currentmodule:: lightning_app.runners + +.. autosummary:: + :toctree: generated/ + :nosignatures: + :template: classtemplate_no_index.rst + + ~cloud.CloudRuntime + ~singleprocess.SingleProcessRuntime + ~multiprocess.MultiProcessRuntime diff --git a/docs/source-app/basics.rst b/docs/source-app/basics.rst index f818c9ed7eae7..57f04fd103066 100644 --- a/docs/source-app/basics.rst +++ b/docs/source-app/basics.rst @@ -253,7 +253,7 @@ When running the above app, we see the following logs: ---- *********** -Next steps +Next Steps *********** To keep learning about Lightning, build a :ref:`ui_and_frontends`. diff --git a/docs/source-app/code_samples/convert_pl_to_app/app.py b/docs/source-app/code_samples/convert_pl_to_app/app.py new file mode 100644 index 0000000000000..fe003dbb4329f --- /dev/null +++ b/docs/source-app/code_samples/convert_pl_to_app/app.py @@ -0,0 +1,17 @@ +import lightning as L +from lightning.app.components.python import TracerPythonScript + + +class RootFlow(L.LightningFlow): + def __init__(self): + super().__init__() + self.runner = TracerPythonScript( + "train.py", + cloud_compute=L.CloudCompute("gpu"), + ) + + def run(self): + self.runner.run() + + +app = L.LightningApp(RootFlow()) diff --git a/docs/source-app/code_samples/convert_pl_to_app/requirements.py b/docs/source-app/code_samples/convert_pl_to_app/requirements.py new file mode 100644 index 0000000000000..e8fb43ef7dc83 --- /dev/null +++ b/docs/source-app/code_samples/convert_pl_to_app/requirements.py @@ -0,0 +1,3 @@ +torch +torchvision +pytorch_lightning diff --git a/docs/source-app/code_samples/convert_pl_to_app/train.py b/docs/source-app/code_samples/convert_pl_to_app/train.py new file mode 100644 index 0000000000000..bf64295c5c7e1 --- /dev/null +++ b/docs/source-app/code_samples/convert_pl_to_app/train.py @@ -0,0 +1,47 @@ +import os +import torch +from torch import nn +import torch.nn.functional as F +from torchvision.datasets import MNIST +from torch.utils.data import DataLoader, random_split +from torchvision import transforms as T +import pytorch_lightning as pl + + +class LitAutoEncoder(pl.LightningModule): + def __init__(self): + super().__init__() + self.encoder = nn.Sequential( + nn.Linear(28 * 28, 128), nn.ReLU(), nn.Linear(128, 3)) + + self.decoder = nn.Sequential( + nn.Linear(3, 128), nn.ReLU(), nn.Linear(128, 28 * 28)) + + def forward(self, x): + # in lightning, + # forward defines the prediction/inference actions + embedding = self.encoder(x) + return embedding + + def training_step(self, batch, batch_idx): + # training_step defines the train loop. + # It is independent of forward + x, y = batch + x = x.view(x.size(0), -1) + z = self.encoder(x) + x_hat = self.decoder(z) + loss = F.mse_loss(x_hat, x) + self.log("train_loss", loss) + return loss + + def configure_optimizers(self): + optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) + return optimizer + + +dataset = MNIST(os.getcwd(), download=True, transform=T.ToTensor()) +train, val = random_split(dataset, [55000, 5000]) + +autoencoder = LitAutoEncoder() +trainer = pl.Trainer(accelerator="auto") +trainer.fit(autoencoder, DataLoader(train), DataLoader(val)) diff --git a/docs/source-app/core_api/core_api.rst b/docs/source-app/core_api/core_api.rst new file mode 100644 index 0000000000000..594433acce2aa --- /dev/null +++ b/docs/source-app/core_api/core_api.rst @@ -0,0 +1,40 @@ +:orphan: + +.. _core_api: + +############################### +Learn more about Lightning Core +############################### + +.. raw:: html + +

+
+ +.. displayitem:: + :header: Level-up with Lightning Apps + :description: From Basics to Advanced Skills + :col_css: col-md-6 + :button_link: ../levels/basic/index.html + :height: 180 + +.. displayitem:: + :header: Understand Lightning App + :description: Detailed description + :col_css: col-md-6 + :button_link: lightning_app/index.html + :height: 180 + +.. displayitem:: + :header: Understand Lightning Flow + :description: Detailed description + :col_css: col-md-6 + :button_link: lightning_flow.html + :height: 180 + +.. displayitem:: + :header: Understand Lightning Work + :description: Detailed description + :col_css: col-md-6 + :button_link: lightning_work/index.html + :height: 180 diff --git a/docs/source-app/core_api/lightning_app/lightning_app.rst b/docs/source-app/core_api/lightning_app/lightning_app.rst index f4ad65268f7e4..497dde20d2825 100644 --- a/docs/source-app/core_api/lightning_app/lightning_app.rst +++ b/docs/source-app/core_api/lightning_app/lightning_app.rst @@ -9,3 +9,4 @@ LightningApp .. autoclass:: lightning_app.core.app.LightningApp :exclude-members: _run, connect, get_component_by_name, maybe_apply_changes, set_state + :noindex: diff --git a/docs/source-app/core_api/lightning_work/compute_content.rst b/docs/source-app/core_api/lightning_work/compute_content.rst index b37c54da38008..9fe9a1c59c56b 100644 --- a/docs/source-app/core_api/lightning_work/compute_content.rst +++ b/docs/source-app/core_api/lightning_work/compute_content.rst @@ -92,9 +92,9 @@ By providing **idle_timeout=X Seconds**, the work is automatically stopped **X s ---- -############# +************ CloudCompute -############# +************ .. autoclass:: lightning_app.utilities.packaging.cloud_compute.CloudCompute :noindex: diff --git a/docs/source-app/core_api/lightning_work/lightning_work.rst b/docs/source-app/core_api/lightning_work/lightning_work.rst index fdf9ea2809d68..f4f490dd1fd39 100644 --- a/docs/source-app/core_api/lightning_work/lightning_work.rst +++ b/docs/source-app/core_api/lightning_work/lightning_work.rst @@ -8,3 +8,4 @@ LightningWork .. autoclass:: lightning_app.core.work.LightningWork :exclude-members: _aggregate_status_timeout, _is_state_attribute, _is_state_attribute, set_state + :noindex: diff --git a/docs/source-app/examples/dag/dag.rst b/docs/source-app/examples/dag/dag.rst index e3cc249ab334f..8b1d71dd2431f 100644 --- a/docs/source-app/examples/dag/dag.rst +++ b/docs/source-app/examples/dag/dag.rst @@ -2,6 +2,8 @@ Build a Directed Acyclic Graph (DAG) #################################### +.. _dag_example: + **Audience:** Users coming from MLOps to Lightning Apps, looking for more flexibility. A typical ML training workflow can be implemented with a simple DAG. @@ -60,11 +62,9 @@ Below is a pseudo-code to run several works in parallel using a built-in :class: ---- ********** -Next steps +Next Steps ********** -Depending on your use case, you might want to check one of these out next. - .. raw:: html
diff --git a/docs/source-app/examples/file_server/file_server.rst b/docs/source-app/examples/file_server/file_server.rst index cd7297596ca43..430333a875533 100644 --- a/docs/source-app/examples/file_server/file_server.rst +++ b/docs/source-app/examples/file_server/file_server.rst @@ -1,3 +1,6 @@ + +.. _fileserver_example: + ################### Build a File Server ################### diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner.rst index e8775a91a3d59..affb115b74b5e 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner.rst @@ -1,8 +1,9 @@ +.. _github_repo_script_runner_example: + ################################# Build a Github Repo Script Runner ################################# - **Audience:** Users that want to create interactive applications which runs Github Repo in the cloud at any scale for multiple users. **Prerequisite**: Reach :ref:`level 16+ ` and read the docstring of of :class:`~lightning_app.components.python.tracer.TracerPythonScript` component. diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst index 9492d2af82a5b..e73f205d236d3 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst @@ -17,7 +17,9 @@ Here is a recording of the final application built in this example. The example .. raw:: html - + ---- diff --git a/docs/source-app/examples/hands_on_example.rst b/docs/source-app/examples/hands_on_example.rst new file mode 100644 index 0000000000000..57fa1e5ff114a --- /dev/null +++ b/docs/source-app/examples/hands_on_example.rst @@ -0,0 +1,50 @@ +:orphan: + +################# +Hands-on Examples +################# + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Build a DAG + :description: Learn how to orchestrate workflows + :col_css: col-md-6 + :button_link: dag/dag.html + :height: 180 + +.. displayitem:: + :header: Build a File Server + :description: Learn how to upload and download files + :col_css: col-md-6 + :button_link: file_server/file_server.html + :height: 180 + +.. displayitem:: + :header: Build a Github Repo Script Runner + :description: Learn how to configure dynamic execution from the UI + :col_css: col-md-6 + :button_link: github_repo_runner/github_repo_runner.html + :height: 180 + +.. displayitem:: + :header: Build a HPO Sweeper + :description: Learn how to scale your training + :col_css: col-md-6 + :button_link: hpo/hpo.html + :height: 180 + +.. displayitem:: + :header: Build a Model Server + :description: Learn how to server your models + :col_css: col-md-6 + :button_link: model_server_app_content.html + :height: 180 + +.. raw:: html + +
+
diff --git a/docs/source-app/examples/hpo/hpo.rst b/docs/source-app/examples/hpo/hpo.rst index c2db676c0731f..2849a62653e42 100644 --- a/docs/source-app/examples/hpo/hpo.rst +++ b/docs/source-app/examples/hpo/hpo.rst @@ -1,10 +1,11 @@ .. hpo: +.. _hpo_example: + ####################################################### Build a Lightning Hyperparameter Optimization (HPO) App ####################################################### - ******************* A bit of background ******************* diff --git a/docs/source-app/examples/model_server_app/model_server_app.rst b/docs/source-app/examples/model_server_app/model_server_app.rst index 65545e4e393df..09d361992920d 100644 --- a/docs/source-app/examples/model_server_app/model_server_app.rst +++ b/docs/source-app/examples/model_server_app/model_server_app.rst @@ -1,5 +1,7 @@ :orphan: +.. _model_server_example: + #################### Build a Model Server #################### diff --git a/docs/source-app/get_started/add_an_interactive_demo.rst b/docs/source-app/get_started/add_an_interactive_demo.rst new file mode 100644 index 0000000000000..98e6c3fd8a84a --- /dev/null +++ b/docs/source-app/get_started/add_an_interactive_demo.rst @@ -0,0 +1,18 @@ +:orphan: + +####################### +Add an Interactive Demo +####################### + +.. _add_an_interactive_Demo: + +**Required background:** Basic Python familiarity and complete the :ref:`install` guide. + +**Goal:** We'll walk you through the 4 key steps to run a Lightning App that trains and demos a model. + +.. join_slack:: + :align: left + +---- + +.. include:: go_beyond_training_content.rst diff --git a/docs/source-app/get_started/build_model.rst b/docs/source-app/get_started/build_model.rst new file mode 100644 index 0000000000000..34e7b26bc3e7f --- /dev/null +++ b/docs/source-app/get_started/build_model.rst @@ -0,0 +1,76 @@ +:orphan: + +.. _build_model: + +####################### +Build and Train a Model +####################### + +**Required background:** Basic Python familiarity and complete the :ref:`install` guide. + +**Goal:** We'll walk you through the creation of a model using PyTorch Lightning. + +.. join_slack:: + :align: left + +---- + +********************************* +A simple PyTorch Lightning script +********************************* + +Let's assume you already have a folder with those two files. + +.. code-block:: bash + + pl_project/ + train.py # your own script to train your models + requirements.txt # your python requirements. + +If you don't, simply create a ``pl_project`` folder with those two files and add the following `PyTorch Lightning `_ code in the ``train.py`` file. This code trains a simple ``AutoEncoder`` on `MNIST Dataset `_. + +.. literalinclude:: ../code_samples/convert_pl_to_app/train.py + +Add the following to the ``requirements.txt`` file. + +.. literalinclude:: ../code_samples/convert_pl_to_app/requirements.py + +Simply run the following commands in your terminal to install the requirements and train the model. + +.. code-block:: bash + + pip install -r requirements.txt + python train.py + +Get through `PyTorch Lightning Introduction `_ to learn more. + +---- + +********** +Next Steps +********** + +.. raw:: html + +
+
+
+ +.. displayitem:: + :header: Evolve a Model into an ML System + :description: Develop an App to train a model in the cloud + :col_css: col-md-6 + :button_link: training_with_apps.html + :height: 180 + +.. displayitem:: + :header: Start from a Template ML System + :description: Learn about Apps, from a template. + :col_css: col-md-6 + :button_link: go_beyond_training.html + :height: 180 + +.. raw:: html + +
+
diff --git a/docs/source-app/get_started/go_beyond_training.rst b/docs/source-app/get_started/go_beyond_training.rst new file mode 100644 index 0000000000000..eade993e59c67 --- /dev/null +++ b/docs/source-app/get_started/go_beyond_training.rst @@ -0,0 +1,18 @@ +:orphan: + +################################ +Start from an ML system template +################################ + +.. _go_beyond_training: + +**Required background:** Basic Python familiarity and complete the :ref:`install` guide. + +**Goal:** We'll walk you through the 4 key steps to run a Lightning App that trains and demos a model. + +.. join_slack:: + :align: left + +---- + +.. include:: go_beyond_training_content.rst diff --git a/docs/source-app/lightning_apps_intro.rst b/docs/source-app/get_started/go_beyond_training_content.rst similarity index 81% rename from docs/source-app/lightning_apps_intro.rst rename to docs/source-app/get_started/go_beyond_training_content.rst index b3b5d5498c023..a471e91d85a9c 100644 --- a/docs/source-app/lightning_apps_intro.rst +++ b/docs/source-app/get_started/go_beyond_training_content.rst @@ -1,46 +1,23 @@ -############################ -Lightning Apps in 15 minutes -############################ +************************************************ +The *Train & Demo PyTorch Lightning* Application +************************************************ -**Required background:** Basic Python familiarity. +Find the *Train & Demo PyTorch Lightning* application in the `Lightning.ai App Gallery `_. -**Goal:** In this guide, we'll walk you through the 4 key steps to build your first Lightning app. +Here is a recording of this App running locally and in the cloud with the same behavior. ----- - -The App we build in this guide trains and deploys a model. - -.. - (|qs_app|). - - .. |qs_app| raw:: html - -
see the app live here - - -A Lightning App is **Organized Python**, it enables AI researchers and ML engineers to build complex AI workflows without any of the **cloud** boilerplate. - -With Lightning Apps your favorite components can work together on any machine at any scale. Here's an illustration: - -Lightning Apps are: - -- cloud agnostic -- fault-tolerant -- production ready -- locally debuggable -- and much more - -.. join_slack:: - :align: left - :margin: 20 +.. raw:: html +
+ +
+
----- +In the steps below, we are going to show you how to build this application. -***************************** -Who can build Lightning Apps? -***************************** -Anyone who knows Python can build a Lightning App, even without machine learning experience. +Here are `the entire App's code `_ and `its commented components. `_ ---- @@ -48,20 +25,21 @@ Anyone who knows Python can build a Lightning App, even without machine learning Step 1: Install Lightning ************************* -If you are using a :doc:`virtual environment`, don't forget to activate it before running commands. You must do so in every new shell. We highly recommend using virtual environments. +If you are using a :ref:`virtual environment`, don't forget to activate it before running commands. +You must do so in every new shell. + +.. tip:: We highly recommend using virtual environments. .. code:: python pip install lightning -(conda install coming soon) - ---- -******************************** -Step 2: Install Train Deploy App -******************************** -The first Lightning App we'll explore is an app to train and deploy a machine learning model. +**************************************** +Step 2: Install the *Train and Demo* App +**************************************** +The first Lightning App we'll explore is an App to train and demo a machine learning model. .. [|qs_code|], [|qs_live_app|]. @@ -90,33 +68,30 @@ Verify the App was succesfully installed: ---- *************************** -Step 3: Run the app locally +Step 3: Run the App locally *************************** -Run the app locally with the ``run`` command + +Run the app locally with the ``run`` command 🤯 .. code:: bash lightning run app app.py -🤯 - ---- ******************************** -Step 4: Run the app on the cloud +Step 4: Run the App in the cloud ******************************** -Add the ``--cloud`` argument to run on the `Lightning.AI cloud `_. + +Add the ``--cloud`` argument to run on the `Lightning.AI cloud `_. 🤯🤯🤯 .. code:: bash lightning run app app.py --cloud -🤯🤯🤯 - .. Your app should look like this one (|qs_live_app|) - ---- ******************* @@ -124,7 +99,7 @@ Understand the code ******************* The App that we just launched trained a PyTorch Lightning model (although any framework works), then added an interactive demo. -This is the App code: +This is the App's code: .. code:: python @@ -158,7 +133,7 @@ This is the App code: app = L.LightningApp(TrainDeploy()) -Let's break down the App code by each section to understand what it is doing. +Let's break down the code section by section to understand what it is doing. ---- @@ -246,9 +221,9 @@ start a Gradio server for demo purposes. ---- -3: Define how components flow +3: Define how components Flow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Every component has a ``run`` method. The run method defines the 🌊 flow 🌊 of how components interact together. +Every component has a ``run`` method. The run method defines the 🌊 Flow 🌊 of how components interact together. In this case, we train a model (until completion). When it's done AND there exists a checkpoint, we launch a demo server: @@ -286,9 +261,7 @@ demo server: app = L.LightningApp(TrainDeploy()) -.. - If you've used other ML systems you'll be pleasantly surprised to not find decorators or YAML files. - Read here to understand the benefits more. +.. note:: If you've used other ML systems you'll be pleasantly surprised to not find decorators or YAML files. ---- @@ -384,9 +357,8 @@ Any component can work with Lightning AI! ---- ********** -Next steps +Next Steps ********** -Depending on your use case, you might want to check one of these out next. .. raw:: html @@ -397,14 +369,14 @@ Depending on your use case, you might want to check one of these out next. :header: Add components to your App :description: Expand your App by adding components. :col_css: col-md-4 - :button_link: workflows/extend_app.html + :button_link: ../workflows/extend_app.html :height: 180 .. displayitem:: :header: Build a component :description: Learn to build your own component. :col_css: col-md-4 - :button_link: workflows/build_lightning_component/index.html + :button_link: ../workflows/build_lightning_component/index.html :height: 180 .. displayitem:: @@ -415,16 +387,16 @@ Depending on your use case, you might want to check one of these out next. :height: 180 .. displayitem:: - :header: How it works under the hood + :header: Under the hood :description: Explore how it works under the hood. :col_css: col-md-4 - :button_link: core_api/lightning_app/index.html + :button_link: ../core_api/lightning_app/index.html :height: 180 .. displayitem:: :header: Run on your private cloud :description: Run Lightning Apps on your private VPC or on-prem. - :button_link: workflows/run_on_private_cloud.html + :button_link: ../workflows/run_on_private_cloud.html :col_css: col-md-4 :height: 180 diff --git a/docs/source-app/get_started/jumpstart_from_app_gallery.rst b/docs/source-app/get_started/jumpstart_from_app_gallery.rst new file mode 100644 index 0000000000000..998fce9a93470 --- /dev/null +++ b/docs/source-app/get_started/jumpstart_from_app_gallery.rst @@ -0,0 +1,123 @@ +:orphan: + +##################################### +Start from Ready-to-Run Template Apps +##################################### + +.. _jumpstart_from_app_gallery: + +Anyone can build Apps for their own use cases and promote them on the `App Gallery `_. + +In return, you can benefit from the work of others and get started faster by re-using a ready-to-run App close to your own use case. + +.. join_slack:: + :align: left + +---- + +************* +User Workflow +************* + +#. Visit the `App Gallery `_ and look for an App close to your own use case. + + .. raw:: html + +
+ +#. If **Launch** is available, it means the App is live and ready to be used! Take it for a spin. + + .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/launch_button.png + :alt: Launch Button on lightning.ai + :width: 100 % + +#. By clicking **Clone & Run**, a copy of the App is added to your account and an instance starts running. + + .. raw:: html + +
+ + +#. If you found an App that matches what you need, move to **step 5**! Otherwise, go back to **step 1**. + + .. raw:: html + +
+ +#. Copy the installation command (optionally from the clipboard on the right). + + .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/install_command.png + :alt: Install command on lightning.ai + :width: 100 % + +#. Copy the command to your local terminal. + + .. code-block:: bash + + lightning install app lightning/hackernews-app + +#. Go through the installation steps. + + .. raw:: html + +
+ + +#. Run the App locally. + + .. code-block:: bash + + cd LAI-Hackernews-App + lightning run app app.py + + .. raw:: html + +
+ + +#. Open the code with your favorite IDE, modify it, and run it back in the cloud. + + .. raw:: html + +
+ +
+ +---- + +********** +Next Steps +********** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Add Component made by others to your App + :description: Add more functionality to your projects + :col_css: col-md-6 + :button_link: jumpstart_from_component_gallery.html + :height: 180 + +.. displayitem:: + :header: Level-up your skills with Lightning Apps + :description: From Basic to Advanced Skills + :col_css: col-md-6 + :button_link: ../levels/basic/index.html + :height: 180 + +.. raw:: html + +
+
+
diff --git a/docs/source-app/get_started/jumpstart_from_component_gallery.rst b/docs/source-app/get_started/jumpstart_from_component_gallery.rst new file mode 100644 index 0000000000000..327977ccb454b --- /dev/null +++ b/docs/source-app/get_started/jumpstart_from_component_gallery.rst @@ -0,0 +1,152 @@ +:orphan: + +######################################## +Add Component made by others to your App +######################################## + +.. _jumpstart_from_component_gallery: + +Anyone can build components for their own use case and promote them on the `Component Gallery `_. + +In return, you can benefit from the work of others and add new functionalities to your Apps with minimal effort. + +.. join_slack:: + :align: left + +---- + +************* +User Workflow +************* + +#. Visit the `Component Gallery `_ and look for a Component close to something you want to do. + + .. raw:: html + +
+ +#. Check out the code for inspiration or simply install the component from PyPi and use it. + +---- + +************* +Success Story +************* + +The default `Train and Demo Application `_ trains a PyTorch Lightning +model and then starts a demo with `Gradio `_. + +.. code-block:: python + + import os.path as ops + import lightning as L + from quick_start.components import PyTorchLightningScript, ImageServeGradio + + class TrainDeploy(L.LightningFlow): + def __init__(self): + super().__init__() + self.train_work = PyTorchLightningScript( + script_path=ops.join(ops.dirname(__file__), "./train_script.py"), + script_args=["--trainer.max_epochs=5"], + ) + + self.serve_work = ImageServeGradio(L.CloudCompute("cpu")) + + def run(self): + # 1. Run the python script that trains the model + self.train_work.run() + + # 2. when a checkpoint is available, deploy + if self.train_work.best_model_path: + self.serve_work.run(self.train_work.best_model_path) + + def configure_layout(self): + tab_1 = {"name": "Model training", "content": self.train_work} + tab_2 = {"name": "Interactive demo", "content": self.serve_work} + return [tab_1, tab_2] + + app = L.LightningApp(TrainDeploy()) + +However, someone who wants to use this Aop (maybe you) found `Lightning HPO `_ +from browsing the `Component Gallery `_ and decided to give it a spin after checking the associated +`Github Repository `_. + +Once ``lightning_hpo`` installed, they improved the default App by easily adding HPO support to their project. + +Here is the resulting App. It is almost the same code, but it's way more powerful now! + +This is the power of `lightning.ai `_ ecosystem 🔥⚡🔥 + +.. code-block:: python + + import os.path as ops + import lightning as L + from quick_start.components import PyTorchLightningScript, ImageServeGradio + import optuna + from optuna.distributions import LogUniformDistribution + from lightning_hpo import Optimizer, BaseObjective + + class HPOPyTorchLightningScript(PyTorchLightningScript, BaseObjective): + + @staticmethod + def distributions(): + return {"model.lr": LogUniformDistribution(0.0001, 0.1)} + + + class TrainDeploy(L.LightningFlow): + def __init__(self): + super().__init__() + self.train_work = Optimizer( + script_path=ops.join(ops.dirname(__file__), "./train_script.py"), + script_args=["--trainer.max_epochs=5"], + objective_cls=HPOPyTorchLightningScript, + n_trials=4 + ) + + self.serve_work = ImageServeGradio(L.CloudCompute("cpu")) + + def run(self): + # 1. Run the python script that trains the model + self.train_work.run() + + # 2. when a checkpoint is available, deploy + if self.train_work.best_model_path: + self.serve_work.run(self.train_work.best_model_path) + + def configure_layout(self): + tab_1 = {"name": "Model training", "content": self.train_work.hi_plot} + tab_2 = {"name": "Interactive demo", "content": self.serve_work} + return [tab_1, tab_2] + + app = L.LightningApp(TrainDeploy()) + +---- + +********** +Next Steps +********** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Start from Ready-to-Run Template Apps + :description: Jump-start your projects development + :col_css: col-md-6 + :button_link: jumpstart_from_app_gallery.html + :height: 180 + +.. displayitem:: + :header: Level-up your skills with Lightning Apps + :description: From Basic to Advanced Skills + :col_css: col-md-6 + :button_link: ../levels/basic/index.html + :height: 180 + +.. raw:: html + +
+
+
diff --git a/docs/source-app/get_started/lightning_apps_intro.rst b/docs/source-app/get_started/lightning_apps_intro.rst new file mode 100644 index 0000000000000..bcbc9d8dbf44e --- /dev/null +++ b/docs/source-app/get_started/lightning_apps_intro.rst @@ -0,0 +1,14 @@ +############################ +Lightning Apps in 15 minutes +############################ + +**Required background:** Basic Python familiarity. + +**Goal:** Guide you to develop your first Lightning App or use an existing App from the `Apps Gallery `_. + +.. join_slack:: + :align: left + +---- + +.. include:: go_beyond_training_content.rst diff --git a/docs/source-app/get_started/training_with_apps.rst b/docs/source-app/get_started/training_with_apps.rst new file mode 100644 index 0000000000000..8c6e42037ecd5 --- /dev/null +++ b/docs/source-app/get_started/training_with_apps.rst @@ -0,0 +1,136 @@ +:orphan: + +################################ +Evolve a model into an ML system +################################ + +.. _convert_pl_to_app: + +**Required background:** Basic Python familiarity and complete the :ref:`build_model` guide. + +**Goal:** We'll walk you through the two key steps to build your first Lightning App from your existing Pytorch Lightning scripts. + +.. join_slack:: + :align: left + +---- + +******************* +Training and beyond +******************* + +With `PyTorch Lightning `_, we abstracted distributed training and hardware, by organizing PyTorch code. +With `Lightning Apps `_, we unified the local and cloud experience while abstracting infrastructure. + +By using `PyTorch Lightning `_ and `Lightning Apps `_ +together, a completely new world of possibilities emerges. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/pl_to_app_4.png + :alt: From PyTorch Lightning to Lightning App + :width: 100 % + +---- + +****************************************** +1. Write an App to run the train.py script +****************************************** + +This article continues where the :ref:`build_model` guide finished. + +Create an additional file ``app.py`` in the ``pl_project`` folder as follows: + +.. code-block:: bash + + pl_project/ + app.py + train.py + requirements.txt + +Inside the ``app.py`` file, add the following code. + +.. literalinclude:: ../code_samples/convert_pl_to_app/app.py + +This App runs the Pytorch Lightning script contained in the ``train.py`` file using the powerful :class:`~lightning_app.components.python.tracer.TracerPythonScript` component. This is really worth checking out! + +---- + +************************************************ +2. Run the train.py file locally or in the cloud +************************************************ + +First, go to the ``pl_folder`` folder from the local terminal and install the requirements. + +.. code-block:: bash + + cd pl_folder + pip install -r requirements.txt + +To run your app, copy the following command to your local terminal: + +.. code-block:: bash + + lightning run app app.py + +Simply add ``--cloud`` to run this application in the cloud with a GPU machine 🤯 + +.. code-block:: bash + + lightning run app app.py --cloud + + +Congratulations! Now, you know how to run a `PyTorch Lightning `_ script with Lightning Apps. + +Lightning Apps can make your ML system way more powerful, keep reading to learn how. + +---- + +********** +Next Steps +********** + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Level-up with Lightning Apps + :description: From Basics to Advanced Skills + :col_css: col-md-4 + :button_link: ../levels/basic/index.html + :height: 180 + +.. displayitem:: + :header: Add an Interactive Demo + :description: Add a Gradio Demo once the training is finished + :col_css: col-md-4 + :button_link: add_an_interactive_demo.html + :height: 180 + +.. displayitem:: + :header: Add Hyper Parameter Optimization + :description: Add a HPO to optimize your models + :col_css: col-md-4 + :button_link: ../examples/hpo/hpo.html + :height: 180 + +.. displayitem:: + :header: Add Model Serving + :description: Serve and load testing with MLServer and Locust + :col_css: col-md-4 + :button_link: ../examples/model_server_app/model_server_app.html + :height: 180 + +.. displayitem:: + :header: Add DAG Orchestration + :description: Organize your processing, training and metrics collection + :col_css: col-md-4 + :button_link: ../examples/dag/dag.html + :height: 180 + +.. displayitem:: + :header: Add Team Collaboration + :description: Create an app to run any PyTorch Lightning Script from Github + :col_css: col-md-4 + :button_link: ../examples/github_repo_runner/github_repo_runner.html + :height: 180 diff --git a/docs/source-app/get_started/what_app_can_do.rst b/docs/source-app/get_started/what_app_can_do.rst new file mode 100644 index 0000000000000..a2c88a6f85bdc --- /dev/null +++ b/docs/source-app/get_started/what_app_can_do.rst @@ -0,0 +1,196 @@ +:orphan: + +############################################ +Discover what Lightning Apps can do in 5 min +############################################ + +.. _what_app_can_do: + +Lightning Apps can be plenty things, and while a picture is worth a thousand words, videos showing you examples should be worth even more. + +.. join_slack:: + :align: left + +---- + +***************************** +Flashy - Auto ML App (Public) +***************************** + +Train a model on any image or text dataset without writing any code. Flashy uses `React.js `_ for its frontend. + +Find `Flashy `_ on the App Gallery and the `Flashy codebase. `_ on GitHub. + +.. raw:: html + + + +.. ---- + +.. *************************************** +.. NVIDIA Omniverse Sampling App (Private) +.. *************************************** + +.. Use `Nvidia Sampling Omniverse `_ to generate synthetic samples from 3D meshes and train an object detector on that data. + +.. .. raw:: html + +.. + +---- + +********************* +Research App (Public) +********************* + +Share your paper ``bundled`` with the arxiv link, poster, live jupyter notebook, interactive demo to try the model, and more! + +Find the `Research App `_ on the App Gallery and the `Research App codebase. `_ on GitHub. + +.. raw:: html + + + +---- + +************************************************ +ScratchPad - Notebook Manager for Team (Public) +************************************************ + +Run multiple Jupyter Notebooks on cloud CPUs or machines with multiple GPUs. + +Find the `ScratchPad App `_ on the App Gallery and the `ScratchPad App codebase `_ on GitHub. + +.. note:: ScratchPad is `tested end-to-end `_ on every Lightning App commit with `pytest `_. + +.. raw:: html + + + +---- + +****************************** +Lightning HPO Sweeper (Public) +****************************** + +Run a hyperparameter sweep over any model script across hundreds of cloud machines at once. This Lightning App uses Optuna to provide advanced tuning algorithms (from grid and random search to Hyperband). + +Find the `Lightning HPO Sweeper App `_ on the App Gallery and the `Lightning HPO Sweeper codebase. `_ on GitHub. + +.. raw:: html + + + +---- + +*********************** +InVideo Search (Public) +*********************** + +This App lets you find anything you're looking for inside a video. The engine is powered by `Open AI CLIP `_. + +Find the `InVideo Search App `_ on the App Gallery and the `InVideo Search App codebase. `_ in GitHub. + +.. raw:: html + + + +---- + +****************************** +AI-powered HackerNews (Public) +****************************** + +Save yourself time, and get Hacker News story recommendations, chosen for you specifically. This Lightning App was designed to illustrate a full end-to-end MLOPs workflow aimed at enterprise recommendation systems. + +Find the `AI-powered HackerNews App `_ on the App Gallery and the `AI-powered HackerNews App codebase. `_ on GitHub. + +.. raw:: html + + + +---- + +********************************************************************* +Lightning Apps can turn ML into scalable systems in days — not months +********************************************************************* + +Use the Lightning framework to develop any ML system: train and deploy a model, create an ETL pipeline, +or spin up a research demo — using the intuitive principles we pioneered with PyTorch Lightning. + +.. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/apps_logos_2.png + :alt: Apps with Logos + :width: 100 % + +Anyone who knows Python can build a Lightning App, even without machine learning experience. + +Lightning Apps are: + +- cloud agnostic +- fault-tolerant, distributed, cost optimized +- production ready +- local and cloud debuggable +- highly reactive & interactive +- connect multiple UIs together +- built for team collaboration +- framework agnostic, use your own stack +- and much more + +.. raw:: html + +
+ +
+
+ +********** +Next Steps +********** + +.. raw:: html + +
+
+
+ +.. displayitem:: + :header: Build & Train a Model + :description: Discover PyTorch Lightning and train your first Model. + :col_css: col-md-4 + :button_link: build_model.html + :height: 180 + +.. displayitem:: + :header: Evolve a Model into an ML System + :description: Develop an App to train a model in the cloud + :col_css: col-md-4 + :button_link: training_with_apps.html + :height: 180 + +.. displayitem:: + :header: Start from an ML system template + :description: Learn about Apps, from a template. + :col_css: col-md-4 + :button_link: go_beyond_training.html + :height: 180 + +.. raw:: html + +
+
diff --git a/docs/source-app/glossary/app_tree.rst b/docs/source-app/glossary/app_tree.rst index 120d5d2e4ef69..ba52ccfbd0d3a 100644 --- a/docs/source-app/glossary/app_tree.rst +++ b/docs/source-app/glossary/app_tree.rst @@ -10,9 +10,9 @@ App Component Tree ---- -**************************************** +************************************** What is an Application Component Tree? -**************************************** +************************************** Components can be nested to form component trees where the LightningFlows are its branches and LightningWorks are its leaves. diff --git a/docs/source-app/glossary/event_loop.rst b/docs/source-app/glossary/event_loop.rst index d0f97f50e708a..2b651267e6e08 100644 --- a/docs/source-app/glossary/event_loop.rst +++ b/docs/source-app/glossary/event_loop.rst @@ -2,10 +2,10 @@ Event loop ########## -.. _app_event_loop: - Drawing inspiration from modern web frameworks like `React.js `_, the Lightning App runs all flows in an **event loop** (forever), which is triggered several times a second after collecting any works' state change. .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/lightning_loop.gif When running a Lightning App in the cloud, the ``LightningWork`` run on different machines. LightningWork communicates any state changes to the **event loop** which re-executes the flow with the newly-collected works' state. + +.. _app_event_loop: diff --git a/docs/source-app/glossary/index.rst b/docs/source-app/glossary/index.rst new file mode 100644 index 0000000000000..8d32358be7984 --- /dev/null +++ b/docs/source-app/glossary/index.rst @@ -0,0 +1,80 @@ +:orphan: + +######## +Glossary +######## + +.. raw:: html + +
+
+ +.. displayitem:: + :header: App Components Tree + :description: Learn how components can be nested to form component trees where the LightningFlows are its branches and LightningWorks are its leaves. + :col_css: col-md-6 + :button_link: app_tree.html + :height: 180 + +.. displayitem:: + :header: Build Configuration + :description: Prepare your requirements, add custom build commands or use docker image + :col_css: col-md-6 + :button_link: build_config/build_config.html + :height: 180 + +.. displayitem:: + :header: DAG + :description: Learn about directed acyclic graph, their properties and usage + :col_css: col-md-6 + :button_link: dag.html + :height: 180 + +.. displayitem:: + :header: Event Loop + :description: Learn how the Infinite Event Loop enables high distributed reactivity by triggering after collecting state changes. + :col_css: col-md-6 + :button_link: event_loop.html + :height: 180 + +.. displayitem:: + :header: Environment Variables + :description: Add secrets such as API keys or access tokens + :col_css: col-md-6 + :button_link: environment_variables.html + :height: 180 + +.. displayitem:: + :header: Frontend + :description: Customize your App View with any framework you want + :col_css: col-md-6 + :button_link: ../workflows/add_web_ui/glossary_front_end.html + :height: 180 + +.. displayitem:: + :header: Sharing Components + :description: Let's create an ecosystem altogether + :col_css: col-md-6 + :button_link: sharing_components.html + :height: 180 + +.. displayitem:: + :header: Scheduling + :description: Orchestrate execution at specific times + :col_css: col-md-6 + :button_link: scheduling.html + :height: 180 + +.. displayitem:: + :header: Storage + :description: Easily share files even across multiple machines + :col_css: col-md-6 + :button_link: storage/storage.html + :height: 180 + +.. displayitem:: + :header: UI + :description: Combine multiple frameworks to create your own UI + :col_css: col-md-6 + :button_link: ../workflows/add_web_ui/glossary_ui.html + :height: 180 diff --git a/docs/source-app/index.rst b/docs/source-app/index.rst index 9de5b2b1b1756..68020dfab2e56 100644 --- a/docs/source-app/index.rst +++ b/docs/source-app/index.rst @@ -12,62 +12,183 @@ Welcome to ⚡ Lightning Apps .. image:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/Lightning.gif :alt: Animation showing how to convert a standard training loop to a Lightning loop :right: - Lightning is a distributed, modular, free, and open framework for - building all AI applications where the components you want, interact together. - The concept of Lightning apps was designed to help you focus on the what you care about, - and automate the rest. - The Lightning framework can be used for any type of AI app, from a simple demo to a production pipeline. + The `open-source Lightning framework `_ gives ML Researchers and Data Scientists, the fastest & most flexible + way to iterate on ML research ideas and deliver scalable ML systems with the performance enterprises requires at the same time. + +.. join_slack:: + :align: center + :margin: 0 + +---- + +***************** +Install Lightning +***************** + .. raw:: html -
+
+ +Pip users + +.. code-block:: bash + + pip install lightning + +.. raw:: html +
-.. join_slack:: - :align: center - :margin: 0 +Conda users + +.. code-block:: bash + + conda install lightning -c conda-forge .. raw:: html
+Or read the :ref:`advanced install ` guide. + ---- -****************** -Install Lightning -****************** +*********** +Get Started +*********** -.. code-block:: bash +.. raw:: html - pip install lightning +
+
+
+ +.. displayitem:: + :header: Discover what Lightning Apps can do in 5 min + :description: Browse through mind-blowing ML Systems + :col_css: col-md-6 + :button_link: get_started/what_app_can_do.html + :height: 180 + +.. displayitem:: + :header: Build and Train a Model + :description: Discover PyTorch Lightning and train your first Model. + :col_css: col-md-6 + :button_link: get_started/build_model.html + :height: 180 + +.. displayitem:: + :header: Evolve a Model into an ML System + :description: Develop an App to train a model in the cloud + :col_css: col-md-6 + :button_link: get_started/training_with_apps.html + :height: 180 + +.. displayitem:: + :header: Start from an ML system template + :description: Learn about Apps, from a template. + :col_css: col-md-6 + :button_link: get_started/go_beyond_training.html + :height: 180 + +.. raw:: html + +
+
---- -*********** -Get Started -*********** +*********************** +Current Lightning Users +*********************** .. raw:: html -
+
+ +Build with Template(s) from the App & Component Gallery +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. raw:: html + +
-.. Add callout items below this line +.. displayitem:: + :header: Start from Ready-to-Run Template Apps + :description: Jump-start your project's development + :col_css: col-md-6 + :button_link: get_started/jumpstart_from_app_gallery.html + :height: 180 + +.. displayitem:: + :header: Add Component made by others to your App + :description: Add more functionalities to your projects + :col_css: col-md-6 + :button_link: get_started/jumpstart_from_component_gallery.html + :height: 180 -.. customcalloutitem:: - :header: Build a Lightning App in 15 minutes - :description: Learn the 4 key steps to build a Lightning app. - :button_link: lightning_apps_intro.html .. raw:: html -
-
+
+
+
+ -.. End of callout item section +Keep Learning +^^^^^^^^^^^^^ + +.. raw:: html + +
+
+ +.. displayitem:: + :header: Level-up with PyTorch Lightning + :description: PyTorch Lightning Tutorials + :col_css: col-md-6 + :button_link: https://pytorch-lightning.readthedocs.io/en/latest/expertise_levels.html + :height: 180 + +.. displayitem:: + :header: Level-up with Lightning Apps + :description: From Basics to Advanced Skills + :col_css: col-md-6 + :button_link: levels/basic/index.html + :height: 180 + +.. displayitem:: + :header: API Reference + :description: Detailed description of each API package + :col_css: col-md-6 + :button_link: api_reference/api_references.html + :height: 180 + +.. displayitem:: + :header: Hands-on Examples + :description: Learn by building Apps and Components. + :col_css: col-md-6 + :button_link: examples/hands_on_example.html + :height: 180 + +.. displayitem:: + :header: Common Workflows + :description: Learn how to do ... + :col_css: col-md-6 + :button_link: workflows/index.html + :height: 180 + +.. displayitem:: + :header: Glossary + :description: Discover Lightning App Concepts + :col_css: col-md-6 + :button_link: glossary/index.html + :height: 180 .. raw:: html @@ -77,13 +198,18 @@ Get Started
+.. toctree:: + :maxdepth: 1 + :caption: Home + + self + .. toctree:: :maxdepth: 1 :caption: Get Started - read_me_first installation - lightning_apps_intro + get_started/lightning_apps_intro .. toctree:: :maxdepth: 1 @@ -95,7 +221,7 @@ Get Started .. toctree:: :maxdepth: 1 - :caption: Hands-on Examples + :caption: Practical Examples Build a DAG Build a File Server @@ -111,7 +237,7 @@ Get Started .. toctree:: :maxdepth: 1 - :caption: How to... + :caption: Common Workflows Add a web user interface (UI) Add a web link @@ -160,9 +286,9 @@ Get Started App Components Tree Build Configuration DAG - Event loop + Event Loop Environment Variables - Front-end + Frontend Sharing Components Scheduling Storage diff --git a/docs/source-app/install_beginner.rst b/docs/source-app/install_beginner.rst index 150f188815945..3ae423a71ccd6 100644 --- a/docs/source-app/install_beginner.rst +++ b/docs/source-app/install_beginner.rst @@ -1,9 +1,7 @@ :orphan: - .. _install_beginner: - ####################### Installation (Beginner) ####################### diff --git a/docs/source-app/installation.rst b/docs/source-app/installation.rst index 1d4ebb6ed6268..7f243e170e188 100644 --- a/docs/source-app/installation.rst +++ b/docs/source-app/installation.rst @@ -1,7 +1,6 @@ .. _install: - ############ Installation ############ @@ -27,8 +26,16 @@ Install with pip 0. Activate your virtual environment. + .. raw:: html + +
+ 1. Install the ``lightning`` package + .. raw:: html + +
+ .. code:: bash python -m pip install -U lightning diff --git a/docs/source-app/levels/intermediate/index.rst b/docs/source-app/levels/intermediate/index.rst index f712743350b41..c55792e0a1c45 100644 --- a/docs/source-app/levels/intermediate/index.rst +++ b/docs/source-app/levels/intermediate/index.rst @@ -49,7 +49,7 @@ Learn to build your own Lightning Apps from scratch and the basics of the framew .. displayitem:: :header: Level 10: Add web UIs :description: Learn how to add web UIs to your Lightning App. - :button_link: level_10 + :button_link: level_10.html :col_css: col-md-6 :height: 150 :tag: intermediate diff --git a/docs/source-app/quickstart.rst b/docs/source-app/quickstart.rst index 52591b3dc6c0e..f14ba98e18348 100644 --- a/docs/source-app/quickstart.rst +++ b/docs/source-app/quickstart.rst @@ -110,7 +110,7 @@ Here is the application timeline: Steps 3 - Build your app in the cloud ************************************** -Simply add **--cloud** to run this application in the cloud 🤯 +Simply add ``--cloud`` to run this application in the cloud 🤯 .. code-block:: bash @@ -128,7 +128,7 @@ Congratulations! You've now run your first application with Lightning. ---- *********** -Next steps +Next Steps *********** To learn how to build and modify apps, go to the :ref:`basics`. diff --git a/docs/source-app/read_me_first.rst b/docs/source-app/read_me_first.rst deleted file mode 100644 index f55684ccd32bd..0000000000000 --- a/docs/source-app/read_me_first.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. toctree:: - :maxdepth: 1 - -################################ -New to Lightning? READ ME FIRST! -################################ - -Lightning is all about the APPS! THE LIGHTNING APPS! - -The Lightning framework is a distributed, modular, free, and open framework you can use to build all AI applications (Lightning Apps) where the components you want, interact together. - -Use the Lightning framework to build anything from production-ready, multi-cloud ML systems to simple research demos. - -With the framework you can run and even train your App models on premise or in the cloud on various CPU and GPU hardware. - -You can create your Apps from scratch, build them from a template, OR check out the `Apps Gallery `_ and -clone and then modify existing Apps for your own use. - -You can also build components for your Apps from scratch, from a template, OR use existing components from the `Components Gallery `_. - ----- - -************************** -I'm totally new (to AI/ML) -************************** - -No worries! Just `install Lightning `_ and then `clone and run Flashy `_! -Flashy will have you run your first ML models in no time, with no coding! - -While Flashy is the fastest way to get started with Machine Learning, eventually you'll want -to create your own Lightning Apps. When you're ready you can clone Apps and components -from the Apps and `App Gallery `_ (then modify them), build Apps or components -from templates, or build Apps and components from scratch. - -********************************************************* -I have some XP in AI/ML. LET'S BUILD SOME LIGHTNING APPS! -********************************************************* - -You have maybe built some demos or some enterprise production-ready ML Solutions, just not with Lightning. - -Let's get you started. Here's a few important things to keep in mind when building your Lightning Apps (App): - -* Always check the `Apps Gallery `_ and `Apps Components `_ first. Why build something from scratch when you can clone an App or add a component and then build off of someone else's work. -* You can run your Apps locally or in the cloud on CPUs or GPUs. To get started, you can only run one App locally. -* Lightning Apps consist of a `root LightningFlow component `_, that optionally contains a tree of 2 types of components: `LightningFlow `_ 🌊 and `LightningWork `_ ⚒️. Key functionality includes: - - * :ref:`A shared state between components. ` - * :ref:`A constantly running event loop for reactivity. ` - * :ref:`Dynamic attachment of components at runtime. ` - * Start and stop functionality of your works. - - -If you're ready to go, here's what you're gonna wanna do: - -#. `Install Lightning `_ -#. Check out the `Lightning in 15 minutes `_ topic. -#. Start learning those Lightning App Building Skills! - - * Start with Lightning's `Basic Skills `_. - * Move on to `Intermediate Skills `_. - * And when you're ready the `Advanced Skills `_. diff --git a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst index 0f7b0ff436eb0..f7a97d72bb975 100644 --- a/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst +++ b/docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst @@ -1,8 +1,9 @@ - ******************************* LightningFlow vs. LightningWork ******************************* +.. _flow_vs_work: + .. raw:: html +
+ +.. displayitem:: + :header: Add a web user interface + :description: Learn how to add React, StreamLit, Dash to your App. + :col_css: col-md-4 + :button_link: add_web_ui/index.html + :height: 180 + +.. displayitem:: + :header: Add a web link + :description: Learn how to embed external websites + :col_css: col-md-4 + :button_link: add_web_link.html + :height: 180 + +.. displayitem:: + :header: Arrange App tabs + :description: Learn how to organize your UI + :col_css: col-md-4 + :button_link: arrange_tabs/index.html + :height: 180 + +.. displayitem:: + :header: Build a Lightning App + :description: Simple App to get started + :col_css: col-md-4 + :button_link: build_lightning_app/index.html + :height: 180 + +.. displayitem:: + :header: Build a Lightning Component + :description: Understand how to separated the glue from the actual work + :col_css: col-md-4 + :button_link: build_lightning_component/index.html + :height: 180 + +.. displayitem:: + :header: Cache Work run calls + :description: Understand how to trigger a work run method + :col_css: col-md-4 + :button_link: run_work_once.html + :height: 180 + +.. displayitem:: + :header: Customize your cloud compute + :description: Select machines to run on + :col_css: col-md-4 + :button_link: ../core_api/lightning_work/compute.html + :height: 180 + +.. displayitem:: + :header: Extend an existing App + :description: Learn where to go next with an App + :col_css: col-md-4 + :button_link: extend_app.html + :height: 180 + +.. displayitem:: + :header: Publish a Lightning Component + :description: Share your components with others + :col_css: col-md-4 + :button_link: build_lightning_component/publish_a_component.html + :height: 180 + +.. displayitem:: + :header: Run a server within a Lightning App + :description: Lightning Work can be infinite jobs + :col_css: col-md-4 + :button_link: add_server/index.html + :height: 180 + +.. displayitem:: + :header: Run an App on the cloud + :description: Learn how to get things done in the cloud with ease + :col_css: col-md-4 + :button_link: run_app_on_cloud/index.html + :height: 180 + +.. displayitem:: + :header: Run Works in parallel + :description: Learn how to make your Work non blocking + :col_css: col-md-4 + :button_link: run_work_in_parallel.html + :height: 180 + +.. displayitem:: + :header: Share an App + :description: Learn how to share your work with others + :col_css: col-md-4 + :button_link: share_app.html + :height: 180 + +.. displayitem:: + :header: Share files between components + :description: Learn how Lightning Storage emulates a single filesystem in a distributed setting + :col_css: col-md-4 + :button_link: share_files_between_components.html + :height: 180 + + +.. raw:: html + +
+
From 7cb51c439487cac7f7be1bba8de863c586adec2d Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 08:12:45 +0100 Subject: [PATCH 017/119] remove image --- docs/source-app/_static/images/brandmark.png | Bin 60816 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/source-app/_static/images/brandmark.png diff --git a/docs/source-app/_static/images/brandmark.png b/docs/source-app/_static/images/brandmark.png deleted file mode 100644 index 76648a7ed99e019dff43a641562fc9b238fc7951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60816 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

|YPrMQ(76*=X&6xdryZZGOjzUA253wEM$}f}5 zX?7;)jugB3dxWM=an*6E=D#-|B|*DCK$K;6-sOM$(0RyznSq(EyhR@Iudiz-c*-oQ zuJ|eVUC_@xd{?a%o_Fe<9j`j*|YHRDX;p2PGi)k)TQ{uLz$ zPQphMqgn4T<2hqg^OCl4Qadz|`9XfLY@Ubb>}HgDHBJBPI4YaH0DUq?YRsa{4y?lJ6g^hu z$KvId8jSHV#ew4yet-o1ZT}mz&#rH8KsruLcvJY4YzoRk=>0x%gKFvQ9zAiT;lSTl zQQ1-j=>5;6#?IsMy;sIYNo+);BrW-lYm7V3Kp%JoS zeIXhD-ux~2z9zdOqbNjFtnTW!j*1ZSP(3=>u4I6*NTBO}pKvVG^_KscvVy)s$AdOr zwr)OZ)RrSk6m4+0Hn58;F94mGeN)~)-WE->p=2-&{|yiQ#3b$wX{8%xf4i}N8>gk1 z@(y}|;<(3u^Bt!t{q^E?JwN@@xi{#jeAM-*;7ks7#o zke{J~*+H*T954OPM7+EcMm}xSc5TQhurv2>FT(EQG{mF^2o%dX|0DmJRgE*@uOh)> z#FQ?!Wj`^eS$V&dyN%0?Bj_JeS`G;OHNDEtfL}Bwz`JSCg)%c-U^cQ8y*oq5*lS7# zXX^vo2|g3dt-j2re{Y5^2a1 z;lIU~cD-}iD_}8dH-Dt=_fj$a%(v)&J^c;xvPKl1w4Nw){aWWCurW=lJ>Q>^FJN#3 zgt2bb`BVR1M%!Gr&iWDSDFY_#%-^-7p@&}b!rx@HGfD>j84e=QHwp9$bSWbl~c8#sJ^H7vnez(5g=}ifS<@FmD~M9VZrcY1lI?2g>yhm3S8h zwobqJ_bU3rW$U=$e_GC5Xx3@4$o#t`w>!`2s2ThdEQ&)*no|y$SrEZ=TkG>E++#U@ zb$w_vz@^DeD^q2Fza*wy>K%aoU!~%UG~rLyHEN4-SLAYT z{yNKz=^O z3?>BeOD8aq*J0e1Z&L=`Shwf#e5_3V=e{qFMfEjYywd2El3 zjsCTj55mqz#gn-4v7!kIz`5i!aK(Wuyl2USGfIItXZ1OM+k~I*m{D623>KDsi<$tV zp3&r+f?!%PM&;GV)X!_d{s+z>hLA-Tc&kWHU zQ#r+z*kmF-z*R8xb+m^qVjl%$V06Jxum5;y~acwOJe n0e|m6T1r%*#(ygq9kI+J8x^wnv-?H_@_Wqj)5mg;THX48Rfu|9 From 7e17c44e5294200531b2177a8761e18bf574eb4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 07:12:47 +0000 Subject: [PATCH 018/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api/lightning_app.core.LightningApp.rst | 2 +- .../api/lightning_app.core.LightningFlow.rst | 2 +- .../api/lightning_app.core.LightningWork.rst | 2 +- .../code_samples/convert_pl_to_app/train.py | 12 ++++++------ .../get_started/jumpstart_from_component_gallery.rst | 7 +++++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/code_samples/convert_pl_to_app/train.py b/docs/source-app/code_samples/convert_pl_to_app/train.py index bf64295c5c7e1..8ab400cdf5c29 100644 --- a/docs/source-app/code_samples/convert_pl_to_app/train.py +++ b/docs/source-app/code_samples/convert_pl_to_app/train.py @@ -1,21 +1,21 @@ import os + import torch -from torch import nn import torch.nn.functional as F -from torchvision.datasets import MNIST +from torch import nn from torch.utils.data import DataLoader, random_split from torchvision import transforms as T +from torchvision.datasets import MNIST + import pytorch_lightning as pl class LitAutoEncoder(pl.LightningModule): def __init__(self): super().__init__() - self.encoder = nn.Sequential( - nn.Linear(28 * 28, 128), nn.ReLU(), nn.Linear(128, 3)) + self.encoder = nn.Sequential(nn.Linear(28 * 28, 128), nn.ReLU(), nn.Linear(128, 3)) - self.decoder = nn.Sequential( - nn.Linear(3, 128), nn.ReLU(), nn.Linear(128, 28 * 28)) + self.decoder = nn.Sequential(nn.Linear(3, 128), nn.ReLU(), nn.Linear(128, 28 * 28)) def forward(self, x): # in lightning, diff --git a/docs/source-app/get_started/jumpstart_from_component_gallery.rst b/docs/source-app/get_started/jumpstart_from_component_gallery.rst index 327977ccb454b..5d0a0e434adc8 100644 --- a/docs/source-app/get_started/jumpstart_from_component_gallery.rst +++ b/docs/source-app/get_started/jumpstart_from_component_gallery.rst @@ -42,6 +42,7 @@ model and then starts a demo with `Gradio `_. import lightning as L from quick_start.components import PyTorchLightningScript, ImageServeGradio + class TrainDeploy(L.LightningFlow): def __init__(self): super().__init__() @@ -65,6 +66,7 @@ model and then starts a demo with `Gradio `_. tab_2 = {"name": "Interactive demo", "content": self.serve_work} return [tab_1, tab_2] + app = L.LightningApp(TrainDeploy()) However, someone who wants to use this Aop (maybe you) found `Lightning HPO `_ @@ -86,8 +88,8 @@ This is the power of `lightning.ai `_ ecosystem 🔥⚡ from optuna.distributions import LogUniformDistribution from lightning_hpo import Optimizer, BaseObjective - class HPOPyTorchLightningScript(PyTorchLightningScript, BaseObjective): + class HPOPyTorchLightningScript(PyTorchLightningScript, BaseObjective): @staticmethod def distributions(): return {"model.lr": LogUniformDistribution(0.0001, 0.1)} @@ -100,7 +102,7 @@ This is the power of `lightning.ai `_ ecosystem 🔥⚡ script_path=ops.join(ops.dirname(__file__), "./train_script.py"), script_args=["--trainer.max_epochs=5"], objective_cls=HPOPyTorchLightningScript, - n_trials=4 + n_trials=4, ) self.serve_work = ImageServeGradio(L.CloudCompute("cpu")) @@ -118,6 +120,7 @@ This is the power of `lightning.ai `_ ecosystem 🔥⚡ tab_2 = {"name": "Interactive demo", "content": self.serve_work} return [tab_1, tab_2] + app = L.LightningApp(TrainDeploy()) ---- From 7993f31f821938019697796d4c17ea9ef703558d Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 09:13:10 +0100 Subject: [PATCH 019/119] update --- .github/workflows/ci-app_block.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/ci-app_block.yml b/.github/workflows/ci-app_block.yml index 7b3ae57d43253..8c2dd772aa1ad 100644 --- a/.github/workflows/ci-app_block.yml +++ b/.github/workflows/ci-app_block.yml @@ -1,16 +1,6 @@ name: Block app edits -on: # Trigger the workflow on push or pull request, but only for the master branch - pull_request: - branches: [master, "release/*"] - types: [opened, reopened, ready_for_review, synchronize] - paths-ignore: - - "src/lightning_app/**" # todo: implement job skip - - "tests/tests_app/**" # todo: implement job skip - - "tests/tests_app_examples/**" # todo: implement job skip - - "examples/app_*" # todo: implement job skip - - "docs/source-app/**" # todo: implement job skip - +on: ["pull_request"] jobs: block: @@ -23,7 +13,6 @@ jobs: - name: Get changed files using defaults id: changed-files uses: tj-actions/changed-files@v23 - - name: List all added files run: | for file in ${{ steps.changed-files.outputs.all_changed_and_modified_files }}; do From 8e54a8cdb0cce49a1b8e701c2bbf87bdc0b73f79 Mon Sep 17 00:00:00 2001 From: Jirka Date: Mon, 11 Jul 2022 15:23:12 +0200 Subject: [PATCH 020/119] ci --- .github/workflows/ci-app_block.yml | 4 ---- .github/workflows/docs-checks.yml | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-app_block.yml b/.github/workflows/ci-app_block.yml index 8c2dd772aa1ad..fd4ae4428b689 100644 --- a/.github/workflows/ci-app_block.yml +++ b/.github/workflows/ci-app_block.yml @@ -18,7 +18,3 @@ jobs: for file in ${{ steps.changed-files.outputs.all_changed_and_modified_files }}; do echo "$file" done - - - name: Block edits in docs/source-app - if: contains(steps.changed-files.outputs.all_changed_and_modified_files, 'docs/source-app') - run: exit 1 diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 25a9b17d6914b..f5c28a028a20f 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -14,6 +14,10 @@ concurrency: jobs: doctest: runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + pkg: ["app", "pytorch"] steps: - uses: actions/checkout@v2 with: @@ -45,8 +49,8 @@ jobs: pip --version pip install -q fire # python -m pip install --upgrade --user pip - pip install -e . --quiet -r requirements/pytorch/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html - pip install -r requirements/pytorch/devel.txt + pip install -e . --quiet -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -r requirements/${{ matrix.pkg }}/devel.txt pip list shell: bash @@ -55,12 +59,17 @@ jobs: SPHINX_MOCK_REQUIREMENTS: 0 working-directory: ./docs run: | + # ToDo: proper parametrize # First run the same pipeline as Read-The-Docs make doctest make coverage make-docs: runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + pkg: ["app", "pytorch"] steps: - uses: actions/checkout@v2 with: @@ -76,7 +85,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-docs-make-pip-${{ hashFiles('requirements/pytorch/base.txt') }} + key: ${{ runner.os }}-docs-make-pip${{ matrix.pkg }} restore-keys: | ${{ runner.os }}-docs-make-pip- @@ -88,7 +97,7 @@ jobs: sudo apt-get update sudo apt-get install -y cmake pandoc pip --version - pip install -e . --quiet -r requirements/pytorch/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -e . --quiet -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html # install Texlive, see https://linuxconfig.org/how-to-install-latex-on-ubuntu-20-04-focal-fossa-linux sudo apt-get update && sudo apt-get install -y texlive-latex-extra dvipng texlive-pictures pip list @@ -97,6 +106,7 @@ jobs: - name: Make Documentation working-directory: ./docs run: | + # ToDo: rather use puthon cmd # First run the same pipeline as Read-The-Docs make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" From 1c7548c5ebff0baa9ac4e52985daa45f1a8136a6 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 14:47:58 +0100 Subject: [PATCH 021/119] update --- .github/workflows/docs-checks.yml | 2 +- .gitignore | 2 ++ docs/source-app/Makefile | 19 +++++++++++++++++++ .../api/lightning_app.core.LightningApp.rst | 2 +- .../api/lightning_app.core.LightningFlow.rst | 2 +- .../api/lightning_app.core.LightningWork.rst | 2 +- docs/source-pytorch/Makefile | 19 +++++++++++++++++++ docs/source-pytorch/conf.py | 2 +- 8 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 docs/source-app/Makefile create mode 100644 docs/source-pytorch/Makefile diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index f5c28a028a20f..3d33e4b3e7296 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -57,7 +57,7 @@ jobs: - name: Test Documentation env: SPHINX_MOCK_REQUIREMENTS: 0 - working-directory: ./docs + working-directory: ./docs/${{ matrix.pkg }} run: | # ToDo: proper parametrize # First run the same pipeline as Read-The-Docs diff --git a/.gitignore b/.gitignore index c27a9e898de68..f52f079f5c032 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,5 @@ cifar-10-batches-py tags .tags src/lightning_app/ui/* +docs/examples/* +docs/source-app/api_reference/api/* \ No newline at end of file diff --git a/docs/source-app/Makefile b/docs/source-app/Makefile new file mode 100644 index 0000000000000..5dede4aa4a23f --- /dev/null +++ b/docs/source-app/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-pytorch/Makefile b/docs/source-pytorch/Makefile new file mode 100644 index 0000000000000..5dede4aa4a23f --- /dev/null +++ b/docs/source-pytorch/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source-pytorch/conf.py b/docs/source-pytorch/conf.py index 6ebb32ce870dc..e75c2ed8638b1 100644 --- a/docs/source-pytorch/conf.py +++ b/docs/source-pytorch/conf.py @@ -188,7 +188,7 @@ def _transform_changelog(path_in: str, path_out: str) -> None: html_theme_options = { "pytorch_project": "https://pytorchlightning.ai", - "canonical_url": pytorch_lightning.__docs_url__, + "canonical_url": pytorch_lightning.__about__.__docs_url__, "collapse_navigation": False, "display_version": True, "logo_only": False, From 43493035322782d26a346e80946ef68abe2795e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:50:48 +0000 Subject: [PATCH 022/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f52f079f5c032..a77167a17bbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,4 @@ tags .tags src/lightning_app/ui/* docs/examples/* -docs/source-app/api_reference/api/* \ No newline at end of file +docs/source-app/api_reference/api/* diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From 547c4ec680fbdf0a84175874d02ca67b69a8e1f7 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:07:23 +0100 Subject: [PATCH 023/119] update --- .github/workflows/docs-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 3d33e4b3e7296..5e8ba029341c8 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -85,7 +85,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-docs-make-pip${{ matrix.pkg }} + key: ${{ runner.os }}-docs-make-pip-${{ hashFiles('requirements/${{ matrix.pkg }}/base.txt') }} restore-keys: | ${{ runner.os }}-docs-make-pip- From 50630f6695ba1580598199f0ee9fd08c74b90ab0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:09:30 +0100 Subject: [PATCH 024/119] update --- .github/workflows/docs-checks.yml | 2 +- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 5e8ba029341c8..61b847420cd38 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -104,7 +104,7 @@ jobs: shell: bash - name: Make Documentation - working-directory: ./docs + working-directory: ./docs/source-${{ matrix.pkg }} run: | # ToDo: rather use puthon cmd # First run the same pipeline as Read-The-Docs diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From 98121657d1e192ee1aa1dd224fe9033bb19e362a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:12:06 +0000 Subject: [PATCH 025/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From 2b817f3b30c9a704509a049d7e9e341729209902 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:18:51 +0100 Subject: [PATCH 026/119] update --- .github/workflows/docs-checks.yml | 2 +- .pre-commit-config.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 61b847420cd38..cf02bceb78866 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -106,7 +106,7 @@ jobs: - name: Make Documentation working-directory: ./docs/source-${{ matrix.pkg }} run: | - # ToDo: rather use puthon cmd + # ToDo: rather use python cmd # First run the same pipeline as Read-The-Docs make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c96c9ceeae7e..39d2fdf9de4bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,6 +77,7 @@ repos: hooks: - id: black name: Format code + exclude: docs/source-app - repo: https://github.com/asottile/blacken-docs rev: v1.12.1 @@ -84,6 +85,7 @@ repos: - id: blacken-docs args: [--line-length=120] additional_dependencies: [black==21.12b0] + exclude: docs/source-app - repo: https://github.com/executablebooks/mdformat rev: 0.7.14 From c93895fc6cb65d099b2e877b9da3bea341239822 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:22:57 +0100 Subject: [PATCH 027/119] update --- .github/workflows/docs-checks.yml | 3 +-- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index cf02bceb78866..6396567ba1c65 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -104,11 +104,10 @@ jobs: shell: bash - name: Make Documentation - working-directory: ./docs/source-${{ matrix.pkg }} run: | # ToDo: rather use python cmd # First run the same pipeline as Read-The-Docs - make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" + cd docs/source-${{ matrix.pkg }} && make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" - name: Upload built docs uses: actions/upload-artifact@v2 diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From 72bf1791f7ba47e6f3f60acbf732b41dd739c1f5 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:27:28 +0100 Subject: [PATCH 028/119] update --- .github/workflows/docs-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 6396567ba1c65..cb37fa212210a 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -57,7 +57,7 @@ jobs: - name: Test Documentation env: SPHINX_MOCK_REQUIREMENTS: 0 - working-directory: ./docs/${{ matrix.pkg }} + working-directory: ./docs/source-${{ matrix.pkg }} run: | # ToDo: proper parametrize # First run the same pipeline as Read-The-Docs From 4531438e39e6b46aac7974ac79578b5d0746c201 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:27:36 +0000 Subject: [PATCH 029/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From d5e03c7d69af40867d80e7ba4f0566f648d6297b Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:27:49 +0100 Subject: [PATCH 030/119] update --- .github/workflows/docs-checks.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index cb37fa212210a..60481251756ef 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -104,10 +104,11 @@ jobs: shell: bash - name: Make Documentation + working-directory: ./docs/source-${{ matrix.pkg }} run: | # ToDo: rather use python cmd # First run the same pipeline as Read-The-Docs - cd docs/source-${{ matrix.pkg }} && make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" + make html --debug --jobs $(nproc) SPHINXOPTS="-W --keep-going" - name: Upload built docs uses: actions/upload-artifact@v2 From 27e9a84d8c8557efd60ab457582617022a69b6fd Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:28:59 +0100 Subject: [PATCH 031/119] update --- .gitignore | 1 + .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a77167a17bbd0..c9577efec9976 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ tags .tags src/lightning_app/ui/* docs/examples/* +*docs/source-app/api_reference/api* docs/source-app/api_reference/api/* diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From cb3be3e00ac41a7547d70af9794071453f740906 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:34:02 +0000 Subject: [PATCH 032/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From 04afa05860c5c124411e64eb505174399c7a2275 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:37:46 +0100 Subject: [PATCH 033/119] update --- .github/workflows/docs-checks.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 60481251756ef..c90618af58e7c 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -35,13 +35,13 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-docs-test-pip-td${{ env.TIME_PERIOD }}-${{ hashFiles('requirements/pytorch/*.txt') }} + key: ${{ runner.os }}-docs-test-pip-td${{ env.TIME_PERIOD }}-${{ hashFiles('requirements/${{ matrix.pkg }}/*.txt') }} restore-keys: | ${{ runner.os }}-docs-test-pip-td${{ env.TIME_PERIOD }}- - name: Install dependencies env: - PACKAGE_NAME: pytorch + PACKAGE_NAME: ${{ matrix.pkg }} FREEZE_REQUIREMENTS: 1 run: | sudo apt-get update @@ -49,7 +49,7 @@ jobs: pip --version pip install -q fire # python -m pip install --upgrade --user pip - pip install -e . --quiet -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -e . --quiet requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html pip install -r requirements/${{ matrix.pkg }}/devel.txt pip list shell: bash @@ -85,19 +85,19 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-docs-make-pip-${{ hashFiles('requirements/${{ matrix.pkg }}/base.txt') }} + key: ${{ runner.os }}-docs-make-pip-${{ hashFiles('requirements/${{ matrix.pkg }}/*.txt') }} restore-keys: | ${{ runner.os }}-docs-make-pip- - name: Install dependencies env: - PACKAGE_NAME: pytorch + PACKAGE_NAME: ${{ matrix.pkg }} FREEZE_REQUIREMENTS: 1 run: | sudo apt-get update sudo apt-get install -y cmake pandoc pip --version - pip install -e . --quiet -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -e . --quiet -r requirements/${{ matrix.pkg }}/base.txt -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html # install Texlive, see https://linuxconfig.org/how-to-install-latex-on-ubuntu-20-04-focal-fossa-linux sudo apt-get update && sudo apt-get install -y texlive-latex-extra dvipng texlive-pictures pip list From 8030af98a7a00f73501343f6b468d4b746da5629 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:40:58 +0100 Subject: [PATCH 034/119] update --- .github/workflows/docs-checks.yml | 2 +- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index c90618af58e7c..5939468de49dd 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -49,7 +49,7 @@ jobs: pip --version pip install -q fire # python -m pip install --upgrade --user pip - pip install -e . --quiet requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -e . --quiet -r requirements/${{ matrix.pkg }}/base.txt -r requirements/${{ matrix.pkg }}/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html pip install -r requirements/${{ matrix.pkg }}/devel.txt pip list shell: bash diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From ab700907a8fef08039de18580db679026a014c4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:42:37 +0000 Subject: [PATCH 035/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From 43278f50e13b1831b8c9a916c94a9c2293cc1b4c Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 15:52:24 +0100 Subject: [PATCH 036/119] update --- .github/workflows/docs-checks.yml | 2 -- MANIFEST.in | 6 ++++++ setup.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 5939468de49dd..d74301e8018b1 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -41,7 +41,6 @@ jobs: - name: Install dependencies env: - PACKAGE_NAME: ${{ matrix.pkg }} FREEZE_REQUIREMENTS: 1 run: | sudo apt-get update @@ -91,7 +90,6 @@ jobs: - name: Install dependencies env: - PACKAGE_NAME: ${{ matrix.pkg }} FREEZE_REQUIREMENTS: 1 run: | sudo apt-get update diff --git a/MANIFEST.in b/MANIFEST.in index a8dbcff69b631..37c72c103b69c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,9 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/setup.py b/setup.py index a542b3c1e0291..6d271cc40b0aa 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") +_PACKAGE_NAME = "" _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From 9f1a2edb2e2d8dbc8e0770730dda7d584ea68a02 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:04:25 +0100 Subject: [PATCH 037/119] update --- .github/workflows/ci-pytorch_test-full.yml | 2 +- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 5d122f8aafb8a..75cd88409ca81 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -89,7 +89,7 @@ jobs: run: | flag=$(python -c "print('--pre' if '${{matrix.release}}' == 'pre' else '')" 2>&1) url=$(python -c "print('test/cpu/torch_test.html' if '${{matrix.release}}' == 'pre' else 'cpu/torch_stable.html')" 2>&1) - pip install -e '.[test]' --upgrade $flag --find-links "https://download.pytorch.org/whl/${url}" + pip install -e .[test] --upgrade $flag --find-links "https://download.pytorch.org/whl/${url}" pip list shell: bash diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From 8a337459ff77279af84b07f74d2d9f505300a3d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 15:06:11 +0000 Subject: [PATCH 038/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From a447a8cfb052ab9a3c4e23e99f29f0cc795fec86 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:10:35 +0100 Subject: [PATCH 039/119] update --- MANIFEST.in | 6 ------ setup.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 37c72c103b69c..a8dbcff69b631 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,9 +3,3 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt diff --git a/setup.py b/setup.py index 6d271cc40b0aa..a542b3c1e0291 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = "" +_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From 5d6a35abc9a0511fc9a9aac9cc94d50a3e91fead Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:11:09 +0100 Subject: [PATCH 040/119] update --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index e6cdd766b033b..573c5527a1fc7 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 52391c7043867..707f3a12487c8 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index c0540bfa3994a..df73cf2f2c4ea 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: + note it does not have :inherited-members: \ No newline at end of file From 694a029c1b9607ae8191c0de82e1988b63f6ab3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 15:13:59 +0000 Subject: [PATCH 041/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../api_reference/api/lightning_app.core.LightningApp.rst | 2 +- .../api_reference/api/lightning_app.core.LightningFlow.rst | 2 +- .../api_reference/api/lightning_app.core.LightningWork.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst index 573c5527a1fc7..e6cdd766b033b 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst @@ -14,4 +14,4 @@ LightningApp .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst index 707f3a12487c8..52391c7043867 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst @@ -14,4 +14,4 @@ LightningFlow .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst index df73cf2f2c4ea..c0540bfa3994a 100644 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst @@ -14,4 +14,4 @@ LightningWork .. autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: \ No newline at end of file + note it does not have :inherited-members: From 441297a827b62229cf2f867561b2e712bd71c22d Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:21:15 +0100 Subject: [PATCH 042/119] update --- .github/workflows/docs-deploy.yml | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/docs-deploy.yml diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000000000..367a71dbc5156 --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,75 @@ +name: "Deploy Docs" +on: + push: + branches: [master] + +jobs: + # https://github.com/marketplace/actions/deploy-to-github-pages + build-docs-deploy: + runs-on: ubuntu-20.04 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v2 + # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. + with: + persist-credentials: false + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v0' + with: + credentials_json: ${{ secrets.GCS_SA_KEY }} + + - name: Setup gcloud + uses: 'google-github-actions/setup-gcloud@v0' + with: + project_id: ${{ secrets.GCS_PROJECT }} + + # Note: This uses an internal pip API and may not always work + # https://github.com/actions/cache/blob/master/examples.md#multiple-oss-in-a-workflow + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-deploy-docs-pip-${{ hashFiles('requirements/app/*.txt') }} + restore-keys: | + ${{ runner.os }}-deploy-docs-pip- + + - name: Install dependencies + env: + FREEZE_REQUIREMENTS: 1 + run: | + sudo apt-get update + sudo apt-get install -y cmake pandoc + pip --version + pip install -e . --quiet -r requirements/app/base.txt -r requirements/app/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + sudo apt-get update && sudo apt-get install -y texlive-latex-extra dvipng texlive-pictures + pip list + shell: bash + + - name: Make Documentation + working-directory: ./docs/source-app + run: | + # First run the same pipeline as Read-The-Docs + make clean + make html --jobs 2 + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@4.1.4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages # The branch the action should deploy to. + folder: docs/build/html # The folder the action should deploy. + clean: true # Automatically remove deleted files from the deploy branch + target-folder: docs # If you'd like to push the contents of the deployment folder into a specific directory + single-commit: true # you'd prefer to have a single commit on the deployment branch instead of full history + if: success() + + # Uploading docs to GCS so they can be served on lightning.ai + - name: Upload to GCS 🪣 + run: |- + gsutil -m rsync -d -R docs/build/html/ gs://${{ secrets.GCS_BUCKET }} + if: success() From d0cabd136220adb38db194658bd7d9e0148284a0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:27:19 +0100 Subject: [PATCH 043/119] update --- .pre-commit-config.yaml | 1 + docs/source-app/_templates/classtemplate.rst | 4 ---- .../_templates/classtemplate_no_index.rst | 4 ---- .../api/lightning_app.core.LightningApp.rst | 17 ----------------- .../api/lightning_app.core.LightningFlow.rst | 17 ----------------- .../api/lightning_app.core.LightningWork.rst | 17 ----------------- 6 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst delete mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst delete mode 100644 docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39d2fdf9de4bd..86d301d1119ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -102,3 +102,4 @@ repos: hooks: - id: flake8 name: Check PEP8 + exclude: docs/source-app diff --git a/docs/source-app/_templates/classtemplate.rst b/docs/source-app/_templates/classtemplate.rst index 00f2d0b767436..5b7f465516787 100644 --- a/docs/source-app/_templates/classtemplate.rst +++ b/docs/source-app/_templates/classtemplate.rst @@ -7,7 +7,3 @@ .. autoclass:: {{ name }} :members: - -.. - autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: diff --git a/docs/source-app/_templates/classtemplate_no_index.rst b/docs/source-app/_templates/classtemplate_no_index.rst index 1455912e1d04f..858c37b51567a 100644 --- a/docs/source-app/_templates/classtemplate_no_index.rst +++ b/docs/source-app/_templates/classtemplate_no_index.rst @@ -10,7 +10,3 @@ .. autoclass:: {{ name }} :members: :noindex: - -.. - autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst deleted file mode 100644 index e6cdd766b033b..0000000000000 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningApp.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. role:: hidden - :class: hidden-section -.. currentmodule:: lightning_app.core - - -LightningApp -============ - -.. autoclass:: LightningApp - :members: - :noindex: - -.. - autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst deleted file mode 100644 index 52391c7043867..0000000000000 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningFlow.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. role:: hidden - :class: hidden-section -.. currentmodule:: lightning_app.core - - -LightningFlow -============= - -.. autoclass:: LightningFlow - :members: - :noindex: - -.. - autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: diff --git a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst b/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst deleted file mode 100644 index c0540bfa3994a..0000000000000 --- a/docs/source-app/api_reference/api/lightning_app.core.LightningWork.rst +++ /dev/null @@ -1,17 +0,0 @@ -:orphan: - -.. role:: hidden - :class: hidden-section -.. currentmodule:: lightning_app.core - - -LightningWork -============= - -.. autoclass:: LightningWork - :members: - :noindex: - -.. - autogenerated from source-app/_templates/classtemplate.rst - note it does not have :inherited-members: From 9b70eec4dbf14d9b7ac74d393368491b9fc1f3b3 Mon Sep 17 00:00:00 2001 From: Jirka Date: Mon, 11 Jul 2022 17:39:16 +0200 Subject: [PATCH 044/119] make --- docs/Makefile | 19 ------------------ docs/source-app/Makefile | 2 +- docs/{ => source-app}/make.bat | 4 ++-- docs/source-pytorch/Makefile | 2 +- docs/source-pytorch/make.bat | 35 ++++++++++++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 23 deletions(-) delete mode 100644 docs/Makefile rename docs/{ => source-app}/make.bat (93%) create mode 100644 docs/source-pytorch/make.bat diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index fe8ff0e5fb0ec..0000000000000 --- a/docs/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = source-pytorch -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source-app/Makefile b/docs/source-app/Makefile index 5dede4aa4a23f..68be4c930ef52 100644 --- a/docs/source-app/Makefile +++ b/docs/source-app/Makefile @@ -5,7 +5,7 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . -BUILDDIR = build +BUILDDIR = ../build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/make.bat b/docs/source-app/make.bat similarity index 93% rename from docs/make.bat rename to docs/source-app/make.bat index 09a132f69a33a..9b565142aecbf 100644 --- a/docs/make.bat +++ b/docs/source-app/make.bat @@ -7,8 +7,8 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=source-pytorch -set BUILDDIR=build +set SOURCEDIR=. +set BUILDDIR=../build if "%1" == "" goto help diff --git a/docs/source-pytorch/Makefile b/docs/source-pytorch/Makefile index 5dede4aa4a23f..68be4c930ef52 100644 --- a/docs/source-pytorch/Makefile +++ b/docs/source-pytorch/Makefile @@ -5,7 +5,7 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . -BUILDDIR = build +BUILDDIR = ../build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/source-pytorch/make.bat b/docs/source-pytorch/make.bat new file mode 100644 index 0000000000000..9b565142aecbf --- /dev/null +++ b/docs/source-pytorch/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=../build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From 075aba4b815bd9ab1a38333984f75f03578d9526 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:47:02 +0100 Subject: [PATCH 045/119] Update .gitignore Co-authored-by: Jirka Borovec --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index c9577efec9976..a77167a17bbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -160,5 +160,4 @@ tags .tags src/lightning_app/ui/* docs/examples/* -*docs/source-app/api_reference/api* docs/source-app/api_reference/api/* From aa09ef575ae428fb1e259e7042c1245c000442ff Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 16:48:56 +0100 Subject: [PATCH 046/119] Update .github/workflows/docs-deploy.yml Co-authored-by: Jirka Borovec --- .github/workflows/docs-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 367a71dbc5156..cb6e090e5a0e3 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -45,7 +45,7 @@ jobs: sudo apt-get update sudo apt-get install -y cmake pandoc pip --version - pip install -e . --quiet -r requirements/app/base.txt -r requirements/app/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + pip install -e . --quiet -r requirements/app/docs.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html sudo apt-get update && sudo apt-get install -y texlive-latex-extra dvipng texlive-pictures pip list shell: bash From 63e80fc690409fe0e7d3159d32052e3dc6b5b1cf Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 17:49:21 +0100 Subject: [PATCH 047/119] update --- .gitignore | 1 + MANIFEST.in | 20 +++++ examples/app_commands/.lightning | 1 + examples/app_commands/app.py | 30 ++++++++ setup.py | 2 +- src/lightning_app/cli/lightning_cli.py | 77 +++++++++++++++++-- src/lightning_app/core/api.py | 46 ++++++++++- src/lightning_app/core/app.py | 32 ++++++++ src/lightning_app/core/flow.py | 42 +++++++--- src/lightning_app/core/queues.py | 10 +++ src/lightning_app/runners/backends/backend.py | 3 +- src/lightning_app/runners/multiprocess.py | 2 + .../tests_app/core/scripts/command_example.py | 22 ++++++ tests/tests_app/core/test_lightning_flow.py | 62 ++++++++++++--- 14 files changed, 316 insertions(+), 34 deletions(-) create mode 100644 examples/app_commands/.lightning create mode 100644 examples/app_commands/app.py create mode 100644 tests/tests_app/core/scripts/command_example.py diff --git a/.gitignore b/.gitignore index 47b9bfff92523..c27a9e898de68 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cifar-10-batches-py # ctags tags .tags +src/lightning_app/ui/* diff --git a/MANIFEST.in b/MANIFEST.in index a8dbcff69b631..b2c6bd31d5624 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,23 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/examples/app_commands/.lightning b/examples/app_commands/.lightning new file mode 100644 index 0000000000000..e198944084960 --- /dev/null +++ b/examples/app_commands/.lightning @@ -0,0 +1 @@ +name: airborne-elbakyan-3490 diff --git a/examples/app_commands/app.py b/examples/app_commands/app.py new file mode 100644 index 0000000000000..4f3de3c355522 --- /dev/null +++ b/examples/app_commands/app.py @@ -0,0 +1,30 @@ +from lightning import LightningFlow +from lightning_app.core.app import LightningApp + + +class ChildFlow(LightningFlow): + def trigger_method(self, name: str): + print(name) + + def configure_commands(self): + return [{"nested_trigger_command": self.trigger_method}] + + +class FlowCommands(LightningFlow): + def __init__(self): + super().__init__() + self.names = [] + self.child_flow = ChildFlow() + + def run(self): + if len(self.names): + print(self.names) + + def trigger_method(self, name: str): + self.names.append(name) + + def configure_commands(self): + return [{"flow_trigger_command": self.trigger_method}] + self.child_flow.configure_commands() + + +app = LightningApp(FlowCommands()) diff --git a/setup.py b/setup.py index a542b3c1e0291..6d271cc40b0aa 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") +_PACKAGE_NAME = "" _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index fb39f743ec3a2..62b65aca06adf 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -1,9 +1,10 @@ import logging import os from pathlib import Path -from typing import Tuple, Union +from typing import List, Optional, Tuple, Union import click +import requests from requests.exceptions import ConnectionError from lightning_app import __version__ as ver @@ -14,6 +15,7 @@ from lightning_app.utilities.cli_helpers import _format_input_env_variables from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth +from lightning_app.utilities.state import headers_for logger = logging.getLogger(__name__) @@ -116,6 +118,73 @@ def run_app( _run_app(file, cloud, without_server, no_cache, name, blocking, open_ui, env) +@main.group() +def exec(): + """exec your application.""" + + +def retrieve_application_url(app_id_or_name: Optional[str]): + try: + url = "http://127.0.0.1:7501" + response = requests.get(f"{url}/api/v1/commands") + assert response.status_code == 200 + return url, response.json() + except ConnectionError: + from lightning_app.utilities.cloud import _get_project + from lightning_app.utilities.network import LightningClient + + client = LightningClient() + project = _get_project(client) + list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) + + if not app_id_or_name: + raise Exception("Provide an application name or id with --app_id_or_name ...") + + for lightningapp in list_lightningapps.lightningapps: + if lightningapp.id == app_id_or_name or lightningapp.name == app_id_or_name: + response = requests.get(lightningapp.status.url + "/api/v1/commands") + assert response.status_code == 200 + return lightningapp.status.url, response.json() + return None, None + + +@exec.command("app") +@click.argument("command", type=str, default="") +@click.option("--args", type=str, default=[], multiple=True, help="Env variables to be set for the app.") +@click.option("--app_id_or_name", help="The current application name", default="", type=str) +def exec_app( + command: str, + args: List[str], + app_id_or_name: Optional[str] = None, +): + """Run an app from a file.""" + url, commands = retrieve_application_url(app_id_or_name) + if url is None or commands is None: + raise Exception("We couldn't find any matching running app.") + + if not commands: + raise Exception("This application doesn't expose any commands yet.") + + command_names = [c["command"] for c in commands] + if command not in command_names: + raise Exception(f"The provided command {command} isn't available in {command_names}") + + command_metadata = [c for c in commands if c["command"] == command][0] + params = command_metadata["params"] + kwargs = {k.split("=")[0]: k.split("=")[1] for k in args} + for param in params: + if param not in kwargs: + raise Exception(f"The argument --args {param}=X hasn't been provided.") + + json = { + "command_name": command, + "command_arguments": kwargs, + "affiliation": command_metadata["affiliation"], + } + response = requests.post(url + "/api/v1/commands", json=json, headers=headers_for({})) + assert response.status_code == 200, response.json() + + @main.group(hidden=True) def fork(): """Fork an application.""" @@ -263,10 +332,4 @@ def _prepare_file(file: str) -> str: if exists: return file - if not exists and file == "quick_start.py": - from lightning_app.demo.quick_start import app - - logger.info(f"For demo purposes, Lightning will run the {app.__file__} file.") - return app.__file__ - raise FileNotFoundError(f"The provided file {file} hasn't been found.") diff --git a/src/lightning_app/core/api.py b/src/lightning_app/core/api.py index 024eb712389b2..96b27efb3a5b9 100644 --- a/src/lightning_app/core/api.py +++ b/src/lightning_app/core/api.py @@ -40,6 +40,9 @@ class SessionMiddleware: frontend_static_dir = os.path.join(FRONTEND_DIR, "static") api_app_delta_queue: Queue = None +api_commands_requests_queue: Queue = None +api_commands_metadata_queue: Queue = None + template = {"ui": {}, "app": {}} templates = Jinja2Templates(directory=FRONTEND_DIR) @@ -50,6 +53,7 @@ class SessionMiddleware: lock = Lock() app_spec: Optional[List] = None +app_commands_metadata: Optional[Dict] = None logger = logging.getLogger(__name__) @@ -59,9 +63,10 @@ class SessionMiddleware: class UIRefresher(Thread): - def __init__(self, api_publish_state_queue) -> None: + def __init__(self, api_publish_state_queue, api_commands_metadata_queue) -> None: super().__init__(daemon=True) self.api_publish_state_queue = api_publish_state_queue + self.api_commands_metadata_queue = api_commands_metadata_queue self._exit_event = Event() def run(self): @@ -78,6 +83,14 @@ def run_once(self): except queue.Empty: pass + try: + metadata = self.api_commands_metadata_queue.get(timeout=0) + with lock: + global app_commands_metadata + app_commands_metadata = metadata + except queue.Empty: + pass + def join(self, timeout: Optional[float] = None) -> None: self._exit_event.set() super().join(timeout) @@ -146,6 +159,30 @@ async def get_spec( return app_spec or [] +@fastapi_service.post("/api/v1/commands", response_class=JSONResponse) +async def post_command( + request: Request, +) -> None: + data = await request.json() + command_name = data.get("command_name", None) + if not command_name: + raise Exception("The provided command name is empty.") + command_arguments = data.get("command_arguments", None) + if not command_arguments: + raise Exception("The provided command metadata is empty.") + affiliation = data.get("affiliation", None) + if not affiliation: + raise Exception("The provided affiliation is empty.") + api_commands_requests_queue.put(await request.json()) + + +@fastapi_service.get("/api/v1/commands", response_class=JSONResponse) +async def get_commands() -> Optional[Dict]: + global app_commands_metadata + with lock: + return app_commands_metadata + + @fastapi_service.post("/api/v1/delta") async def post_delta( request: Request, @@ -279,6 +316,8 @@ async def check_is_started(self, queue): def start_server( api_publish_state_queue, api_delta_queue, + commands_requests_queue, + commands_metadata_queue, has_started_queue: Optional[Queue] = None, host="127.0.0.1", port=8000, @@ -288,16 +327,19 @@ def start_server( ): global api_app_delta_queue global global_app_state_store + global api_commands_requests_queue global app_spec app_spec = spec api_app_delta_queue = api_delta_queue + api_commands_requests_queue = commands_requests_queue + api_commands_metadata_queue = commands_metadata_queue if app_state_store is not None: global_app_state_store = app_state_store global_app_state_store.add(TEST_SESSION_UUID) - refresher = UIRefresher(api_publish_state_queue) + refresher = UIRefresher(api_publish_state_queue, api_commands_metadata_queue) refresher.setDaemon(True) refresher.start() diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index 81a1a2115e523..bfc84c7fa53d8 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -1,3 +1,4 @@ +import inspect import logging import os import pickle @@ -72,6 +73,8 @@ def __init__( # queues definition. self.delta_queue: t.Optional[BaseQueue] = None self.readiness_queue: t.Optional[BaseQueue] = None + self.commands_requests_queue: t.Optional[BaseQueue] = None + self.commands_metadata_queue: t.Optional[BaseQueue] = None self.api_publish_state_queue: t.Optional[BaseQueue] = None self.api_delta_queue: t.Optional[BaseQueue] = None self.error_queue: t.Optional[BaseQueue] = None @@ -321,6 +324,33 @@ def maybe_apply_changes(self) -> bool: self.set_state(state) self._has_updated = True + def apply_commands(self): + commands = self.root.configure_commands() + commands_metadata = [] + command_names = set() + for command in commands: + for name, method in command.items(): + if name in command_names: + raise Exception(f"The component name {name} has already been used. They need to be unique.") + command_names.add(name) + params = inspect.signature(method).parameters + commands_metadata.append( + { + "command": name, + "affiliation": method.__self__.name, + "params": list(params.keys()), + } + ) + + self.commands_metadata_queue.put(commands_metadata) + + command_query = self.get_state_changed_from_queue(self.commands_requests_queue) + if command_query: + for command in commands: + for command_name, method in command.items(): + if command_query["command_name"] == command_name: + method(**command_query["command_arguments"]) + def run_once(self): """Method used to collect changes and run the root Flow once.""" done = False @@ -345,6 +375,8 @@ def run_once(self): elif self.stage == AppStage.RESTARTING: return self._apply_restarting() + self.apply_commands() + try: self.check_error_queue() t0 = time() diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index a5dcfd0a77e2e..7a1e36492a521 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -79,15 +79,19 @@ def __init__(self): .. doctest:: >>> from lightning import LightningFlow + ... >>> class RootFlow(LightningFlow): + ... ... def __init__(self): ... super().__init__() ... self.counter = 0 + ... ... def run(self): ... self.counter += 1 ... >>> flow = RootFlow() >>> flow.run() + ... >>> assert flow.counter == 1 >>> assert flow.state["vars"]["counter"] == 1 """ @@ -352,12 +356,11 @@ def schedule( from lightning_app import LightningFlow - class Flow(LightningFlow): + def run(self): if self.schedule("hourly"): # run some code once every hour. - print("run this every hour") Arguments: cron_pattern: The cron pattern to provide. Learn more at https://crontab.guru/. @@ -372,8 +375,8 @@ def run(self): from lightning_app import LightningFlow from lightning_app.structures import List - class SchedulerDAG(LightningFlow): + def __init__(self): super().__init__() self.dags = List() @@ -484,10 +487,8 @@ def configure_layout(self) -> Union[Dict[str, Any], List[Dict[str, Any]], Fronte from lightning_app.frontend import StaticWebFrontend - class Flow(LightningFlow): ... - def configure_layout(self): return StaticWebFrontend("path/to/folder/to/serve") @@ -497,19 +498,13 @@ def configure_layout(self): from lightning_app.frontend import StaticWebFrontend - class Flow(LightningFlow): ... - def configure_layout(self): return StreamlitFrontend(render_fn=my_streamlit_ui) - def my_streamlit_ui(state): # add your streamlit code here! - import streamlit as st - - st.button("Hello!") **Example:** Arrange the UI of my children in tabs (default UI by Lightning). @@ -517,11 +512,11 @@ def my_streamlit_ui(state): class Flow(LightningFlow): ... - def configure_layout(self): return [ dict(name="First Tab", content=self.child0), dict(name="Second Tab", content=self.child1), + ... # You can include direct URLs too dict(name="Lightning", content="https://lightning.ai"), ] @@ -608,3 +603,26 @@ def experimental_iterate(self, iterable: Iterable, run_once: bool = True, user_k yield value self._calls[call_hash].update({"has_finished": True}) + + def configure_commands(self): + """Configure the commands of this LightningFlow. + + **Example:** Returns a list of dictionaries mapping a client callback to a flow method. + + .. code-block:: python + + class Flow(LightningFlow): + ... + def __init__(self): + super().__init__() + self.names = [] + + def handle_name_request(name: str) + self.names.append(name) + + def configure_commands(self): + return [ + {"add_name": self.handle_name_request} + ] + """ + raise NotImplementedError diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index 3b88d896536fe..640d369977d80 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -36,6 +36,8 @@ ORCHESTRATOR_COPY_REQUEST_CONSTANT = "ORCHESTRATOR_COPY_REQUEST" ORCHESTRATOR_COPY_RESPONSE_CONSTANT = "ORCHESTRATOR_COPY_RESPONSE" WORK_QUEUE_CONSTANT = "WORK_QUEUE" +COMMANDS_REQUESTS_QUEUE_CONSTANT = "COMMANDS_REQUESTS_QUEUE" +COMMANDS_METADATA_QUEUE_CONSTANT = "COMMANDS_METADATA_QUEUE" class QueuingSystem(Enum): @@ -51,6 +53,14 @@ def _get_queue(self, queue_name: str) -> "BaseQueue": else: return SingleProcessQueue(queue_name, default_timeout=STATE_UPDATE_TIMEOUT) + def get_commands_requests_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": + queue_name = f"{queue_id}_{COMMANDS_REQUESTS_QUEUE_CONSTANT}" if queue_id else COMMANDS_REQUESTS_QUEUE_CONSTANT + return self._get_queue(queue_name) + + def get_commands_metadata_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": + queue_name = f"{queue_id}_{COMMANDS_METADATA_QUEUE_CONSTANT}" if queue_id else COMMANDS_METADATA_QUEUE_CONSTANT + return self._get_queue(queue_name) + def get_readiness_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": queue_name = f"{queue_id}_{READINESS_QUEUE_CONSTANT}" if queue_id else READINESS_QUEUE_CONSTANT return self._get_queue(queue_name) diff --git a/src/lightning_app/runners/backends/backend.py b/src/lightning_app/runners/backends/backend.py index 80ceb105bbbd1..643f15f0cf6d7 100644 --- a/src/lightning_app/runners/backends/backend.py +++ b/src/lightning_app/runners/backends/backend.py @@ -82,9 +82,10 @@ def _prepare_queues(self, app): kw = dict(queue_id=self.queue_id) app.delta_queue = self.queues.get_delta_queue(**kw) app.readiness_queue = self.queues.get_readiness_queue(**kw) + app.commands_requests_queue = self.queues.get_commands_requests_queue(**kw) + app.commands_metadata_queue = self.queues.get_commands_metadata_queue(**kw) app.error_queue = self.queues.get_error_queue(**kw) app.delta_queue = self.queues.get_delta_queue(**kw) - app.readiness_queue = self.queues.get_readiness_queue(**kw) app.error_queue = self.queues.get_error_queue(**kw) app.api_publish_state_queue = self.queues.get_api_state_publish_queue(**kw) app.api_delta_queue = self.queues.get_api_delta_queue(**kw) diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 4c58c816c566c..3ec4ebf9206ae 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -66,6 +66,8 @@ def dispatch(self, *args: Any, on_before_run: Optional[Callable] = None, **kwarg api_publish_state_queue=self.app.api_publish_state_queue, api_delta_queue=self.app.api_delta_queue, has_started_queue=has_started_queue, + commands_requests_queue=self.app.commands_requests_queue, + commands_metadata_queue=self.app.commands_metadata_queue, spec=extract_metadata_from_app(self.app), ) server_proc = multiprocessing.Process(target=start_server, kwargs=kwargs) diff --git a/tests/tests_app/core/scripts/command_example.py b/tests/tests_app/core/scripts/command_example.py new file mode 100644 index 0000000000000..f9bba430d03be --- /dev/null +++ b/tests/tests_app/core/scripts/command_example.py @@ -0,0 +1,22 @@ +from lightning import LightningFlow +from lightning_app.core.app import LightningApp + + +class FlowCommands(LightningFlow): + def __init__(self): + super().__init__() + self.names = [] + + def run(self): + if len(self.names): + print(self.names) + self._exit() + + def trigger_method(self, name: str): + self.names.append(name) + + def configure_commands(self): + return [{"user_command": self.trigger_method}] + + +app = LightningApp(FlowCommands()) diff --git a/tests/tests_app/core/test_lightning_flow.py b/tests/tests_app/core/test_lightning_flow.py index 26841e057621b..7ac7a72d08313 100644 --- a/tests/tests_app/core/test_lightning_flow.py +++ b/tests/tests_app/core/test_lightning_flow.py @@ -3,21 +3,24 @@ from collections import Counter from copy import deepcopy from dataclasses import dataclass -from time import time +from multiprocessing import Process +from time import sleep, time from unittest.mock import ANY import pytest +from click.testing import CliRunner from deepdiff import DeepDiff, Delta -from lightning_app import LightningApp -from lightning_app.core.flow import LightningFlow -from lightning_app.core.work import LightningWork -from lightning_app.runners import MultiProcessRuntime, SingleProcessRuntime -from lightning_app.storage import Path -from lightning_app.storage.path import storage_root_dir -from lightning_app.testing.helpers import EmptyFlow, EmptyWork -from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef -from lightning_app.utilities.exceptions import ExitAppException +from lightning.app import LightningApp +from lightning.app.cli.lightning_cli import exec_app +from lightning.app.core.flow import LightningFlow +from lightning.app.core.work import LightningWork +from lightning.app.runners import MultiProcessRuntime, SingleProcessRuntime +from lightning.app.storage import Path +from lightning.app.storage.path import storage_root_dir +from lightning.app.testing.helpers import EmptyFlow, EmptyWork +from lightning.app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef +from lightning.app.utilities.exceptions import ExitAppException def test_empty_component(): @@ -543,7 +546,7 @@ def run(self): def test_flow_path_assignment(): - """Test that paths in the lit format lit:// get converted to a proper lightning_app.storage.Path object.""" + """Test that paths in the lit format lit:// get converted to a proper lightning.app.storage.Path object.""" class Flow(LightningFlow): def __init__(self): @@ -635,3 +638,40 @@ def run(self): assert len(self._calls["scheduling"]) == 8 Flow().run() + + +class FlowCommands(LightningFlow): + def __init__(self): + super().__init__() + self.names = [] + + def run(self): + if len(self.names): + print(self.names) + self._exit() + + def trigger_method(self, name: str): + self.names.append(name) + + def configure_commands(self): + return [{"user_command": self.trigger_method}] + + +def target(): + app = LightningApp(FlowCommands()) + MultiProcessRuntime(app).dispatch() + + +def test_configure_commands(): + process = Process(target=target) + process.start() + sleep(5) + runner = CliRunner() + result = runner.invoke( + exec_app, + ["user_command", "--args", "name=something"], + catch_exceptions=False, + ) + sleep(2) + assert result.exit_code == 0 + assert process.exitcode == 0 From 8c940420cb8197c51c0fd3a87e62a28871412190 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 17:51:32 +0100 Subject: [PATCH 048/119] update --- .gitignore | 1 + src/lightning_app/core/flow.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c27a9e898de68..f5c967ad77390 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ cifar-10-batches-py tags .tags src/lightning_app/ui/* +examples/template_react_ui/* \ No newline at end of file diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index 7a1e36492a521..e8b9ff55e6c45 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -607,7 +607,7 @@ def experimental_iterate(self, iterable: Iterable, run_once: bool = True, user_k def configure_commands(self): """Configure the commands of this LightningFlow. - **Example:** Returns a list of dictionaries mapping a client callback to a flow method. + Returns a list of dictionaries mapping a command name to a flow method. .. code-block:: python @@ -624,5 +624,18 @@ def configure_commands(self): return [ {"add_name": self.handle_name_request} ] + + Once the app is running with the following command: + + .. code-block:: bash + + lightning run app app.py command + + .. code-block:: bash + + lightning exec app add_name --args name=my_own_name + + + """ raise NotImplementedError From 39e50369c0e1e4d29d0423349baa6412f43a297d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:53:25 +0000 Subject: [PATCH 049/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- src/lightning_app/core/flow.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f5c967ad77390..9574088077f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cifar-10-batches-py tags .tags src/lightning_app/ui/* -examples/template_react_ui/* \ No newline at end of file +examples/template_react_ui/* diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index e8b9ff55e6c45..3f2b044e9ab57 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -634,8 +634,5 @@ def configure_commands(self): .. code-block:: bash lightning exec app add_name --args name=my_own_name - - - """ raise NotImplementedError From 0b6c763a8e78c6f02d94d8e303dd3dc958210aef Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:00:03 +0100 Subject: [PATCH 050/119] update --- MANIFEST.in | 6 ++++++ examples/app_commands/.lightning | 2 +- src/lightning_app/core/flow.py | 3 --- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index b2c6bd31d5624..85d718e3293da 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -23,3 +23,9 @@ recursive-include src *.md recursive-include requirements *.txt recursive-include src *.md recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/examples/app_commands/.lightning b/examples/app_commands/.lightning index e198944084960..04d8bc7849e19 100644 --- a/examples/app_commands/.lightning +++ b/examples/app_commands/.lightning @@ -1 +1 @@ -name: airborne-elbakyan-3490 +name: commands diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index e8b9ff55e6c45..3f2b044e9ab57 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -634,8 +634,5 @@ def configure_commands(self): .. code-block:: bash lightning exec app add_name --args name=my_own_name - - - """ raise NotImplementedError From 2e8e40636be5bced94995c8aa803573aea065ddb Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:03:59 +0100 Subject: [PATCH 051/119] update --- .gitignore | 2 ++ .../lightning_app/communication_content.rst | 14 -------------- .../core_api/lightning_work/compute_content.rst | 2 +- .../source-app/core_api/lightning_work/payload.rst | 2 +- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index c9577efec9976..36984660a5680 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,5 @@ src/lightning_app/ui/* docs/examples/* *docs/source-app/api_reference/api* docs/source-app/api_reference/api/* +examples/template_react_ui +examples/template_react_ui/* \ No newline at end of file diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index 1191d61b9cbdd..ce2e77752dddd 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -97,20 +97,6 @@ And here's the output you get when running the App using the **Lightning CLI**: ---- -************************************************* -Communication from LightningFlow to LightningFlow -************************************************* - -Every LightningFlow (Flow) can access the state of any of its children Flow's. - -Here's an example to better understand communication from Flow to Flow. - -.. code-block:: python - - import lightning as L - ----- - ************************************************* Communication from LightningFlow to LightningWork ************************************************* diff --git a/docs/source-app/core_api/lightning_work/compute_content.rst b/docs/source-app/core_api/lightning_work/compute_content.rst index 9fe9a1c59c56b..68853c949e12c 100644 --- a/docs/source-app/core_api/lightning_work/compute_content.rst +++ b/docs/source-app/core_api/lightning_work/compute_content.rst @@ -66,7 +66,7 @@ The up-to-date prices for these instances can be found `here `_. +**Prerequisite**: Reach Level 16+, know about the `pandas DataFrames `_ and read and read the `Access app state guide <../../access_app_state.html>`_. ---- From 8c74f2fb321eb6ac20826e0e1a66b70e58b897b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:06:32 +0000 Subject: [PATCH 052/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d9d277d3f3bde..62d4b8e870759 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,4 @@ src/lightning_app/ui/* docs/examples/* docs/source-app/api_reference/api/* examples/template_react_ui -examples/template_react_ui/* \ No newline at end of file +examples/template_react_ui/* From 608c4a615cc50a7581a8fd43ec68115f5f5d1d25 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:08:22 +0100 Subject: [PATCH 053/119] update --- MANIFEST.in | 4 ++++ src/lightning_app/core/app.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 85d718e3293da..d559f6c0f22c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -29,3 +29,7 @@ recursive-include src *.md recursive-include requirements *.txt recursive-include src *.md recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index bfc84c7fa53d8..5d04c00d9c1b0 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -16,7 +16,7 @@ from lightning_app.core.queues import BaseQueue, SingleProcessQueue from lightning_app.frontend import Frontend from lightning_app.storage.path import storage_root_dir -from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef +from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef, is_overridden from lightning_app.utilities.component import _convert_paths_after_init from lightning_app.utilities.enum import AppStage from lightning_app.utilities.exceptions import CacheMissException, ExitAppException @@ -325,6 +325,10 @@ def maybe_apply_changes(self) -> bool: self._has_updated = True def apply_commands(self): + if not is_overridden("configure_commands", self.root): + return + + # Populate commands metadata commands = self.root.configure_commands() commands_metadata = [] command_names = set() @@ -344,6 +348,7 @@ def apply_commands(self): self.commands_metadata_queue.put(commands_metadata) + # Collect requests metadata command_query = self.get_state_changed_from_queue(self.commands_requests_queue) if command_query: for command in commands: From 772d4a574927e0e5712af641d697cadb704ac784 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:24:55 +0100 Subject: [PATCH 054/119] update --- src/lightning_app/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/lightning_app/CHANGELOG.md diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md new file mode 100644 index 0000000000000..666d9c4912f76 --- /dev/null +++ b/src/lightning_app/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). + + +## [0.5.2] - 2022-MM-DD + +### Added + +- Add support for Lightning App Commands through the `configure_commands` hook on the Lightning Flow ([#13602](https://github.com/PyTorchLightning/pytorch-lightning/pull/13602)) + + +### Changed + +### Deprecated + +### Fixed \ No newline at end of file From dca63dec025ee3284e578c8ccd8510ebcea2cdc0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:25:37 +0100 Subject: [PATCH 055/119] update --- MANIFEST.in | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index d559f6c0f22c3..f9f1be1d09e1f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,34 +2,4 @@ exclude *.toml # project config exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py -include *.cff # citation info -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt +include *.cff # citation info \ No newline at end of file From 0091f4ef30db4980d5e1baf87d2ab1ba8a9ad09b Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:25:59 +0100 Subject: [PATCH 056/119] update --- MANIFEST.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index f9f1be1d09e1f..bf2f2c7b9706b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ exclude *.toml # project config exclude requirements.txt exclude __pycache__ -include .actions/setup_tools.py -include *.cff # citation info \ No newline at end of file +include .actions/setup_tools.py \ No newline at end of file From ee3f7ad0f972bef69bcb05337e8b2ca6d6087d58 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:26:25 +0100 Subject: [PATCH 057/119] update --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6d271cc40b0aa..a542b3c1e0291 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = "" +_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From b06ec89b6591c7b1c883ebe4ec60f0cc8129cc32 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:31:54 +0000 Subject: [PATCH 058/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- MANIFEST.in | 2 +- src/lightning_app/CHANGELOG.md | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index bf2f2c7b9706b..f388cf0d66a8c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ exclude *.toml # project config exclude requirements.txt exclude __pycache__ -include .actions/setup_tools.py \ No newline at end of file +include .actions/setup_tools.py diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md index 666d9c4912f76..b881d12779874 100644 --- a/src/lightning_app/CHANGELOG.md +++ b/src/lightning_app/CHANGELOG.md @@ -4,16 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - -## [0.5.2] - 2022-MM-DD +## \[0.5.2\] - 2022-MM-DD ### Added - Add support for Lightning App Commands through the `configure_commands` hook on the Lightning Flow ([#13602](https://github.com/PyTorchLightning/pytorch-lightning/pull/13602)) - ### Changed ### Deprecated -### Fixed \ No newline at end of file +### Fixed From 59661c2bf9a974a9d1fa4fdf9ab0f065d3fa4fe1 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:35:02 +0100 Subject: [PATCH 059/119] update --- .gitignore | 2 +- tests/tests_app/core/test_lightning_api.py | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9574088077f4c..ad4422b1a7ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cifar-10-batches-py tags .tags src/lightning_app/ui/* -examples/template_react_ui/* +*examples/template_react_ui* diff --git a/tests/tests_app/core/test_lightning_api.py b/tests/tests_app/core/test_lightning_api.py index 81ba6fe0ba179..0173d45fcf9ce 100644 --- a/tests/tests_app/core/test_lightning_api.py +++ b/tests/tests_app/core/test_lightning_api.py @@ -161,10 +161,11 @@ def test_update_publish_state_and_maybe_refresh_ui(): app = AppStageTestingApp(FlowA(), debug=True) publish_state_queue = MockQueue("publish_state_queue") + commands_metadata_queue = MockQueue("commands_metadata_queue") publish_state_queue.put(app.state_with_changes) - thread = UIRefresher(publish_state_queue) + thread = UIRefresher(publish_state_queue, commands_metadata_queue) thread.run_once() assert global_app_state_store.get_app_state("1234") == app.state_with_changes @@ -190,11 +191,19 @@ def get(self, timeout: int = 0): publish_state_queue = InfiniteQueue("publish_state_queue") change_state_queue = MockQueue("change_state_queue") has_started_queue = MockQueue("has_started_queue") + commands_requests_queue = MockQueue("commands_requests_queue") + commands_metadata_queue = MockQueue("commands_metadata_queue") state = app.state_with_changes publish_state_queue.put(state) spec = extract_metadata_from_app(app) ui_refresher = start_server( - publish_state_queue, change_state_queue, has_started_queue=has_started_queue, uvicorn_run=False, spec=spec + publish_state_queue, + change_state_queue, + commands_requests_queue, + commands_metadata_queue, + has_started_queue=has_started_queue, + uvicorn_run=False, + spec=spec, ) headers = headers_for({"type": x_lightning_type}) @@ -331,10 +340,14 @@ def test_start_server_started(): api_publish_state_queue = mp.Queue() api_delta_queue = mp.Queue() has_started_queue = mp.Queue() + commands_requests_queue = mp.Queue() + commands_metadata_queue = mp.Queue() kwargs = dict( api_publish_state_queue=api_publish_state_queue, api_delta_queue=api_delta_queue, has_started_queue=has_started_queue, + commands_requests_queue=commands_requests_queue, + commands_metadata_queue=commands_metadata_queue, port=1111, ) @@ -354,12 +367,16 @@ def test_start_server_info_message(ui_refresher, uvicorn_run, caplog, monkeypatc api_publish_state_queue = MockQueue() api_delta_queue = MockQueue() has_started_queue = MockQueue() + commands_requests_queue = MockQueue() + commands_metadata_queue = MockQueue() kwargs = dict( host=host, port=1111, api_publish_state_queue=api_publish_state_queue, api_delta_queue=api_delta_queue, has_started_queue=has_started_queue, + commands_requests_queue=commands_requests_queue, + commands_metadata_queue=commands_metadata_queue, ) monkeypatch.setattr(api, "logger", logging.getLogger()) From 239a879217b670a824f5223acce2685d2e0df9d0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:36:03 +0100 Subject: [PATCH 060/119] update --- MANIFEST.in | 1 + value_all | Bin 0 -> 28 bytes value_b | Bin 0 -> 16 bytes value_c | Bin 0 -> 16 bytes 4 files changed, 1 insertion(+) create mode 100644 value_all create mode 100644 value_b create mode 100644 value_c diff --git a/MANIFEST.in b/MANIFEST.in index f388cf0d66a8c..a8dbcff69b631 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ exclude *.toml # project config exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py +include *.cff # citation info diff --git a/value_all b/value_all new file mode 100644 index 0000000000000000000000000000000000000000..1de6bc3eb49615e97739125faaa8dd060d2677c3 GIT binary patch literal 28 dcmZo*nJUNt0kKmwdKew2^e{RBvGbHvJpfeS2LJ#7 literal 0 HcmV?d00001 diff --git a/value_b b/value_b new file mode 100644 index 0000000000000000000000000000000000000000..8d6af037feec3cdbdf3678362270d72f19616a7b GIT binary patch literal 16 ScmZo*naaul0X>XPQ}h58j{>j& literal 0 HcmV?d00001 diff --git a/value_c b/value_c new file mode 100644 index 0000000000000000000000000000000000000000..01cf19d810572605e27c594c63551c10bb4365d5 GIT binary patch literal 16 ScmZo*naaul0X>Y)Q}h58k^->+ literal 0 HcmV?d00001 From 855148247864de31a364e99b21c5d97aae4677ae Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:36:28 +0100 Subject: [PATCH 061/119] update --- MANIFEST.in | 2 ++ setup.py | 2 +- value_all | Bin 28 -> 0 bytes value_b | Bin 16 -> 0 bytes value_c | Bin 16 -> 0 bytes 5 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 value_all delete mode 100644 value_b delete mode 100644 value_c diff --git a/MANIFEST.in b/MANIFEST.in index a8dbcff69b631..ddde80aef9ed8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,5 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info +recursive-include src *.md +recursive-include requirements *.txt diff --git a/setup.py b/setup.py index a542b3c1e0291..6d271cc40b0aa 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") +_PACKAGE_NAME = "" _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/value_all b/value_all deleted file mode 100644 index 1de6bc3eb49615e97739125faaa8dd060d2677c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28 dcmZo*nJUNt0kKmwdKew2^e{RBvGbHvJpfeS2LJ#7 diff --git a/value_b b/value_b deleted file mode 100644 index 8d6af037feec3cdbdf3678362270d72f19616a7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16 ScmZo*naaul0X>XPQ}h58j{>j& diff --git a/value_c b/value_c deleted file mode 100644 index 01cf19d810572605e27c594c63551c10bb4365d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16 ScmZo*naaul0X>Y)Q}h58k^->+ From 215452022ba426bd2935f1f0d8df9fb13dc4a390 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:40:40 +0100 Subject: [PATCH 062/119] update --- src/lightning_app/cli/lightning_cli.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 62b65aca06adf..b63c46cd8e564 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -123,13 +123,19 @@ def exec(): """exec your application.""" -def retrieve_application_url(app_id_or_name: Optional[str]): - try: - url = "http://127.0.0.1:7501" - response = requests.get(f"{url}/api/v1/commands") - assert response.status_code == 200 - return url, response.json() - except ConnectionError: +def _retrieve_application_url(app_id_or_name: Optional[str]): + failed_locally = False + + if app_id_or_name is None: + try: + url = "http://127.0.0.1:7501" + response = requests.get(f"{url}/api/v1/commands") + assert response.status_code == 200 + return url, response.json() + except ConnectionError: + failed_locally = True + + if app_id_or_name or failed_locally: from lightning_app.utilities.cloud import _get_project from lightning_app.utilities.network import LightningClient @@ -137,8 +143,10 @@ def retrieve_application_url(app_id_or_name: Optional[str]): project = _get_project(client) list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) + lightningapp_names = [lightningapp.name for lightningapp in list_lightningapps.lightningapps] + if not app_id_or_name: - raise Exception("Provide an application name or id with --app_id_or_name ...") + raise Exception(f"Provide an application name or id with --app_id_or_name=X. Found {lightningapp_names}") for lightningapp in list_lightningapps.lightningapps: if lightningapp.id == app_id_or_name or lightningapp.name == app_id_or_name: @@ -158,7 +166,7 @@ def exec_app( app_id_or_name: Optional[str] = None, ): """Run an app from a file.""" - url, commands = retrieve_application_url(app_id_or_name) + url, commands = _retrieve_application_url(app_id_or_name) if url is None or commands is None: raise Exception("We couldn't find any matching running app.") From 6c95cf53f688467c8017d18fef75aa2aa50ba0c3 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 18:42:04 +0100 Subject: [PATCH 063/119] update --- src/lightning_app/cli/lightning_cli.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index b63c46cd8e564..811835e49cfe1 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -129,9 +129,10 @@ def _retrieve_application_url(app_id_or_name: Optional[str]): if app_id_or_name is None: try: url = "http://127.0.0.1:7501" - response = requests.get(f"{url}/api/v1/commands") - assert response.status_code == 200 - return url, response.json() + resp = requests.get(f"{url}/api/v1/commands") + if resp.status_code != 200: + raise Exception(f"The server didn't process the request properly. Found {resp.json()}") + return url, resp.json() except ConnectionError: failed_locally = True @@ -150,9 +151,10 @@ def _retrieve_application_url(app_id_or_name: Optional[str]): for lightningapp in list_lightningapps.lightningapps: if lightningapp.id == app_id_or_name or lightningapp.name == app_id_or_name: - response = requests.get(lightningapp.status.url + "/api/v1/commands") - assert response.status_code == 200 - return lightningapp.status.url, response.json() + resp = requests.get(lightningapp.status.url + "/api/v1/commands") + if resp.status_code != 200: + raise Exception(f"The server didn't process the request properly. Found {resp.json()}") + return lightningapp.status.url, resp.json() return None, None @@ -189,8 +191,8 @@ def exec_app( "command_arguments": kwargs, "affiliation": command_metadata["affiliation"], } - response = requests.post(url + "/api/v1/commands", json=json, headers=headers_for({})) - assert response.status_code == 200, response.json() + resp = requests.post(url + "/api/v1/commands", json=json, headers=headers_for({})) + assert resp.status_code == 200, resp.json() @main.group(hidden=True) From 350d11e7a61da030c1db078db368c47ff1eec9f0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Mon, 11 Jul 2022 20:59:39 +0100 Subject: [PATCH 064/119] update --- MANIFEST.in | 2 -- setup.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index ddde80aef9ed8..a8dbcff69b631 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,3 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info -recursive-include src *.md -recursive-include requirements *.txt diff --git a/setup.py b/setup.py index 6d271cc40b0aa..a542b3c1e0291 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = "" +_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ From 2846842d403f6512d3f5ca88b8189b4c51b74524 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 09:08:44 +0100 Subject: [PATCH 065/119] update --- .actions/delete_cloud_lightning_apps.py | 34 +++++++++++ .actions/download_frontend.py | 5 ++ MANIFEST.in | 14 +++++ examples/app_commands/.lightning | 2 +- setup.py | 2 +- src/lightning_app/cli/lightning_cli.py | 67 ++++++--------------- src/lightning_app/core/app.py | 10 ++- src/lightning_app/core/flow.py | 2 +- src/lightning_app/utilities/cli_helpers.py | 58 +++++++++++++++++- tests/tests_app/core/test_lightning_flow.py | 4 +- 10 files changed, 142 insertions(+), 56 deletions(-) create mode 100644 .actions/delete_cloud_lightning_apps.py create mode 100644 .actions/download_frontend.py diff --git a/.actions/delete_cloud_lightning_apps.py b/.actions/delete_cloud_lightning_apps.py new file mode 100644 index 0000000000000..f682847f1a608 --- /dev/null +++ b/.actions/delete_cloud_lightning_apps.py @@ -0,0 +1,34 @@ +import os + +from lightning_cloud.openapi.rest import ApiException + +from lightning_app.utilities.cloud import _get_project +from lightning_app.utilities.network import LightningClient + +client = LightningClient() + +try: + PR_NUMBER = int(os.getenv("PR_NUMBER", None)) +except (TypeError, ValueError): + # Failed when the PR is running master or 'PR_NUMBER' isn't defined. + PR_NUMBER = "" + +APP_NAME = os.getenv("TEST_APP_NAME", "") + +project = _get_project(client) +list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) + +print([lightningapp.name for lightningapp in list_lightningapps.lightningapps]) + +for lightningapp in list_lightningapps.lightningapps: + if PR_NUMBER and APP_NAME and not lightningapp.name.startswith(f"test-{PR_NUMBER}-{APP_NAME}-"): + continue + print(f"Deleting {lightningapp.name}") + try: + res = client.lightningapp_instance_service_delete_lightningapp_instance( + project_id=project.project_id, + id=lightningapp.id, + ) + assert res == {} + except ApiException as e: + print(f"Failed to delete {lightningapp.name}. Exception {e}") diff --git a/.actions/download_frontend.py b/.actions/download_frontend.py new file mode 100644 index 0000000000000..44d38865803b5 --- /dev/null +++ b/.actions/download_frontend.py @@ -0,0 +1,5 @@ +import lightning_app +from lightning_app.utilities.packaging.lightning_utils import download_frontend + +if __name__ == "__main__": + download_frontend(lightning_app._PROJECT_ROOT) diff --git a/MANIFEST.in b/MANIFEST.in index a8dbcff69b631..2506b4fa19c0b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,17 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt +recursive-include src *.md +recursive-include requirements *.txt diff --git a/examples/app_commands/.lightning b/examples/app_commands/.lightning index 04d8bc7849e19..cebe537814b39 100644 --- a/examples/app_commands/.lightning +++ b/examples/app_commands/.lightning @@ -1 +1 @@ -name: commands +name: test_13 diff --git a/setup.py b/setup.py index a542b3c1e0291..6d271cc40b0aa 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") +_PACKAGE_NAME = "" _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 811835e49cfe1..05269f1890d94 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -12,7 +12,10 @@ from lightning_app.core.constants import get_lightning_cloud_url, LOCAL_LAUNCH_ADMIN_VIEW from lightning_app.runners.runtime import dispatch from lightning_app.runners.runtime_type import RuntimeType -from lightning_app.utilities.cli_helpers import _format_input_env_variables +from lightning_app.utilities.cli_helpers import ( + _format_input_env_variables, + _retrieve_application_url_and_available_commands, +) from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth from lightning_app.utilities.state import headers_for @@ -118,57 +121,25 @@ def run_app( _run_app(file, cloud, without_server, no_cache, name, blocking, open_ui, env) -@main.group() -def exec(): - """exec your application.""" - - -def _retrieve_application_url(app_id_or_name: Optional[str]): - failed_locally = False - - if app_id_or_name is None: - try: - url = "http://127.0.0.1:7501" - resp = requests.get(f"{url}/api/v1/commands") - if resp.status_code != 200: - raise Exception(f"The server didn't process the request properly. Found {resp.json()}") - return url, resp.json() - except ConnectionError: - failed_locally = True - - if app_id_or_name or failed_locally: - from lightning_app.utilities.cloud import _get_project - from lightning_app.utilities.network import LightningClient - - client = LightningClient() - project = _get_project(client) - list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) - - lightningapp_names = [lightningapp.name for lightningapp in list_lightningapps.lightningapps] - - if not app_id_or_name: - raise Exception(f"Provide an application name or id with --app_id_or_name=X. Found {lightningapp_names}") - - for lightningapp in list_lightningapps.lightningapps: - if lightningapp.id == app_id_or_name or lightningapp.name == app_id_or_name: - resp = requests.get(lightningapp.status.url + "/api/v1/commands") - if resp.status_code != 200: - raise Exception(f"The server didn't process the request properly. Found {resp.json()}") - return lightningapp.status.url, resp.json() - return None, None - - -@exec.command("app") +@run.command("command") @click.argument("command", type=str, default="") -@click.option("--args", type=str, default=[], multiple=True, help="Env variables to be set for the app.") -@click.option("--app_id_or_name", help="The current application name", default="", type=str) -def exec_app( +@click.option( + "--args", + type=str, + default=[], + multiple=True, + help="Arguments to be passed to the method executed in the running app.", +) +@click.option( + "--id", help="Unique identifier for the application. It can be its ID, its url or its name.", default=None, type=str +) +def command( command: str, args: List[str], - app_id_or_name: Optional[str] = None, + id: Optional[str] = None, ): - """Run an app from a file.""" - url, commands = _retrieve_application_url(app_id_or_name) + """Execute a function in a running application from its name.""" + url, commands = _retrieve_application_url_and_available_commands(id) if url is None or commands is None: raise Exception("We couldn't find any matching running app.") diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index 5d04c00d9c1b0..a93ed5261cb50 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -325,10 +325,13 @@ def maybe_apply_changes(self) -> bool: self._has_updated = True def apply_commands(self): + """This method is used to apply remotely a collection of commands (methods) from the CLI to a running + app.""" + if not is_overridden("configure_commands", self.root): return - # Populate commands metadata + # 1: Populate commands metadata commands = self.root.configure_commands() commands_metadata = [] command_names = set() @@ -346,14 +349,17 @@ def apply_commands(self): } ) + # 1.2: Pass the collected commands through the queue to the Rest API. self.commands_metadata_queue.put(commands_metadata) - # Collect requests metadata + # 2: Collect requests metadata command_query = self.get_state_changed_from_queue(self.commands_requests_queue) if command_query: for command in commands: for command_name, method in command.items(): if command_query["command_name"] == command_name: + # 2.1: Evaluate the method associated to a specific command. + # Validation is done on the CLI side. method(**command_query["command_arguments"]) def run_once(self): diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index 3f2b044e9ab57..c4295430af797 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -633,6 +633,6 @@ def configure_commands(self): .. code-block:: bash - lightning exec app add_name --args name=my_own_name + lightning run command add_name --args name=my_own_name """ raise NotImplementedError diff --git a/src/lightning_app/utilities/cli_helpers.py b/src/lightning_app/utilities/cli_helpers.py index b573440501b3e..c8e8018c49119 100644 --- a/src/lightning_app/utilities/cli_helpers.py +++ b/src/lightning_app/utilities/cli_helpers.py @@ -1,5 +1,11 @@ import re -from typing import Dict +from typing import Dict, Optional + +import requests + +from lightning_app.core.constants import APP_SERVER_PORT +from lightning_app.utilities.cloud import _get_project +from lightning_app.utilities.network import LightningClient def _format_input_env_variables(env_list: tuple) -> Dict[str, str]: @@ -35,3 +41,53 @@ def _format_input_env_variables(env_list: tuple) -> Dict[str, str]: env_vars_dict[var_name] = value return env_vars_dict + + +def _is_url(id: Optional[str]) -> bool: + if isinstance(id, str) and (id.startswith("https://") or id.startswith("http://")): + return True + return False + + +def _retrieve_application_url_and_available_commands(app_id_or_name_or_url: Optional[str]): + """This function is used to retrieve the current url associated with an id.""" + + if _is_url(app_id_or_name_or_url): + url = app_id_or_name_or_url + assert url + resp = requests.get(url + "/api/v1/commands") + if resp.status_code != 200: + raise Exception(f"The server didn't process the request properly. Found {resp.json()}") + return url, resp.json() + + # 2: If no identifier has been provided, evaluate the local application + failed_locally = False + + if app_id_or_name_or_url is None: + try: + url = f"http://127.0.0.1:{APP_SERVER_PORT}" + resp = requests.get(f"{url}/api/v1/commands") + if resp.status_code != 200: + raise Exception(f"The server didn't process the request properly. Found {resp.json()}") + return url, resp.json() + except ConnectionError: + failed_locally = True + + # 3: If an identified was provided or the local evaluation has failed, evaluate the cloud. + if app_id_or_name_or_url or failed_locally: + client = LightningClient() + project = _get_project(client) + list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) + + lightningapp_names = [lightningapp.name for lightningapp in list_lightningapps.lightningapps] + + if not app_id_or_name_or_url: + raise Exception(f"Provide an application name, id or url with --id=X. Found {lightningapp_names}") + + for lightningapp in list_lightningapps.lightningapps: + if lightningapp.id == app_id_or_name_or_url or lightningapp.name == app_id_or_name_or_url: + resp = requests.get(lightningapp.status.url + "/api/v1/commands") + if resp.status_code != 200: + raise Exception(f"The server didn't process the request properly. Found {resp.json()}") + return lightningapp.status.url, resp.json() + return None, None diff --git a/tests/tests_app/core/test_lightning_flow.py b/tests/tests_app/core/test_lightning_flow.py index 7ac7a72d08313..4b38aaa169207 100644 --- a/tests/tests_app/core/test_lightning_flow.py +++ b/tests/tests_app/core/test_lightning_flow.py @@ -12,7 +12,7 @@ from deepdiff import DeepDiff, Delta from lightning.app import LightningApp -from lightning.app.cli.lightning_cli import exec_app +from lightning.app.cli.lightning_cli import command from lightning.app.core.flow import LightningFlow from lightning.app.core.work import LightningWork from lightning.app.runners import MultiProcessRuntime, SingleProcessRuntime @@ -668,7 +668,7 @@ def test_configure_commands(): sleep(5) runner = CliRunner() result = runner.invoke( - exec_app, + command, ["user_command", "--args", "name=something"], catch_exceptions=False, ) From 4ae4e426feff6543565f33b76c0e23b34bc6d4b2 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:50:23 +0100 Subject: [PATCH 066/119] Update docs/source-app/_templates/theme_variables.jinja Co-authored-by: Rohit Gupta --- docs/source-app/_templates/theme_variables.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/_templates/theme_variables.jinja b/docs/source-app/_templates/theme_variables.jinja index 203431909446f..447390ecc96fa 100644 --- a/docs/source-app/_templates/theme_variables.jinja +++ b/docs/source-app/_templates/theme_variables.jinja @@ -1,8 +1,8 @@ {%- set external_urls = { 'github': 'https://github.com/Lightning-AI/lightning', 'github_issues': 'https://github.com/Lightning-AI/lightning/issues', - 'contributing': 'https://github.com/Lightning-AI/pytorch-lightning/blob/master/CONTRIBUTING.md', - 'governance': 'https://github.com/Lightning-AI/pytorch-lightning/blob/master/governance.md', + 'contributing': 'https://github.com/Lightning-AI/lightning/blob/master/.github/CONTRIBUTING.md', + 'governance': 'https://github.com/Lightning-AI/lightning/blob/master/docs/source-pytorch/governance.rst', 'docs': 'https://lightning.rtfd.io/en/latest', 'twitter': 'https://twitter.com/PyTorchLightnin', 'discuss': 'https://pytorch-lightning.slack.com', From ece3f23c627222c90388643c7fd282e17c7da9d1 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:50:31 +0100 Subject: [PATCH 067/119] Update docs/source-app/api_reference/api_references.rst Co-authored-by: Rohit Gupta --- docs/source-app/api_reference/api_references.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/api_reference/api_references.rst b/docs/source-app/api_reference/api_references.rst index c6e9e30358ed5..81ab28bd5b1d0 100644 --- a/docs/source-app/api_reference/api_references.rst +++ b/docs/source-app/api_reference/api_references.rst @@ -67,8 +67,8 @@ _______ :nosignatures: :template: classtemplate_no_index.rst - ~path.Path ~drive.Drive + ~path.Path ~payload.Payload Learn more about :ref:`Storage `. From e98fd71625736329b17f306f3fbbc669e9abe6ed Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:50:40 +0100 Subject: [PATCH 068/119] Update docs/source-app/api_reference/api_references.rst Co-authored-by: Rohit Gupta --- docs/source-app/api_reference/api_references.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/api_reference/api_references.rst b/docs/source-app/api_reference/api_references.rst index 81ab28bd5b1d0..4bf110ecccb7f 100644 --- a/docs/source-app/api_reference/api_references.rst +++ b/docs/source-app/api_reference/api_references.rst @@ -32,10 +32,10 @@ ___________________ :nosignatures: :template: classtemplate_no_index.rst + ~serve.serve.ModelInferenceAPI ~python.popen.PopenPythonScript - ~python.tracer.TracerPythonScript ~serve.gradio.ServeGradio - ~serve.serve.ModelInferenceAPI + ~python.tracer.TracerPythonScript ---- From b8c76c90a5d6a7ef50a3f9e1ab0ca6bb7e9b4d33 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:50:51 +0100 Subject: [PATCH 069/119] Update docs/source-app/api_reference/api_references.rst Co-authored-by: Rohit Gupta --- docs/source-app/api_reference/api_references.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/api_reference/api_references.rst b/docs/source-app/api_reference/api_references.rst index 4bf110ecccb7f..42cfcb7aed3d5 100644 --- a/docs/source-app/api_reference/api_references.rst +++ b/docs/source-app/api_reference/api_references.rst @@ -86,5 +86,5 @@ _______ :template: classtemplate_no_index.rst ~cloud.CloudRuntime - ~singleprocess.SingleProcessRuntime ~multiprocess.MultiProcessRuntime + ~singleprocess.SingleProcessRuntime From dc7f77f9f4191acd776441ae15a2eacd8b33d0da Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:51:03 +0100 Subject: [PATCH 070/119] Update docs/source-app/code_samples/convert_pl_to_app/train.py Co-authored-by: Rohit Gupta --- docs/source-app/code_samples/convert_pl_to_app/train.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source-app/code_samples/convert_pl_to_app/train.py b/docs/source-app/code_samples/convert_pl_to_app/train.py index 8ab400cdf5c29..6dd3042577def 100644 --- a/docs/source-app/code_samples/convert_pl_to_app/train.py +++ b/docs/source-app/code_samples/convert_pl_to_app/train.py @@ -14,7 +14,6 @@ class LitAutoEncoder(pl.LightningModule): def __init__(self): super().__init__() self.encoder = nn.Sequential(nn.Linear(28 * 28, 128), nn.ReLU(), nn.Linear(128, 3)) - self.decoder = nn.Sequential(nn.Linear(3, 128), nn.ReLU(), nn.Linear(128, 28 * 28)) def forward(self, x): From c8f5bf2accce3789538a67cd753f77354db7b01e Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:52:31 +0100 Subject: [PATCH 071/119] Update docs/source-app/core_api/lightning_app/communication_content.rst Co-authored-by: Rohit Gupta --- .../source-app/core_api/lightning_app/communication_content.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index ce2e77752dddd..8e793901c53ff 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -1,6 +1,6 @@ ******************************** -Communication between components +Communication Between Components ******************************** When creating interactive Lightning Apps (App) with multiple components, you may need your components to share information with each other and rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc. From 192e991efb985b03796a60bc6f48e1ab0c66ae66 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:52:40 +0100 Subject: [PATCH 072/119] Update docs/source-app/examples/model_server_app/model_server_app_content.rst Co-authored-by: Rohit Gupta --- .../examples/model_server_app/model_server_app_content.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source-app/examples/model_server_app/model_server_app_content.rst b/docs/source-app/examples/model_server_app/model_server_app_content.rst index 0b8007aafe159..8675de939aea0 100644 --- a/docs/source-app/examples/model_server_app/model_server_app_content.rst +++ b/docs/source-app/examples/model_server_app/model_server_app_content.rst @@ -23,11 +23,11 @@ System Design In order to create such application, we need to build several components: -* A Model Train Component that trains a model and provide its trained weights +* A Model Train Component that trains a model and provides its trained weights -* A Model Server Component that serves to an API endpoint the model generated by the **Model Train Component**. +* A Model Server Component that serves as an API endpoint for the model generated by the **Model Train Component**. -* A Load Testing Component that test the model server works as expected. This could be used to CI/CD the performance of newly generated models (left to the users). +* A Load Testing Component that tests the model server works as expected. This could be used to CI/CD the performance of newly generated models (left to the users). .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/model_server_app_2.png From 824d4c8aa70ce0678d9fe48c087757e5fb8da9ef Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:52:52 +0100 Subject: [PATCH 073/119] Update docs/source-app/get_started/training_with_apps.rst Co-authored-by: Rohit Gupta --- docs/source-app/get_started/training_with_apps.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/get_started/training_with_apps.rst b/docs/source-app/get_started/training_with_apps.rst index 8c6e42037ecd5..f2dd7fbf936da 100644 --- a/docs/source-app/get_started/training_with_apps.rst +++ b/docs/source-app/get_started/training_with_apps.rst @@ -22,7 +22,7 @@ Training and beyond With `PyTorch Lightning `_, we abstracted distributed training and hardware, by organizing PyTorch code. With `Lightning Apps `_, we unified the local and cloud experience while abstracting infrastructure. -By using `PyTorch Lightning `_ and `Lightning Apps `_ +By using `PyTorch Lightning `_ and `Lightning Apps `_ together, a completely new world of possibilities emerges. .. figure:: https://pl-flash-data.s3.amazonaws.com/assets_lightning/pl_to_app_4.png From b806cbf5213dbe768398b0716c58a9c636d5d6d9 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:53:01 +0100 Subject: [PATCH 074/119] Update docs/source-app/get_started/training_with_apps.rst Co-authored-by: Rohit Gupta --- docs/source-app/get_started/training_with_apps.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/get_started/training_with_apps.rst b/docs/source-app/get_started/training_with_apps.rst index f2dd7fbf936da..16fbc8a37e3d8 100644 --- a/docs/source-app/get_started/training_with_apps.rst +++ b/docs/source-app/get_started/training_with_apps.rst @@ -78,7 +78,7 @@ Simply add ``--cloud`` to run this application in the cloud with a GPU machine lightning run app app.py --cloud -Congratulations! Now, you know how to run a `PyTorch Lightning `_ script with Lightning Apps. +Congratulations! Now, you know how to run a `PyTorch Lightning `_ script with Lightning Apps. Lightning Apps can make your ML system way more powerful, keep reading to learn how. From 4b76381a1bd0f6dfd2f307fec819af0cc81f1862 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:53:08 +0100 Subject: [PATCH 075/119] Update docs/source-app/get_started/training_with_apps.rst Co-authored-by: Rohit Gupta --- docs/source-app/get_started/training_with_apps.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/get_started/training_with_apps.rst b/docs/source-app/get_started/training_with_apps.rst index 16fbc8a37e3d8..a7061cae562fb 100644 --- a/docs/source-app/get_started/training_with_apps.rst +++ b/docs/source-app/get_started/training_with_apps.rst @@ -19,7 +19,7 @@ Evolve a model into an ML system Training and beyond ******************* -With `PyTorch Lightning `_, we abstracted distributed training and hardware, by organizing PyTorch code. +With `PyTorch Lightning `_, we abstracted distributed training and hardware, by organizing PyTorch code. With `Lightning Apps `_, we unified the local and cloud experience while abstracting infrastructure. By using `PyTorch Lightning `_ and `Lightning Apps `_ From e9ff9f81ede960d14cb5f19ca8dacbeb399b1d1c Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:53:15 +0100 Subject: [PATCH 076/119] Update docs/source-app/examples/hpo/hpo.py Co-authored-by: Rohit Gupta --- docs/source-app/examples/hpo/hpo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/hpo/hpo.py b/docs/source-app/examples/hpo/hpo.py index dc4a2d62a7a9c..fd05cc2e327e6 100644 --- a/docs/source-app/examples/hpo/hpo.py +++ b/docs/source-app/examples/hpo/hpo.py @@ -36,7 +36,7 @@ # Run the work objective_work.run(trial_id=trial._trial_id, **trial.params) - # With Lighting, the `objective_work` will run asynchronously + # With Lightning, the `objective_work` will run asynchronously # and the metric will be prodcued after X amount of time. # The Lightning Infinite Loop would have run a very large number of times by then. if objective_work.metric and not objective_work.has_told_study: From f2309e10f5fd57e0b2f10256017726115b91c9fc Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:53:28 +0100 Subject: [PATCH 077/119] Update docs/source-app/core_api/lightning_app/communication_content.rst Co-authored-by: Rohit Gupta --- .../source-app/core_api/lightning_app/communication_content.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index 8e793901c53ff..15da1301a3cc8 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -9,7 +9,7 @@ To accomplish that, Lightning components can communicate using the App State. Th All attributes of all **LightningWork (Work)** components are accessible in the **LightningFlow (Flow)** components in real-time. -By design, the Flows communicate to all **Works** within the application. However, Works can't communicate between each other directly, they must use Flows as a proxy to communicate. +By design, the Flows communicate to all **Works** within the application. However, Works can't communicate with each other directly, they must use Flows as a proxy to communicate. Once a Work is running, any updates to the Work's state is automatically communicated to the Flow, as a delta (using `DeepDiff `_). The state communication isn't bi-directional, communication is only done from Work to Flow. From fda708cfed0fca0a884b4ccbe7cbb41af5922a9a Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:53:41 +0100 Subject: [PATCH 078/119] Update docs/source-app/core_api/lightning_app/communication_content.rst Co-authored-by: Rohit Gupta --- .../source-app/core_api/lightning_app/communication_content.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/core_api/lightning_app/communication_content.rst b/docs/source-app/core_api/lightning_app/communication_content.rst index 15da1301a3cc8..8e251412f3956 100644 --- a/docs/source-app/core_api/lightning_app/communication_content.rst +++ b/docs/source-app/core_api/lightning_app/communication_content.rst @@ -71,7 +71,7 @@ Here is the associated illustration: :alt: Mechanism showing how delta are sent. :width: 100 % -Here's another example that is slightly different. Here we define a Flow and Work, where the Work increments a counter indefinitely and the Flow prints its state which contains the Work. +Here's another example that is slightly different. Here we define a Flow and Work, where the Work increments a counter indefinitely and the Flow prints its state which contain the Work. You can easily check the state of your entire app as follows: From 2e08cd35124ef8f8ca542a0a7b4eb0ede98f30e3 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:55:02 +0100 Subject: [PATCH 079/119] Update docs/source-app/core_api/lightning_app/dynamic_work_content.rst Co-authored-by: Rohit Gupta --- .../core_api/lightning_app/dynamic_work_content.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst index 293b29c7ca675..e7411b85069d6 100644 --- a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst +++ b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst @@ -41,9 +41,9 @@ There are a couple of ways you can add a dynamic Work: def run(self): if not hasattr(self, "work"): - # The `Work` component is created and attached here. + # The `Work` component is created and attached here. setattr(self, "work", Work()) - # Run the `Work` component. + # Run the `Work` component. getattr(self, "work").run() **OPTION 2:** Use the built-in Lightning classes :class:`~lightning_app.structures.Dict` or :class:`~lightning_app.structures.List` From cfa45d45b6164cb2ab11cec2ef5885e15c64105c Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:57:33 +0100 Subject: [PATCH 080/119] Update docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst Co-authored-by: Rohit Gupta --- .../examples/github_repo_runner/github_repo_runner_content.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst index e73f205d236d3..5d5106bc5b7fb 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst @@ -5,7 +5,7 @@ Objective Create a simple application where users can enter information in a UI to run a given PyTorch Lightning Script from a given Github Repo with optionally some extra python requirements and arguments. -Futhermore, the users should be able to monitor their training progress in real-time, view the logs and get the best monitored metric and associated checkpoint for their models. +Furthermore, the users should be able to monitor their training progress in real-time, view the logs, and get the best-monitored metric and associated checkpoint for their models. ---- From de252f3b1048d3ee836d386ee75ef917a868836a Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:57:50 +0100 Subject: [PATCH 081/119] Update docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst Co-authored-by: Rohit Gupta --- .../examples/github_repo_runner/github_repo_runner_content.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst index 5d5106bc5b7fb..335b21ce7f601 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst @@ -13,7 +13,7 @@ Furthermore, the users should be able to monitor their training progress in real Final Application ***************** -Here is a recording of the final application built in this example. The example is around 200 lines in total and should give you a great fundation to build your own Lightning App. +Here is a recording of the final application built in this example. The example is around 200 lines in total and should give you a great foundation to build your own Lightning App. .. raw:: html From a1460b981777f3b2c0676ec979d7d6437fe8b1b3 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 15:59:53 +0100 Subject: [PATCH 082/119] Update docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst Co-authored-by: Rohit Gupta --- .../examples/github_repo_runner/github_repo_runner_step_2.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst index 08858649df18e..c0825fa8eafa4 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst @@ -4,13 +4,13 @@ 2. Implement the PyTorch Lightning GithubRepoRunner Component ************************************************************* -The PyTorch Lightning GithubRepoRunner Component subclasses the GithubRepoRunner but tailor the execution experience to PyTorch Lightning. +The PyTorch Lightning GithubRepoRunner Component subclasses the GithubRepoRunner but tailors the execution experience to PyTorch Lightning. As a matter of fact, this component adds two primary tailored features for PyTorch Lightning users: * It injects dynamically a custom callback ``TensorboardServerLauncher`` in the PyTorch Lightning Trainer to start a tensorboard server so it can be exposed in Lightning App UI. -* Once the script has runned, the ``on_after_run`` hook of the :class:`~lightning_app.components.python.tracer.TracerPythonScript` is invoked with the script globals, meaning we can collect anything we need. In particular, we are reloading the best model, torch scripting it and storing its path in the state along side the best metric score. +* Once the script has run, the ``on_after_run`` hook of the :class:`~lightning_app.components.python.tracer.TracerPythonScript` is invoked with the script globals, meaning we can collect anything we need. In particular, we are reloading the best model, torch scripting it, and storing its path in the state alongside the best metric score. Let's dive in on how to create such a component with the code below. From cc03e3990ab578a56fb5d9a6b885f5cb903c425a Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:00:08 +0100 Subject: [PATCH 083/119] Update docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst Co-authored-by: Rohit Gupta --- .../examples/github_repo_runner/github_repo_runner_step_4.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst index 4c7a190b7bbc3..2716adbaf8328 100644 --- a/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst +++ b/docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst @@ -4,11 +4,11 @@ 4. Implement the UI with StreamLit ********************************** -In step 3, we have implemented a flow which dynamically create a work when a new request is added to the requests list. +In step 3, we have implemented a flow that dynamically creates a Work when a new request is added to the requests list. From the UI, we create 3 pages with `StreamLit `_: -* **Page 1**: Create a form with add a new request to the flow state **requests**. +* **Page 1**: Create a form to add a new request to the flow state **requests**. * **Page 2**: Iterate through all the requests and display associated information. From 690f99b54c5e62b855595d054403bcfc0b284b4b Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:00:21 +0100 Subject: [PATCH 084/119] Update docs/source-app/examples/model_server_app/model_server.py Co-authored-by: Rohit Gupta --- docs/source-app/examples/model_server_app/model_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source-app/examples/model_server_app/model_server.py b/docs/source-app/examples/model_server_app/model_server.py index fa170a42b56da..1b914b01945f6 100644 --- a/docs/source-app/examples/model_server_app/model_server.py +++ b/docs/source-app/examples/model_server_app/model_server.py @@ -27,14 +27,14 @@ def __init__( name: str, implementation: str, workers: int = 1, - **kw, + **kwargs, ): super().__init__( parallel=True, cloud_build_config=BuildConfig( requirements=["mlserver", "mlserver-sklearn"], ), - **kw, + **kwargs, ) # 1: Collect the config's. self.settings = { From 8c679cb22a136946a364d45a0826f42bf3899358 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:00:37 +0100 Subject: [PATCH 085/119] update --- .../core_api/lightning_app/dynamic_work_content.rst | 5 +++-- docs/source-app/examples/github_repo_runner/app.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst index 293b29c7ca675..5e74cd15845e5 100644 --- a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst +++ b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst @@ -60,7 +60,7 @@ There are a couple of ways you can add a dynamic Work: def run(self): if "work" not in self.dict: - # The `Work` component is attached here. + # The `Work` component is attached here. self.dict["work"] = Work() self.dict["work"].run() @@ -105,6 +105,7 @@ In order to do that, we are iterating over the list of ``jupyter_config_requests import lightning as L + class JupyterLabManager(L.LightningFlow): """This flow manages the users notebooks running within works."""" @@ -146,7 +147,7 @@ In order to do that, we are iterating over the list of ``jupyter_config_requests self.jupyter_config_requests.pop(idx) def configure_layout(self): - return StreamlitFrontend(render_fn=render_fn) + return L.app.frontend.StreamlitFrontend(render_fn=render_fn) ---- diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index 0efc0e02b3839..d8f39a9a1bd97 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -42,10 +42,10 @@ def __init__( script_args=script_args, cloud_compute=cloud_compute, cloud_build_config=BuildConfig(requirements=requirements), + **kwargs ) self.id = id self.github_repo = github_repo - self.kwargs = kwargs self.logs = [] def run(self, *args, **kwargs): From d3b189230497798c33cad82f06d64240c2e7a82d Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:01:08 +0100 Subject: [PATCH 086/119] Update docs/source-app/core_api/lightning_app/dynamic_work_content.rst Co-authored-by: Rohit Gupta --- .../core_api/lightning_app/dynamic_work_content.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst index 84d63600981e9..d1f76d4b1fe43 100644 --- a/docs/source-app/core_api/lightning_app/dynamic_work_content.rst +++ b/docs/source-app/core_api/lightning_app/dynamic_work_content.rst @@ -158,8 +158,10 @@ Continuing from the Jupyter Notebook example, in the UI, we receive the **state* .. code-block:: python + import streamlit as st + + def render_fn(state): - import streamlit as st # Step 1: Enable users to select their notebooks and create them column_1, column_2, column_3 = st.columns(3) From 5dc5f75d5fd2d8c94720f588bf39bc32c3cbb573 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:01:26 +0100 Subject: [PATCH 087/119] Update docs/source-app/examples/github_repo_runner/app.py Co-authored-by: Rohit Gupta --- docs/source-app/examples/github_repo_runner/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index d8f39a9a1bd97..18c2f47c48992 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -5,7 +5,7 @@ from copy import deepcopy from functools import partial from subprocess import Popen -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from lightning import BuildConfig, CloudCompute, LightningApp, LightningFlow from lightning.app import structures From 8114c7c8d9a3bb1d5ed7424b77ca1fce061c8d5f Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Tue, 12 Jul 2022 16:01:36 +0100 Subject: [PATCH 088/119] Update docs/source-app/examples/github_repo_runner/app.py Co-authored-by: Rohit Gupta --- docs/source-app/examples/github_repo_runner/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index 18c2f47c48992..c41f89d52ee71 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -24,7 +24,7 @@ def __init__( script_args: List[str], requirements: List[str], cloud_compute: Optional[CloudCompute] = None, - **kwargs, + **kwargs: Any, ): """The GithubRepoRunner Component clones a repo, runs a specific script with provided arguments and collect logs. From c36277402c709c590dd9f9a8478f2567b12b0acd Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 09:38:36 +0100 Subject: [PATCH 089/119] update --- .azure/gpu-tests.yml | 12 ++++++++++++ .azure/hpu-tests.yml | 12 ++++++++++++ .azure/ipu-tests.yml | 13 +++++++++++++ 3 files changed, 37 insertions(+) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index f84463a6615b3..df36f0b1ecc66 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,6 +12,18 @@ trigger: - "master" - "release/*" - "refs/tags/*" + paths: + include: + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" + exclude: + - "src/lightning_app/**" + - "tests/tests_app/**" + - "tests/tests_app_examples/**" + - "examples/app_*" + - "requirements/app/**" pr: - "master" - "release/*" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index a3041ce32daae..5754164705ede 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -9,6 +9,18 @@ trigger: - "master" - "release/*" - "refs/tags/*" + paths: + include: + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" + exclude: + - "src/lightning_app/**" + - "tests/tests_app/**" + - "tests/tests_app_examples/**" + - "examples/app_*" + - "requirements/app/**" pr: - "master" - "release/*" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 418a70d6fa72e..38ff7ef684e76 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -7,6 +7,19 @@ trigger: - master - release/* - refs/tags/* + paths: + include: + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" + exclude: + - "src/lightning_app/**" + - "tests/tests_app/**" + - "tests/tests_app_examples/**" + - "examples/app_*" + - "requirements/app/**" + pr: - master - release/* From 509640bdb5ed741719328db9dd9a937b133b2465 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 09:43:34 +0100 Subject: [PATCH 090/119] update --- MANIFEST.in | 14 ----- examples/app_commands/.lightning | 1 - examples/app_commands/app.py | 30 --------- setup.py | 2 +- src/lightning_app/README.md | 2 +- src/lightning_app/cli/lightning_cli.py | 60 +++--------------- src/lightning_app/core/api.py | 46 +------------- src/lightning_app/core/app.py | 45 +------------- src/lightning_app/core/flow.py | 52 ++++------------ src/lightning_app/core/queues.py | 10 --- src/lightning_app/runners/backends/backend.py | 3 +- src/lightning_app/runners/multiprocess.py | 2 - src/lightning_app/utilities/cli_helpers.py | 58 +---------------- tests/tests_app/core/test_lightning_api.py | 21 +------ tests/tests_app/core/test_lightning_flow.py | 62 ++++--------------- 15 files changed, 40 insertions(+), 368 deletions(-) delete mode 100644 examples/app_commands/.lightning delete mode 100644 examples/app_commands/app.py diff --git a/MANIFEST.in b/MANIFEST.in index 2506b4fa19c0b..a8dbcff69b631 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,17 +3,3 @@ exclude requirements.txt exclude __pycache__ include .actions/setup_tools.py include *.cff # citation info -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt -recursive-include src *.md -recursive-include requirements *.txt diff --git a/examples/app_commands/.lightning b/examples/app_commands/.lightning deleted file mode 100644 index cebe537814b39..0000000000000 --- a/examples/app_commands/.lightning +++ /dev/null @@ -1 +0,0 @@ -name: test_13 diff --git a/examples/app_commands/app.py b/examples/app_commands/app.py deleted file mode 100644 index 4f3de3c355522..0000000000000 --- a/examples/app_commands/app.py +++ /dev/null @@ -1,30 +0,0 @@ -from lightning import LightningFlow -from lightning_app.core.app import LightningApp - - -class ChildFlow(LightningFlow): - def trigger_method(self, name: str): - print(name) - - def configure_commands(self): - return [{"nested_trigger_command": self.trigger_method}] - - -class FlowCommands(LightningFlow): - def __init__(self): - super().__init__() - self.names = [] - self.child_flow = ChildFlow() - - def run(self): - if len(self.names): - print(self.names) - - def trigger_method(self, name: str): - self.names.append(name) - - def configure_commands(self): - return [{"flow_trigger_command": self.trigger_method}] + self.child_flow.configure_commands() - - -app = LightningApp(FlowCommands()) diff --git a/setup.py b/setup.py index 6d271cc40b0aa..a542b3c1e0291 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ from setuptools import setup -_PACKAGE_NAME = "" +_PACKAGE_NAME = os.environ.get("PACKAGE_NAME", "") _PACKAGE_MAPPING = {"pytorch": "pytorch_lightning", "app": "lightning_app"} _REAL_PKG_NAME = _PACKAGE_MAPPING.get(_PACKAGE_NAME, _PACKAGE_NAME) # https://packaging.python.org/guides/single-sourcing-package-version/ diff --git a/src/lightning_app/README.md b/src/lightning_app/README.md index 996dce559b03a..5871813a111ba 100644 --- a/src/lightning_app/README.md +++ b/src/lightning_app/README.md @@ -1,6 +1,6 @@

- + **With Lightning Apps, you build exactly what you need: from production-ready, multi-cloud ML systems to simple research demos.** diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 05269f1890d94..fb39f743ec3a2 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -1,10 +1,9 @@ import logging import os from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import Tuple, Union import click -import requests from requests.exceptions import ConnectionError from lightning_app import __version__ as ver @@ -12,13 +11,9 @@ from lightning_app.core.constants import get_lightning_cloud_url, LOCAL_LAUNCH_ADMIN_VIEW from lightning_app.runners.runtime import dispatch from lightning_app.runners.runtime_type import RuntimeType -from lightning_app.utilities.cli_helpers import ( - _format_input_env_variables, - _retrieve_application_url_and_available_commands, -) +from lightning_app.utilities.cli_helpers import _format_input_env_variables from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth -from lightning_app.utilities.state import headers_for logger = logging.getLogger(__name__) @@ -121,51 +116,6 @@ def run_app( _run_app(file, cloud, without_server, no_cache, name, blocking, open_ui, env) -@run.command("command") -@click.argument("command", type=str, default="") -@click.option( - "--args", - type=str, - default=[], - multiple=True, - help="Arguments to be passed to the method executed in the running app.", -) -@click.option( - "--id", help="Unique identifier for the application. It can be its ID, its url or its name.", default=None, type=str -) -def command( - command: str, - args: List[str], - id: Optional[str] = None, -): - """Execute a function in a running application from its name.""" - url, commands = _retrieve_application_url_and_available_commands(id) - if url is None or commands is None: - raise Exception("We couldn't find any matching running app.") - - if not commands: - raise Exception("This application doesn't expose any commands yet.") - - command_names = [c["command"] for c in commands] - if command not in command_names: - raise Exception(f"The provided command {command} isn't available in {command_names}") - - command_metadata = [c for c in commands if c["command"] == command][0] - params = command_metadata["params"] - kwargs = {k.split("=")[0]: k.split("=")[1] for k in args} - for param in params: - if param not in kwargs: - raise Exception(f"The argument --args {param}=X hasn't been provided.") - - json = { - "command_name": command, - "command_arguments": kwargs, - "affiliation": command_metadata["affiliation"], - } - resp = requests.post(url + "/api/v1/commands", json=json, headers=headers_for({})) - assert resp.status_code == 200, resp.json() - - @main.group(hidden=True) def fork(): """Fork an application.""" @@ -313,4 +263,10 @@ def _prepare_file(file: str) -> str: if exists: return file + if not exists and file == "quick_start.py": + from lightning_app.demo.quick_start import app + + logger.info(f"For demo purposes, Lightning will run the {app.__file__} file.") + return app.__file__ + raise FileNotFoundError(f"The provided file {file} hasn't been found.") diff --git a/src/lightning_app/core/api.py b/src/lightning_app/core/api.py index 96b27efb3a5b9..024eb712389b2 100644 --- a/src/lightning_app/core/api.py +++ b/src/lightning_app/core/api.py @@ -40,9 +40,6 @@ class SessionMiddleware: frontend_static_dir = os.path.join(FRONTEND_DIR, "static") api_app_delta_queue: Queue = None -api_commands_requests_queue: Queue = None -api_commands_metadata_queue: Queue = None - template = {"ui": {}, "app": {}} templates = Jinja2Templates(directory=FRONTEND_DIR) @@ -53,7 +50,6 @@ class SessionMiddleware: lock = Lock() app_spec: Optional[List] = None -app_commands_metadata: Optional[Dict] = None logger = logging.getLogger(__name__) @@ -63,10 +59,9 @@ class SessionMiddleware: class UIRefresher(Thread): - def __init__(self, api_publish_state_queue, api_commands_metadata_queue) -> None: + def __init__(self, api_publish_state_queue) -> None: super().__init__(daemon=True) self.api_publish_state_queue = api_publish_state_queue - self.api_commands_metadata_queue = api_commands_metadata_queue self._exit_event = Event() def run(self): @@ -83,14 +78,6 @@ def run_once(self): except queue.Empty: pass - try: - metadata = self.api_commands_metadata_queue.get(timeout=0) - with lock: - global app_commands_metadata - app_commands_metadata = metadata - except queue.Empty: - pass - def join(self, timeout: Optional[float] = None) -> None: self._exit_event.set() super().join(timeout) @@ -159,30 +146,6 @@ async def get_spec( return app_spec or [] -@fastapi_service.post("/api/v1/commands", response_class=JSONResponse) -async def post_command( - request: Request, -) -> None: - data = await request.json() - command_name = data.get("command_name", None) - if not command_name: - raise Exception("The provided command name is empty.") - command_arguments = data.get("command_arguments", None) - if not command_arguments: - raise Exception("The provided command metadata is empty.") - affiliation = data.get("affiliation", None) - if not affiliation: - raise Exception("The provided affiliation is empty.") - api_commands_requests_queue.put(await request.json()) - - -@fastapi_service.get("/api/v1/commands", response_class=JSONResponse) -async def get_commands() -> Optional[Dict]: - global app_commands_metadata - with lock: - return app_commands_metadata - - @fastapi_service.post("/api/v1/delta") async def post_delta( request: Request, @@ -316,8 +279,6 @@ async def check_is_started(self, queue): def start_server( api_publish_state_queue, api_delta_queue, - commands_requests_queue, - commands_metadata_queue, has_started_queue: Optional[Queue] = None, host="127.0.0.1", port=8000, @@ -327,19 +288,16 @@ def start_server( ): global api_app_delta_queue global global_app_state_store - global api_commands_requests_queue global app_spec app_spec = spec api_app_delta_queue = api_delta_queue - api_commands_requests_queue = commands_requests_queue - api_commands_metadata_queue = commands_metadata_queue if app_state_store is not None: global_app_state_store = app_state_store global_app_state_store.add(TEST_SESSION_UUID) - refresher = UIRefresher(api_publish_state_queue, api_commands_metadata_queue) + refresher = UIRefresher(api_publish_state_queue) refresher.setDaemon(True) refresher.start() diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index a93ed5261cb50..81a1a2115e523 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -1,4 +1,3 @@ -import inspect import logging import os import pickle @@ -16,7 +15,7 @@ from lightning_app.core.queues import BaseQueue, SingleProcessQueue from lightning_app.frontend import Frontend from lightning_app.storage.path import storage_root_dir -from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef, is_overridden +from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef from lightning_app.utilities.component import _convert_paths_after_init from lightning_app.utilities.enum import AppStage from lightning_app.utilities.exceptions import CacheMissException, ExitAppException @@ -73,8 +72,6 @@ def __init__( # queues definition. self.delta_queue: t.Optional[BaseQueue] = None self.readiness_queue: t.Optional[BaseQueue] = None - self.commands_requests_queue: t.Optional[BaseQueue] = None - self.commands_metadata_queue: t.Optional[BaseQueue] = None self.api_publish_state_queue: t.Optional[BaseQueue] = None self.api_delta_queue: t.Optional[BaseQueue] = None self.error_queue: t.Optional[BaseQueue] = None @@ -324,44 +321,6 @@ def maybe_apply_changes(self) -> bool: self.set_state(state) self._has_updated = True - def apply_commands(self): - """This method is used to apply remotely a collection of commands (methods) from the CLI to a running - app.""" - - if not is_overridden("configure_commands", self.root): - return - - # 1: Populate commands metadata - commands = self.root.configure_commands() - commands_metadata = [] - command_names = set() - for command in commands: - for name, method in command.items(): - if name in command_names: - raise Exception(f"The component name {name} has already been used. They need to be unique.") - command_names.add(name) - params = inspect.signature(method).parameters - commands_metadata.append( - { - "command": name, - "affiliation": method.__self__.name, - "params": list(params.keys()), - } - ) - - # 1.2: Pass the collected commands through the queue to the Rest API. - self.commands_metadata_queue.put(commands_metadata) - - # 2: Collect requests metadata - command_query = self.get_state_changed_from_queue(self.commands_requests_queue) - if command_query: - for command in commands: - for command_name, method in command.items(): - if command_query["command_name"] == command_name: - # 2.1: Evaluate the method associated to a specific command. - # Validation is done on the CLI side. - method(**command_query["command_arguments"]) - def run_once(self): """Method used to collect changes and run the root Flow once.""" done = False @@ -386,8 +345,6 @@ def run_once(self): elif self.stage == AppStage.RESTARTING: return self._apply_restarting() - self.apply_commands() - try: self.check_error_queue() t0 = time() diff --git a/src/lightning_app/core/flow.py b/src/lightning_app/core/flow.py index c4295430af797..a5dcfd0a77e2e 100644 --- a/src/lightning_app/core/flow.py +++ b/src/lightning_app/core/flow.py @@ -79,19 +79,15 @@ def __init__(self): .. doctest:: >>> from lightning import LightningFlow - ... >>> class RootFlow(LightningFlow): - ... ... def __init__(self): ... super().__init__() ... self.counter = 0 - ... ... def run(self): ... self.counter += 1 ... >>> flow = RootFlow() >>> flow.run() - ... >>> assert flow.counter == 1 >>> assert flow.state["vars"]["counter"] == 1 """ @@ -356,11 +352,12 @@ def schedule( from lightning_app import LightningFlow - class Flow(LightningFlow): + class Flow(LightningFlow): def run(self): if self.schedule("hourly"): # run some code once every hour. + print("run this every hour") Arguments: cron_pattern: The cron pattern to provide. Learn more at https://crontab.guru/. @@ -375,8 +372,8 @@ def run(self): from lightning_app import LightningFlow from lightning_app.structures import List - class SchedulerDAG(LightningFlow): + class SchedulerDAG(LightningFlow): def __init__(self): super().__init__() self.dags = List() @@ -487,8 +484,10 @@ def configure_layout(self) -> Union[Dict[str, Any], List[Dict[str, Any]], Fronte from lightning_app.frontend import StaticWebFrontend + class Flow(LightningFlow): ... + def configure_layout(self): return StaticWebFrontend("path/to/folder/to/serve") @@ -498,13 +497,19 @@ def configure_layout(self): from lightning_app.frontend import StaticWebFrontend + class Flow(LightningFlow): ... + def configure_layout(self): return StreamlitFrontend(render_fn=my_streamlit_ui) + def my_streamlit_ui(state): # add your streamlit code here! + import streamlit as st + + st.button("Hello!") **Example:** Arrange the UI of my children in tabs (default UI by Lightning). @@ -512,11 +517,11 @@ def my_streamlit_ui(state): class Flow(LightningFlow): ... + def configure_layout(self): return [ dict(name="First Tab", content=self.child0), dict(name="Second Tab", content=self.child1), - ... # You can include direct URLs too dict(name="Lightning", content="https://lightning.ai"), ] @@ -603,36 +608,3 @@ def experimental_iterate(self, iterable: Iterable, run_once: bool = True, user_k yield value self._calls[call_hash].update({"has_finished": True}) - - def configure_commands(self): - """Configure the commands of this LightningFlow. - - Returns a list of dictionaries mapping a command name to a flow method. - - .. code-block:: python - - class Flow(LightningFlow): - ... - def __init__(self): - super().__init__() - self.names = [] - - def handle_name_request(name: str) - self.names.append(name) - - def configure_commands(self): - return [ - {"add_name": self.handle_name_request} - ] - - Once the app is running with the following command: - - .. code-block:: bash - - lightning run app app.py command - - .. code-block:: bash - - lightning run command add_name --args name=my_own_name - """ - raise NotImplementedError diff --git a/src/lightning_app/core/queues.py b/src/lightning_app/core/queues.py index 640d369977d80..3b88d896536fe 100644 --- a/src/lightning_app/core/queues.py +++ b/src/lightning_app/core/queues.py @@ -36,8 +36,6 @@ ORCHESTRATOR_COPY_REQUEST_CONSTANT = "ORCHESTRATOR_COPY_REQUEST" ORCHESTRATOR_COPY_RESPONSE_CONSTANT = "ORCHESTRATOR_COPY_RESPONSE" WORK_QUEUE_CONSTANT = "WORK_QUEUE" -COMMANDS_REQUESTS_QUEUE_CONSTANT = "COMMANDS_REQUESTS_QUEUE" -COMMANDS_METADATA_QUEUE_CONSTANT = "COMMANDS_METADATA_QUEUE" class QueuingSystem(Enum): @@ -53,14 +51,6 @@ def _get_queue(self, queue_name: str) -> "BaseQueue": else: return SingleProcessQueue(queue_name, default_timeout=STATE_UPDATE_TIMEOUT) - def get_commands_requests_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": - queue_name = f"{queue_id}_{COMMANDS_REQUESTS_QUEUE_CONSTANT}" if queue_id else COMMANDS_REQUESTS_QUEUE_CONSTANT - return self._get_queue(queue_name) - - def get_commands_metadata_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": - queue_name = f"{queue_id}_{COMMANDS_METADATA_QUEUE_CONSTANT}" if queue_id else COMMANDS_METADATA_QUEUE_CONSTANT - return self._get_queue(queue_name) - def get_readiness_queue(self, queue_id: Optional[str] = None) -> "BaseQueue": queue_name = f"{queue_id}_{READINESS_QUEUE_CONSTANT}" if queue_id else READINESS_QUEUE_CONSTANT return self._get_queue(queue_name) diff --git a/src/lightning_app/runners/backends/backend.py b/src/lightning_app/runners/backends/backend.py index 643f15f0cf6d7..80ceb105bbbd1 100644 --- a/src/lightning_app/runners/backends/backend.py +++ b/src/lightning_app/runners/backends/backend.py @@ -82,10 +82,9 @@ def _prepare_queues(self, app): kw = dict(queue_id=self.queue_id) app.delta_queue = self.queues.get_delta_queue(**kw) app.readiness_queue = self.queues.get_readiness_queue(**kw) - app.commands_requests_queue = self.queues.get_commands_requests_queue(**kw) - app.commands_metadata_queue = self.queues.get_commands_metadata_queue(**kw) app.error_queue = self.queues.get_error_queue(**kw) app.delta_queue = self.queues.get_delta_queue(**kw) + app.readiness_queue = self.queues.get_readiness_queue(**kw) app.error_queue = self.queues.get_error_queue(**kw) app.api_publish_state_queue = self.queues.get_api_state_publish_queue(**kw) app.api_delta_queue = self.queues.get_api_delta_queue(**kw) diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 3ec4ebf9206ae..4c58c816c566c 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -66,8 +66,6 @@ def dispatch(self, *args: Any, on_before_run: Optional[Callable] = None, **kwarg api_publish_state_queue=self.app.api_publish_state_queue, api_delta_queue=self.app.api_delta_queue, has_started_queue=has_started_queue, - commands_requests_queue=self.app.commands_requests_queue, - commands_metadata_queue=self.app.commands_metadata_queue, spec=extract_metadata_from_app(self.app), ) server_proc = multiprocessing.Process(target=start_server, kwargs=kwargs) diff --git a/src/lightning_app/utilities/cli_helpers.py b/src/lightning_app/utilities/cli_helpers.py index c8e8018c49119..b573440501b3e 100644 --- a/src/lightning_app/utilities/cli_helpers.py +++ b/src/lightning_app/utilities/cli_helpers.py @@ -1,11 +1,5 @@ import re -from typing import Dict, Optional - -import requests - -from lightning_app.core.constants import APP_SERVER_PORT -from lightning_app.utilities.cloud import _get_project -from lightning_app.utilities.network import LightningClient +from typing import Dict def _format_input_env_variables(env_list: tuple) -> Dict[str, str]: @@ -41,53 +35,3 @@ def _format_input_env_variables(env_list: tuple) -> Dict[str, str]: env_vars_dict[var_name] = value return env_vars_dict - - -def _is_url(id: Optional[str]) -> bool: - if isinstance(id, str) and (id.startswith("https://") or id.startswith("http://")): - return True - return False - - -def _retrieve_application_url_and_available_commands(app_id_or_name_or_url: Optional[str]): - """This function is used to retrieve the current url associated with an id.""" - - if _is_url(app_id_or_name_or_url): - url = app_id_or_name_or_url - assert url - resp = requests.get(url + "/api/v1/commands") - if resp.status_code != 200: - raise Exception(f"The server didn't process the request properly. Found {resp.json()}") - return url, resp.json() - - # 2: If no identifier has been provided, evaluate the local application - failed_locally = False - - if app_id_or_name_or_url is None: - try: - url = f"http://127.0.0.1:{APP_SERVER_PORT}" - resp = requests.get(f"{url}/api/v1/commands") - if resp.status_code != 200: - raise Exception(f"The server didn't process the request properly. Found {resp.json()}") - return url, resp.json() - except ConnectionError: - failed_locally = True - - # 3: If an identified was provided or the local evaluation has failed, evaluate the cloud. - if app_id_or_name_or_url or failed_locally: - client = LightningClient() - project = _get_project(client) - list_lightningapps = client.lightningapp_instance_service_list_lightningapp_instances(project.project_id) - - lightningapp_names = [lightningapp.name for lightningapp in list_lightningapps.lightningapps] - - if not app_id_or_name_or_url: - raise Exception(f"Provide an application name, id or url with --id=X. Found {lightningapp_names}") - - for lightningapp in list_lightningapps.lightningapps: - if lightningapp.id == app_id_or_name_or_url or lightningapp.name == app_id_or_name_or_url: - resp = requests.get(lightningapp.status.url + "/api/v1/commands") - if resp.status_code != 200: - raise Exception(f"The server didn't process the request properly. Found {resp.json()}") - return lightningapp.status.url, resp.json() - return None, None diff --git a/tests/tests_app/core/test_lightning_api.py b/tests/tests_app/core/test_lightning_api.py index 0173d45fcf9ce..81ba6fe0ba179 100644 --- a/tests/tests_app/core/test_lightning_api.py +++ b/tests/tests_app/core/test_lightning_api.py @@ -161,11 +161,10 @@ def test_update_publish_state_and_maybe_refresh_ui(): app = AppStageTestingApp(FlowA(), debug=True) publish_state_queue = MockQueue("publish_state_queue") - commands_metadata_queue = MockQueue("commands_metadata_queue") publish_state_queue.put(app.state_with_changes) - thread = UIRefresher(publish_state_queue, commands_metadata_queue) + thread = UIRefresher(publish_state_queue) thread.run_once() assert global_app_state_store.get_app_state("1234") == app.state_with_changes @@ -191,19 +190,11 @@ def get(self, timeout: int = 0): publish_state_queue = InfiniteQueue("publish_state_queue") change_state_queue = MockQueue("change_state_queue") has_started_queue = MockQueue("has_started_queue") - commands_requests_queue = MockQueue("commands_requests_queue") - commands_metadata_queue = MockQueue("commands_metadata_queue") state = app.state_with_changes publish_state_queue.put(state) spec = extract_metadata_from_app(app) ui_refresher = start_server( - publish_state_queue, - change_state_queue, - commands_requests_queue, - commands_metadata_queue, - has_started_queue=has_started_queue, - uvicorn_run=False, - spec=spec, + publish_state_queue, change_state_queue, has_started_queue=has_started_queue, uvicorn_run=False, spec=spec ) headers = headers_for({"type": x_lightning_type}) @@ -340,14 +331,10 @@ def test_start_server_started(): api_publish_state_queue = mp.Queue() api_delta_queue = mp.Queue() has_started_queue = mp.Queue() - commands_requests_queue = mp.Queue() - commands_metadata_queue = mp.Queue() kwargs = dict( api_publish_state_queue=api_publish_state_queue, api_delta_queue=api_delta_queue, has_started_queue=has_started_queue, - commands_requests_queue=commands_requests_queue, - commands_metadata_queue=commands_metadata_queue, port=1111, ) @@ -367,16 +354,12 @@ def test_start_server_info_message(ui_refresher, uvicorn_run, caplog, monkeypatc api_publish_state_queue = MockQueue() api_delta_queue = MockQueue() has_started_queue = MockQueue() - commands_requests_queue = MockQueue() - commands_metadata_queue = MockQueue() kwargs = dict( host=host, port=1111, api_publish_state_queue=api_publish_state_queue, api_delta_queue=api_delta_queue, has_started_queue=has_started_queue, - commands_requests_queue=commands_requests_queue, - commands_metadata_queue=commands_metadata_queue, ) monkeypatch.setattr(api, "logger", logging.getLogger()) diff --git a/tests/tests_app/core/test_lightning_flow.py b/tests/tests_app/core/test_lightning_flow.py index 4b38aaa169207..26841e057621b 100644 --- a/tests/tests_app/core/test_lightning_flow.py +++ b/tests/tests_app/core/test_lightning_flow.py @@ -3,24 +3,21 @@ from collections import Counter from copy import deepcopy from dataclasses import dataclass -from multiprocessing import Process -from time import sleep, time +from time import time from unittest.mock import ANY import pytest -from click.testing import CliRunner from deepdiff import DeepDiff, Delta -from lightning.app import LightningApp -from lightning.app.cli.lightning_cli import command -from lightning.app.core.flow import LightningFlow -from lightning.app.core.work import LightningWork -from lightning.app.runners import MultiProcessRuntime, SingleProcessRuntime -from lightning.app.storage import Path -from lightning.app.storage.path import storage_root_dir -from lightning.app.testing.helpers import EmptyFlow, EmptyWork -from lightning.app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef -from lightning.app.utilities.exceptions import ExitAppException +from lightning_app import LightningApp +from lightning_app.core.flow import LightningFlow +from lightning_app.core.work import LightningWork +from lightning_app.runners import MultiProcessRuntime, SingleProcessRuntime +from lightning_app.storage import Path +from lightning_app.storage.path import storage_root_dir +from lightning_app.testing.helpers import EmptyFlow, EmptyWork +from lightning_app.utilities.app_helpers import _delta_to_appstate_delta, _LightningAppRef +from lightning_app.utilities.exceptions import ExitAppException def test_empty_component(): @@ -546,7 +543,7 @@ def run(self): def test_flow_path_assignment(): - """Test that paths in the lit format lit:// get converted to a proper lightning.app.storage.Path object.""" + """Test that paths in the lit format lit:// get converted to a proper lightning_app.storage.Path object.""" class Flow(LightningFlow): def __init__(self): @@ -638,40 +635,3 @@ def run(self): assert len(self._calls["scheduling"]) == 8 Flow().run() - - -class FlowCommands(LightningFlow): - def __init__(self): - super().__init__() - self.names = [] - - def run(self): - if len(self.names): - print(self.names) - self._exit() - - def trigger_method(self, name: str): - self.names.append(name) - - def configure_commands(self): - return [{"user_command": self.trigger_method}] - - -def target(): - app = LightningApp(FlowCommands()) - MultiProcessRuntime(app).dispatch() - - -def test_configure_commands(): - process = Process(target=target) - process.start() - sleep(5) - runner = CliRunner() - result = runner.invoke( - command, - ["user_command", "--args", "name=something"], - catch_exceptions=False, - ) - sleep(2) - assert result.exit_code == 0 - assert process.exitcode == 0 From 9b2b29480da0db1f2d0605d7633c30a310d36593 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 09:45:37 +0100 Subject: [PATCH 091/119] update --- src/lightning_app/CHANGELOG.md | 2 +- .../tests_app/core/scripts/command_example.py | 22 ------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 tests/tests_app/core/scripts/command_example.py diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md index b881d12779874..d1edc3553ad2a 100644 --- a/src/lightning_app/CHANGELOG.md +++ b/src/lightning_app/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added -- Add support for Lightning App Commands through the `configure_commands` hook on the Lightning Flow ([#13602](https://github.com/PyTorchLightning/pytorch-lightning/pull/13602)) +- Update the Lightning App docs ([#13537](https://github.com/PyTorchLightning/pytorch-lightning/pull/13537)) ### Changed diff --git a/tests/tests_app/core/scripts/command_example.py b/tests/tests_app/core/scripts/command_example.py deleted file mode 100644 index f9bba430d03be..0000000000000 --- a/tests/tests_app/core/scripts/command_example.py +++ /dev/null @@ -1,22 +0,0 @@ -from lightning import LightningFlow -from lightning_app.core.app import LightningApp - - -class FlowCommands(LightningFlow): - def __init__(self): - super().__init__() - self.names = [] - - def run(self): - if len(self.names): - print(self.names) - self._exit() - - def trigger_method(self, name: str): - self.names.append(name) - - def configure_commands(self): - return [{"user_command": self.trigger_method}] - - -app = LightningApp(FlowCommands()) From c3d14d5e3c4f53fbc8d427d11c95dc46d27c88b8 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 09:49:08 +0100 Subject: [PATCH 092/119] update --- .actions/{ => app}/delete_cloud_lightning_apps.py | 0 .actions/{ => app}/download_frontend.py | 0 .azure/gpu-benchmark.yml | 13 +++++++++++++ .azure/gpu-tests.yml | 1 + .azure/hpu-tests.yml | 1 + .azure/ipu-tests.yml | 1 + .gitignore | 4 ---- .pre-commit-config.yaml | 3 --- 8 files changed, 16 insertions(+), 7 deletions(-) rename .actions/{ => app}/delete_cloud_lightning_apps.py (100%) rename .actions/{ => app}/download_frontend.py (100%) diff --git a/.actions/delete_cloud_lightning_apps.py b/.actions/app/delete_cloud_lightning_apps.py similarity index 100% rename from .actions/delete_cloud_lightning_apps.py rename to .actions/app/delete_cloud_lightning_apps.py diff --git a/.actions/download_frontend.py b/.actions/app/download_frontend.py similarity index 100% rename from .actions/download_frontend.py rename to .actions/app/download_frontend.py diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 4d3eaddd41f90..fc0cd6c0f2476 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -12,6 +12,19 @@ trigger: - "master" - "release/*" - "refs/tags/*" + paths: + include: + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" + exclude: + - "src/lightning_app/**" + - "tests/tests_app/**" + - "tests/tests_app_examples/**" + - "examples/app_*" + - "requirements/app/**" + - ".actions/app/**" pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index df36f0b1ecc66..65abf964c07bc 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -24,6 +24,7 @@ trigger: - "tests/tests_app_examples/**" - "examples/app_*" - "requirements/app/**" + - ".actions/app/**" pr: - "master" - "release/*" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 5754164705ede..8d51f2343e60f 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -21,6 +21,7 @@ trigger: - "tests/tests_app_examples/**" - "examples/app_*" - "requirements/app/**" + - ".actions/app/**" pr: - "master" - "release/*" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 38ff7ef684e76..2d24e8746d7b4 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -19,6 +19,7 @@ trigger: - "tests/tests_app_examples/**" - "examples/app_*" - "requirements/app/**" + - ".actions/app/**" pr: - master diff --git a/.gitignore b/.gitignore index 2bcc0f34a848c..47b9bfff92523 100644 --- a/.gitignore +++ b/.gitignore @@ -158,7 +158,3 @@ cifar-10-batches-py # ctags tags .tags -src/lightning_app/ui/* -docs/examples/* -docs/source-app/api_reference/api/* -*examples/template_react_ui* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86d301d1119ab..0c96c9ceeae7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,7 +77,6 @@ repos: hooks: - id: black name: Format code - exclude: docs/source-app - repo: https://github.com/asottile/blacken-docs rev: v1.12.1 @@ -85,7 +84,6 @@ repos: - id: blacken-docs args: [--line-length=120] additional_dependencies: [black==21.12b0] - exclude: docs/source-app - repo: https://github.com/executablebooks/mdformat rev: 0.7.14 @@ -102,4 +100,3 @@ repos: hooks: - id: flake8 name: Check PEP8 - exclude: docs/source-app From ba4b78456970bf7d88b9e02983fa72d918a78cb2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 08:52:42 +0000 Subject: [PATCH 093/119] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source-app/examples/github_repo_runner/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source-app/examples/github_repo_runner/app.py b/docs/source-app/examples/github_repo_runner/app.py index c41f89d52ee71..a020b69c02275 100644 --- a/docs/source-app/examples/github_repo_runner/app.py +++ b/docs/source-app/examples/github_repo_runner/app.py @@ -42,7 +42,7 @@ def __init__( script_args=script_args, cloud_compute=cloud_compute, cloud_build_config=BuildConfig(requirements=requirements), - **kwargs + **kwargs, ) self.id = id self.github_repo = github_repo From c9586aec7560a046d9c7a4859d4d6d9a1ec9a7fa Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:00:53 +0100 Subject: [PATCH 094/119] update --- .azure/gpu-benchmark.yml | 28 +++++++++++++++------------- .azure/gpu-tests.yml | 28 +++++++++++++++------------- .azure/hpu-tests.yml | 28 +++++++++++++++------------- .azure/ipu-tests.yml | 27 ++++++++++++++------------- .pre-commit-config.yaml | 3 +++ 5 files changed, 62 insertions(+), 52 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index fc0cd6c0f2476..3491b2fc08cfa 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -12,19 +12,21 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" - exclude: - - "src/lightning_app/**" - - "tests/tests_app/**" - - "tests/tests_app_examples/**" - - "examples/app_*" - - "requirements/app/**" - - ".actions/app/**" + +paths: + include: + - "src/pytorch_lightning/*" + - "tests/tests_pytorch/*" + - "examples/pl_*" + - "requirements/pytorch/*" + exclude: + - "src/lightning_app/*" + - "tests/tests_app/*" + - "tests/tests_app_examples/*" + - "examples/app_*" + - "requirements/app/*" + - ".actions/app/*" + pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 65abf964c07bc..cbd56de0031ec 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,19 +12,21 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" - exclude: - - "src/lightning_app/**" - - "tests/tests_app/**" - - "tests/tests_app_examples/**" - - "examples/app_*" - - "requirements/app/**" - - ".actions/app/**" + +paths: + include: + - "src/pytorch_lightning/*" + - "tests/tests_pytorch/*" + - "examples/pl_*" + - "requirements/pytorch/*" + exclude: + - "src/lightning_app/*" + - "tests/tests_app/*" + - "tests/tests_app_examples/*" + - "examples/app_*" + - "requirements/app/*" + - ".actions/app/*" + pr: - "master" - "release/*" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 8d51f2343e60f..e6ea6f7cb4049 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -9,19 +9,21 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" - exclude: - - "src/lightning_app/**" - - "tests/tests_app/**" - - "tests/tests_app_examples/**" - - "examples/app_*" - - "requirements/app/**" - - ".actions/app/**" + +paths: + include: + - "src/pytorch_lightning/*" + - "tests/tests_pytorch/*" + - "examples/pl_*" + - "requirements/pytorch/*" + exclude: + - "src/lightning_app/*" + - "tests/tests_app/*" + - "tests/tests_app_examples/*" + - "examples/app_*" + - "requirements/app/*" + - ".actions/app/*" + pr: - "master" - "release/*" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 2d24e8746d7b4..bf457ee43f2a7 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -7,19 +7,20 @@ trigger: - master - release/* - refs/tags/* - paths: - include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" - exclude: - - "src/lightning_app/**" - - "tests/tests_app/**" - - "tests/tests_app_examples/**" - - "examples/app_*" - - "requirements/app/**" - - ".actions/app/**" + +paths: + include: + - "src/pytorch_lightning/*" + - "tests/tests_pytorch/*" + - "examples/pl_*" + - "requirements/pytorch/*" + exclude: + - "src/lightning_app/*" + - "tests/tests_app/*" + - "tests/tests_app_examples/*" + - "examples/app_*" + - "requirements/app/*" + - ".actions/app/*" pr: - master diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c96c9ceeae7e..86d301d1119ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,6 +77,7 @@ repos: hooks: - id: black name: Format code + exclude: docs/source-app - repo: https://github.com/asottile/blacken-docs rev: v1.12.1 @@ -84,6 +85,7 @@ repos: - id: blacken-docs args: [--line-length=120] additional_dependencies: [black==21.12b0] + exclude: docs/source-app - repo: https://github.com/executablebooks/mdformat rev: 0.7.14 @@ -100,3 +102,4 @@ repos: hooks: - id: flake8 name: Check PEP8 + exclude: docs/source-app From 3bea56327e5a65c2fd19f133965d2e20b74f20a6 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:05:59 +0100 Subject: [PATCH 095/119] update --- .azure/gpu-benchmark.yml | 20 ++++++-------------- .azure/gpu-tests.yml | 20 ++++++-------------- .azure/hpu-tests.yml | 20 ++++++-------------- .azure/ipu-tests.yml | 20 ++++++-------------- 4 files changed, 24 insertions(+), 56 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 3491b2fc08cfa..90696d9324852 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -12,20 +12,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - -paths: - include: - - "src/pytorch_lightning/*" - - "tests/tests_pytorch/*" - - "examples/pl_*" - - "requirements/pytorch/*" - exclude: - - "src/lightning_app/*" - - "tests/tests_app/*" - - "tests/tests_app_examples/*" - - "examples/app_*" - - "requirements/app/*" - - ".actions/app/*" + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index cbd56de0031ec..edaf65626a9c7 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,20 +12,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - -paths: - include: - - "src/pytorch_lightning/*" - - "tests/tests_pytorch/*" - - "examples/pl_*" - - "requirements/pytorch/*" - exclude: - - "src/lightning_app/*" - - "tests/tests_app/*" - - "tests/tests_app_examples/*" - - "examples/app_*" - - "requirements/app/*" - - ".actions/app/*" + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index e6ea6f7cb4049..dea5823c79231 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -9,20 +9,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - -paths: - include: - - "src/pytorch_lightning/*" - - "tests/tests_pytorch/*" - - "examples/pl_*" - - "requirements/pytorch/*" - exclude: - - "src/lightning_app/*" - - "tests/tests_app/*" - - "tests/tests_app_examples/*" - - "examples/app_*" - - "requirements/app/*" - - ".actions/app/*" + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index bf457ee43f2a7..423f492dc1e2f 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -7,20 +7,12 @@ trigger: - master - release/* - refs/tags/* - -paths: - include: - - "src/pytorch_lightning/*" - - "tests/tests_pytorch/*" - - "examples/pl_*" - - "requirements/pytorch/*" - exclude: - - "src/lightning_app/*" - - "tests/tests_app/*" - - "tests/tests_app_examples/*" - - "examples/app_*" - - "requirements/app/*" - - ".actions/app/*" + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - master From b6b78b6f659101bd5f2ad9453f01266f9abfb327 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:09:05 +0100 Subject: [PATCH 096/119] update --- .azure/gpu-benchmark.yml | 9 ++++----- .azure/gpu-tests.yml | 12 ++++++------ .azure/hpu-tests.yml | 8 ++++---- .azure/ipu-tests.yml | 8 ++++---- .github/workflows/ci-pytorch_test-full.yml | 11 +++++------ 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 90696d9324852..6d6c569ee4ec3 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -14,11 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch - + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index edaf65626a9c7..dd32aae0e947e 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,12 +12,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index dea5823c79231..ad49050970745 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,10 +11,10 @@ trigger: - "refs/tags/*" paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 423f492dc1e2f..0035afe331214 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,10 +9,10 @@ trigger: - refs/tags/* paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - master diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 4a53a1e1b10d3..2318d549c9359 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -7,12 +7,11 @@ on: # Trigger the workflow on push or pull request, but only for the master bra pull_request: branches: [master, "release/*"] types: [opened, reopened, ready_for_review, synchronize] - paths-ignore: - - "src/lightning_app/**" # todo: implement job skip - - "tests/tests_app/**" # todo: implement job skip - - "tests/tests_app_examples/**" # todo: implement job skip - - "examples/app_*" # todo: implement job skip - - "docs/source-app/**" # todo: implement job skip + paths: + - "src/pytorch_lightning" + - "tests/tests_pytorch" + - "examples/pl_*" + - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} From a7423091b9a818d58e4d9417618e567d0c658530 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:11:30 +0100 Subject: [PATCH 097/119] update --- .azure/gpu-benchmark.yml | 12 ++++++------ .azure/gpu-tests.yml | 12 ++++++------ .azure/hpu-tests.yml | 12 ++++++------ .azure/ipu-tests.yml | 12 ++++++------ .github/workflows/ci-pytorch_test-conda.yml | 11 +++++------ .github/workflows/ci-pytorch_test-slow.yml | 11 +++++------ 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 6d6c569ee4ec3..6a19ae6015ed7 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -12,12 +12,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index dd32aae0e947e..36b1c214d3567 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,12 +12,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index ad49050970745..06203a9fc79af 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -9,12 +9,12 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 0035afe331214..4a29cce42ff37 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -7,12 +7,12 @@ trigger: - master - release/* - refs/tags/* - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch pr: - master diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 4e22e757c46af..15399f46cd28d 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -6,12 +6,11 @@ on: # Trigger the workflow on push or pull request, but only for the master bra branches: [master, "release/*"] pull_request: branches: [master, "release/*"] - paths-ignore: - - "src/lightning_app/**" # todo: implement job skip - - "tests/tests_app/**" # todo: implement job skip - - "tests/tests_app_examples/**" # todo: implement job skip - - "examples/app_*" # todo: implement job skip - - "docs/source-app/**" # todo: implement job skip + paths: + - "src/pytorch_lightning" + - "tests/tests_pytorch" + - "examples/pl_*" + - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index 99d1a795cd014..131511443f28f 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -7,12 +7,11 @@ on: # Trigger the workflow on push or pull request, but only for the master bra pull_request: branches: [master, "release/*"] types: [opened, reopened, ready_for_review, synchronize] - paths-ignore: - - "src/lightning_app/**" # todo: implement job skip - - "tests/tests_app/**" # todo: implement job skip - - "tests/tests_app_examples/**" # todo: implement job skip - - "examples/app_*" # todo: implement job skip - - "docs/source-app/**" # todo: implement job skip + paths: + - "src/pytorch_lightning" + - "tests/tests_pytorch" + - "examples/pl_*" + - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} From a235c883ff0b69ab440f40577cd8b604fc416cc0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:14:44 +0100 Subject: [PATCH 098/119] update --- .azure/gpu-benchmark.yml | 8 ++++---- .azure/gpu-tests.yml | 8 ++++---- .azure/hpu-tests.yml | 8 ++++---- .azure/ipu-tests.yml | 8 ++++---- .circleci/config.yml | 7 +++++++ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 6a19ae6015ed7..73869fe87441b 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -14,10 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 36b1c214d3567..7de76a1e8db60 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,10 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 06203a9fc79af..5a248beebced3 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,10 +11,10 @@ trigger: - "refs/tags/*" paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 4a29cce42ff37..59b33f69014ad 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,10 +9,10 @@ trigger: - refs/tags/* paths: include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*" + - "requirements/pytorch/**" pr: - master diff --git a/.circleci/config.yml b/.circleci/config.yml index c608680e1c168..06fc7aa4f0847 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,13 @@ trigger: - "master" - "release/*" - "refs/tags/*" + paths: + include: + - src/pytorch_lightning + - tests/tests_pytorch + - examples/pl_* + - requirements/pytorch + pr: - "master" - "release/*" From f9c6e652f7d33eaad1b8bec02db3b06786b1ac15 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:19:00 +0100 Subject: [PATCH 099/119] update --- .azure/gpu-benchmark.yml | 6 ------ .azure/gpu-tests.yml | 5 +---- .azure/hpu-tests.yml | 5 +---- .azure/ipu-tests.yml | 5 +---- .github/workflows/ci-pytorch_test-full.yml | 2 +- 5 files changed, 4 insertions(+), 19 deletions(-) diff --git a/.azure/gpu-benchmark.yml b/.azure/gpu-benchmark.yml index 73869fe87441b..4d3eaddd41f90 100644 --- a/.azure/gpu-benchmark.yml +++ b/.azure/gpu-benchmark.yml @@ -12,12 +12,6 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" pr: none diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 7de76a1e8db60..9546c601e7f24 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,10 +14,7 @@ trigger: - "refs/tags/*" paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" + - docs pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 5a248beebced3..4a54686e17fdf 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,10 +11,7 @@ trigger: - "refs/tags/*" paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" + - docs pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 59b33f69014ad..b391ae0ca2e5b 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,10 +9,7 @@ trigger: - refs/tags/* paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*" - - "requirements/pytorch/**" + - docs pr: - master diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 2318d549c9359..8339ef3b2f2ec 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -1,4 +1,4 @@ -name: Test PyTorch full +name: Test PyTorch full # see: https://help.github.com/en/actions/reference/events-that-trigger-workflows on: # Trigger the workflow on push or pull request, but only for the master branch From 1c25b4d38bfb805a2bc95be97a19425778a2c06f Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:23:11 +0100 Subject: [PATCH 100/119] update --- .azure/gpu-tests.yml | 2 +- .azure/hpu-tests.yml | 2 +- .azure/ipu-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 9546c601e7f24..df898cea58933 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,7 +14,7 @@ trigger: - "refs/tags/*" paths: include: - - docs + - docs/source-pytorch/** pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 4a54686e17fdf..d99d33a36b78f 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,7 +11,7 @@ trigger: - "refs/tags/*" paths: include: - - docs + - docs/source-pytorch/** pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index b391ae0ca2e5b..6823d7875cf1e 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,7 +9,7 @@ trigger: - refs/tags/* paths: include: - - docs + - docs/source-pytorch/** pr: - master From ba01f8d35dfdafde2daca9cae8a422a02c5f79be Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:28:40 +0100 Subject: [PATCH 101/119] update --- .azure/gpu-tests.yml | 5 ++++- .azure/hpu-tests.yml | 5 ++++- .azure/ipu-tests.yml | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index df898cea58933..595105811789f 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,7 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - docs/source-pytorch/** + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*/**" + - "requirements/pytorch/**" pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index d99d33a36b78f..909150fe593f7 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,7 +11,10 @@ trigger: - "refs/tags/*" paths: include: - - docs/source-pytorch/** + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*/**" + - "requirements/pytorch/**" pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 6823d7875cf1e..25afe90007f2c 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,7 +9,10 @@ trigger: - refs/tags/* paths: include: - - docs/source-pytorch/** + - "src/pytorch_lightning/**" + - "tests/tests_pytorch/**" + - "examples/pl_*/**" + - "requirements/pytorch/**" pr: - master From 75686f199ce1d1621ffe5c36b83b2ee6717990ea Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:37:33 +0100 Subject: [PATCH 102/119] update --- .azure/gpu-tests.yml | 8 ++++---- .azure/hpu-tests.yml | 8 ++++---- .azure/ipu-tests.yml | 8 ++++---- .circleci/config.yml | 6 ------ 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 595105811789f..211ef4a613e75 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,10 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*/**" - - "requirements/pytorch/**" + - /src/pytorch_lightning/**/* + - /tests/tests_pytorch/**/* + - /examples/pl_*/**/* + - /requirements/pytorch/**/* pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 909150fe593f7..28b4b121d4c32 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,10 +11,10 @@ trigger: - "refs/tags/*" paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*/**" - - "requirements/pytorch/**" + - /src/pytorch_lightning/**/* + - /tests/tests_pytorch/**/* + - /examples/pl_*/**/* + - /requirements/pytorch/**/* pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 25afe90007f2c..3cdc8fc1fe775 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,10 +9,10 @@ trigger: - refs/tags/* paths: include: - - "src/pytorch_lightning/**" - - "tests/tests_pytorch/**" - - "examples/pl_*/**" - - "requirements/pytorch/**" + - /src/pytorch_lightning/**/* + - /tests/tests_pytorch/**/* + - /examples/pl_*/**/* + - /requirements/pytorch/**/* pr: - master diff --git a/.circleci/config.yml b/.circleci/config.yml index 06fc7aa4f0847..29fe5fcc1afca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,12 +14,6 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - src/pytorch_lightning - - tests/tests_pytorch - - examples/pl_* - - requirements/pytorch pr: - "master" From bfeb07e7059b927a8a650f3fd0c8818462d177ac Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 10:40:48 +0100 Subject: [PATCH 103/119] update --- .azure/gpu-tests.yml | 8 ++++---- .azure/hpu-tests.yml | 8 ++++---- .azure/ipu-tests.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 211ef4a613e75..01405bf8b269f 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -14,10 +14,10 @@ trigger: - "refs/tags/*" paths: include: - - /src/pytorch_lightning/**/* - - /tests/tests_pytorch/**/* - - /examples/pl_*/**/* - - /requirements/pytorch/**/* + - "src/pytorch_lightning/**/*" + - "tests/tests_pytorch/**/*" + - "examples/pl_*/**/*" + - "requirements/pytorch/**/*" pr: - "master" diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 28b4b121d4c32..403489f374fd7 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -11,10 +11,10 @@ trigger: - "refs/tags/*" paths: include: - - /src/pytorch_lightning/**/* - - /tests/tests_pytorch/**/* - - /examples/pl_*/**/* - - /requirements/pytorch/**/* + - "src/pytorch_lightning/**/*" + - "tests/tests_pytorch/**/*" + - "examples/pl_*/**/*" + - "requirements/pytorch/**/*" pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index 3cdc8fc1fe775..c0767e05663ac 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -9,10 +9,10 @@ trigger: - refs/tags/* paths: include: - - /src/pytorch_lightning/**/* - - /tests/tests_pytorch/**/* - - /examples/pl_*/**/* - - /requirements/pytorch/**/* + - "src/pytorch_lightning/**/*" + - "tests/tests_pytorch/**/*" + - "examples/pl_*/**/*" + - "requirements/pytorch/**/*" pr: - master From 00ba276f7bccacc601cfc3e12c74314ad0ac324e Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 11:06:13 +0100 Subject: [PATCH 104/119] update --- .actions/job_skipper.py | 6 ++++++ .github/workflows/ci-app_block.yml | 1 + .github/workflows/ci-pytorch_test-conda.yml | 19 ++++++++++++++----- 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .actions/job_skipper.py diff --git a/.actions/job_skipper.py b/.actions/job_skipper.py new file mode 100644 index 0000000000000..aa0d3e846730a --- /dev/null +++ b/.actions/job_skipper.py @@ -0,0 +1,6 @@ +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--changed_files", type=list, action="store", required=True) +hparams = parser.parse_args() +print(hparams.changed_files) diff --git a/.github/workflows/ci-app_block.yml b/.github/workflows/ci-app_block.yml index fd4ae4428b689..fa582e99acd8d 100644 --- a/.github/workflows/ci-app_block.yml +++ b/.github/workflows/ci-app_block.yml @@ -13,6 +13,7 @@ jobs: - name: Get changed files using defaults id: changed-files uses: tj-actions/changed-files@v23 + - name: List all added files run: | for file in ${{ steps.changed-files.outputs.all_changed_and_modified_files }}; do diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 15399f46cd28d..53ecf9fae8b0d 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -6,11 +6,6 @@ on: # Trigger the workflow on push or pull request, but only for the master bra branches: [master, "release/*"] pull_request: branches: [master, "release/*"] - paths: - - "src/pytorch_lightning" - - "tests/tests_pytorch" - - "examples/pl_*" - - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} @@ -34,12 +29,26 @@ jobs: - {python-version: "3.9", pytorch-version: "1.11"} timeout-minutes: 30 + steps: - name: Workaround for https://github.com/actions/checkout/issues/760 run: git config --global --add safe.directory /__w/lightning/lightning - uses: actions/checkout@v2 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v23.1 + + - name: List all changed files + run: | + set -e + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "$file was changed" + done + python ./actions/job_skipper.py --changed_files ${{ steps.changed-files.outputs.all_changed_files }} + exit 1 + - name: Update base dependencies env: PACKAGE_NAME: pytorch From bbcaa9a493f4aaa067c74c4a9fe730dff6aad30e Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 11:12:36 +0100 Subject: [PATCH 105/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 53ecf9fae8b0d..b5ffc59533734 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -46,7 +46,7 @@ jobs: for file in ${{ steps.changed-files.outputs.all_changed_files }}; do echo "$file was changed" done - python ./actions/job_skipper.py --changed_files ${{ steps.changed-files.outputs.all_changed_files }} + python .actions/job_skipper.py --changed_files ${{ steps.changed-files.outputs.all_changed_files }} exit 1 - name: Update base dependencies From 393f797bd6e1b6ff1623ebdc22d9c415f2be87ba Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 11:29:01 +0100 Subject: [PATCH 106/119] update --- .actions/job_skipper.py | 6 - .github/workflows/ci-pytorch_test-conda.yml | 25 ++- changed.txt | 212 ++++++++++++++++++++ 3 files changed, 228 insertions(+), 15 deletions(-) delete mode 100644 .actions/job_skipper.py create mode 100644 changed.txt diff --git a/.actions/job_skipper.py b/.actions/job_skipper.py deleted file mode 100644 index aa0d3e846730a..0000000000000 --- a/.actions/job_skipper.py +++ /dev/null @@ -1,6 +0,0 @@ -import argparse - -parser = argparse.ArgumentParser() -parser.add_argument("--changed_files", type=list, action="store", required=True) -hparams = parser.parse_args() -print(hparams.changed_files) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index b5ffc59533734..4c2a9e834b8fc 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -40,16 +40,19 @@ jobs: id: changed-files uses: tj-actions/changed-files@v23.1 - - name: List all changed files + - name: Decide if the test should be skipped + id: skip run: | - set -e - for file in ${{ steps.changed-files.outputs.all_changed_files }}; do - echo "$file was changed" - done - python .actions/job_skipper.py --changed_files ${{ steps.changed-files.outputs.all_changed_files }} - exit 1 + FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' + MATCHES=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | grep -E '$FILTER') + if [ -z "$MATCHES" ]; then + echo "::set-output continue="0" + else + echo "::set-output continue="1" + fi - name: Update base dependencies + if: ${{ steps.skip.outputs.continue }} == "1" env: PACKAGE_NAME: pytorch FREEZE_REQUIREMENTS: 1 @@ -59,10 +62,12 @@ jobs: pip install -e .[test] - name: DocTests + if: ${{ steps.skip.outputs.continue }} == "1" working-directory: ./src run: pytest pytorch_lightning --cov=pytorch_lightning - name: Update all dependencies + if: ${{ steps.skip.outputs.continue }} == "1" env: HOROVOD_BUILD_ARCH_FLAGS: "-mfma" HOROVOD_WITHOUT_MXNET: 1 @@ -81,9 +86,11 @@ jobs: python requirements/pytorch/check-avail-extras.py - name: Pull legacy checkpoints + if: ${{ steps.skip.outputs.continue }} == "1" run: bash .actions/pull_legacy_checkpoints.sh - name: Testing PyTorch + if: ${{ steps.skip.outputs.continue }} == "1" working-directory: tests/tests_pytorch run: coverage run --source pytorch_lightning -m pytest -v --timeout 150 --durations=50 --junitxml=results-${{ runner.os }}-torch${{ matrix.pytorch-version }}.xml @@ -95,7 +102,7 @@ jobs: if: failure() - name: Statistics - if: success() + if: success() && ${{ steps.skip.outputs.continue }} == "1" working-directory: tests/tests_pytorch run: | coverage report @@ -103,7 +110,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: always() + if: success() && ${{ steps.skip.outputs.continue }} == "1" # see: https://github.com/actions/toolkit/issues/399 continue-on-error: true with: diff --git a/changed.txt b/changed.txt new file mode 100644 index 0000000000000..877378d35e60a --- /dev/null +++ b/changed.txt @@ -0,0 +1,212 @@ +.actions/app/delete_cloud_lightning_apps.py +.actions/app/download_frontend.py +.actions/job_skipper.py +.azure/gpu-tests.yml +.azure/hpu-tests.yml +.azure/ipu-tests.yml +.circleci/config.yml +.github/workflows/ci-app_block.yml +.github/workflows/ci-pytorch_test-conda.yml +.github/workflows/ci-pytorch_test-full.yml +.github/workflows/ci-pytorch_test-slow.yml +.github/workflows/docs-checks.yml +.github/workflows/docs-deploy.yml +.pre-commit-config.yaml +docs/source-app/Makefile +docs/source-app/_templates/classtemplate.rst +docs/source-app/_templates/classtemplate_no_index.rst +docs/source-app/_templates/layout.html +docs/source-app/_templates/theme_variables.jinja +docs/source-app/api_reference/api_references.rst +docs/source-app/basics.rst +docs/source-app/code_samples/basics/0.py +docs/source-app/code_samples/basics/1.py +docs/source-app/code_samples/convert_pl_to_app/app.py +docs/source-app/code_samples/convert_pl_to_app/requirements.py +docs/source-app/code_samples/convert_pl_to_app/train.py +docs/source-app/code_samples/quickstart/app/app_0.py +docs/source-app/code_samples/quickstart/app/app_1.py +docs/source-app/code_samples/quickstart/app_01.py +docs/source-app/code_samples/quickstart/app_02.py +docs/source-app/code_samples/quickstart/app_03.py +docs/source-app/code_samples/quickstart/app_comp.py +docs/source-app/code_samples/quickstart/hello_world/app.py +docs/source-app/code_samples/quickstart/hello_world/app_ui.py +docs/source-app/conf.py +docs/source-app/contribute_app.rst +docs/source-app/core_api/core_api.rst +docs/source-app/core_api/lightning_app/app.py +docs/source-app/core_api/lightning_app/communication.rst +docs/source-app/core_api/lightning_app/communication_content.rst +docs/source-app/core_api/lightning_app/dynamic_work.rst +docs/source-app/core_api/lightning_app/dynamic_work_content.rst +docs/source-app/core_api/lightning_app/lightning_app.rst +docs/source-app/core_api/lightning_flow.rst +docs/source-app/core_api/lightning_work/compute.rst +docs/source-app/core_api/lightning_work/compute_content.rst +docs/source-app/core_api/lightning_work/handling_app_exception.rst +docs/source-app/core_api/lightning_work/handling_app_exception_content.rst +docs/source-app/core_api/lightning_work/lightning_work.rst +docs/source-app/core_api/lightning_work/payload.rst +docs/source-app/core_api/lightning_work/payload_content.rst +docs/source-app/core_api/lightning_work/status.rst +docs/source-app/core_api/lightning_work/status_content.rst +docs/source-app/examples/dag/dag.rst +docs/source-app/examples/dag/dag_from_scratch.rst +docs/source-app/examples/data_explore_app.rst +docs/source-app/examples/etl_app.rst +docs/source-app/examples/file_server/app.py +docs/source-app/examples/file_server/file_server.rst +docs/source-app/examples/file_server/file_server_content.rst +docs/source-app/examples/file_server/file_server_step_1.rst +docs/source-app/examples/file_server/file_server_step_2.rst +docs/source-app/examples/file_server/file_server_step_3.rst +docs/source-app/examples/file_server/file_server_step_4.rst +docs/source-app/examples/github_repo_runner/.lightning +docs/source-app/examples/github_repo_runner/app.py +docs/source-app/examples/github_repo_runner/github_repo_runner.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst +docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst +docs/source-app/examples/hands_on_example.rst +docs/source-app/examples/hpo/build_from_scratch.rst +docs/source-app/examples/hpo/hpo.py +docs/source-app/examples/hpo/hpo.rst +docs/source-app/examples/hpo/hpo_wi.rst +docs/source-app/examples/hpo/hpo_wo.rst +docs/source-app/examples/hpo/lightning_hpo.rst +docs/source-app/examples/hpo/lightning_hpo_target.py +docs/source-app/examples/hpo/objective.py +docs/source-app/examples/hpo/optuna_reference.py +docs/source-app/examples/model_server_app/app.py +docs/source-app/examples/model_server_app/load_testing.rst +docs/source-app/examples/model_server_app/locust_component.py +docs/source-app/examples/model_server_app/locustfile.py +docs/source-app/examples/model_server_app/model_server.py +docs/source-app/examples/model_server_app/model_server.rst +docs/source-app/examples/model_server_app/model_server_app.rst +docs/source-app/examples/model_server_app/model_server_app_content.rst +docs/source-app/examples/model_server_app/putting_everything_together.rst +docs/source-app/examples/model_server_app/train.py +docs/source-app/examples/model_server_app/train.rst +docs/source-app/examples/research_demo_app.rst +docs/source-app/get_started/add_an_interactive_demo.rst +docs/source-app/get_started/build_model.rst +docs/source-app/get_started/go_beyond_training.rst +docs/source-app/get_started/go_beyond_training_content.rst +docs/source-app/get_started/jumpstart_from_app_gallery.rst +docs/source-app/get_started/jumpstart_from_component_gallery.rst +docs/source-app/get_started/lightning_apps_intro.rst +docs/source-app/get_started/training_with_apps.rst +docs/source-app/get_started/what_app_can_do.rst +docs/source-app/glossary/app_tree.rst +docs/source-app/glossary/build_config/build_config_basic.rst +docs/source-app/glossary/dag.rst +docs/source-app/glossary/debug_app.rst +docs/source-app/glossary/distributed_fe.rst +docs/source-app/glossary/distributed_hardware.rst +docs/source-app/glossary/event_loop.rst +docs/source-app/glossary/fault_tolerance.rst +docs/source-app/glossary/index.rst +docs/source-app/glossary/lightning_app_overview/index.rst +docs/source-app/glossary/scheduling.rst +docs/source-app/glossary/sharing_components.rst +docs/source-app/glossary/storage/drive.rst +docs/source-app/glossary/storage/drive_content.rst +docs/source-app/glossary/storage/path.rst +docs/source-app/glossary/storage/storage.rst +docs/source-app/index.rst +docs/source-app/install_beginner.rst +docs/source-app/installation.rst +docs/source-app/intro.rst +docs/source-app/levels/advanced/index.rst +docs/source-app/levels/advanced/level_16.rst +docs/source-app/levels/advanced/level_17.rst +docs/source-app/levels/advanced/level_18.rst +docs/source-app/levels/advanced/level_19.rst +docs/source-app/levels/advanced/level_20.rst +docs/source-app/levels/basic/index.rst +docs/source-app/levels/basic/level_1.rst +docs/source-app/levels/basic/level_2.rst +docs/source-app/levels/basic/level_3.rst +docs/source-app/levels/basic/level_4.rst +docs/source-app/levels/basic/level_5.rst +docs/source-app/levels/basic/level_6.rst +docs/source-app/levels/basic/level_7.rst +docs/source-app/levels/intermediate/index.rst +docs/source-app/levels/intermediate/level_10.rst +docs/source-app/levels/intermediate/level_11.rst +docs/source-app/levels/intermediate/level_12.rst +docs/source-app/levels/intermediate/level_13.rst +docs/source-app/levels/intermediate/level_14.rst +docs/source-app/levels/intermediate/level_15.rst +docs/source-app/levels/intermediate/level_8.rst +docs/source-app/levels/intermediate/level_9.rst +docs/source-app/make.bat +docs/source-app/moving_to_the_cloud.rst +docs/source-app/quickstart.rst +docs/source-app/testing.rst +docs/source-app/ui_and_frontends.rst +docs/source-app/workflows/add_components/index.rst +docs/source-app/workflows/add_server/any_server.rst +docs/source-app/workflows/add_server/flask_basic.rst +docs/source-app/workflows/add_web_link.rst +docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst +docs/source-app/workflows/add_web_ui/dash/basic.rst +docs/source-app/workflows/add_web_ui/dash/index.rst +docs/source-app/workflows/add_web_ui/dash/intermediate.rst +docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py +docs/source-app/workflows/add_web_ui/dash/intermediate_state.py +docs/source-app/workflows/add_web_ui/gradio/basic.rst +docs/source-app/workflows/add_web_ui/gradio/index.rst +docs/source-app/workflows/add_web_ui/gradio/intermediate.rst +docs/source-app/workflows/add_web_ui/html/basic.rst +docs/source-app/workflows/add_web_ui/html/index.rst +docs/source-app/workflows/add_web_ui/index.rst +docs/source-app/workflows/add_web_ui/index_content.rst +docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst +docs/source-app/workflows/add_web_ui/jupyter_basic.rst +docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst +docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst +docs/source-app/workflows/add_web_ui/react/index.rst +docs/source-app/workflows/add_web_ui/streamlit/basic.rst +docs/source-app/workflows/add_web_ui/streamlit/index.rst +docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst +docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst +docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst +docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst +docs/source-app/workflows/build_lightning_app/from_scratch.rst +docs/source-app/workflows/build_lightning_app/from_scratch_content.rst +docs/source-app/workflows/build_lightning_component/basic.rst +docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst +docs/source-app/workflows/build_lightning_component/index_content.rst +docs/source-app/workflows/build_lightning_component/intermediate.rst +docs/source-app/workflows/build_lightning_component/publish_a_component.rst +docs/source-app/workflows/debug_locally.rst +docs/source-app/workflows/enable_fault_tolerance.rst +docs/source-app/workflows/extend_app.rst +docs/source-app/workflows/index.rst +docs/source-app/workflows/run_app_on_cloud/cloud_files.rst +docs/source-app/workflows/run_app_on_cloud/index.rst +docs/source-app/workflows/run_app_on_cloud/index_content.rst +docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst +docs/source-app/workflows/run_app_on_cloud/on_prem.rst +docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst +docs/source-app/workflows/run_components_on_different_hardware.rst +docs/source-app/workflows/run_work_in_parallel.rst +docs/source-app/workflows/run_work_in_parallel_content.rst +docs/source-app/workflows/run_work_once.rst +docs/source-app/workflows/run_work_once_content.rst +docs/source-app/workflows/schedule_apps.rst +docs/source-app/workflows/share_app.rst +docs/source-app/workflows/share_files_between_components.rst +docs/source-app/workflows/share_files_between_components/app.py +docs/source-app/workflows/test_an_app.rst +docs/source-pytorch/Makefile +docs/source-pytorch/conf.py +docs/source-pytorch/make.bat +src/lightning_app/CHANGELOG.md +examples/pl_app From e84fd7c8252a5432b705e4eab1cc5fc1c2f1ca99 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 11:29:09 +0100 Subject: [PATCH 107/119] update --- changed.txt | 212 ---------------------------------------------------- 1 file changed, 212 deletions(-) delete mode 100644 changed.txt diff --git a/changed.txt b/changed.txt deleted file mode 100644 index 877378d35e60a..0000000000000 --- a/changed.txt +++ /dev/null @@ -1,212 +0,0 @@ -.actions/app/delete_cloud_lightning_apps.py -.actions/app/download_frontend.py -.actions/job_skipper.py -.azure/gpu-tests.yml -.azure/hpu-tests.yml -.azure/ipu-tests.yml -.circleci/config.yml -.github/workflows/ci-app_block.yml -.github/workflows/ci-pytorch_test-conda.yml -.github/workflows/ci-pytorch_test-full.yml -.github/workflows/ci-pytorch_test-slow.yml -.github/workflows/docs-checks.yml -.github/workflows/docs-deploy.yml -.pre-commit-config.yaml -docs/source-app/Makefile -docs/source-app/_templates/classtemplate.rst -docs/source-app/_templates/classtemplate_no_index.rst -docs/source-app/_templates/layout.html -docs/source-app/_templates/theme_variables.jinja -docs/source-app/api_reference/api_references.rst -docs/source-app/basics.rst -docs/source-app/code_samples/basics/0.py -docs/source-app/code_samples/basics/1.py -docs/source-app/code_samples/convert_pl_to_app/app.py -docs/source-app/code_samples/convert_pl_to_app/requirements.py -docs/source-app/code_samples/convert_pl_to_app/train.py -docs/source-app/code_samples/quickstart/app/app_0.py -docs/source-app/code_samples/quickstart/app/app_1.py -docs/source-app/code_samples/quickstart/app_01.py -docs/source-app/code_samples/quickstart/app_02.py -docs/source-app/code_samples/quickstart/app_03.py -docs/source-app/code_samples/quickstart/app_comp.py -docs/source-app/code_samples/quickstart/hello_world/app.py -docs/source-app/code_samples/quickstart/hello_world/app_ui.py -docs/source-app/conf.py -docs/source-app/contribute_app.rst -docs/source-app/core_api/core_api.rst -docs/source-app/core_api/lightning_app/app.py -docs/source-app/core_api/lightning_app/communication.rst -docs/source-app/core_api/lightning_app/communication_content.rst -docs/source-app/core_api/lightning_app/dynamic_work.rst -docs/source-app/core_api/lightning_app/dynamic_work_content.rst -docs/source-app/core_api/lightning_app/lightning_app.rst -docs/source-app/core_api/lightning_flow.rst -docs/source-app/core_api/lightning_work/compute.rst -docs/source-app/core_api/lightning_work/compute_content.rst -docs/source-app/core_api/lightning_work/handling_app_exception.rst -docs/source-app/core_api/lightning_work/handling_app_exception_content.rst -docs/source-app/core_api/lightning_work/lightning_work.rst -docs/source-app/core_api/lightning_work/payload.rst -docs/source-app/core_api/lightning_work/payload_content.rst -docs/source-app/core_api/lightning_work/status.rst -docs/source-app/core_api/lightning_work/status_content.rst -docs/source-app/examples/dag/dag.rst -docs/source-app/examples/dag/dag_from_scratch.rst -docs/source-app/examples/data_explore_app.rst -docs/source-app/examples/etl_app.rst -docs/source-app/examples/file_server/app.py -docs/source-app/examples/file_server/file_server.rst -docs/source-app/examples/file_server/file_server_content.rst -docs/source-app/examples/file_server/file_server_step_1.rst -docs/source-app/examples/file_server/file_server_step_2.rst -docs/source-app/examples/file_server/file_server_step_3.rst -docs/source-app/examples/file_server/file_server_step_4.rst -docs/source-app/examples/github_repo_runner/.lightning -docs/source-app/examples/github_repo_runner/app.py -docs/source-app/examples/github_repo_runner/github_repo_runner.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_content.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_step_1.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_step_2.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_step_3.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_step_4.rst -docs/source-app/examples/github_repo_runner/github_repo_runner_step_5.rst -docs/source-app/examples/hands_on_example.rst -docs/source-app/examples/hpo/build_from_scratch.rst -docs/source-app/examples/hpo/hpo.py -docs/source-app/examples/hpo/hpo.rst -docs/source-app/examples/hpo/hpo_wi.rst -docs/source-app/examples/hpo/hpo_wo.rst -docs/source-app/examples/hpo/lightning_hpo.rst -docs/source-app/examples/hpo/lightning_hpo_target.py -docs/source-app/examples/hpo/objective.py -docs/source-app/examples/hpo/optuna_reference.py -docs/source-app/examples/model_server_app/app.py -docs/source-app/examples/model_server_app/load_testing.rst -docs/source-app/examples/model_server_app/locust_component.py -docs/source-app/examples/model_server_app/locustfile.py -docs/source-app/examples/model_server_app/model_server.py -docs/source-app/examples/model_server_app/model_server.rst -docs/source-app/examples/model_server_app/model_server_app.rst -docs/source-app/examples/model_server_app/model_server_app_content.rst -docs/source-app/examples/model_server_app/putting_everything_together.rst -docs/source-app/examples/model_server_app/train.py -docs/source-app/examples/model_server_app/train.rst -docs/source-app/examples/research_demo_app.rst -docs/source-app/get_started/add_an_interactive_demo.rst -docs/source-app/get_started/build_model.rst -docs/source-app/get_started/go_beyond_training.rst -docs/source-app/get_started/go_beyond_training_content.rst -docs/source-app/get_started/jumpstart_from_app_gallery.rst -docs/source-app/get_started/jumpstart_from_component_gallery.rst -docs/source-app/get_started/lightning_apps_intro.rst -docs/source-app/get_started/training_with_apps.rst -docs/source-app/get_started/what_app_can_do.rst -docs/source-app/glossary/app_tree.rst -docs/source-app/glossary/build_config/build_config_basic.rst -docs/source-app/glossary/dag.rst -docs/source-app/glossary/debug_app.rst -docs/source-app/glossary/distributed_fe.rst -docs/source-app/glossary/distributed_hardware.rst -docs/source-app/glossary/event_loop.rst -docs/source-app/glossary/fault_tolerance.rst -docs/source-app/glossary/index.rst -docs/source-app/glossary/lightning_app_overview/index.rst -docs/source-app/glossary/scheduling.rst -docs/source-app/glossary/sharing_components.rst -docs/source-app/glossary/storage/drive.rst -docs/source-app/glossary/storage/drive_content.rst -docs/source-app/glossary/storage/path.rst -docs/source-app/glossary/storage/storage.rst -docs/source-app/index.rst -docs/source-app/install_beginner.rst -docs/source-app/installation.rst -docs/source-app/intro.rst -docs/source-app/levels/advanced/index.rst -docs/source-app/levels/advanced/level_16.rst -docs/source-app/levels/advanced/level_17.rst -docs/source-app/levels/advanced/level_18.rst -docs/source-app/levels/advanced/level_19.rst -docs/source-app/levels/advanced/level_20.rst -docs/source-app/levels/basic/index.rst -docs/source-app/levels/basic/level_1.rst -docs/source-app/levels/basic/level_2.rst -docs/source-app/levels/basic/level_3.rst -docs/source-app/levels/basic/level_4.rst -docs/source-app/levels/basic/level_5.rst -docs/source-app/levels/basic/level_6.rst -docs/source-app/levels/basic/level_7.rst -docs/source-app/levels/intermediate/index.rst -docs/source-app/levels/intermediate/level_10.rst -docs/source-app/levels/intermediate/level_11.rst -docs/source-app/levels/intermediate/level_12.rst -docs/source-app/levels/intermediate/level_13.rst -docs/source-app/levels/intermediate/level_14.rst -docs/source-app/levels/intermediate/level_15.rst -docs/source-app/levels/intermediate/level_8.rst -docs/source-app/levels/intermediate/level_9.rst -docs/source-app/make.bat -docs/source-app/moving_to_the_cloud.rst -docs/source-app/quickstart.rst -docs/source-app/testing.rst -docs/source-app/ui_and_frontends.rst -docs/source-app/workflows/add_components/index.rst -docs/source-app/workflows/add_server/any_server.rst -docs/source-app/workflows/add_server/flask_basic.rst -docs/source-app/workflows/add_web_link.rst -docs/source-app/workflows/add_web_ui/angular_js_intermediate.rst -docs/source-app/workflows/add_web_ui/dash/basic.rst -docs/source-app/workflows/add_web_ui/dash/index.rst -docs/source-app/workflows/add_web_ui/dash/intermediate.rst -docs/source-app/workflows/add_web_ui/dash/intermediate_plot.py -docs/source-app/workflows/add_web_ui/dash/intermediate_state.py -docs/source-app/workflows/add_web_ui/gradio/basic.rst -docs/source-app/workflows/add_web_ui/gradio/index.rst -docs/source-app/workflows/add_web_ui/gradio/intermediate.rst -docs/source-app/workflows/add_web_ui/html/basic.rst -docs/source-app/workflows/add_web_ui/html/index.rst -docs/source-app/workflows/add_web_ui/index.rst -docs/source-app/workflows/add_web_ui/index_content.rst -docs/source-app/workflows/add_web_ui/integrate_any_javascript_framework.rst -docs/source-app/workflows/add_web_ui/jupyter_basic.rst -docs/source-app/workflows/add_web_ui/react/communicate_between_react_and_lightning.rst -docs/source-app/workflows/add_web_ui/react/connect_react_and_lightning.rst -docs/source-app/workflows/add_web_ui/react/index.rst -docs/source-app/workflows/add_web_ui/streamlit/basic.rst -docs/source-app/workflows/add_web_ui/streamlit/index.rst -docs/source-app/workflows/add_web_ui/streamlit/intermediate.rst -docs/source-app/workflows/add_web_ui/vue_js_intermediate.rst -docs/source-app/workflows/arrange_tabs/arrange_app_basic.rst -docs/source-app/workflows/build_lightning_app/from_pytorch_lightning_script.rst -docs/source-app/workflows/build_lightning_app/from_scratch.rst -docs/source-app/workflows/build_lightning_app/from_scratch_content.rst -docs/source-app/workflows/build_lightning_component/basic.rst -docs/source-app/workflows/build_lightning_component/from_scratch_component_content.rst -docs/source-app/workflows/build_lightning_component/index_content.rst -docs/source-app/workflows/build_lightning_component/intermediate.rst -docs/source-app/workflows/build_lightning_component/publish_a_component.rst -docs/source-app/workflows/debug_locally.rst -docs/source-app/workflows/enable_fault_tolerance.rst -docs/source-app/workflows/extend_app.rst -docs/source-app/workflows/index.rst -docs/source-app/workflows/run_app_on_cloud/cloud_files.rst -docs/source-app/workflows/run_app_on_cloud/index.rst -docs/source-app/workflows/run_app_on_cloud/index_content.rst -docs/source-app/workflows/run_app_on_cloud/lightning_cloud.rst -docs/source-app/workflows/run_app_on_cloud/on_prem.rst -docs/source-app/workflows/run_app_on_cloud/on_your_own_machine.rst -docs/source-app/workflows/run_components_on_different_hardware.rst -docs/source-app/workflows/run_work_in_parallel.rst -docs/source-app/workflows/run_work_in_parallel_content.rst -docs/source-app/workflows/run_work_once.rst -docs/source-app/workflows/run_work_once_content.rst -docs/source-app/workflows/schedule_apps.rst -docs/source-app/workflows/share_app.rst -docs/source-app/workflows/share_files_between_components.rst -docs/source-app/workflows/share_files_between_components/app.py -docs/source-app/workflows/test_an_app.rst -docs/source-pytorch/Makefile -docs/source-pytorch/conf.py -docs/source-pytorch/make.bat -src/lightning_app/CHANGELOG.md -examples/pl_app From c1799fdcb7b552102e302525050e4776463a8d0a Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 11:57:10 +0100 Subject: [PATCH 108/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 4c2a9e834b8fc..30c7336592cc7 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -44,10 +44,12 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - MATCHES=$(echo ${{ steps.changed-files.outputs.all_changed_files }} | grep -E '$FILTER') + MATCHES=$(echo "${{ steps.changed-files.outputs.all_changed_files }}"" | grep -E $FILTER) if [ -z "$MATCHES" ]; then + echo "Skip" echo "::set-output continue="0" else + echo "Continue" echo "::set-output continue="1" fi From 1bb2279cb2889e1372ce1c287ded189f4bcc5208 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 12:06:20 +0100 Subject: [PATCH 109/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 30c7336592cc7..640d567f82b6a 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -44,7 +44,7 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - MATCHES=$(echo "${{ steps.changed-files.outputs.all_changed_files }}"" | grep -E $FILTER) + MATCHES=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | grep -E $FILTER) if [ -z "$MATCHES" ]; then echo "Skip" echo "::set-output continue="0" From d38eb01185dee3851ed661847e08a55d589ebc38 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:03:32 +0100 Subject: [PATCH 110/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 1 + test.sh | 0 2 files changed, 1 insertion(+) create mode 100644 test.sh diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 640d567f82b6a..3fc59ee88b493 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -45,6 +45,7 @@ jobs: run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' MATCHES=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | grep -E $FILTER) + echo $MATCHES if [ -z "$MATCHES" ]; then echo "Skip" echo "::set-output continue="0" diff --git a/test.sh b/test.sh new file mode 100644 index 0000000000000..e69de29bb2d1d From ee35d07176fbdea6a97f9f03ab404864ef609878 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:03:43 +0100 Subject: [PATCH 111/119] update --- test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test.sh diff --git a/test.sh b/test.sh deleted file mode 100644 index e69de29bb2d1d..0000000000000 From a2dcd416d0cde2c7fbbba01618c6c39cfed18d81 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:12:47 +0100 Subject: [PATCH 112/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 3fc59ee88b493..082a9e42247d8 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -44,7 +44,8 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - MATCHES=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | grep -E $FILTER) + echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + MATCHES=$(cat changed_files.txt | grep -E $FILTER) echo $MATCHES if [ -z "$MATCHES" ]; then echo "Skip" From 98b14c4270b7424b4475bd76695249b50afac24b Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:21:58 +0100 Subject: [PATCH 113/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 082a9e42247d8..f7afbd36c7c70 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -49,10 +49,10 @@ jobs: echo $MATCHES if [ -z "$MATCHES" ]; then echo "Skip" - echo "::set-output continue="0" + echo "::set-output name=continue::0" else echo "Continue" - echo "::set-output continue="1" + echo "::set-output name=continue::1" fi - name: Update base dependencies From b776c8a906170758ebd054dc3eaa99bafc0122ac Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:45:16 +0100 Subject: [PATCH 114/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index f7afbd36c7c70..c8f3372162834 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -54,9 +54,10 @@ jobs: echo "Continue" echo "::set-output name=continue::1" fi + echo ${{ steps.skip.outputs.continue }} - name: Update base dependencies - if: ${{ steps.skip.outputs.continue }} == "1" + if: ${{ (steps.skip.outputs.continue != '1') }} env: PACKAGE_NAME: pytorch FREEZE_REQUIREMENTS: 1 @@ -66,12 +67,12 @@ jobs: pip install -e .[test] - name: DocTests - if: ${{ steps.skip.outputs.continue }} == "1" + if: ${{ (steps.skip.outputs.continue != '1') }} working-directory: ./src run: pytest pytorch_lightning --cov=pytorch_lightning - name: Update all dependencies - if: ${{ steps.skip.outputs.continue }} == "1" + if: ${{ (steps.skip.outputs.continue != '1') }} env: HOROVOD_BUILD_ARCH_FLAGS: "-mfma" HOROVOD_WITHOUT_MXNET: 1 @@ -90,11 +91,11 @@ jobs: python requirements/pytorch/check-avail-extras.py - name: Pull legacy checkpoints - if: ${{ steps.skip.outputs.continue }} == "1" + if: ${{ (steps.skip.outputs.continue != '1') }} run: bash .actions/pull_legacy_checkpoints.sh - name: Testing PyTorch - if: ${{ steps.skip.outputs.continue }} == "1" + if: ${{ (steps.skip.outputs.continue != '1') }} working-directory: tests/tests_pytorch run: coverage run --source pytorch_lightning -m pytest -v --timeout 150 --durations=50 --junitxml=results-${{ runner.os }}-torch${{ matrix.pytorch-version }}.xml @@ -106,7 +107,7 @@ jobs: if: failure() - name: Statistics - if: success() && ${{ steps.skip.outputs.continue }} == "1" + if: ${{ success() && (steps.skip.outputs.continue != '1') }} working-directory: tests/tests_pytorch run: | coverage report @@ -114,7 +115,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: success() && ${{ steps.skip.outputs.continue }} == "1" + if: ${{ success() && (steps.skip.outputs.continue != '1') }} # see: https://github.com/actions/toolkit/issues/399 continue-on-error: true with: From e9a4783763035189d0e967dacde29443b3773aad Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 13:57:10 +0100 Subject: [PATCH 115/119] update --- .github/workflows/ci-pytorch_test-conda.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index c8f3372162834..8790c156e9f08 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -49,15 +49,14 @@ jobs: echo $MATCHES if [ -z "$MATCHES" ]; then echo "Skip" - echo "::set-output name=continue::0" + echo "::set-output name=continue::'0'" else echo "Continue" - echo "::set-output name=continue::1" + echo "::set-output name=continue::'1'" fi - echo ${{ steps.skip.outputs.continue }} - name: Update base dependencies - if: ${{ (steps.skip.outputs.continue != '1') }} + if: ${{ (steps.skip.outputs.continue == '1') }} env: PACKAGE_NAME: pytorch FREEZE_REQUIREMENTS: 1 @@ -67,12 +66,12 @@ jobs: pip install -e .[test] - name: DocTests - if: ${{ (steps.skip.outputs.continue != '1') }} + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: ./src run: pytest pytorch_lightning --cov=pytorch_lightning - name: Update all dependencies - if: ${{ (steps.skip.outputs.continue != '1') }} + if: ${{ (steps.skip.outputs.continue == '1') }} env: HOROVOD_BUILD_ARCH_FLAGS: "-mfma" HOROVOD_WITHOUT_MXNET: 1 @@ -91,11 +90,11 @@ jobs: python requirements/pytorch/check-avail-extras.py - name: Pull legacy checkpoints - if: ${{ (steps.skip.outputs.continue != '1') }} + if: ${{ (steps.skip.outputs.continue == '1') }} run: bash .actions/pull_legacy_checkpoints.sh - name: Testing PyTorch - if: ${{ (steps.skip.outputs.continue != '1') }} + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch run: coverage run --source pytorch_lightning -m pytest -v --timeout 150 --durations=50 --junitxml=results-${{ runner.os }}-torch${{ matrix.pytorch-version }}.xml @@ -107,7 +106,7 @@ jobs: if: failure() - name: Statistics - if: ${{ success() && (steps.skip.outputs.continue != '1') }} + if: ${{ success() && (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch run: | coverage report @@ -115,7 +114,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: ${{ success() && (steps.skip.outputs.continue != '1') }} + if: ${{ success() && (steps.skip.outputs.continue == '1') }} # see: https://github.com/actions/toolkit/issues/399 continue-on-error: true with: From 217126e8757dd62c9a3f8487abdca59ed27749f0 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 14:11:56 +0100 Subject: [PATCH 116/119] update --- .azure/gpu-tests.yml | 6 --- .github/workflows/ci-pytorch_test-full.yml | 48 ++++++++++++++++++---- .github/workflows/ci-pytorch_test-slow.yml | 30 ++++++++++++-- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index 01405bf8b269f..f739d3258e79f 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -12,12 +12,6 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**/*" - - "tests/tests_pytorch/**/*" - - "examples/pl_*/**/*" - - "requirements/pytorch/**/*" pr: - "master" diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 8339ef3b2f2ec..d09f40d883481 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -42,35 +42,60 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v23.1 + + - name: Decide if the test should be skipped + id: skip + run: | + FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' + echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + MATCHES=$(cat changed_files.txt | grep -E $FILTER) + echo $MATCHES + if [ -z "$MATCHES" ]; then + echo "Skip" + echo "::set-output name=continue::'0'" + else + echo "Continue" + echo "::set-output name=continue::'1'" + fi + - name: Reset caching + if: ${{ (steps.skip.outputs.continue == '1') }} run: python -c "import time; days = time.time() / 60 / 60 / 24; print(f'TIME_PERIOD=d{int(days / 2) * 2}')" >> $GITHUB_ENV - name: basic setup + if: ${{ (steps.skip.outputs.continue == '1') }} run: | pip --version pip install -q fire # Github Actions: Run step on specific OS: https://stackoverflow.com/a/57948488/4521646 - name: Setup macOS - if: runner.os == 'macOS' + if: ${{ (runner.os == 'macOS') && (steps.skip.outputs.continue == '1') }} run: | brew install openmpi libuv # Horovod on macOS requires OpenMPI, Gloo not currently supported + - name: Setup Windows - if: runner.os == 'windows' + if: ${{ (runner.os == 'windows') && (steps.skip.outputs.continue == '1') }} run: | python .actions/assistant.py requirements_prune_pkgs horovod + - name: Set min. dependencies - if: matrix.requires == 'oldest' + if: ${{ (matrix.requires == 'oldest') && (steps.skip.outputs.continue == '1') }} run: | python .actions/assistant.py replace_oldest_ver # Note: This uses an internal pip API and may not always work # https://github.com/actions/cache/blob/master/examples.md#multiple-oss-in-a-workflow - name: Get pip cache dir + if: ${{ (steps.skip.outputs.continue == '1') }} id: pip-cache run: echo "::set-output name=dir::$(pip cache dir)" - name: pip cache + if: ${{ (steps.skip.outputs.continue == '1') }} uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} @@ -79,9 +104,11 @@ jobs: ${{ runner.os }}-pip-td${{ env.TIME_PERIOD }}-py${{ matrix.python-version }}-${{ matrix.release }}-${{ matrix.requires }}- - name: Pull legacy checkpoints + if: ${{ (steps.skip.outputs.continue == '1') }} run: bash .actions/pull_legacy_checkpoints.sh - name: Install dependencies + if: ${{ (steps.skip.outputs.continue == '1') }} env: PACKAGE_NAME: pytorch FREEZE_REQUIREMENTS: 1 @@ -93,10 +120,12 @@ jobs: shell: bash - name: DocTests + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: ./src run: pytest pytorch_lightning --cov=pytorch_lightning - name: Install extra dependencies + if: ${{ (steps.skip.outputs.continue == '1') }} run: | # adjust versions according installed Torch version python ./requirements/pytorch/adjust-versions.py requirements/pytorch/extra.txt @@ -105,7 +134,7 @@ jobs: shell: bash - name: Reinstall Horovod if necessary - if: runner.os != 'windows' + if: ${{ (runner.os != 'windows') && (steps.skip.outputs.continue == '1') }} env: HOROVOD_BUILD_ARCH_FLAGS: "-mfma" HOROVOD_WITHOUT_MXNET: 1 @@ -122,38 +151,43 @@ jobs: shell: bash - name: Cache datasets + if: ${{ (steps.skip.outputs.continue == '1') }} uses: actions/cache@v2 with: path: Datasets key: pl-dataset - name: Sanity check + if: ${{ (steps.skip.outputs.continue == '1') }} run: python requirements/pytorch/check-avail-extras.py - name: Testing PyTorch + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch # NOTE: do not include coverage report here, see: https://github.com/nedbat/coveragepy/issues/1003 run: coverage run --source pytorch_lightning -m pytest -v --durations=50 --junitxml=results-${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.requires }}-${{ matrix.release }}.xml - name: Upload pytest results + if: ${{ (failure()) && (steps.skip.outputs.continue == '1') }} uses: actions/upload-artifact@v3 with: name: unittest-results-${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.requires }}-${{ matrix.release }} path: tests/tests_pytorch/results-${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.requires }}-${{ matrix.release }}.xml - if: failure() - name: Prepare Examples + if: ${{ (steps.skip.outputs.continue == '1') }} run: | # adjust versions according installed Torch version python ./requirements/pytorch/adjust-versions.py requirements/pytorch/examples.txt pip install -r requirements/pytorch/examples.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html --upgrade - name: Run Examples + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: ./examples run: python -m pytest test_pl_examples.py -v --durations=10 - name: Statistics - if: success() + if: ${{ (success()) && (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch run: | coverage report @@ -161,7 +195,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: always() + if: ${{ (always()) && (steps.skip.outputs.continue == '1') }} # see: https://github.com/actions/toolkit/issues/399 continue-on-error: true with: diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index 131511443f28f..fbdb8cd240f7b 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -36,15 +36,37 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v23.1 + + - name: Decide if the test should be skipped + id: skip + run: | + FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' + echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + MATCHES=$(cat changed_files.txt | grep -E $FILTER) + echo $MATCHES + if [ -z "$MATCHES" ]; then + echo "Skip" + echo "::set-output name=continue::'0'" + else + echo "Continue" + echo "::set-output name=continue::'1'" + fi + - name: Reset caching + if: ${{ (steps.skip.outputs.continue == '1') }} run: python -c "import time; days = time.time() / 60 / 60 / 24; print(f'TIME_PERIOD=d{int(days / 2) * 2}')" >> $GITHUB_ENV - name: Get pip cache + if: ${{ (steps.skip.outputs.continue == '1') }} id: pip-cache run: | python -c "from pip._internal.locations import USER_CACHE_DIR; print('::set-output name=dir::' + USER_CACHE_DIR)" - name: Cache pip + if: ${{ (steps.skip.outputs.continue == '1') }} uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} @@ -53,6 +75,7 @@ jobs: ${{ runner.os }}-pip-td${{ env.TIME_PERIOD }}-py${{ matrix.python-version }}- - name: Install dependencies + if: ${{ (steps.skip.outputs.continue == '1') }} env: PACKAGE_NAME: pytorch FREEZE_REQUIREMENTS: 1 @@ -64,20 +87,21 @@ jobs: shell: bash - name: Testing PyTorch + if: ${{ (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch run: coverage run --source pytorch_lightning -m pytest -v --junitxml=results-${{ runner.os }}-py${{ matrix.python-version }}.xml env: PL_RUN_SLOW_TESTS: 1 - name: Upload pytest test results + if: ${{ (failure()) && (steps.skip.outputs.continue == '1') }} uses: actions/upload-artifact@v3 with: name: unittest-results-${{ runner.os }}-py${{ matrix.python-version }} path: tests/tests_pytorch/results-${{ runner.os }}-py${{ matrix.python-version }}.xml - if: failure() - name: Statistics - if: success() + if: ${{ (success()) && (steps.skip.outputs.continue == '1') }} working-directory: tests/tests_pytorch run: | coverage report @@ -85,7 +109,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - if: success() + if: ${{ (success()) && (steps.skip.outputs.continue == '1') }} # see: https://github.com/actions/toolkit/issues/399 continue-on-error: true with: From feb66e837eb0e36ad407a91ca5f36f7b135887a1 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 14:18:15 +0100 Subject: [PATCH 117/119] update --- .azure/gpu-tests.yml | 13 +++++++++++++ .github/workflows/ci-pytorch_test-full.yml | 5 ----- .github/workflows/ci-pytorch_test-slow.yml | 5 ----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.azure/gpu-tests.yml b/.azure/gpu-tests.yml index f739d3258e79f..0f29eca5dd54b 100644 --- a/.azure/gpu-tests.yml +++ b/.azure/gpu-tests.yml @@ -38,6 +38,19 @@ jobs: steps: + - bash: | + CHANGED_FILES=$(git diff --name-status master | awk '{print $2}') + echo $CHANGED_FILES > changed_files.txt + MATCHES=$(cat changed_files.txt | grep -E $FILTER) + echo $MATCHES + if [ -z "$MATCHES" ]; then + echo "Skip" + else + echo "Continue" + fi + + displayName: Decide if skipping should be done. + - bash: | lspci | egrep 'VGA|3D' whereis nvidia diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index d09f40d883481..3a2e8e6c8c719 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -7,11 +7,6 @@ on: # Trigger the workflow on push or pull request, but only for the master bra pull_request: branches: [master, "release/*"] types: [opened, reopened, ready_for_review, synchronize] - paths: - - "src/pytorch_lightning" - - "tests/tests_pytorch" - - "examples/pl_*" - - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index fbdb8cd240f7b..53cd3f99ed66c 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -7,11 +7,6 @@ on: # Trigger the workflow on push or pull request, but only for the master bra pull_request: branches: [master, "release/*"] types: [opened, reopened, ready_for_review, synchronize] - paths: - - "src/pytorch_lightning" - - "tests/tests_pytorch" - - "examples/pl_*" - - "requirements/pytorch" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} From 2beb4bb1876b8fa77391358b9100ca565e897d9b Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 14:22:51 +0100 Subject: [PATCH 118/119] update --- .github/workflows/ci-pytorch_test-full.yml | 10 ++++++---- .github/workflows/ci-pytorch_test-slow.yml | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 3a2e8e6c8c719..6d84d1dfb02df 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -32,10 +32,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - name: Get changed files id: changed-files @@ -56,6 +52,12 @@ jobs: echo "::set-output name=continue::'1'" fi + - name: Set up Python ${{ matrix.python-version }} + if: ${{ (steps.skip.outputs.continue == '1') }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Reset caching if: ${{ (steps.skip.outputs.continue == '1') }} run: python -c "import time; days = time.time() / 60 / 60 / 24; print(f'TIME_PERIOD=d{int(days / 2) * 2}')" >> $GITHUB_ENV diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index 53cd3f99ed66c..f221a03692eb3 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -27,9 +27,6 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - name: Get changed files id: changed-files @@ -50,6 +47,11 @@ jobs: echo "::set-output name=continue::'1'" fi + - uses: actions/setup-python@v2 + if: ${{ (steps.skip.outputs.continue == '1') }} + with: + python-version: ${{ matrix.python-version }} + - name: Reset caching if: ${{ (steps.skip.outputs.continue == '1') }} run: python -c "import time; days = time.time() / 60 / 60 / 24; print(f'TIME_PERIOD=d{int(days / 2) * 2}')" >> $GITHUB_ENV From a197a428074a165ac1528aaceaa2f99fbb033928 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Thu, 14 Jul 2022 14:27:55 +0100 Subject: [PATCH 119/119] update --- .azure/hpu-tests.yml | 6 ------ .azure/ipu-tests.yml | 6 ------ .github/workflows/ci-pytorch_test-conda.yml | 2 +- .github/workflows/ci-pytorch_test-full.yml | 2 +- .github/workflows/ci-pytorch_test-slow.yml | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.azure/hpu-tests.yml b/.azure/hpu-tests.yml index 403489f374fd7..e530ace901bfa 100644 --- a/.azure/hpu-tests.yml +++ b/.azure/hpu-tests.yml @@ -9,12 +9,6 @@ trigger: - "master" - "release/*" - "refs/tags/*" - paths: - include: - - "src/pytorch_lightning/**/*" - - "tests/tests_pytorch/**/*" - - "examples/pl_*/**/*" - - "requirements/pytorch/**/*" pr: - "master" diff --git a/.azure/ipu-tests.yml b/.azure/ipu-tests.yml index c0767e05663ac..dccde83869adf 100644 --- a/.azure/ipu-tests.yml +++ b/.azure/ipu-tests.yml @@ -7,12 +7,6 @@ trigger: - master - release/* - refs/tags/* - paths: - include: - - "src/pytorch_lightning/**/*" - - "tests/tests_pytorch/**/*" - - "examples/pl_*/**/*" - - "requirements/pytorch/**/*" pr: - master diff --git a/.github/workflows/ci-pytorch_test-conda.yml b/.github/workflows/ci-pytorch_test-conda.yml index 8790c156e9f08..241bdaa5b84df 100644 --- a/.github/workflows/ci-pytorch_test-conda.yml +++ b/.github/workflows/ci-pytorch_test-conda.yml @@ -44,7 +44,7 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr " " "\n" > changed_files.txt MATCHES=$(cat changed_files.txt | grep -E $FILTER) echo $MATCHES if [ -z "$MATCHES" ]; then diff --git a/.github/workflows/ci-pytorch_test-full.yml b/.github/workflows/ci-pytorch_test-full.yml index 6d84d1dfb02df..c488cfef9fd0a 100644 --- a/.github/workflows/ci-pytorch_test-full.yml +++ b/.github/workflows/ci-pytorch_test-full.yml @@ -41,7 +41,7 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr " " "\n" > changed_files.txt MATCHES=$(cat changed_files.txt | grep -E $FILTER) echo $MATCHES if [ -z "$MATCHES" ]; then diff --git a/.github/workflows/ci-pytorch_test-slow.yml b/.github/workflows/ci-pytorch_test-slow.yml index f221a03692eb3..b42e529dce0fe 100644 --- a/.github/workflows/ci-pytorch_test-slow.yml +++ b/.github/workflows/ci-pytorch_test-slow.yml @@ -36,7 +36,7 @@ jobs: id: skip run: | FILTER='src/pytorch_lightning|requirements/pytorch|tests/tests_pytorch|examples/pl_' - echo ${{ steps.changed-files.outputs.all_changed_files }} | tr " " "\n" > changed_files.txt + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr " " "\n" > changed_files.txt MATCHES=$(cat changed_files.txt | grep -E $FILTER) echo $MATCHES if [ -z "$MATCHES" ]; then