From 2ad478dff97a666f0f3eb647fd2a54b5f225ec28 Mon Sep 17 00:00:00 2001 From: pdmurray Date: Tue, 26 Sep 2023 13:43:34 -0700 Subject: [PATCH] [Doc] Upgrade doc dependencies Disable most recent `master` changes to CSS Insert svg logo for ray train; add official ray blue color Remove sphinx-tabs, it breaks per-page css/js and PST has tabs already Fix tip nesting inside tab-item Ray train logo is in place Fix "Edit on GitHub" buttons Hide "Hide Search Matches" button Add developer guides subsections Fix css on example gallery Finished example gallery Add Ray Data user guides Fix "more-frameworks" logo placement; improve code block dev docs Set fixed width for autosummary tables Add back in CSAT widget and styles Remove special tooling to make ecosystem grid Fix ray-libraries card layout _without_ js Remove js which builds top nav; fix assistant styles Assistant widget now blurs background correctly Remove _toc.yml; start adding missing sidebar links via toctree In the midst of fixing the sidebar... Well, this definitely works to add links to the sidebar. But the problem is that each worker is inserting into a separate copy of the sidebar Nav, so the links that are available in your sidebar depend on which worker was building the particular page you're looking at. Switch to using toctree in sidebar Implemented toctree as sidenav Clean up styles; add back in marked.js, hl.js Remove versionwarning extension; it's broken and we aren't using it Add call to highlight code in splash.js Include google analytics via PST; update hljs; fix ray intro video size; use ray blue for various styles Finish final changes to styling before rebase --- .readthedocs.yaml | 2 +- doc/Makefile | 2 +- doc/requirements-doc.txt | 32 +- doc/source/_static/css/assistant.css | 136 +++ doc/source/_static/css/csat.css | 77 ++ doc/source/_static/css/custom.css | 1019 +++-------------- doc/source/_static/css/examples.css | 184 ++- doc/source/_static/css/ray-libraries.css | 12 + doc/source/_static/css/ray-train.css | 14 + doc/source/_static/css/splash.css | 130 +++ doc/source/_static/img/LandingPageBG.jpg | Bin 35876 -> 0 bytes doc/source/_static/img/ray_logo.svg | 11 + doc/source/_static/img/search-icon.svg | 3 + doc/source/_static/js/assistant.js | 50 +- doc/source/_static/js/custom.js | 132 +-- doc/source/_static/js/docsearch.js | 35 - doc/source/_static/js/examples.js | 111 ++ doc/source/_static/js/splash.js | 26 + doc/source/_static/js/tags.js | 400 ------- doc/source/_static/js/top-navigation.js | 148 --- doc/source/_templates/breadcrumbs.html | 15 - doc/source/_templates/csat.html | 4 +- doc/source/_templates/examples-sidebar.html | 139 +++ doc/source/_templates/layout.html | 20 +- doc/source/_templates/main-sidebar.html | 11 + doc/source/_templates/navbar-anyscale.html | 9 + doc/source/_templates/navbar-links.html | 9 + doc/source/_templates/navbar-ray-logo.html | 13 + doc/source/_templates/sbt-sidebar-nav.html | 21 - .../_templates/sections/footer-content.html | 24 - doc/source/_toc.yml | 398 ------- doc/source/cluster/getting-started.rst | 14 + doc/source/cluster/kubernetes/benchmarks.md | 8 +- doc/source/cluster/kubernetes/examples.md | 12 + .../cluster/kubernetes/getting-started.md | 9 + doc/source/cluster/kubernetes/index.md | 41 +- .../cluster/kubernetes/k8s-ecosystem.md | 10 + .../k8s-ecosystem/prometheus-grafana.md | 11 +- .../cluster/kubernetes/troubleshooting.md | 9 +- .../rayservice-troubleshooting.md | 12 +- .../troubleshooting/troubleshooting.md | 9 +- doc/source/cluster/kubernetes/user-guides.md | 25 + .../user-guides/k8s-cluster-setup.md | 7 + .../kubernetes/user-guides/rayservice.md | 12 +- doc/source/cluster/vms/examples/index.md | 6 + doc/source/cluster/vms/getting-started.rst | 85 +- doc/source/cluster/vms/index.md | 35 +- .../vms/user-guides/community/index.rst | 7 + doc/source/cluster/vms/user-guides/index.md | 10 + doc/source/conf.py | 369 ++++-- doc/source/data/batch_inference.rst | 32 +- doc/source/data/data.rst | 10 + doc/source/data/examples/batch_training.ipynb | 22 +- doc/source/data/key-concepts.rst | 2 +- doc/source/index.rst | 28 + doc/source/navbar.yml | 53 + doc/source/ray-contribute/docs.ipynb | 11 +- doc/source/ray-core/examples/overview.rst | 7 + doc/source/ray-core/walkthrough.rst | 12 +- doc/source/ray-more-libs/index.rst | 13 + doc/source/ray-observability/index.md | 10 +- .../ray-observability/reference/api.rst | 6 +- .../ray-observability/reference/index.md | 10 +- .../ray-observability/user-guides/cli-sdk.rst | 178 +-- .../user-guides/debug-apps/index.md | 13 +- .../ray-observability/user-guides/index.md | 13 +- doc/source/ray-overview/eco-gallery.yml | 190 --- doc/source/ray-overview/examples.rst | 238 ++-- doc/source/ray-overview/getting-started.md | 2 +- doc/source/ray-overview/index.md | 56 +- doc/source/ray-overview/installation.rst | 10 +- doc/source/ray-overview/ray-libraries.rst | 443 ++++++- doc/source/ray-overview/use-cases.rst | 17 + doc/source/rllib/index.rst | 14 +- doc/source/rllib/rllib-cli.md | 36 +- doc/source/rllib/rllib-training.rst | 12 +- doc/source/rllib/user-guides.rst | 20 + .../serve/advanced-guides/dev-workflow.md | 4 +- doc/source/serve/advanced-guides/index.md | 15 + doc/source/serve/index.md | 67 +- doc/source/serve/production-guide/index.md | 12 + doc/source/serve/tutorials/rllib.md | 2 +- doc/source/{index.md => splash.html} | 189 ++- doc/source/train/deepspeed.rst | 16 +- .../gptj_deepspeed_fine_tuning.ipynb | 23 +- .../huggingface_text_classification.ipynb | 37 +- .../getting-started-pytorch-lightning.rst | 106 +- doc/source/train/getting-started-pytorch.rst | 38 +- .../train/getting-started-transformers.rst | 56 +- doc/source/train/huggingface-accelerate.rst | 61 +- doc/source/train/images/logo.png | Bin 148696 -> 0 bytes doc/source/train/images/logo.svg | 20 + doc/source/train/more-frameworks.rst | 48 +- doc/source/train/overview.rst | 16 +- doc/source/train/train.rst | 24 +- doc/source/train/user-guides.rst | 2 +- .../data-loading-preprocessing.rst | 62 +- .../train/user-guides/experiment-tracking.rst | 150 +-- doc/source/tune/api/api.rst | 2 +- doc/source/tune/examples/batch_tuning.ipynb | 40 +- .../tune/examples/experiment-tracking.rst | 9 + doc/source/tune/examples/hpo-frameworks.rst | 14 + doc/source/tune/examples/index.rst | 10 + doc/source/tune/examples/ml-frameworks.rst | 17 +- doc/source/tune/examples/pbt_guide.ipynb | 10 +- doc/source/tune/index.rst | 10 + doc/source/tune/tutorials/overview.rst | 20 + doc/source/workflows/index.rst | 12 + 108 files changed, 3139 insertions(+), 3289 deletions(-) create mode 100644 doc/source/_static/css/assistant.css create mode 100644 doc/source/_static/css/csat.css create mode 100644 doc/source/_static/css/ray-libraries.css create mode 100644 doc/source/_static/css/ray-train.css create mode 100644 doc/source/_static/css/splash.css delete mode 100644 doc/source/_static/img/LandingPageBG.jpg create mode 100644 doc/source/_static/img/ray_logo.svg create mode 100644 doc/source/_static/img/search-icon.svg delete mode 100644 doc/source/_static/js/docsearch.js create mode 100644 doc/source/_static/js/examples.js create mode 100644 doc/source/_static/js/splash.js delete mode 100644 doc/source/_static/js/tags.js delete mode 100644 doc/source/_static/js/top-navigation.js delete mode 100644 doc/source/_templates/breadcrumbs.html create mode 100644 doc/source/_templates/examples-sidebar.html create mode 100644 doc/source/_templates/main-sidebar.html create mode 100644 doc/source/_templates/navbar-anyscale.html create mode 100644 doc/source/_templates/navbar-links.html create mode 100644 doc/source/_templates/navbar-ray-logo.html delete mode 100644 doc/source/_templates/sbt-sidebar-nav.html delete mode 100644 doc/source/_templates/sections/footer-content.html delete mode 100644 doc/source/_toc.yml create mode 100644 doc/source/index.rst create mode 100644 doc/source/navbar.yml delete mode 100644 doc/source/ray-overview/eco-gallery.yml rename doc/source/{index.md => splash.html} (74%) delete mode 100644 doc/source/train/images/logo.png create mode 100644 doc/source/train/images/logo.svg diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f891a96290cc..fb0bdb55d3d8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/doc/Makefile b/doc/Makefile index b60cf1ffe4c4..0edf0ce6eb11 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = -a -E +SPHINXOPTS = -a -E -W -j auto SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build diff --git a/doc/requirements-doc.txt b/doc/requirements-doc.txt index 5a5c37e359f5..1d29fab317fa 100644 --- a/doc/requirements-doc.txt +++ b/doc/requirements-doc.txt @@ -1,35 +1,33 @@ # Production requirements. This is what readthedocs.com picks up -ray -watchfiles # Required because sphinx-click doesn't support mocking +git+https://github.com/ray-project/tune-sklearn@master#tune-sklearn +watchfiles==0.20.0 # Required because sphinx-click doesn't support mocking # Syntax highlighting -Pygments==2.13.0 +Pygments==2.16.1 # Sphinx -sphinx==4.3.2 -sphinx-click==3.0.2 -sphinx-copybutton==0.4.0 +sphinx==7.2.6 +sphinx-click==5.0.1 +sphinx-copybutton==0.5.2 sphinxemoji==0.2.0 -sphinx-jsonschema==1.17.2 +sphinx-jsonschema==1.19.1 sphinx-version-warning==1.1.2 -sphinx-book-theme==0.3.3 -sphinx-external-toc==0.2.4 -sphinx-sitemap==2.2.0 +sphinx-sitemap==2.5.1 sphinxcontrib-redoc==1.6.0 -sphinx-tabs==3.4.0 sphinx-remove-toctrees==0.0.3 -autodoc_pydantic==1.6.1 -sphinx_design==0.4.1 +autodoc_pydantic==1.9.0 +sphinx_design==0.5.0 +pydata-sphinx-theme==0.14.1 -pydantic<2 # Pydantic is required by autodoc_pydantic, but must be <2 for ray +pydantic<2 # MyST -myst-parser==0.15.2 -myst-nb==0.13.1 +myst-parser==2.0.0 # Needed to parse markdown +myst-nb==1.0.0rc0 # Most recent version of myst-nb; pin when new release is made # Jupyter conversion -jupytext==1.13.6 +jupytext==1.15.2 # Pin urllib to avoid downstream ssl incompatibility issues urllib3 < 1.27 diff --git a/doc/source/_static/css/assistant.css b/doc/source/_static/css/assistant.css new file mode 100644 index 000000000000..62c1a24b2f5b --- /dev/null +++ b/doc/source/_static/css/assistant.css @@ -0,0 +1,136 @@ +/* Kapa Ask AI button */ +#kapa-widget-container figure { + padding: 0 !important; + } + + .mantine-Modal-root figure { + padding: 0 !important; + } + +.container-xl.blurred { + filter: blur(5px); +} + +.chat-widget { + position: fixed; + bottom: 10px; + right: 10px; + z-index: 1000; +} + +.chat-popup { + display: none; + position: fixed; + top: 20%; + left: 50%; + transform: translate(-50%, -20%); + width: 50%; + height: 70%; + background-color: var(--pst-color-surface); + border: 1px solid var(--pst-color-border); + border-radius: 10px; + box-shadow: 0 5px 10px var(--pst-color-shadow); + z-index: 1032; + max-height: 1000px; + overflow: hidden; + padding-bottom: 40px; +} + +.chatFooter { + position: absolute; + bottom: 0; + right: 0; + width: 100%; + background-color: var(--pst-color-surface); +} + +#openChatBtn { + background-color: var(--pst-color-on-background); + color: var(--pst-color-text-base); + width: 70px; + height: 70px; + border-radius: 10px; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + +#closeChatBtn { + border: none; + background-color: transparent; + color: var(--pst-color-text-base); + font-size: 1.2em; +} + +#closeChatBtn:hover { + color: var(--pst-color-link-hover); +} + +#searchBar { + border: 1px solid var(--pst-color-border); + background-color: var(--pst-color-on-surface); +} + +.chatHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: .5rem; +} + +.chatHeader > .header-wrapper { + text-align: center; + width: 100%; +} + +.chatContentContainer { + padding: 15px; + max-height: calc(100% - 80px); + overflow-y: auto; +} + +.chatContentContainer input { + margin-top: 10px; + margin-bottom: 10px; +} + +#result{ + padding: 15px; + border-radius: 10px; + margin-top: 10px; + margin-bottom: 10px; + background-color: var(--pst-color-on-surface); + max-height: calc(100% - 20px); + overflow-y: auto; +} + +.chatContentContainer textarea { + flex-grow: 1; + min-width: 50px; + max-height: 40px; + resize: none; +} + +.searchBtn { + white-space: nowrap; +} + +.input-group { + display: flex; + align-items: stretch; +} + +#blurDiv { + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; + backdrop-filter: blur(5px); + z-index: 1031; +} + +#blurDiv.blurDiv-hidden { + display: none !important; +} diff --git a/doc/source/_static/css/csat.css b/doc/source/_static/css/csat.css new file mode 100644 index 000000000000..bfba0053fb61 --- /dev/null +++ b/doc/source/_static/css/csat.css @@ -0,0 +1,77 @@ +/* CSAT widgets */ +#csat-inputs { + display: flex; + flex-direction: row; + align-items: center; +} + +.csat-hidden { + display: none !important; +} + +#csat-feedback-label { + color: var(--pst-color-text-base); + font-weight: 500; +} + +.csat-button { + margin-left: 16px; + padding: 8px 16px 8px 16px; + border-radius: 4px; + border: 1px solid var(--pst-color-border); + background: var(--pst-color-background); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + cursor: pointer; + width: 85px; +} + +#csat-textarea-group { + display: flex; + flex-direction: column; +} + +#csat-submit { + margin-left: auto; + font-weight: 700; + border: none; + margin-top: 12px; + cursor: pointer; +} + +#csat-feedback-received { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.csat-button-active { + border: 1px solid var(--pst-color-border); +} + +.csat-icon { + margin-right: 4px; +} + +#csat { + padding: 1em; + min-width: 60%; +} + +#csat-textarea { + resize: none; + background-color: var(--pst-color-on-background); + border: 1px solid var(--pst-color-border); + border-radius: 4px; +} + +#csat-textarea::placeholder { + color: var(--pst-color-text-base); +} + +.csat-icon > path { + fill: var(--pst-color-text-base); +} diff --git a/doc/source/_static/css/custom.css b/doc/source/_static/css/custom.css index d0510ee3142e..5713de86d9b5 100644 --- a/doc/source/_static/css/custom.css +++ b/doc/source/_static/css/custom.css @@ -1,915 +1,196 @@ -/* override default colors used in the Sphinx theme */ -:root { - --tabs-color-label-active: #0475DE; - --tabs-color-label-hover: #0475DE; - --buttons-color-blue: #0475DE; - --tabs-color-label-inactive: #9E9E9E; - --tabs-color-overline: #e0e0e0; - --tabs-color-underline: #e0e0e0; - --border-color-gray: #e0e0e0; - --background-color-light-gray:#fafafa; - --background-color-disabled: #9E9E9E; - --pst-color-link: 4, 117, 222; - --pst-color-primary: 4, 117, 222; - --pst-color-text-secondary: #616161; - --blue: #0475DE; - --sidebar-top: 5em; -} - -/* Remove flicker for announcement top bar replacement */ -.header-item.announcement { - background-color: white; - color: white; - padding: 0; -} - -/* Make the book theme secondary nav stick below the new main top nav */ -.header-article { - top: 58px; - z-index: 900 !important; -} - -.toctree-l1.has-children { - font-weight: bold; -} - -.toctree-l2 { - font-weight: normal; -} - -div.navbar-brand-box { - padding-top: 4em; -} - -td p { - margin-left: 0.75rem; -} - -table.longtable.table.autosummary { - table-layout: fixed; -} - -.table.autosummary td { - width: 100%; -} - -tr.row-odd { - background-color: #f9fafb; -} - -/* For Algolia search box - * overflow-y: to flow-over horizontally into main content - * height: to prevent topbar overlap -*/ -#site-navigation { - overflow-y: auto; - height: calc(100vh - var(--sidebar-top)); - position: sticky; - top: var(--sidebar-top) !important; -} - -/* Center the algolia search bar*/ -#search-input { - text-align: center; -} -.algolia-autocomplete { - width: 100%; - margin: auto; -} - -/* Hide confusing "<-" back arrow in navigation for larger displays */ -@media (min-width: 768px) { - #navbar-toggler { - display: none; - } -} - -/* Make navigation scrollable on mobile, by making algolia not overflow */ -@media (max-width: 768px) { - #site-navigation { - overflow-y: scroll; - } - - .algolia-autocomplete .ds-dropdown-menu{ - min-width: 250px; - } -} - -/* sphinx-panels overrides the content width to 1140 for large displays.*/ -@media (min-width: 1200px) { - .container, .container-lg, .container-md, .container-sm, .container-xl { - max-width: 1400px !important; - } -} - -.bottom-right-promo-banner { - position: fixed; - bottom: 100px; - right: 20px; - width: 270px; -} - -@media (max-width: 1500px) { - .bottom-right-promo-banner { - display: none; - } -} - -@media screen and (max-width: 767px) { - .remove-mobile { - display: none; - } - } - - @media screen and (max-width: 767px) { - .row-2-column { - flex-direction: column; - margin-top: 20px; - } - } - -/* Make Algolia search box scrollable */ -.algolia-autocomplete .ds-dropdown-menu { - height: 60vh !important; - overflow-y: scroll !important; -} - -.bd-sidebar__content { - overflow-y: unset !important; -} - -.bd-sidebar__top { - display: flex; - flex-direction: column; -} - -.bd-sidebar li { - position: relative; - word-wrap: break-word; -} - -nav.bd-links { - flex: 1; -} - -nav.bd-links::-webkit-scrollbar-thumb { - background-color: #ccc; -} - -nav.bd-links::-webkit-scrollbar { - width: 5px; -} - -dt:target, span.highlighted { - background-color: white; -} - -div.sphx-glr-bigcontainer { - display: inline-block; - width: 100%; -} - -td.tune-colab, -th.tune-colab { - border: 1px solid #dddddd; - text-align: left; - padding: 8px; -} - -/* Adjustment to Sphinx Book Theme */ -.table td { - /* Remove row spacing on the left */ - padding-left: 0; -} - -.table thead th { - /* Remove row spacing on the left */ - padding-left: 0; -} - -img.inline-figure { - /* Override the display: block for img */ - display: inherit !important; -} - -#version-warning-banner { - /* Make version warning clickable */ - z-index: 1; - margin-left: 0; - /* 20% is for ToC rightbar */ - /* 2 * 1.5625em is for horizontal margins */ - width: calc(100% - 20% - 2 * 1.5625em); -} - -/* allow scrollable images */ -.figure { - max-width: 100%; - overflow-x: auto; -} -img.horizontal-scroll { - max-width: none; -} - -.clear-both { - clear: both; - min-height: 100px; - margin-top: 15px; -} - -.buttons-float-left { - width: 150px; - float: left; -} - -.buttons-float-right { - width: 150px; - float: right; -} - -.card-body { - padding: 0.5rem !important; -} - -/* custom css for pre elements */ -pre { - /* Wrap code blocks instead of horizontal scrolling. */ - white-space: pre-wrap; - box-shadow: none; - border-color: var(--border-color-gray); - background-color: var(--background-color-light-gray); - border-radius:0.25em; -} - -/* notebook formatting */ -.cell .cell_output { - max-height: 250px; - overflow-y: auto; - font-weight: bold; -} - -/* Yellow doesn't render well on light background */ -.cell .cell_output pre .-Color-Yellow { - color: #785840; -} - -/* Newlines (\a) and spaces (\20) before each parameter */ -.sig-param::before { - content: "\a\20\20\20\20"; - white-space: pre; -} - -/* custom css for outlined buttons */ -.btn-outline-info:hover span, .btn-outline-primary:hover span { - color: #fff; -} - -.btn-outline-info, .btn-outline-primary{ - border-color: var(--buttons-color-blue); -} - -.btn-outline-info:hover, .btn-outline-primary:hover{ - border-color: var(--buttons-color-blue); - background-color: var(--buttons-color-blue); -} - -.btn-outline-info.active:not(:disabled):not(.disabled), .btn-outline-info:not(:disabled):not(.disabled):active, .show>.btn-outline-info.dropdown-toggle { - border-color: var(--buttons-color-blue); - background-color: var(--buttons-color-blue); - color: #fff; -} - -.btn-info, .btn-info:hover, .btn-info:focus { - border-color: var(--buttons-color-blue); - background-color: var(--buttons-color-blue); -} - -.btn-info:hover{ - opacity: 90%; -} - -.btn-info:disabled{ - border-color: var(--background-color-disabled); - background-color: var(--background-color-disabled); - opacity: 100%; -} - -.btn-info.active:not(:disabled):not(.disabled), .btn-info:not(:disabled):not(.disabled):active, .show>.btn-info.dropdown-toggle { - border-color: var(--buttons-color-blue); - background-color: var(--buttons-color-blue); -} - - -.topnav { - background-color: white; - border-bottom: 1px solid rgba(0, 0, 0, .1); - display: flex; - align-items: center; -} - -/* Content wrapper for the unified nav link / menus */ -.top-nav-content { - max-width: 1400px; - width: 100%; - margin-left: auto; - margin-right: auto; - padding: 0 1.5rem; - display: flex; - align-items: center; - justify-content: space-between; -} - -@media (max-width: 900px) { - /* If the window is too small, hide the custom sticky navigation bar at the top of the page. - Also make the pydata-sphinx-theme nav bar, which usually sits below the top nav bar, stick - to the top of the page. - */ - .top-nav-content { - display: none; - } - div.header-article.row.sticky-top.noprint { - position: sticky; - top: 0; - } -} - -/* Styling the links and menus in the top nav */ -.top-nav-content a { - text-decoration: none; - color: black; - font-size: 17px; -} - -.top-nav-content a:hover { - color: #007bff; -} - -/* The left part are the links and menus */ -.top-nav-content > .left { - display: flex; - white-space: nowrap; -} - -.top-nav-content .left > * { - margin-right: 8px; -} - -.top-nav-content .left > a, -.top-nav-content .left > .menu > a { - text-align: center; - padding: 14px 16px; - border-bottom: 2px solid white; -} - -.top-nav-content .menu:hover > a, -.top-nav-content .left > a:hover { - border-bottom: 2px solid #007bff; -} - -/* Special styling for the Ray logo */ -.top-nav-content .left > a.ray-logo { - width: 90px; - padding: 10px 0; -} -.top-nav-content .left > a.ray-logo:hover { - border-bottom: 2px solid white; -} - -/* Styling the dropdown menus */ -.top-nav-content .menu { - display: flex; -} -.top-nav-content .menu > a > .down-caret { - margin-left: 8px; -} -.top-nav-content .menu > ul { - display: none; -} - -.top-nav-content > button.try-anyscale > span { - margin: 0 12px; -} - -.top-nav-content .menu:hover > ul { - display: flex; - flex-direction: column; - align-items: flex-start; - box-shadow: 0 5px 15px 0 rgb(0 0 0 / 10%); - padding: 15px; - width: 330px; - position: absolute; - z-index: 2000; - background-color: white; - top: 58px; -} - -.top-nav-content .menu:hover > ul > li { - list-style: none; - padding: 5px 0; -} - -.top-nav-content .menu:hover > ul > li span { - display: block; -} - -.top-nav-content .menu:hover > ul > li span.secondary { - color: #787878; -} - -/* Styling the "Try Anyscale" button */ -.top-nav-content > button.try-anyscale { - float: right; - border-radius: 6px; - background-color: #e7f2fa; - padding-left: 12px; - padding-right: 12px; - margin-left: 12px; - height: 40px; - border: none; - white-space: nowrap; -} - -@media (max-width: 1000px) { - .top-nav-content > button.try-anyscale { - display: none; - } -} - -/* custom css for tabs*/ -.tabbed-set>label,.tabbed-set>label:hover { - border-bottom: 1px solid var(--border-color-gray); - color:var(--tabs-color-label-inactive); - font-weight: 500; -} - -.tabbed-set>input:checked+label{ - border-bottom: 0.125em solid; - color:var(--tabs-color-label-active); -} - - -.tabbed-label{ - margin-bottom:0; -} - -/* custom css for jupyter cells */ -div.cell div.cell_input{ - border: 1px var(--border-color-gray) solid; - background-color: var(--background-color-light-gray); - border-radius:0.25em; - border-left-color: var(--green); - border-left-width: medium; -} - -/* custom css for table */ -table { - border-color: var(--border-color-gray); -} - -/* custom css for topic component */ -div.topic{ - border: 1px solid var(--border-color-gray); - border-radius:0.25em; -} - -.topic { - background-color: var(--background-color-light-gray); -} - -/* custom css for card component */ -.card{ - border-color: var(--border-color-gray); -} - -.card-footer{ - background-color: var(--background-color-light-gray); - border-top-color: var(--border-color-gray); -} - -/* custom css for section navigation component */ -.bd-toc nav>.nav { - border-left-color: var(--border-color-gray); -} - -/* custom css for up and down arrows in collapsible cards */ -details.dropdown .summary-up, details.dropdown .summary-down { - top: 1em; -} - -/* remove focus border in collapsible admonition buttons */ -.toggle.admonition button.toggle-button:focus { - outline: none; -} - -/* custom css for shadow class */ -.shadow { - box-shadow: 0 0.2rem 0.5rem rgb(0 0 0 / 5%), 0 0 0.0625rem rgb(0 0 0 / 10%) !important; -} - -/* custom css for text area */ -textarea { - border-color: var(--border-color-gray); -} - -/* custom css for footer */ -footer { - margin-top: 1rem; - padding:1em 0; - border-top-color: var(--border-color-gray); -} - -.footer p{ - color: var(--pst-color-text-secondary); -} - -/* Make the hover color of tag/gallery buttons differ from "active" */ -.tag.btn-outline-primary:hover { - background-color: rgba(20, 99, 208, 0.62) !important; -} - -span.rst-current-version > span.fa.fa-book { - /* Move the book icon away from the top right - * corner of the version flyout menu */ - margin: 10px 0px 0px 5px; -} - - -/*Extends the docstring signature box.*/ -.rst-content dl:not(.docutils) dt { - display: block; - padding: 10px; - word-wrap: break-word; - padding-right: 100px; -} - -/*Lists in an admonition note do not have awkward whitespace below.*/ -.rst-content .admonition-note .section ul { - margin-bottom: 0; -} - -/*Properties become blue (classmethod, staticmethod, property)*/ -.rst-content dl dt em.property { - color: #2980b9; - text-transform: uppercase; -} - -.rst-content .section ol p, -.rst-content .section ul p { - margin-bottom: 0; -} - - -/* Adjustment to Version block */ -.rst-versions { - z-index: 1200 !important; -} - -.image-header { - display: flex; - flex-direction: row; - align-items: center; - padding-left: 16px; - padding-right:16px; - gap: 16px; -} - -.info-box { - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.05); - border-radius: 8px; - padding: 20px; -} - -.info-box:hover{ - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1); -} - -.no-underline{ - text-decoration: none; -} -.no-underline:hover{ - text-decoration: none; -} - -.icon-hover:hover{ - height: 30px ; - width: 30px; -} - -.info-box-2 { - background-color: #F9FAFB; - border-radius: 8px; - padding-right: 16px; - padding-left: 16px; - padding-bottom: 24px; - padding-top: 4px; -} - - -.bold-link { - color: #000000 !important; - font-weight: 600; -} - -.community-box { - border: 1px solid #D2DCE6; - border-radius: 8px; - display: flex; - margin-bottom: 16px; -} - -.community-box:hover { - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.05); - text-decoration: none; -} - -.community-box p { - margin-top: 1rem !important; -} - -.tab-pane pre { - margin: 0; - padding: 0; - max-height: 252px; - overflow-y: auto; -} - -.grid-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); - grid-gap: 16px; -} - -.grid-item { -padding: 20px; -} - - -.nav-pills { - background-color: #F9FAFB; - color: #000000; - padding: 8px; - border-bottom:none; - border-radius: 8px; -} - -.nav-pills .nav-link.active { - background-color: #FFFFFF !important; - box-shadow: 0px 3px 14px 2px rgba(3,28,74,0.12); - border-radius: 8px; - padding: 20px; - color: #000000; - font-weight: 500; -} - -.searchDiv { - width: 100%; - position: relative; - display: block; -} - -.searchTerm { - width: 80%; - border: 2px solid var(--blue); - padding: 5px; - height: 45px; - border-radius: 5px; - outline: none; -} - -.searchButton { - width: 40px; - height: 45px; - border: 1px solid var(--blue); - background: var(--blue); - color: #fff; - border-radius: 5px; - cursor: pointer; - font-size: 20px; -} - -/*Resize the wrap to see the search bar change!*/ -.searchWrap { - width: 100%; - position: relative; - margin: 15px; - top: 50%; - left: 50%; - transform: translate(-50%, -10%); - text-align: center; -} - -.sd-card { - border: none !important; -} - -.tag { - margin-bottom: 5px; - font-size: small; -} - -/* Override float positioning of next-prev buttons so that - they take up space normally, and we can put other stuff at - the bottom of the page. */ -.prev-next-area { - display: flex; - flex-direction: row; -} -.prev-next-area a.left-prev { - margin-right: auto; - width: fit-content; - float: none; -} -.prev-next-area a.right-next { - margin-left: auto; - width: fit-content; - float: none; -} - -/* CSAT widgets */ -#csat-inputs { - display: flex; - flex-direction: row; +/* Override default colors used in the Sphinx theme. See +* https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/styling.html#css-theme-variables +* for more information. `important!` is needed below to override +* dark/light theme specific values, which normally take precedence over the PST defaults. +* */ +html { + --anyscale-blue: #0641AC; + --ray-blue: #3C8AE9; /* Ray blue color - use this for all ray branding */ + --pst-color-primary: var(--ray-blue) !important; + --pst-color-link-hover: var(--ray-blue) !important; + --pst-color-inline-code-links: var(--ray-blue) !important; + + /* Transparent highlight color; default yellow is hard on the eyes */ + --pst-color-target: #FFFFFF00 !important; +} + +/* Hide the "Hide Search Matches" button (we aren't highlighting search terms anyway) */ +#searchbox { + display: none; +} + +/* Top navbar styling */ +.navbar-toplevel p { + margin: 0; + padding-inline-start: 0; +} +.ref-container > p { + height: 100%; +} +div.navbar-dropdown { + display: none; + position: relative; + left: -50%; + color: var(--pst-color-text-muted); +} +span.navbar-link-title { + color: var(--pst-color-text-base); +} +.navbar-sublevel p a.reference { + text-decoration: none; + color: var(--pst-color-text-muted); +} +.navbar-sublevel p a.reference:hover > span.navbar-link-title { + text-decoration: underline; + color: var(--pst-color-link-hover); +} +.navbar-toplevel li { + display: inline-flex; + justify-content: center; align-items: center; + height: 100%; + padding: 0em 1em; } - -.csat-hidden { - display: none !important; +ul.navbar-toplevel li:hover > div.navbar-dropdown { + display: block; } - -#csat-feedback-label { - color: #000; - font-weight: 500; +ul.navbar-toplevel > li:hover { + border-bottom: } - -.csat-button { - margin-left: 16px; - padding: 8px 16px 8px 16px; - border-radius: 4px; - border: 1px solid #D2DCE6; - background: #FFF; +ul.navbar-toplevel { display: flex; flex-direction: row; - align-items: center; justify-content: center; - cursor: pointer; - width: 85px; -} - -#csat-textarea-group { + align-items: center; + margin: 0; + height: 100%; +} +.navbar-content ul.navbar-sublevel { + position: absolute; + background: var(--pst-color-on-background); + white-space: pre; + padding: 0em 1em; display: flex; flex-direction: column; + align-items: baseline; + box-shadow: 0 5px 15px 0 rgb(0 0 0 / 10%); } - -#csat-submit { - margin-left: auto; - font-weight: 700; - border: none; - margin-top: 12px; - cursor: pointer; -} - -#csat-feedback-received { +div.navbar-content a { display: flex; - flex-direction: row; - align-items: center; + flex-direction: column; + align-items: start; + white-space: pre; justify-content: center; } - -.csat-button-active { - border: 1px solid #000; +div.navbar-content { + height: 100%; } - -.csat-icon { - margin-right: 4px; +nav.navbar-nav { + height: 100%; } - -footer.col.footer { +.ref-container { display: flex; flex-direction: row; + justify-content: center; + align-items: center; + gap: 0.5em; + height: 100%; } - -footer.col.footer > p { - margin-left: auto; +.navbar-header-items__end { + /* Prevent the anyscale button from wrapping */ + flex-flow: nowrap !important; } -#csat { - min-width: 60%; +/* Highlight active nav bar link; offset content to compensate for border size */ +li.active-link { + border-bottom: 4px solid var(--ray-blue); } - -#csat-textarea { - resize: none; +li.active-link > .ref-container { + transform: translateY(2px); } - -/* Ray Assistant */ - -.container-xl.blurred { - filter: blur(5px); +/* Ray logo */ +.navbar-brand.logo > svg { + width: 120px; } - -.chat-widget { - position: fixed; - bottom: 10px; - right: 10px; - z-index: 1000; +.navbar-brand.logo > svg path#ray-text { + fill: var(--pst-color-text-base); } - -.chat-popup { - display: none; - position: fixed; - top: 20%; - left: 50%; - transform: translate(-50%, -20%); - width: 50%; - height: 70%; - background-color: white; - border: 1px solid #ccc; - border-radius: 10px; - box-shadow: 0 5px 10px rgba(0,0,0,0.1); - z-index: 1001; - max-height: 1000px; - overflow: hidden; - padding-bottom: 40px; +.navbar-brand.logo > svg path#ray-logo { + fill: var(--ray-blue); } -.chatFooter { - position: absolute; - bottom: 0; - right: 0; - width: 100%; - background-color: #f8f9fa; +/* Anyscale branding */ +#anyscale-logo > path { + fill: var(--anyscale-blue); } - -#openChatBtn { - background-color: #000; - color: #fff; - width: 70px; - height: 70px; - border-radius: 10px; - border: none; - display: flex; - align-items: center; - justify-content: center; +#try-anyscale { + color: var(--pst-color-text-base); + background-color: var(--pst-color-surface); + border-radius: 6px; + white-space: nowrap; + padding: 0px 12px; + height: 40px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 1em; + border: 1px solid var(--pst-color-border); } -#closeChatBtn { - border: none; - background-color: transparent; - color: #000; - font-size: 1.2em; +/* Center the search button in the sidebar */ +div.sidebar-primary-item:nth-child(1) { + display: flex; + flex-direction: row; + justify-content: center; } -#closeChatBtn:hover { - color: #888; +/* Disable the "Back to top" button that appears if you scroll down */ +button#pst-back-to-top { + display: none !important; } -.chatHeader { - display: flex; - justify-content: space-between; - align-items: center; +/* Hide the rtd-footer-container; we aren't using it */ +#rtd-footer-container { + display: none; } -.chatContentContainer { - padding: 15px; - max-height: calc(100% - 80px); - overflow-y: auto; +.bottom-right-promo-banner { + position: fixed; + bottom: 100px; + right: 20px; + width: 270px; } -.chatContentContainer input { - margin-top: 10px; - margin-bottom: 10px; +@media (max-width: 1500px) { + .bottom-right-promo-banner { + display: none; + } } -#result{ - padding: 15px; - border-radius: 10px; - margin-top: 10px; - margin-bottom: 10px; - background-color: #f8f9fa; - max-height: calc(100% - 20px); - overflow-y: auto; +/* Nav sidebar styles */ +/* Sidebar checkboxes are toggled by clicking on the label; hide actual checkboxes */ +.sidebar-checkbox { + display: none; } - -.chatContentContainer textarea { - flex-grow: 1; - min-width: 50px; - max-height: 40px; - resize: none; +.sidebar-checkbox[type="checkbox"]:checked ~ dd { + display: none; } -.searchBtn { - white-space: nowrap; +/* Fix some spacing issues associated with competition with PST styles */ +.sidebar-content dl { + margin-bottom: 0; } - -.input-group { - display: flex; - align-items: stretch; +.sidebar-content ol li > p:first-child, ul li > p:first-child { + margin-top: 0 !important; } -/* Kapa Ask AI button */ -#kapa-widget-container figure { - padding: 0 !important; - } - - .mantine-Modal-root figure { - padding: 0 !important; - } \ No newline at end of file +/* Set autosummary API docs to have fixed two-col format, with alternating different background +* on rows */ +table.autosummary { + table-layout: fixed; +} +table.autosummary .row-odd { + background-color: var(--pst-color-surface); +} diff --git a/doc/source/_static/css/examples.css b/doc/source/_static/css/examples.css index bb641d75147f..cff0aa9ddd58 100644 --- a/doc/source/_static/css/examples.css +++ b/doc/source/_static/css/examples.css @@ -1,89 +1,108 @@ :root { --ray-example-gallery-gap-x: 18px; --ray-example-gallery-gap-y: 22px; - --sidebar-top: 5em; } -#site-navigation { - width: 330px !important; - border-right: none; - margin-left: 32px; - overflow-y: auto; - max-height: calc(100vh - var(--sidebar-top)); - position: sticky; - top: var(--sidebar-top) !important; - z-index: 1000; +.gallery-sidebar { + width: 100%; } -#site-navigation h5 { +.gallery-sidebar h5 { font-size: 16px; font-weight: 600; - color: #000; } -#site-navigation h6 { +.gallery-sidebar h6 { font-size: 14px; font-weight: 600; - color: #000; text-transform: uppercase; } -/* Hide the default sidebar content */ -#site-navigation > div.bd-sidebar__content { - display: none; +.searchDiv { + margin-bottom: 2em; + display: flex; + flex-direction: row; + border: 1px solid var(--pst-color-border); + border-radius: 4px; + background-color: var(--pst-color-on-background); + justify-content: center; + align-items: center; + height: 50px; } -#site-navigation > div.rtd-footer-container { - display: none; + +#search-input-label { + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; } -.searchDiv { - margin-bottom: 2em; +.searchDiv:focus-within { + outline: 2px solid var(--pst-color-border); +} + +#search-icon { + fill: var(--pst-color-text-base); + margin: 0em 1em; } #searchInput { width: 100%; - color: #5F6469; - border: 1px solid #D2DCE6; - height: 50px; - border-radius: 4px; - background-color: #F9FAFB; - background-image: url("data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 25 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Systems / search-line' clip-path='url(%23clip0_1_150)'%3E%3Crect width='24' height='24' transform='translate(0.398529 0.0546875)' fill='%23F9FAFB'/%3E%3Cg id='Group'%3E%3Cpath id='Vector' d='M18.4295 16.6717L22.7125 20.9537L21.2975 22.3687L17.0155 18.0857C15.4223 19.3629 13.4405 20.0576 11.3985 20.0547C6.43053 20.0547 2.39853 16.0227 2.39853 11.0547C2.39853 6.08669 6.43053 2.05469 11.3985 2.05469C16.3665 2.05469 20.3985 6.08669 20.3985 11.0547C20.4014 13.0967 19.7068 15.0784 18.4295 16.6717ZM16.4235 15.9297C17.6926 14.6246 18.4014 12.8751 18.3985 11.0547C18.3985 7.18669 15.2655 4.05469 11.3985 4.05469C7.53053 4.05469 4.39853 7.18669 4.39853 11.0547C4.39853 14.9217 7.53053 18.0547 11.3985 18.0547C13.219 18.0576 14.9684 17.3488 16.2735 16.0797L16.4235 15.9297V15.9297Z' fill='%238C9196'/%3E%3C/g%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_1_150'%3E%3Crect width='24' height='24' fill='white' transform='translate(0.398529 0.0546875)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A"); - background-repeat: no-repeat; - background-position-x: 0.5em; - background-position-y: center; - background-size: 1.5em; - padding-left: 3em; + color: var(--pst-color-text-base); + border: none; + background-color: transparent; +} + +#searchInput:focus-visible { + outline: none; } #searchInput::placeholder { - color: #5F6469; + color: var(--pst-color-text-muted); opacity: 1; } -.tag { - margin-bottom: 5px; - font-size: small; - color: #000000; - border: 1px solid #D2DCE6; - border-radius: 14px; - display: flex; - flex-direction: row; - align-items: center; - width: fit-content; - gap: 1em; +/* Tag button styling */ +.tag-group { + display: flex; + flex-flow: wrap; + gap: 0em 0.5em; } - -.tag.btn-outline-primary { - color: #000000; +.tag { + margin-bottom: 5px; + font-size: small; + background-color: var(--pst-color-on-background); + border: 1px solid var(--pst-color-border); + border-radius: 14px; + display: flex; + flex-direction: row; + align-items: center; + width: fit-content; + gap: 0.5em; + color: var(--pst-color-text-base); padding: 3px 12px 3px 12px; line-height: 20px; } -.tag-btn-wrapper { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 1em; +.tag > svg > path { + fill: var(--pst-color-text-base); +} +.tag:hover { + background-color: var(--pst-color-info-highlight) !important; + color: var(--pst-color-info-text) !important; +} +.tag:hover > svg > path { + fill: var(--pst-color-info-text) !important; +} + +/* Selected tag buttons */ +.tag.btn-primary { + background-color: var(--pst-color-info-bg); +} +/* Inactive tag buttons */ +.tag.btn-outline-primary { + background-color: var(--pst-color-on-background); } div.sd-container-fluid.docutils > div { @@ -103,9 +122,10 @@ div.gallery-item { width: auto; } +/* Override sphinx-design box shadow styles */ div.gallery-item > div.sd-card { - border-radius: 8px; - box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.05) !important; + box-shadow: 0 .125rem .25rem var(--pst-color-shadow) !important; + border: none; } /* Example gallery "Tutorial" title */ @@ -148,18 +168,6 @@ div.sd-card-title > span.sd-bg-info.sd-bg-text-info { background-color: initial !important; } -div.sd-card-body > p.sd-card-text > a { - text-align: initial; -} - -div.sd-card-body > p.sd-card-text > a > span { - color: rgb(81, 81, 81); -} - -#main-content { - max-width: 100%; -} - #noMatches { display: flex; flex-direction: column; @@ -177,47 +185,3 @@ div.sd-card-body > p.sd-card-text > a > span { #noMatches.hidden,.gallery-item.hidden { display: none !important; } - -.btn-primary { - color: #004293; - background: rgba(61, 138, 233, 0.20); - padding: 3px 12px 3px 12px; - border: 1px solid #D2DCE6; -} - -button.try-anyscale { - background-color: initial !important; - width: fit-content; - padding: 0 !important; - margin-left: auto !important; - float: initial !important; -} - -button.try-anyscale > svg { - display: none; -} - -button.try-anyscale > i { - display: none; -} - -button.try-anyscale > span { - margin: 0; - text-decoration-line: underline; - font-weight: 500; - color: #000; -} - -.top-nav-content { - justify-content: initial; -} - -/* Hide nav bar that has github, fullscreen, and print icons */ -div.header-article.row.sticky-top.noprint { - display: none !important; -} - -/* Hide the footer with 'prev article' and 'next article' buttons */ -.footer-article.hidden { - display: none !important; -} diff --git a/doc/source/_static/css/ray-libraries.css b/doc/source/_static/css/ray-libraries.css new file mode 100644 index 000000000000..20c171fcbb77 --- /dev/null +++ b/doc/source/_static/css/ray-libraries.css @@ -0,0 +1,12 @@ +.sd-card.body { + display: flex; + flex-direction: column; +} +.sd-card-body > figure { + height: 10em; +} +.card-figure { + object-fit: contain; + width: 100%; + height: 100%; +} diff --git a/doc/source/_static/css/ray-train.css b/doc/source/_static/css/ray-train.css new file mode 100644 index 000000000000..ffe5475e7c96 --- /dev/null +++ b/doc/source/_static/css/ray-train.css @@ -0,0 +1,14 @@ +#train-logo > #train-logo-icon > path, #train-logo > #train-logo-icon > circle { + stroke: var(--ray-blue); + stroke-width: 10; + fill: transparent; +} + +#train-logo > #train-logo-text { + stroke: var(--pst-color-text-base); + fill: var(--pst-color-text-base); +} + +#train-logo { + margin: 3em 0em; +} diff --git a/doc/source/_static/css/splash.css b/doc/source/_static/css/splash.css new file mode 100644 index 000000000000..cf091ace1b3d --- /dev/null +++ b/doc/source/_static/css/splash.css @@ -0,0 +1,130 @@ +.image-header { + display: flex; + flex-direction: row; + align-items: center; + padding-left: 16px; + padding-right:16px; + gap: 16px; +} + +.info-box { + background-color: var(--pst-color-surface); + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.05); + border-radius: 8px; + padding: 20px; +} + +.info-box:hover{ + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1); +} + +.no-underline{ + text-decoration: none; +} +.no-underline:hover{ + text-decoration: none; +} + +.icon-hover:hover{ + height: 30px ; + width: 30px; +} + +.info-box-2 { + background-color: var(--pst-color-surface); + border-radius: 8px; + padding-right: 16px; + padding-left: 16px; + padding-bottom: 24px; + padding-top: 4px; +} + + +.bold-link { + color: var(--pst-color-link) !important; + font-weight: 600; +} + +.community-box { + border: 1px solid var(--pst-color-border); + border-radius: 8px; + display: flex; + margin-bottom: 16px; +} + +.community-box:hover { + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.05); + text-decoration: none; +} + +.community-box p { + margin-top: 1rem !important; +} + +.tab-pane pre { + margin: 0; + padding: 0; + max-height: 252px; + overflow-y: auto; + animation: fadeEffect 1s; /* Fading effect takes 1 second */ +} + +.grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 16px; +} + +.grid-item { +padding: 20px; +} + +.nav-pills { + background-color: var(--pst-color-surface); + padding: 8px; + border-bottom:none; + border-radius: 8px; +} + +.nav-pills .nav-link.active { + background-color: var(--pst-color-background) !important; + box-shadow: 0px 3px 14px 2px var(--pst-color-shadow); + border-radius: 8px; + padding-top: 1rem; + padding-bottom: 1rem; + color: var(--pst-color-text-base); + font-weight: 500; +} + +#v-pills-tab > .nav-link:hover { + text-decoration: none; +} + +#v-pills-tab > a { + cursor: pointer; +} + +#v-pills-tab > .nav-link { + color: var(--pst-color-text-base); +} + +#v-pills-tabContent { + box-shadow: 0px 6px 30px 5px var(--pst-color-shadow); + border-radius:8px; +} + +#v-pills-data { + user-select: none; +} + +/* Go from zero to full opacity */ +@keyframes fadeEffect { + from {opacity: 0;} + to {opacity: 1;} +} + +#ray-intro-video { + width: 100%; + height: 100%; + border-radius: 10px; +} diff --git a/doc/source/_static/img/LandingPageBG.jpg b/doc/source/_static/img/LandingPageBG.jpg deleted file mode 100644 index 63daa159a8f4e29f828e3d42a0ff62d983d652c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35876 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 G(f1z^w53r1 diff --git a/doc/source/_static/img/ray_logo.svg b/doc/source/_static/img/ray_logo.svg new file mode 100644 index 000000000000..1b8c1e8e4151 --- /dev/null +++ b/doc/source/_static/img/ray_logo.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/doc/source/_static/img/search-icon.svg b/doc/source/_static/img/search-icon.svg new file mode 100644 index 000000000000..c9ed1ed5cfbf --- /dev/null +++ b/doc/source/_static/img/search-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/doc/source/_static/js/assistant.js b/doc/source/_static/js/assistant.js index cf6ebaf55245..b8874552c5aa 100644 --- a/doc/source/_static/js/assistant.js +++ b/doc/source/_static/js/assistant.js @@ -11,40 +11,44 @@ var chatPopupDiv = document.createElement("div"); chatPopupDiv.className = "chat-popup"; chatPopupDiv.id = "chatPopup"; chatPopupDiv.innerHTML = ` -
-
-
- Ray Docs AI - Ask a question -
- +
+
+

Ray Docs AI - Ask a question

-
-
- -
- -
-
-
-
-
- © Copyright 2023, The Ray Team. + +
+
+
+ +
+ +
+
+
+
+ © Copyright 2023, The Ray Team. +
` document.body.appendChild(chatPopupDiv); +const blurDiv = document.createElement('div') +blurDiv.id = "blurDiv" +blurDiv.classList.add("blurDiv-hidden") +document.body.appendChild(blurDiv); + // blur background when chat popup is open document.getElementById('openChatBtn').addEventListener('click', function() { - document.getElementById('chatPopup').style.display = 'block'; - document.querySelector('.container-xl').classList.add('blurred'); + document.getElementById('chatPopup').style.display = 'block'; + blurDiv.classList.remove("blurDiv-hidden"); }); // un-blur background when chat popup is closed document.getElementById('closeChatBtn').addEventListener('click', function() { - document.getElementById('chatPopup').style.display = 'none'; - document.querySelector('.container-xl').classList.remove('blurred'); + document.getElementById('chatPopup').style.display = 'none'; + blurDiv.classList.add("blurDiv-hidden"); }); // set code highlighting options diff --git a/doc/source/_static/js/custom.js b/doc/source/_static/js/custom.js index b4cee62b852d..d3da273bb762 100644 --- a/doc/source/_static/js/custom.js +++ b/doc/source/_static/js/custom.js @@ -28,128 +28,11 @@ window.addEventListener("scroll", loadVisibleTermynals); createTermynals(); loadVisibleTermynals(); -// Reintroduce dropdown icons on the sidebar. This is a hack, as we can't -// programmatically figure out which nav items have children anymore. +// Send GA events any time a code block is copied document.addEventListener("DOMContentLoaded", function() { - let navItems = document.querySelectorAll(".bd-sidenav li"); - - const defaultStyle = {"fontWeight": "bold"} - - const stringList = [ - {"text": "User Guides"}, - {"text": "Examples"}, - // Ray Core - {"text": "Ray Core"}, - {"text": "Ray Core API"}, - // Ray Cluster - {"text": "Ray Clusters"}, - {"text": "Deploying on Kubernetes"}, - {"text": "Deploying on VMs"}, - {"text": "Applications Guide"}, - {"text": "Ray Cluster Management API"}, - {"text": "Getting Started with KubeRay"}, - {"text": "KubeRay Ecosystem"}, - {"text": "KubeRay Benchmarks"}, - {"text": "KubeRay Troubleshooting"}, - // Ray Data - {"text": "Ray Data"}, - {"text": "Ray Data API"}, - {"text": "Integrations"}, - // Ray Train - {"text": "Ray Train"}, - {"text": "More Frameworks"}, - {"text": "Advanced Topics"}, - {"text": "Internals"}, - {"text": "Ray Train API"}, - // Ray Tune - {"text": "Ray Tune"}, - {"text": "Ray Tune Examples"}, - {"text": "Ray Tune API"}, - // Ray Serve - {"text": "Ray Serve"}, - {"text": "Ray Serve API"}, - {"text": "Production Guide"}, - {"text": "Advanced Guides"}, - {"text": "Deploy Many Models"}, - // Ray RLlib - {"text": "Ray RLlib"}, - {"text": "Ray RLlib API"}, - // More libraries - {"text": "More Libraries"}, - {"text": "Ray Workflows (Alpha)"}, - // Monitoring/debugging - {"text": "Monitoring and Debugging"}, - // References - {"text": "References"}, - {"text": "Use Cases", "style": {}}, // Don't use default style: https://github.com/ray-project/ray/issues/39172 - // Developer guides - {"text": "Developer Guides"}, - {"text": "Getting Involved / Contributing"}, - ]; - - Array.from(navItems).filter( - item => stringList.some(({text}) => item.innerText === text) && ! item.classList.contains('current') - ).forEach((item, i) => { - if (item.classList.contains('toctree-l1')) { - const { style } = stringList.find(({text}) => item.innerText == text) - - // Set the style on the menu items - Object.entries(style ?? defaultStyle).forEach(([key, value]) => { - item.style[key] = value - }) - - } - item.innerHTML += - `` - + '' - + '' - }) -}); - -// Dynamically adjust the height of all panel elements in a gallery to be the same as -// that of the max-height element. -document.addEventListener("DOMContentLoaded", function() { - let images = document.getElementsByClassName("fixed-height-img"); - let maxHeight = 0; - - for (let i = 0; i < images.length; i++) { - if (images[i].height > maxHeight) { - maxHeight = images[i].height; - } - } - - for (let i = 0; i < images.length; i++) { - let margin = Math.floor((maxHeight - images[i].height) / 2); - images[i].style.cssText = "margin-top: " + margin + "px !important;" + - "margin-bottom: " + margin + "px !important;" - } -}); - -// Remember the scroll position when the page is unloaded. -window.onload = function() { - let sidebar = document.querySelector("#site-navigation"); - - window.onbeforeunload = function() { - let scroll = sidebar.scrollTop; - localStorage.setItem("scroll", scroll); - } - - let storedScrollPosition = localStorage.getItem("scroll"); - if (storedScrollPosition) { - sidebar.scrollTop = storedScrollPosition; - localStorage.removeItem("scroll"); - } -}; - -// When the document is fully loaded -document.addEventListener("DOMContentLoaded", function() { - // find all the code blocks' copy buttons let codeButtons = document.querySelectorAll(".copybtn"); for (let i = 0; i < codeButtons.length; i++) { const button = codeButtons[i]; - // and add a click event listener to each one for Google Analytics. button.addEventListener("click", function() { gtag("event", "code_copy_click", { "send_to": "UA-110413294-1", @@ -161,3 +44,16 @@ document.addEventListener("DOMContentLoaded", function() { }); } }); + +document.addEventListener("DOMContentLoaded", function() { + let anyscaleButton = document.getElementById("try-anyscale") + anyscaleButton.onclick = () => { + gtag("event", "try_anyscale", { + "send_to": "UA-110413294-1", + "event_category": "TryAnyscale", + "event_label": "TryAnyscale", + "value": 1, + }); + window.open('https://www.anyscale.com', '_blank'); + } +}); diff --git a/doc/source/_static/js/docsearch.js b/doc/source/_static/js/docsearch.js deleted file mode 100644 index e8851333e4ab..000000000000 --- a/doc/source/_static/js/docsearch.js +++ /dev/null @@ -1,35 +0,0 @@ -docsearch({ - apiKey: '6c42f30d9669d8e42f6fc92f44028596', - indexName: 'docs-ray', - appId: 'LBHF0PABBL', - inputSelector: '#search-input', - debug: false, - algoliaOptions: { - hitsPerPage: 10, - }, - autocompleteOptions: { - autoselect: false, - }, - handleSelected: function (input, event, suggestion, datasetNumber, context) { - if (context.selectionMethod === 'click') { - input.setVal(''); - const windowReference = window.open(suggestion.url, "_self"); - windowReference.focus(); - } - } -}); - -const searchInput = document.getElementById("search-input"); -searchInput.addEventListener("keydown", function (e) { - if (e.code === "Enter") { - var searchForm = document.getElementsByClassName("bd-search")[0]; - const text = searchInput.value; - - const pageUrl = window.location.href - const res = pageUrl.split("/"); - const version = (res.length <= 3) ? "latest" : res[4]; - - searchForm.action = "https://docs.ray.io/en/" + version + "/search.html?q=" + text; - searchForm.submit(); - } -}); diff --git a/doc/source/_static/js/examples.js b/doc/source/_static/js/examples.js new file mode 100644 index 000000000000..b31e36f9abfa --- /dev/null +++ b/doc/source/_static/js/examples.js @@ -0,0 +1,111 @@ +/** + * Check whether a panel matches the selected filter tags. + * + * @param {any} panel Example gallery item + * @param {Array>} groupedActiveTags Groups of tags selected by the user. + * @returns {boolean} True if the panel should be shown, false otherwise + */ +function panelMatchesTags(panel, groupedActiveTags) { + // Show the panel if every tagGroup has at least one active tag in the classList, + // or if no tag in a group is selected. + return groupedActiveTags.every(tagGroup => { + return tagGroup.length === 0 || Array.from(panel.classList).some(tag => tagGroup.includes(tag)) + }) +} + + +window.addEventListener('load', () => { + + /* Fetch the tags that the user can filter on from the buttons in the sidebar + * Additionally retrieve the elements that we need for filtering. + */ + const tags = {} + document.querySelectorAll('div.tag-section').forEach(group => { + tags[group.id] = group.querySelectorAll('div.tag-group > div.tag.btn') + }) + const noMatchesElement = document.querySelector("#noMatches"); + const panels = document.querySelectorAll('.gallery-item') + + /** + * Filter the links to the examples in the example gallery + * by the selected tags and the current search query. + */ + function filterPanels() { + const query = document.getElementById("searchInput").value.toLowerCase(); + const activeTags = Array.from(document.querySelectorAll('.tag.btn-primary')).map(el => el.id); + const groupedActiveTags = Object.values(tags).map(group => { + const tagNames = Array.from(group).map(element => element.id); + return activeTags.filter(activeTag => tagNames.includes(activeTag)); + }) + + // Show all panels first + panels.forEach(panel => panel.classList.remove("hidden")); + + let toHide = []; + let toShow = []; + + // Show each panel if it has every active tag and matches the search query + panels.forEach(panel => { + const text = (panel.textContent + panel.classList.toString()).toLowerCase(); + // const hasTag = activeTags.every(tag => panel.classList.contains(tag)); + const hasTag = panelMatchesTags(panel, groupedActiveTags) + const hasText = text.includes(query.toLowerCase()); + + if (hasTag && hasText) { + toShow.push(panel); + } else { + toHide.push(panel); + } + }) + + toShow.forEach(panel => panel.classList.remove("hidden")); + toHide.forEach(panel => panel.classList.add("hidden")); + + // If no matches are found, display the noMatches element + if (toShow.length === 0) { + noMatchesElement.classList.remove("hidden"); + } else { + noMatchesElement.classList.add("hidden"); + } + + // Set the URL to match the active tags using query parameters + history.replaceState(null, null, activeTags.length === 0 ? location.pathname : `?tags=${activeTags.join(',')}`); + } + + // Generate the callback triggered when a user clicks on a tag filter button. + document.querySelectorAll('.tag').forEach(tag => { + tag.addEventListener('click', () => { + // Toggle "tag" buttons on click. + if (tag.classList.contains('btn-primary')) { + // deactivate filter button + tag.classList.replace('btn-primary', 'btn-outline-primary'); + } else { + // activate filter button + tag.classList.replace('btn-outline-primary', 'btn-primary'); + } + filterPanels() + }); + }); + + // Add event listener for keypresses in the search bar + const searchInput = document.getElementById("searchInput"); + if (searchInput) { + searchInput.addEventListener("keyup", function (event) { + event.preventDefault(); + filterPanels(); + }); + } + + // Add the ability to provide URL query parameters to filter examples on page load + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.size > 0) { + const urlTagParams = urlParams.get('tags').split(','); + urlTagParams.forEach(tag => { + const tagButton = document.getElementById(tag); + if (tagButton) { + tagButton.classList.replace('btn-outline-primary', 'btn-primary'); + } + }); + filterPanels(); + } +}); diff --git a/doc/source/_static/js/splash.js b/doc/source/_static/js/splash.js new file mode 100644 index 000000000000..b41a72a20045 --- /dev/null +++ b/doc/source/_static/js/splash.js @@ -0,0 +1,26 @@ +function animateTabs() { + const tabs = Array.from(document.getElementById("v-pills-tab").children) + const contentTabs = Array.from(document.getElementById("v-pills-tabContent").children) + + tabs.forEach((item, index) => { + item.onclick = () => { + tabs.forEach((tab, i) => { + if (i === index) { + item.classList.add('active') + } else { + tab.classList.remove('active') + } + }) + contentTabs.forEach((tab, i) => { + if (i === index) { + tab.classList.add('active', 'show') + } else { + tab.classList.remove('active', 'show') + } + }) + } + }) +} + +document.addEventListener("DOMContentLoaded", animateTabs) +document.addEventListener("DOMContentLoaded", () => hljs.highlightAll()) diff --git a/doc/source/_static/js/tags.js b/doc/source/_static/js/tags.js deleted file mode 100644 index 99ac9db7d5ca..000000000000 --- a/doc/source/_static/js/tags.js +++ /dev/null @@ -1,400 +0,0 @@ -/** - * Generate a button that will be used for filtering example gallery items. - * - * @param {string} id ID of the element to use - * @param {string} text Text do display to the user inside the button - * @param {string} icon Icon to show next to the text. - * @returns {string} Inner HTML that contains the tag filter button. - */ -function generateTagButton(id, text, icon) { - return ` -
${icon}${text}
- `; -} - -/** - * Generate the inner HTML for the tag filter buttons in the sidebar. - * - * @param {string} name Name of the section - * @param {Array} tags Array of tags HTML generated from generateTagButton - * @returns {string} Inner HTML to display the sidebar - */ -function generateTagSection(name, tags) { - return ` -
${name}
-
${tags.join('')}
- ` -} - -/** - * Check whether a panel matches the selected filter tags. - * - * @param {any} panel Example gallery item - * @param {Array>} groupedActiveTags Groups of tags selected by the user. - * @returns {boolean} True if the panel should be shown, false otherwise - */ -function panelMatchesTags(panel, groupedActiveTags) { - // Show the panel if every tagGroup has at least one active tag in the classList, - // or if no tag in a group is selected. - return groupedActiveTags.every(tagGroup => { - return tagGroup.length === 0 || Array.from(panel.classList).some(tag => tagGroup.includes(tag)) - }) -} - - -/** - * Filter the links to the examples in the example gallery - * by the selected tags and the current search query. - * - * @param {object} tags Object with grouped arrays of tags, each with className, displayName, and - * icon - */ -function filterPanels(tags) { - const noMatchesElement = document.querySelector("#noMatches"); - const query = document.getElementById("searchInput").value.toLowerCase(); - const panels = document.querySelectorAll('.gallery-item') - const activeTags = Array.from(document.querySelectorAll('.tag.btn-primary')).map(el => el.id); - const groupedActiveTags = Object.values(tags).map(tagGroup => { - const classes = tagGroup.map(({className}) => className); - return activeTags.filter(activeTag => classes.includes(activeTag)); - }) - - // Show all panels first - panels.forEach(panel => panel.classList.remove("hidden")); - - let toHide = []; - let toShow = []; - - // Show each panel if it has every active tag and matches the search query - panels.forEach(panel => { - const text = (panel.textContent + panel.classList.toString()).toLowerCase(); - // const hasTag = activeTags.every(tag => panel.classList.contains(tag)); - const hasTag = panelMatchesTags(panel, groupedActiveTags) - const hasText = text.includes(query.toLowerCase()); - - if (hasTag && hasText) { - toShow.push(panel); - } else { - toHide.push(panel); - } - }) - - toShow.forEach(panel => panel.classList.remove("hidden")); - toHide.forEach(panel => panel.classList.add("hidden")); - - // If no matches are found, display the noMatches element - if (toShow.length === 0) { - noMatchesElement.classList.remove("hidden"); - } else { - noMatchesElement.classList.add("hidden"); - } - - // Set the URL to match the active tags using query parameters - history.replaceState(null, null, activeTags.length === 0 ? '' : `?tags=${activeTags.join(',')}`); -} - -/** - * Generate the callback triggered when a user clicks on a tag filter button. - * @param {string} tag The element corresponding to the tag - * @returns {() => void} The callback that will be called when the user clicks a tag filter button - */ -function generateTagClickHandler(tag, tags) { - return () => { - // Toggle "tag" buttons on click. - if (tag.classList.contains('btn-primary')) { - // deactivate filter button - tag.classList.replace('btn-primary', 'btn-outline-primary'); - } else { - // activate filter button - tag.classList.replace('btn-outline-primary', 'btn-primary'); - } - filterPanels(tags) - } -} - -window.addEventListener('load', () => { - // Sidebar icons - const rlIcon = ` - - - - `; - const otherIcon = ` - - - - `; - const llmIcon = ` - - - - `; - const genAiIcon = ` - - - - `; - const nlpIcon = ` - - - - `; - const timeSeriesIcon = ` - - - - `; - const computerVisionIcon = ` - - - - ` - const batchInferenceIcon = ` - - - - ` - const dataPreprocessingIcon = ` - - - - ` - const dataValidationIcon = ` - - - - ` - const experimentTrackingIcon = ` - - - - ` - const hyperparameterTuningIcon = ` - - - - ` - const modelTrainingIcon = ` - - - - ` - const monitoringIcon = ` - - - - ` - const orchestrationIcon = ` - - - - ` - const servingIcon = ` - - - - ` - const tensorflowIcon = ` - - - - - - image/svg+xml - - - - - - - - - - - - - - ` - const pytorchIcon = ` - - - - - - - ` - const kerasIcon = ` - - - - - - - - - - - - - ` - const huggingfaceIcon = ` - - - - - - - - - ` - - const isGallery = window.location.pathname.includes("ray-overview/examples.html") - if (isGallery) { - const tags = { - useCaseTags: [ - { - className: 'llm', - displayName: 'Large Language Models', - icon: llmIcon - }, - { - className: 'gen-ai', - displayName: 'Generative AI', - icon: genAiIcon - }, - { - className: 'cv', - displayName: 'Computer Vision', - icon: computerVisionIcon, - }, - { - className: 'ts', - displayName: 'Time-series', - icon: timeSeriesIcon, - }, - { - className: 'nlp', - displayName: 'Natural Language Processing', - icon: nlpIcon, - }, - { - className: 'rl', - displayName: 'Reinforcement Learning', - icon: rlIcon, - }, - ], - workloadTags: [ - { - className: 'data-processing', - displayName: 'Data Processing', - icon: dataPreprocessingIcon, - }, - { - className: 'training', - displayName: 'Model Training & Fine-tuning', - icon: modelTrainingIcon, - }, - { - className: 'tuning', - displayName: 'Hyperparameter Tuning', - icon: hyperparameterTuningIcon, - }, - { - className: 'inference', - displayName: 'Batch Inference', - icon: batchInferenceIcon, - }, - { - className: 'serving', - displayName: 'Model Serving', - icon: servingIcon, - }, - ], - mlopsTags: [ - { - className: 'tracking', - displayName: 'Experiment Tracking', - icon: experimentTrackingIcon, - }, - { - className: 'monitoring', - displayName: 'Monitoring', - icon: monitoringIcon, - }, - ], - frameworkTags: [ - { - className: 'pytorch', - displayName: 'PyTorch', - icon: pytorchIcon, - }, - { - className: 'huggingface', - displayName: 'HuggingFace', - icon: huggingfaceIcon, - }, - { - className: 'tensorflow', - displayName: 'Tensorflow/Keras', - icon: tensorflowIcon, - }, - ] - } - - const useCaseTags = tags.useCaseTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const workloadTags = tags.workloadTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const mlopsTags = tags.mlopsTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - const frameworkTags = tags.frameworkTags.map(({className, displayName, icon}) => { - return generateTagButton(className, displayName, icon) - }) - - - const tagString = "
\n" + - "
Filter by

\n" + - generateTagSection("USE CASES", useCaseTags) + - generateTagSection("ML WORKLOADS", workloadTags) + - generateTagSection("MLOPS", mlopsTags) + - generateTagSection("FRAMEWORKS", frameworkTags) + - "
"; - - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = tagString; - const newNav = tempDiv.firstChild; - - // Populate the sidebar with buttons that filter for different example tags - document.getElementById("site-navigation").appendChild(newNav); - - document.querySelectorAll('.tag').forEach(tag => { - tag.addEventListener('click', generateTagClickHandler(tag, tags)); - }); - - const searchInput = document.getElementById("searchInput"); - if (searchInput) { - searchInput.addEventListener("keyup", function (event) { - event.preventDefault(); - filterPanels(tags); - }); - } - - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.size > 0) { - const urlTagParams = urlParams.get('tags').split(','); - urlTagParams.forEach(tag => { - const tagButton = document.getElementById(tag); - if (tagButton) { - tagButton.classList.replace('btn-outline-primary', 'btn-primary'); - } - }); - filterPanels(tags); - } - } -}); diff --git a/doc/source/_static/js/top-navigation.js b/doc/source/_static/js/top-navigation.js deleted file mode 100644 index d09beac95b36..000000000000 --- a/doc/source/_static/js/top-navigation.js +++ /dev/null @@ -1,148 +0,0 @@ -// Remove the black background from the announcement banner. We abuse the -// sphinx-book-theme announcement feature to place a navigation bar on top of the -// documentation. This javascript file replaces the announcement banner with the -// navigation bar. -document.getElementsByClassName("announcement")[0].classList.remove("header-item") - -// Make the new navigation bar sticky, but remove that property on the -// top bar that ships with the sphinx-book-theme. -document.getElementsByClassName("announcement")[0].classList.add("sticky-top") - -// Get the right relative URL for a given path -function getNavURL(url) { - references = document.getElementsByClassName("reference internal") - for (let i = 0; i < references.length; i++) { - if (references[i].href.includes(url)) { - return references[i].href - } - } -} - -is_examples = window.location.href.endsWith("ray-overview/examples.html") -is_get_started = window.location.href.endsWith("ray-overview/getting-started.html") -is_use_cases = window.location.href.endsWith("ray-overview/use-cases.html") -is_libraries = window.location.href.includes("/ray-core/") || - window.location.href.includes("/data/") || - window.location.href.includes("/train/") || - window.location.href.includes("/tune/") || - window.location.href.includes("/serve/") || - window.location.href.includes("/rllib/") -is_ecosystem = window.location.href.endsWith("ray-overview/ray-libraries.html") -is_documentation = !(is_get_started || is_use_cases || is_examples || is_libraries || is_ecosystem) - -downCaret = '' -rayLogoSvg = "" - -topNavContent = document.createElement("div"); -topNavContent.setAttribute("class", "top-nav-content") - -// The left part that contains links and menus -topNavContentLeft = document.createElement("div"); -topNavContentLeft.setAttribute("class", "left") - -//-- The Ray link -linkRay = document.createElement("a") -linkRay.setAttribute("href", "https://ray.io") -linkRay.setAttribute("class", "ray-logo") -linkRay.innerHTML += rayLogoSvg; -topNavContentLeft.append(linkRay) - -//-- The Get started link -getStartedLink = document.createElement("a") -getStartedLink.innerText = "Get started" -getStartedLink.setAttribute("href", getNavURL("ray-overview/getting-started.html")) -if (is_get_started) { - getStartedLink.style.borderBottom = "2px solid #007bff" -} -topNavContentLeft.append(getStartedLink) - -//-- The Blog link -// blogLink = document.createElement("a") -// blogLink.innerText = "Blog" -// blogLink.setAttribute("href", "https://www.anyscale.com/blog") -// topNavContentLeft.append(blogLink) - -//-- The Use Cases link -useCasesLink = document.createElement("a") -useCasesLink.innerText = "Use cases" -useCasesLink.setAttribute("href", getNavURL("ray-overview/use-cases.html")) -if (is_use_cases) { - useCasesLink.style.borderBottom = "2px solid #007bff" -} -topNavContentLeft.append(useCasesLink) - -//-- Example gallery link -let examplesLink = document.createElement("a") -examplesLink.innerText = "Examples" -// since we surgically remove the nav bar for the examples, we need to resort to a trick. -let examplesURL = getNavURL("ray-overview/use-cases.html").replace("use-cases.html", "examples.html").replace(/#$/, "") -examplesLink.setAttribute("href", examplesURL) -if (is_examples) { - examplesLink.style.borderBottom = "2px solid #007bff" -} -topNavContentLeft.append(examplesLink) - -//-- The Libraries menu -librariesMenu = document.createElement("div") -librariesMenu.setAttribute("class", "menu") -librariesMenu.innerHTML = "Libraries" + downCaret + "" -librariesList = document.createElement("ul") -librariesList.innerHTML += "
  • Ray CoreScale general Python applications
  • " -librariesList.innerHTML += "
  • Ray DataScale data ingest and preprocessing
  • " -librariesList.innerHTML += "
  • Ray TrainScale machine learning training
  • " -librariesList.innerHTML += "
  • Ray TuneScale hyperparameter tuning
  • " -librariesList.innerHTML += "
  • Ray ServeScale model serving
  • " -librariesList.innerHTML += "
  • Ray RLlibScale reinforcement learning
  • " -librariesMenu.append(librariesList) -if (is_libraries) { - librariesMenu.style.borderBottom = "2px solid #007bff" -} -topNavContentLeft.append(librariesMenu) - -//-- The Documentation link -documentationLink = document.createElement("a") -documentationLink.innerText = "Docs" -documentationLink.setAttribute("href", getNavURL("ray-overview/index.html").replace("ray-overview/index.html", "index.html")) -if (is_documentation) { - documentationLink.style.borderBottom = "2px solid #007bff" -} -topNavContentLeft.append(documentationLink) - -//-- The Resources menu -learnMenu = document.createElement("div") -learnMenu.setAttribute("class", "menu") -learnMenu.innerHTML = "Resources" + downCaret + "" -learnList = document.createElement("ul") -learnList.innerHTML += "
  • Discussion ForumGet your Ray questions answered
  • " -learnList.innerHTML += "
  • TrainingHands-on learning
  • " -learnList.innerHTML += "
  • BlogUpdates, best practices, user-stories
  • " -learnList.innerHTML += "
  • EventsWebinars, meetups, office hours
  • " -learnList.innerHTML += "
  • Success StoriesReal-world workload examples
  • " -learnList.innerHTML += "
  • EcosystemLibraries integrated with Ray
  • " -learnList.innerHTML += "
  • CommunityConnect with us
  • " -learnMenu.append(learnList) -topNavContentLeft.append(learnMenu) - -topNavContent.append(topNavContentLeft) - -// The right part that contains the Anyscale trial button -anyscaleButton = document.createElement("button"); -anyscaleButton.setAttribute("class", "try-anyscale"); -anyscaleLogoSvg = - ''; -anyscaleButton.innerHTML = - anyscaleLogoSvg + - 'Managed Ray on Anyscale'; -anyscaleButton.onclick = function () { - gtag("event", "try_anyscale", { - "send_to": "UA-110413294-1", - "event_category": "TryAnyscale", - "event_label": "TryAnyscale", - "value": 1, - }); - window.open('https://www.anyscale.com', '_blank'); -}; - -topNavContent.append(anyscaleButton) - -document.getElementsByClassName("topnav")[0].append(topNavContent) diff --git a/doc/source/_templates/breadcrumbs.html b/doc/source/_templates/breadcrumbs.html deleted file mode 100644 index f906c40db140..000000000000 --- a/doc/source/_templates/breadcrumbs.html +++ /dev/null @@ -1,15 +0,0 @@ - -
    -
      - {% block breadcrumbs %} -
    • »
    • - {% for doc in parents %} -
    • {{ doc.title }} »
    • - {% endfor %} -
    • {{ title }}
    • - {% endblock %} -
    • - Doc suggestion? -
    • -
    -
    diff --git a/doc/source/_templates/csat.html b/doc/source/_templates/csat.html index dca315fcfe7b..368af0d322d2 100644 --- a/doc/source/_templates/csat.html +++ b/doc/source/_templates/csat.html @@ -6,13 +6,13 @@ Was this helpful?
    - + Yes
    - + No
    diff --git a/doc/source/_templates/examples-sidebar.html b/doc/source/_templates/examples-sidebar.html new file mode 100644 index 000000000000..c16d49676b42 --- /dev/null +++ b/doc/source/_templates/examples-sidebar.html @@ -0,0 +1,139 @@ + diff --git a/doc/source/_templates/layout.html b/doc/source/_templates/layout.html index 3e90455b5d68..b14b340b1e20 100644 --- a/doc/source/_templates/layout.html +++ b/doc/source/_templates/layout.html @@ -2,26 +2,21 @@ {%- block extrahead %} - - + - + + + + + + - - - + {{ super() }} {% endblock %} diff --git a/doc/source/_templates/main-sidebar.html b/doc/source/_templates/main-sidebar.html new file mode 100644 index 000000000000..9fb3d72e9b80 --- /dev/null +++ b/doc/source/_templates/main-sidebar.html @@ -0,0 +1,11 @@ +{%- set sidebar_nav_html = generate_toctree_html("sidebar", +startdepth=0, +show_nav_level=0, +maxdepth=theme_navigation_depth|int, +collapse=theme_collapse_navigation|tobool, +includehidden=True, +titles_only=True) +-%} + diff --git a/doc/source/_templates/navbar-anyscale.html b/doc/source/_templates/navbar-anyscale.html new file mode 100644 index 000000000000..228fbc952e2b --- /dev/null +++ b/doc/source/_templates/navbar-anyscale.html @@ -0,0 +1,9 @@ + +
    + + Managed Ray on Anyscale + +
    +
    diff --git a/doc/source/_templates/navbar-links.html b/doc/source/_templates/navbar-links.html new file mode 100644 index 000000000000..0519d6be46d4 --- /dev/null +++ b/doc/source/_templates/navbar-links.html @@ -0,0 +1,9 @@ + diff --git a/doc/source/_templates/navbar-ray-logo.html b/doc/source/_templates/navbar-ray-logo.html new file mode 100644 index 000000000000..9c0cacfce8cb --- /dev/null +++ b/doc/source/_templates/navbar-ray-logo.html @@ -0,0 +1,13 @@ +{# Logo link generation -#} +{% if not theme_logo.get("link") %} + {% set href = pathto(root_doc) %} +{% elif hasdoc(theme_logo.get("link")) %} + {% set href = pathto(theme_logo.get("link")) %} {# internal page #} +{% else %} + {% set href = theme_logo.get("link") %} {# external url #} +{% endif %} + +{#- Logo HTML and image #} + diff --git a/doc/source/_templates/sbt-sidebar-nav.html b/doc/source/_templates/sbt-sidebar-nav.html deleted file mode 100644 index a10e9d7985e2..000000000000 --- a/doc/source/_templates/sbt-sidebar-nav.html +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/doc/source/_templates/sections/footer-content.html b/doc/source/_templates/sections/footer-content.html deleted file mode 100644 index 022af5bb92bb..000000000000 --- a/doc/source/_templates/sections/footer-content.html +++ /dev/null @@ -1,24 +0,0 @@ - -{% extends "!sections/footer-content.html" %} - -

    - {% if author %} - {{ translate('By') }} {{ author }}
    - {% endif %} - {%- if show_copyright %} - {%- if hasdoc('copyright') %} - {% trans prefix=translate('Copyright'), path=pathto('copyright'), copyright=copyright|e %}© {{ prefix }} {{ copyright }}.{% endtrans %}
    - {%- else %} - {% trans prefix=translate('Copyright'), copyright=copyright|e %}© {{ prefix }} {{ copyright }}.{% endtrans %}
    - {%- endif %} - {%- endif %} - {%- if last_updated %} - {% trans prefix=translate('Last updated on'), last_updated=last_updated|e %}{{ prefix }} {{ last_updated }}.{% endtrans %}
    - {%- endif %} - {%- if theme_extra_footer %} -

    - {%- endif %} -

    -{% include 'csat.html' %} diff --git a/doc/source/_toc.yml b/doc/source/_toc.yml deleted file mode 100644 index 1977d90409d8..000000000000 --- a/doc/source/_toc.yml +++ /dev/null @@ -1,398 +0,0 @@ -format: jb-book -root: index -parts: - - caption: Ray - chapters: - - file: ray-overview/index - title: "Overview" - - file: ray-overview/getting-started - title: "Getting Started" - - file: ray-overview/installation - title: "Installation" - - file: ray-overview/use-cases - title: "Use Cases" - sections: - - file: ray-air/getting-started - - - file: ray-overview/examples - title: "Example Gallery" - - file: ray-overview/ray-libraries - title: "Ecosystem" - - - file: ray-core/walkthrough - title: "Ray Core" - sections: - - file: ray-core/key-concepts - title: "Key Concepts" - - file: ray-core/user-guide - title: "User Guides" - - file: ray-core/examples/overview - title: "Examples" - sections: - - file: ray-core/examples/gentle_walkthrough - - file: ray-core/examples/monte_carlo_pi - - file: ray-core/examples/plot_example-lm - - file: ray-core/examples/plot_hyperparameter - - file: ray-core/examples/plot_parameter_server - - file: ray-core/examples/plot_pong_example - - file: ray-core/examples/highly_parallel - - file: ray-core/examples/batch_prediction - - file: ray-core/examples/batch_training - - file: ray-core/examples/automl_for_time_series - - file: ray-core/examples/web-crawler - - file: ray-core/examples/map_reduce - - file: ray-core/api/index - - - file: data/data - title: Ray Data - sections: - - file: data/overview - title: Overview - - file: data/key-concepts - - file: data/user-guide - - file: data/examples/index - - file: data/api/api - - file: data/data-internals - - - file: train/train - title: Ray Train - sections: - - file: train/overview - title: Overview - - file: train/getting-started-pytorch - title: PyTorch Guide - - file: train/getting-started-pytorch-lightning - title: PyTorch Lightning Guide - - file: train/getting-started-transformers - title: Hugging Face Transformers Guide - - file: train/more-frameworks - sections: - - file: train/huggingface-accelerate - title: Hugging Face Accelerate Guide - - file: train/deepspeed - title: DeepSpeed Guide - - file: train/distributed-tensorflow-keras - title: TensorFlow and Keras Guide - - file: train/distributed-xgboost-lightgbm - title: XGBoost and LightGBM Guide - - file: train/horovod - title: Horovod Guide - - file: train/user-guides - title: User Guides - - file: train/examples - title: "Examples" - - file: train/benchmarks - title: "Benchmarks" - - file: train/api/api - - - file: tune/index - title: Ray Tune - sections: - - file: tune/getting-started - title: "Getting Started" - - file: tune/key-concepts - title: "Key Concepts" - - file: tune/tutorials/overview - sections: - - file: tune/tutorials/tune-run - title: "Running Basic Experiments" - - file: tune/tutorials/tune-output - - file: tune/tutorials/tune-resources - title: "Setting Trial Resources" - - file: tune/tutorials/tune-search-spaces - title: "Using Search Spaces" - - file: tune/tutorials/tune-stopping - - file: tune/tutorials/tune-trial-checkpoints - - file: tune/tutorials/tune-storage - - file: tune/tutorials/tune-fault-tolerance - - file: tune/tutorials/tune-metrics - title: "Using Callbacks and Metrics" - - file: tune/tutorials/tune_get_data_in_and_out - - file: tune/examples/tune_analyze_results - - file: tune/examples/pbt_guide - sections: - - file: tune/examples/pbt_visualization/pbt_visualization - title: "Visualizing and Understanding PBT" - - file: tune/tutorials/tune-distributed - title: "Deploying Tune in the Cloud" - - file: tune/tutorials/tune-lifecycle - title: "Tune Architecture" - - file: tune/tutorials/tune-scalability - title: "Scalability Benchmarks" - - file: tune/examples/index - sections: - - file: tune/examples/ml-frameworks - sections: - - file: tune/examples/tune-sklearn - title: "Scikit-Learn Example" - - file: tune/examples/tune_mnist_keras - title: "Keras Example" - - file: tune/examples/tune-pytorch-cifar - title: "PyTorch Example" - - file: tune/examples/tune-pytorch-lightning - title: "PyTorch Lightning Example" - - file: tune/examples/tune-serve-integration-mnist - title: "Ray Serve Example" - - file: tune/examples/pbt_ppo_example - title: "Ray RLlib Example" - - file: tune/examples/tune-xgboost - title: "XGBoost Example" - - file: tune/examples/lightgbm_example - title: "LightGBM Example" - - file: tune/examples/horovod_simple - title: "Horovod Example" - - file: tune/examples/pbt_transformers - title: "Hugging Face Transformers Example" - - file: tune/examples/experiment-tracking - sections: - - file: tune/examples/tune-wandb - title: "Weights & Biases Example" - - file: tune/examples/tune-mlflow - title: "MLflow Example" - - file: tune/examples/tune-aim - title: "Aim Example" - - file: tune/examples/tune-comet - title: "Comet Example" - - file: tune/examples/hpo-frameworks - sections: - - file: tune/examples/ax_example - title: "Ax Example" - - file: tune/examples/dragonfly_example - title: "Dragonfly Example" - - file: tune/examples/hyperopt_example - title: "HyperOpt Example" - - file: tune/examples/bayesopt_example - title: "Bayesopt Example" - - file: tune/examples/flaml_example - title: "FLAML Example" - - file: tune/examples/bohb_example - title: "BOHB Example" - - file: tune/examples/nevergrad_example - title: "Nevergrad Example" - - file: tune/examples/optuna_example - title: "Optuna Example" - - file: tune/examples/sigopt_example - title: "SigOpt Example" - - file: tune/examples/other-examples - title: "Other Examples" - - file: tune/examples/exercises - title: "Exercises" - - file: tune/faq - - file: tune/api/api.rst - - - file: serve/index - title: Ray Serve - sections: - - file: serve/getting_started - - file: serve/key-concepts - - file: serve/develop-and-deploy - - file: serve/model_composition - - file: serve/multi-app - - file: serve/model-multiplexing - - file: serve/configure-serve-deployment - - file: serve/http-guide - - file: serve/production-guide/index - title: Production Guide - sections: - - file: serve/production-guide/config - - file: serve/production-guide/kubernetes - - file: serve/production-guide/docker - - file: serve/production-guide/fault-tolerance - - file: serve/production-guide/handling-dependencies - - file: serve/production-guide/best-practices - - file: serve/monitoring - - file: serve/resource-allocation - - file: serve/autoscaling-guide - - file: serve/advanced-guides/index - sections: - - file: serve/advanced-guides/app-builder-guide - - file: serve/advanced-guides/advanced-autoscaling - - file: serve/advanced-guides/performance - - file: serve/advanced-guides/dyn-req-batch - - file: serve/advanced-guides/inplace-updates - - file: serve/advanced-guides/dev-workflow - - file: serve/advanced-guides/grpc-guide - - file: serve/advanced-guides/deployment-graphs - - file: serve/advanced-guides/managing-java-deployments - - file: serve/advanced-guides/deploy-vm - - file: serve/architecture - - file: serve/tutorials/index - - file: serve/api/index - - - file: rllib/index - title: Ray RLlib - sections: - - file: rllib/rllib-training - - file: rllib/key-concepts - - file: rllib/rllib-env - - file: rllib/rllib-algorithms - - file: rllib/user-guides - sections: - - file: rllib/rllib-advanced-api - - file: rllib/rllib-models - - file: rllib/rllib-saving-and-loading-algos-and-policies - - file: rllib/rllib-concepts - - file: rllib/rllib-sample-collection - - file: rllib/rllib-replay-buffers - - file: rllib/rllib-offline - - file: rllib/rllib-catalogs - - file: rllib/rllib-connector - - file: rllib/rllib-rlmodule - - file: rllib/rllib-learner - - file: rllib/rllib-torch2x - - file: rllib/rllib-fault-tolerance - - file: rllib/rllib-dev - - file: rllib/rllib-cli - - file: rllib/rllib-examples - - file: rllib/package_ref/index - - - file: ray-more-libs/index - title: More Libraries - sections: - - file: ray-more-libs/joblib - - file: ray-more-libs/multiprocessing - - file: ray-more-libs/ray-collective - - file: ray-more-libs/dask-on-ray - - file: ray-more-libs/raydp - - file: ray-more-libs/mars-on-ray - - file: ray-more-libs/modin/index - - file: workflows/index - title: Ray Workflows (Alpha) - sections: - - file: workflows/key-concepts - - file: workflows/basics - - file: workflows/management - - file: workflows/metadata - - file: workflows/events - - file: workflows/comparison - - file: workflows/advanced - - file: workflows/api/api - - - file: cluster/getting-started - title: "Ray Clusters" - sections: - - file: cluster/key-concepts - title: Key Concepts - - file: cluster/kubernetes/index - title: Deploying on Kubernetes - sections: - - file: cluster/kubernetes/getting-started - sections: - - file: cluster/kubernetes/getting-started/raycluster-quick-start.md - - file: cluster/kubernetes/getting-started/rayjob-quick-start.md - - file: cluster/kubernetes/getting-started/rayservice-quick-start.md - - file: cluster/kubernetes/user-guides - sections: - - file: cluster/kubernetes/user-guides/rayservice.md - title: Deploy Ray Serve Apps - - file: cluster/kubernetes/user-guides/rayservice-high-availability.md - - file: cluster/kubernetes/user-guides/observability.md - - file: cluster/kubernetes/user-guides/upgrade-guide.md - - file: cluster/kubernetes/user-guides/k8s-cluster-setup.md - sections: - - file: cluster/kubernetes/user-guides/aws-eks-gpu-cluster.md - - file: cluster/kubernetes/user-guides/gcp-gke-gpu-cluster.md - - file: cluster/kubernetes/user-guides/storage.md - - file: cluster/kubernetes/user-guides/config.md - - file: cluster/kubernetes/user-guides/configuring-autoscaling.md - - file: cluster/kubernetes/user-guides/kuberay-gcs-ft.md - - file: cluster/kubernetes/user-guides/gke-gcs-bucket.md - - file: cluster/kubernetes/user-guides/logging.md - - file: cluster/kubernetes/user-guides/gpu.md - - file: cluster/kubernetes/user-guides/rayserve-dev-doc.md - - file: cluster/kubernetes/user-guides/pod-command.md - - file: cluster/kubernetes/user-guides/pod-security.md - - file: cluster/kubernetes/user-guides/helm-chart-rbac.md - - file: cluster/kubernetes/user-guides/tls.md - - file: cluster/kubernetes/user-guides/k8s-autoscaler.md - - file: cluster/kubernetes/user-guides/static-ray-cluster-without-kuberay.md - - file: cluster/kubernetes/examples - sections: - - file: cluster/kubernetes/examples/ml-example.md - - file: cluster/kubernetes/examples/gpu-training-example.md - - file: cluster/kubernetes/examples/stable-diffusion-rayservice.md - - file: cluster/kubernetes/examples/mobilenet-rayservice.md - - file: cluster/kubernetes/examples/text-summarizer-rayservice.md - - file: cluster/kubernetes/examples/rayjob-batch-inference-example.md - - file: cluster/kubernetes/k8s-ecosystem - sections: - - file: cluster/kubernetes/k8s-ecosystem/ingress.md - - file: cluster/kubernetes/k8s-ecosystem/prometheus-grafana.md - - file: cluster/kubernetes/k8s-ecosystem/pyspy.md - - file: cluster/kubernetes/k8s-ecosystem/volcano.md - - file: cluster/kubernetes/k8s-ecosystem/kubeflow.md - - file: cluster/kubernetes/benchmarks - sections: - - file: cluster/kubernetes/benchmarks/memory-scalability-benchmark.md - - file: cluster/kubernetes/troubleshooting - sections: - - file: cluster/kubernetes/troubleshooting/troubleshooting.md - - file: cluster/kubernetes/troubleshooting/rayservice-troubleshooting.md - - file: cluster/kubernetes/references - - file: cluster/vms/index - title: Deploying on VMs - sections: - - file: cluster/vms/getting-started - - file: cluster/vms/user-guides/index - title: User Guides - sections: - - file: cluster/vms/user-guides/launching-clusters/index - - file: cluster/vms/user-guides/large-cluster-best-practices - - file: cluster/vms/user-guides/configuring-autoscaling - - file: cluster/vms/user-guides/logging - - file: cluster/vms/user-guides/community/index - title: Community-supported Cluster Managers - sections: - - file: cluster/vms/user-guides/community/yarn - - file: cluster/vms/user-guides/community/slurm - - file: cluster/vms/user-guides/community/lsf - - file: cluster/vms/examples/index - title: Examples - sections: - - file: cluster/vms/examples/ml-example - - file: cluster/vms/references/index - - file: cluster/metrics - - file: cluster/configure-manage-dashboard - - file: cluster/running-applications/index - title: Applications Guide - - file: cluster/faq - - file: cluster/package-overview - - file: cluster/usage-stats - - - file: ray-observability/index - title: "Monitoring and Debugging" - sections: - - file: ray-observability/getting-started - - file: ray-observability/key-concepts - - file: ray-observability/user-guides/index - title: User Guides - sections: - - file: ray-observability/user-guides/debug-apps/index - title: Debugging Applications - sections: - - file: ray-observability/user-guides/debug-apps/general-debugging - - file: ray-observability/user-guides/debug-apps/debug-memory - - file: ray-observability/user-guides/debug-apps/debug-hangs - - file: ray-observability/user-guides/debug-apps/debug-failures - - file: ray-observability/user-guides/debug-apps/optimize-performance - - file: ray-observability/user-guides/debug-apps/ray-debugging - - file: ray-observability/user-guides/cli-sdk - - file: ray-observability/user-guides/configure-logging - - file: ray-observability/user-guides/profiling - - file: ray-observability/user-guides/add-app-metrics - - file: ray-observability/user-guides/ray-tracing - - file: ray-observability/reference/index - title: Reference - sections: - - file: ray-observability/reference/api - - file: ray-observability/reference/cli - - file: ray-observability/reference/system-metrics - - - file: ray-contribute/index - title: Developer Guides - - - file: ray-references/glossary - title: "Glossary" - - - file: ray-security/index - title: "Security" diff --git a/doc/source/cluster/getting-started.rst b/doc/source/cluster/getting-started.rst index 8bcf33c082b4..df6fae05b1bb 100644 --- a/doc/source/cluster/getting-started.rst +++ b/doc/source/cluster/getting-started.rst @@ -3,6 +3,20 @@ Ray Clusters Overview ===================== +.. toctree:: + :hidden: + + Key Concepts + Deploying on Kubernetes + Deploying on VMs + metrics + configure-manage-dashboard + Applications Guide + faq + package-overview + usage-stats + + Ray enables seamless scaling of workloads from a laptop to a large cluster. While Ray works out of the box on single machines with just a call to ``ray.init``, to run Ray applications on multiple nodes you must first *deploy a Ray cluster*. diff --git a/doc/source/cluster/kubernetes/benchmarks.md b/doc/source/cluster/kubernetes/benchmarks.md index 0bf5221971f6..53bd18e4dea4 100644 --- a/doc/source/cluster/kubernetes/benchmarks.md +++ b/doc/source/cluster/kubernetes/benchmarks.md @@ -2,4 +2,10 @@ # KubeRay Benchmarks -- {ref}`kuberay-mem-scalability` \ No newline at end of file +```{toctree} +:hidden: + +benchmarks/memory-scalability-benchmark +``` + +- {ref}`kuberay-mem-scalability` diff --git a/doc/source/cluster/kubernetes/examples.md b/doc/source/cluster/kubernetes/examples.md index cf37b9ca9623..cd97848d4388 100644 --- a/doc/source/cluster/kubernetes/examples.md +++ b/doc/source/cluster/kubernetes/examples.md @@ -2,6 +2,18 @@ # Examples +```{toctree} +:hidden: + +examples/ml-example +examples/gpu-training-example +examples/stable-diffusion-rayservice +examples/mobilenet-rayservice +examples/text-summarizer-rayservice +examples/rayjob-batch-inference-example +``` + + This section presents example Ray workloads to try out on your Kubernetes cluster. - {ref}`kuberay-ml-example` (CPU-only) diff --git a/doc/source/cluster/kubernetes/getting-started.md b/doc/source/cluster/kubernetes/getting-started.md index 13a936d2637d..cc52e66c8f82 100644 --- a/doc/source/cluster/kubernetes/getting-started.md +++ b/doc/source/cluster/kubernetes/getting-started.md @@ -2,6 +2,15 @@ # Getting Started with KubeRay +```{toctree} +:hidden: + +getting-started/raycluster-quick-start +getting-started/rayjob-quick-start +getting-started/rayservice-quick-start +``` + + ## Custom Resource Definitions (CRDs) [KubeRay](https://github.com/ray-project/kuberay) is a powerful, open-source Kubernetes operator that simplifies the deployment and management of Ray applications on Kubernetes. diff --git a/doc/source/cluster/kubernetes/index.md b/doc/source/cluster/kubernetes/index.md index 451575bfb68a..1fd18c084ebe 100644 --- a/doc/source/cluster/kubernetes/index.md +++ b/doc/source/cluster/kubernetes/index.md @@ -1,4 +1,17 @@ # Ray on Kubernetes + +```{toctree} +:hidden: + +getting-started +user-guides +examples +k8s-ecosystem +benchmarks +troubleshooting +references +``` + (kuberay-index)= ## Overview @@ -36,14 +49,14 @@ The Ray docs present all the information you need to start running Ray workloads .. grid:: 1 2 2 2 :gutter: 1 :class-container: container pb-3 - + .. grid-item-card:: **Getting Started** ^^^ - + Learn how to start a Ray cluster and deploy Ray applications on Kubernetes. - + +++ .. button-ref:: kuberay-quickstart :color: primary @@ -56,9 +69,9 @@ The Ray docs present all the information you need to start running Ray workloads **User Guides** ^^^ - + Learn best practices for configuring Ray clusters on Kubernetes. - + +++ .. button-ref:: kuberay-guides :color: primary @@ -71,9 +84,9 @@ The Ray docs present all the information you need to start running Ray workloads **Examples** ^^^ - + Try example Ray workloads on Kubernetes. - + +++ .. button-ref:: kuberay-examples :color: primary @@ -86,9 +99,9 @@ The Ray docs present all the information you need to start running Ray workloads **Ecosystem** ^^^ - + Integrate KubeRay with third party Kubernetes ecosystem tools. - + +++ .. button-ref:: kuberay-ecosystem-integration :color: primary @@ -101,9 +114,9 @@ The Ray docs present all the information you need to start running Ray workloads **Benchmarks** ^^^ - + Check the KubeRay benchmark results. - + +++ .. button-ref:: kuberay-benchmarks :color: primary @@ -111,14 +124,14 @@ The Ray docs present all the information you need to start running Ray workloads :expand: Benchmark results - + .. grid-item-card:: **Troubleshooting** ^^^ - + Consult the KubeRay troubleshooting guides. - + +++ .. button-ref:: kuberay-troubleshooting :color: primary diff --git a/doc/source/cluster/kubernetes/k8s-ecosystem.md b/doc/source/cluster/kubernetes/k8s-ecosystem.md index 758a9ae45236..bfb67af72d7d 100644 --- a/doc/source/cluster/kubernetes/k8s-ecosystem.md +++ b/doc/source/cluster/kubernetes/k8s-ecosystem.md @@ -2,6 +2,16 @@ # KubeRay Ecosystem +```{toctree} +:hidden: + +k8s-ecosystem/ingress +k8s-ecosystem/prometheus-grafana +k8s-ecosystem/pyspy +k8s-ecosystem/volcano +k8s-ecosystem/kubeflow +``` + * {ref}`kuberay-ingress` * {ref}`kuberay-prometheus-grafana` * {ref}`kuberay-pyspy-integration` diff --git a/doc/source/cluster/kubernetes/k8s-ecosystem/prometheus-grafana.md b/doc/source/cluster/kubernetes/k8s-ecosystem/prometheus-grafana.md index 28847e41bd4a..b53e40a63043 100644 --- a/doc/source/cluster/kubernetes/k8s-ecosystem/prometheus-grafana.md +++ b/doc/source/cluster/kubernetes/k8s-ecosystem/prometheus-grafana.md @@ -97,7 +97,7 @@ kubectl get service - name: RAY_PROMETHEUS_HOST value: http://prometheus-kube-prometheus-prometheus.prometheus-system.svc:9090 ``` - * Note that we do not deploy Grafana in the head Pod, so we need to set both `RAY_GRAFANA_IFRAME_HOST` and `RAY_GRAFANA_HOST`. + * Note that we do not deploy Grafana in the head Pod, so we need to set both `RAY_GRAFANA_IFRAME_HOST` and `RAY_GRAFANA_HOST`. `RAY_GRAFANA_HOST` is used by the head Pod to send health-check requests to Grafana in the backend. `RAY_GRAFANA_IFRAME_HOST` is used by your browser to fetch the Grafana panels from the Grafana server rather than from the head Pod. Because we forward the port of Grafana to `127.0.0.1:3000` in this example, we set `RAY_GRAFANA_IFRAME_HOST` to `http://127.0.0.1:3000`. @@ -135,8 +135,7 @@ spec: * See [ServiceMonitor official document](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitor) for more details about the configurations. * `release: $HELM_RELEASE`: Prometheus can only detect ServiceMonitor with this label. -
    - +(prometheus-can-only-detect-this-label)= ```sh helm ls -n prometheus-system # ($HELM_RELEASE is "prometheus".) @@ -241,8 +240,8 @@ spec: ) ``` -* The PromQL expression above is: -$$\frac{ number\ of\ update\ resource\ usage\ RPCs\ that\ have\ RTT\ smaller\ then\ 20ms\ in\ last\ 30\ days\ }{total\ number\ of\ update\ resource\ usage\ RPCs\ in\ last\ 30\ days\ } \times 100 $$ +* The PromQL expression above is: +$$\frac{ number\ of\ update\ resource\ usage\ RPCs\ that\ have\ RTT\ smaller\ then\ 20ms\ in\ last\ 30\ days\ }{total\ number\ of\ update\ resource\ usage\ RPCs\ in\ last\ 30\ days\ } \times 100 $$ * The recording rule above is one of rules defined in [prometheusRules.yaml](https://github.com/ray-project/kuberay/blob/master/config/prometheus/rules/prometheusRules.yaml), and it is created by **install.sh**. Hence, no need to create anything here. @@ -356,4 +355,4 @@ kubectl port-forward --address 0.0.0.0 svc/raycluster-embed-grafana-head-svc 826 # Visit http://127.0.0.1:8265/#/metrics in your browser. ``` -![Ray Dashboard with Grafana panels](../images/ray_dashboard_embed_grafana.png) \ No newline at end of file +![Ray Dashboard with Grafana panels](../images/ray_dashboard_embed_grafana.png) diff --git a/doc/source/cluster/kubernetes/troubleshooting.md b/doc/source/cluster/kubernetes/troubleshooting.md index 5f0d3d82a4b9..5bf2257b44f5 100644 --- a/doc/source/cluster/kubernetes/troubleshooting.md +++ b/doc/source/cluster/kubernetes/troubleshooting.md @@ -2,5 +2,12 @@ # KubeRay Troubleshooting +```{toctree} +:hidden: + +troubleshooting/troubleshooting +troubleshooting/rayservice-troubleshooting +``` + - {ref}`kuberay-troubleshootin-guides` -- {ref}`kuberay-raysvc-troubleshoot` \ No newline at end of file +- {ref}`kuberay-raysvc-troubleshoot` diff --git a/doc/source/cluster/kubernetes/troubleshooting/rayservice-troubleshooting.md b/doc/source/cluster/kubernetes/troubleshooting/rayservice-troubleshooting.md index b34d8d9be3f0..afa4b299dff8 100644 --- a/doc/source/cluster/kubernetes/troubleshooting/rayservice-troubleshooting.md +++ b/doc/source/cluster/kubernetes/troubleshooting/rayservice-troubleshooting.md @@ -3,7 +3,7 @@ # RayService troubleshooting RayService is a Custom Resource Definition (CRD) designed for Ray Serve. In KubeRay, creating a RayService will first create a RayCluster and then -create Ray Serve applications once the RayCluster is ready. If the issue pertains to the data plane, specifically your Ray Serve scripts +create Ray Serve applications once the RayCluster is ready. If the issue pertains to the data plane, specifically your Ray Serve scripts or Ray Serve configurations (`serveConfigV2`), troubleshooting may be challenging. This section provides some tips to help you debug these issues. ## Observability @@ -104,7 +104,7 @@ Some tips to help you debug the `serveConfigV2` field: * Check [the documentation](serve-api) for the schema about the Ray Serve Multi-application API `PUT "/api/serve/applications/"`. -* Unlike `serveConfig`, `serveConfigV2` adheres to the snake case naming convention. For example, `numReplicas` is used in `serveConfig`, while `num_replicas` is used in `serveConfigV2`. +* Unlike `serveConfig`, `serveConfigV2` adheres to the snake case naming convention. For example, `numReplicas` is used in `serveConfig`, while `num_replicas` is used in `serveConfigV2`. (kuberay-raysvc-issue3-1)= ### Issue 3-1: The Ray image does not include the required dependencies. @@ -222,9 +222,9 @@ You may encounter the following error message when KubeRay tries to get Serve ap Get "http://${HEAD_SVC_FQDN}:52365/api/serve/applications/": dial tcp $HEAD_IP:52365: connect: connection refused" ``` -As mentioned in [Issue 5](#issue-5-fail-to-create--update-serve-applications), the KubeRay operator submits a `Put` request to the RayCluster for creating Serve applications once the head Pod is ready. -After the successful submission of the `Put` request to the dashboard agent, a `Get` request is sent to the dashboard agent port (i.e., 52365). -The successful submission indicates that all the necessary components, including the dashboard agent, are fully operational. +As mentioned in [Issue 5](#kuberay-raysvc-issue5), the KubeRay operator submits a `Put` request to the RayCluster for creating Serve applications once the head Pod is ready. +After the successful submission of the `Put` request to the dashboard agent, a `Get` request is sent to the dashboard agent port (i.e., 52365). +The successful submission indicates that all the necessary components, including the dashboard agent, are fully operational. Therefore, unlike Issue 5, the failure of the `Get` request is not expected. If you consistently encounter this issue, there are several possible causes: @@ -323,7 +323,7 @@ However, Ray Serve does not support deploying both API V1 and API V2 in the clus Hence, if users want to perform in-place upgrades by replacing `serveConfig` with `serveConfigV2`, they may encounter the following error message: ``` -ray.serve.exceptions.RayServeException: You are trying to deploy a multi-application config, however a single-application +ray.serve.exceptions.RayServeException: You are trying to deploy a multi-application config, however a single-application config has been deployed to the current Serve instance already. Mixing single-app and multi-app is not allowed. Please either redeploy using the single-application config format `ServeApplicationSchema`, or shutdown and restart Serve to submit a multi-app config of format `ServeDeploySchema`. If you are using the REST API, you can submit a multi-app config to the diff --git a/doc/source/cluster/kubernetes/troubleshooting/troubleshooting.md b/doc/source/cluster/kubernetes/troubleshooting/troubleshooting.md index d73cbdb8ac66..b274f1f0a8f2 100644 --- a/doc/source/cluster/kubernetes/troubleshooting/troubleshooting.md +++ b/doc/source/cluster/kubernetes/troubleshooting/troubleshooting.md @@ -11,7 +11,6 @@ If you don't find an answer to your question here, please don't hesitate to conn - [Worker init container](#worker-init-container) - [Cluster domain](#cluster-domain) - [RayService](#rayservice) -- [GPU multi-tenancy](#gpu-multitenancy) - [Other questions](#questions) ## Upgrade KubeRay @@ -19,9 +18,10 @@ If you don't find an answer to your question here, please don't hesitate to conn If you have issues upgrading KubeRay, refer to the [upgrade guide](#kuberay-upgrade-guide). Most issues are about the CRD version. +(worker-init-container)= ## Worker init container -The KubeRay operator injects a default [init container](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) into every worker Pod. +The KubeRay operator injects a default [init container](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) into every worker Pod. This init container is responsible for waiting until the Global Control Service (GCS) on the head Pod is ready before establishing a connection to the head. The init container will use `ray health-check` to check the GCS server status continuously. @@ -46,6 +46,7 @@ To disable the injection, set the `ENABLE_INIT_CONTAINER_INJECTION` environment Please refer to [#1069](https://github.com/ray-project/kuberay/pull/1069) and the [KubeRay Helm chart](https://github.com/ray-project/kuberay/blob/ddb5e528c29c2e1fb80994f05b1bd162ecbaf9f2/helm-chart/kuberay-operator/values.yaml#L83-L87) for instructions on how to set the environment variable. Once disabled, you can add your custom init container to the worker Pod template. +(cluster-domain)= ## Cluster domain In KubeRay, we use Fully Qualified Domain Names (FQDNs) to establish connections between workers and the head. @@ -58,12 +59,14 @@ To set a custom cluster domain, adjust the `CLUSTER_DOMAIN` environment variable Helm chart users can make this modification [here](https://github.com/ray-project/kuberay/blob/ddb5e528c29c2e1fb80994f05b1bd162ecbaf9f2/helm-chart/kuberay-operator/values.yaml#L88-L91). For more information, please refer to [#951](https://github.com/ray-project/kuberay/pull/951) and [#938](https://github.com/ray-project/kuberay/pull/938) for more details. +(rayservice)= ## RayService RayService is a Custom Resource Definition (CRD) designed for Ray Serve. In KubeRay, creating a RayService will first create a RayCluster and then -create Ray Serve applications once the RayCluster is ready. If the issue pertains to the data plane, specifically your Ray Serve scripts +create Ray Serve applications once the RayCluster is ready. If the issue pertains to the data plane, specifically your Ray Serve scripts or Ray Serve configurations (`serveConfigV2`), troubleshooting may be challenging. See [rayservice-troubleshooting](kuberay-raysvc-troubleshoot) for more details. +(questions)= ## Questions ### Why are changes to the RayCluster or RayJob CR not taking effect? diff --git a/doc/source/cluster/kubernetes/user-guides.md b/doc/source/cluster/kubernetes/user-guides.md index 2b600aeff174..1cf538a7c8ea 100644 --- a/doc/source/cluster/kubernetes/user-guides.md +++ b/doc/source/cluster/kubernetes/user-guides.md @@ -2,6 +2,31 @@ # User Guides +```{toctree} +:hidden: + +Deploy Ray Serve Apps +user-guides/rayservice-high-availability +user-guides/observability +user-guides/upgrade-guide +user-guides/k8s-cluster-setup +user-guides/storage +user-guides/config +user-guides/configuring-autoscaling +user-guides/kuberay-gcs-ft +user-guides/gke-gcs-bucket +user-guides/logging +user-guides/gpu +user-guides/rayserve-dev-doc +user-guides/pod-command +user-guides/pod-security +user-guides/helm-chart-rbac +user-guides/tls +user-guides/k8s-autoscaler +user-guides/static-ray-cluster-without-kuberay +``` + + :::{note} To learn the basics of Ray on Kubernetes, we recommend taking a look at the {ref}`introductory guide ` first. diff --git a/doc/source/cluster/kubernetes/user-guides/k8s-cluster-setup.md b/doc/source/cluster/kubernetes/user-guides/k8s-cluster-setup.md index caa42e633b4f..bc04ae096a69 100644 --- a/doc/source/cluster/kubernetes/user-guides/k8s-cluster-setup.md +++ b/doc/source/cluster/kubernetes/user-guides/k8s-cluster-setup.md @@ -2,6 +2,13 @@ # Managed Kubernetes services +```{toctree} +:hidden: + +aws-eks-gpu-cluster +gcp-gke-gpu-cluster +``` + The KubeRay operator and Ray can run on any cloud or on-prem Kubernetes cluster. The simplest way to provision a remote Kubernetes cluster is to use a cloud-based managed service. We collect a few helpful links for users who are getting started with a managed Kubernetes service. diff --git a/doc/source/cluster/kubernetes/user-guides/rayservice.md b/doc/source/cluster/kubernetes/user-guides/rayservice.md index 7c80ddd883cb..d9e5968796be 100644 --- a/doc/source/cluster/kubernetes/user-guides/rayservice.md +++ b/doc/source/cluster/kubernetes/user-guides/rayservice.md @@ -69,7 +69,7 @@ kubectl apply -f ray_v1alpha1_rayservice.yaml deployments: ... ``` -## Step 4: Verify the Kubernetes cluster status +## Step 4: Verify the Kubernetes cluster status ```sh # Step 4.1: List all RayService custom resources in the `default` namespace. @@ -113,7 +113,7 @@ If you don't use`rayservice-sample-head-svc`, you need to update the ingress con However, if you use `rayservice-sample-head-svc`, KubeRay automatically updates the selector to point to the new head Pod, eliminating the need to update the ingress configuration. -> Note: Default ports and their definitions. +> Note: Default ports and their definitions. | Port | Definition | |-------|---------------------| @@ -180,8 +180,9 @@ curl -X POST -H 'Content-Type: application/json' rayservice-sample-serve-svc:800 # [Expected output]: "15 pizzas please!" ``` -* `rayservice-sample-serve-svc` is HA in general. It does traffic routing among all the workers which have Serve deployments and always tries to point to the healthy cluster, even during upgrading or failing cases. +* `rayservice-sample-serve-svc` is HA in general. It does traffic routing among all the workers which have Serve deployments and always tries to point to the healthy cluster, even during upgrading or failing cases. +(step-7-in-place-update-for-ray-serve-applications)= ## Step 7: In-place update for Ray Serve applications You can update the configurations for the applications by modifying `serveConfigV2` in the RayService config file. Reapplying the modified config with `kubectl apply` reapplies the new configurations to the existing RayCluster instead of creating a new RayCluster. @@ -214,6 +215,7 @@ curl -X POST -H 'Content-Type: application/json' rayservice-sample-serve-svc:800 # [Expected output]: 8 ``` +(step-8-zero-downtime-upgrade-for-ray-clusters)= ## Step 8: Zero downtime upgrade for Ray clusters In Step 7, modifying `serveConfigV2` doesn't trigger a zero downtime upgrade for Ray clusters. @@ -221,8 +223,8 @@ Instead, it reapplies the new configurations to the existing RayCluster. However, if you modify `spec.rayClusterConfig` in the RayService YAML file, it triggers a zero downtime upgrade for Ray clusters. RayService temporarily creates a new RayCluster and waits for it to be ready, then switches traffic to the new RayCluster by updating the selector of the head service managed by RayService (that is, `rayservice-sample-head-svc`) and terminates the old one. -During the zero downtime upgrade process, RayService creates a new RayCluster temporarily and waits for it to become ready. -Once the new RayCluster is ready, RayService updates the selector of the head service managed by RayService (that is, `rayservice-sample-head-svc`) to point to the new RayCluster to switch the traffic to the new RayCluster. +During the zero downtime upgrade process, RayService creates a new RayCluster temporarily and waits for it to become ready. +Once the new RayCluster is ready, RayService updates the selector of the head service managed by RayService (that is, `rayservice-sample-head-svc`) to point to the new RayCluster to switch the traffic to the new RayCluster. Finally, the old RayCluster is terminated. Certain exceptions don't trigger a zero downtime upgrade. diff --git a/doc/source/cluster/vms/examples/index.md b/doc/source/cluster/vms/examples/index.md index 059be000a548..6426cb41de72 100644 --- a/doc/source/cluster/vms/examples/index.md +++ b/doc/source/cluster/vms/examples/index.md @@ -2,6 +2,12 @@ # Examples +```{toctree} +:hidden: + +ml-example +``` + :::{note} To learn the basics of Ray on Cloud VMs, we recommend taking a look at the {ref}`introductory guide ` first. diff --git a/doc/source/cluster/vms/getting-started.rst b/doc/source/cluster/vms/getting-started.rst index 7dc5cb3109dd..716555d3bfea 100644 --- a/doc/source/cluster/vms/getting-started.rst +++ b/doc/source/cluster/vms/getting-started.rst @@ -29,43 +29,50 @@ Setup Before we start, you will need to install some Python dependencies as follows: -.. tabs:: +.. tab-set:: - .. tab:: Ray Team Supported + .. tab-item:: Ray Team Supported + :sync: Ray Team Supported - .. tabs:: + .. tab-set:: - .. tab:: AWS + .. tab-item:: AWS + :sync: AWS .. code-block:: shell $ pip install -U "ray[default]" boto3 - .. tab:: GCP + .. tab-item:: GCP + :sync: GCP .. code-block:: shell $ pip install -U "ray[default]" google-api-python-client - .. tab:: Community Supported + .. tab-item:: Community Supported + :sync: Community Supported - .. tabs:: + .. tab-set:: - .. tab:: Azure + .. tab-item:: Azure + :sync: Azure .. code-block:: shell $ pip install -U "ray[default]" azure-cli azure-core - .. tab:: Aliyun + .. tab-item:: Aliyun + :sync: Aliyun .. code-block:: shell $ pip install -U "ray[default]" aliyun-python-sdk-core aliyun-python-sdk-ecs - + Aliyun Cluster Launcher Maintainers (GitHub handles): @zhuangzhuang131419, @chenk008 - .. tab:: vSphere + .. tab-item:: vSphere + :sync: vSphere .. code-block:: shell @@ -76,36 +83,43 @@ Before we start, you will need to install some Python dependencies as follows: Next, if you're not set up to use your cloud provider from the command line, you'll have to configure your credentials: -.. tabs:: +.. tab-set:: - .. tab:: Ray Team Supported + .. tab-item:: Ray Team Supported + :sync: Ray Team Supported - .. tabs:: + .. tab-set:: - .. tab:: AWS + .. tab-item:: AWS + :sync: AWS Configure your credentials in ``~/.aws/credentials`` as described in `the AWS docs `_. - .. tab:: GCP + .. tab-item:: GCP + :sync: GCP Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable as described in `the GCP docs `_. - .. tab:: Community Supported + .. tab-item:: Community Supported + :sync: Community Supported - .. tabs:: + .. tab-set:: - .. tab:: Azure + .. tab-item:: Azure + :sync: Azure Log in using ``az login``, then configure your credentials with ``az account set -s ``. - .. tab:: Aliyun + .. tab-item:: Aliyun + :sync: Aliyun Obtain and set the AccessKey pair of the Aliyun account as described in `the docs `__. Make sure to grant the necessary permissions to the RAM user and set the AccessKey pair in your cluster config file. Refer to the provided `aliyun/example-full.yaml `__ for a sample cluster config. - .. tab:: vSphere + .. tab-item:: vSphere + :sync: vSphere .. code-block:: shell @@ -205,18 +219,21 @@ To start a Ray Cluster, first we need to define the cluster configuration. The c A minimal sample cluster configuration file looks as follows: -.. tabs:: +.. tab-set:: - .. tab:: Ray Team Supported + .. tab-item:: Ray Team Supported + :sync: Ray Team Supported - .. tabs:: + .. tab-set:: - .. tab:: AWS + .. tab-item:: AWS + :sync: AWS .. literalinclude:: ../../../../python/ray/autoscaler/aws/example-minimal.yaml :language: yaml - .. tab:: GCP + .. tab-item:: GCP + :sync: GCP .. code-block:: yaml @@ -228,11 +245,13 @@ A minimal sample cluster configuration file looks as follows: type: gcp region: us-west1 - .. tab:: Community Supported + .. tab-item:: Community Supported + :sync: Community Supported - .. tabs:: + .. tab-set:: - .. tab:: Azure + .. tab-item:: Azure + :sync: Azure .. code-block:: yaml @@ -254,13 +273,15 @@ A minimal sample cluster configuration file looks as follows: # changes to this should match what is specified in file_mounts ssh_public_key: ~/.ssh/id_rsa.pub - .. tab:: Aliyun + .. tab-item:: Aliyun + :sync: Aliyun - Please refer to `example-full.yaml `__. + Please refer to `example-full.yaml `__. Make sure your account balance is not less than 100 RMB, otherwise you will receive the error `InvalidAccountStatus.NotEnoughBalance`. - .. tab:: vSphere + .. tab-item:: vSphere + :sync: vSphere .. literalinclude:: ../../../../python/ray/autoscaler/vsphere/example-minimal.yaml :language: yaml diff --git a/doc/source/cluster/vms/index.md b/doc/source/cluster/vms/index.md index 79b3017bdc6c..e472a49e782e 100644 --- a/doc/source/cluster/vms/index.md +++ b/doc/source/cluster/vms/index.md @@ -1,6 +1,15 @@ # Ray on Cloud VMs (cloud-vm-index)= +```{toctree} +:hidden: + +getting-started +User Guides +Examples +references/index +``` + ## Overview In this section we cover how to launch Ray clusters on Cloud VMs. Ray ships with built-in support @@ -23,14 +32,14 @@ The Ray docs present all the information you need to start running Ray workloads .. grid:: 1 2 2 2 :gutter: 1 :class-container: container pb-3 - + .. grid-item-card:: - + **Getting Started** ^^^ - + Learn how to start a Ray cluster and deploy Ray applications in the cloud. - + +++ .. button-ref:: vm-cluster-quick-start :color: primary @@ -38,14 +47,14 @@ The Ray docs present all the information you need to start running Ray workloads :expand: Get Started with Ray on Cloud VMs - + .. grid-item-card:: **Examples** ^^^ - + Try example Ray workloads in the Cloud - + +++ .. button-ref:: vm-cluster-examples :color: primary @@ -53,14 +62,14 @@ The Ray docs present all the information you need to start running Ray workloads :expand: Try example workloads - + .. grid-item-card:: **User Guides** ^^^ - + Learn best practices for configuring cloud clusters - + +++ .. button-ref:: vm-cluster-guides :color: primary @@ -68,14 +77,14 @@ The Ray docs present all the information you need to start running Ray workloads :expand: Read the User Guides - + .. grid-item-card:: **API Reference** ^^^ - + Find API references for cloud clusters - + +++ .. button-ref:: vm-cluster-api-references :color: primary diff --git a/doc/source/cluster/vms/user-guides/community/index.rst b/doc/source/cluster/vms/user-guides/community/index.rst index 2c5c25feb9f9..a21c64920cd1 100644 --- a/doc/source/cluster/vms/user-guides/community/index.rst +++ b/doc/source/cluster/vms/user-guides/community/index.rst @@ -3,6 +3,13 @@ Community Supported Cluster Managers ==================================== +.. toctree:: + :hidden: + + yarn + slurm + lsf + .. note:: If you're using AWS, Azure, GCP or vSphere you can use the :ref:`Ray cluster launcher ` to simplify the cluster setup process. diff --git a/doc/source/cluster/vms/user-guides/index.md b/doc/source/cluster/vms/user-guides/index.md index 01bbf04d0827..1c20784335ee 100644 --- a/doc/source/cluster/vms/user-guides/index.md +++ b/doc/source/cluster/vms/user-guides/index.md @@ -2,6 +2,16 @@ # User Guides +```{toctree} +:hidden: + +launching-clusters/index +large-cluster-best-practices +configuring-autoscaling +logging +Community-supported Cluster Managers +``` + :::{note} To learn the basics of Ray on Cloud VMs, we recommend taking a look at the {ref}`introductory guide ` first. diff --git a/doc/source/conf.py b/doc/source/conf.py index 027f5398a18a..cd2d8a05b8fe 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,18 +1,31 @@ +from typing import List, Dict, Union, Any +import copy +import yaml from datetime import datetime from pathlib import Path from importlib import import_module import os import sys from jinja2.filters import FILTERS +import sphinx +from sphinx.ext import autodoc +from sphinx.util.nodes import make_refnode +from docutils import nodes +import pathlib +import bs4 +import logging + +logger = logging.getLogger(__name__) + sys.path.insert(0, os.path.abspath(".")) -from custom_directives import ( +from custom_directives import ( # noqa DownloadAndPreprocessEcosystemDocs, update_context, LinkcheckSummarizer, - build_gallery, ) + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -39,16 +52,13 @@ "sphinx-jsonschema", "sphinxemoji.sphinxemoji", "sphinx_copybutton", - "versionwarning.extension", "sphinx_sitemap", "myst_nb", "sphinx.ext.doctest", "sphinx.ext.coverage", "sphinx.ext.autosummary", - "sphinx_external_toc", "sphinxcontrib.autodoc_pydantic", "sphinxcontrib.redoc", - "sphinx_tabs.tabs", "sphinx_remove_toctrees", "sphinx_design", "sphinx.ext.intersphinx", @@ -97,13 +107,25 @@ "replacements", ] +myst_heading_anchors = 3 + # Cache notebook outputs in _build/.jupyter_cache -# To prevent notebook execution, set this to "off". To force re-execution, set this to "force". -# To cache previous runs, set this to "cache". -jupyter_execute_notebooks = os.getenv("RUN_NOTEBOOKS", "off") +# To prevent notebook execution, set this to "off". To force re-execution, set this to +# "force". To cache previous runs, set this to "cache". +nb_execution_mode = os.getenv("RUN_NOTEBOOKS", "off") -external_toc_exclude_missing = False -external_toc_path = "_toc.yml" +# Add a render priority for doctest +nb_mime_priority_overrides = [ + ("html", "application/vnd.jupyter.widget-view+json", 10), + ("html", "application/javascript", 20), + ("html", "text/html", 30), + ("html", "image/svg+xml", 40), + ("html", "image/png", 50), + ("html", "image/jpeg", 60), + ("html", "text/markdown", 70), + ("html", "text/latex", 80), + ("html", "text/plain", 90), +] html_extra_path = ["robots.txt"] @@ -124,7 +146,7 @@ # Special mocking of packaging.version.Version is required when using sphinx; # we can't just add this to autodoc_mock_imports, as packaging is imported by # sphinx even before it can be mocked. Instead, we patch it here. -import packaging.version as packaging_version +import packaging.version as packaging_version # noqa Version = packaging_version.Version @@ -139,35 +161,9 @@ def __init__(self, version: str): packaging_version.Version = MockVersion -# This is used to suppress warnings about explicit "toctree" directives. -suppress_warnings = ["etoc.toctree"] - -versionwarning_admonition_type = "note" -versionwarning_banner_title = "Join the Ray Discuss Forums!" - -FORUM_LINK = "https://discuss.ray.io" -versionwarning_messages = { - # Re-enable this after Ray Summit. - # "latest": ( - # "This document is for the latest pip release. " - # 'Visit the master branch documentation here.' - # ), - # "master": ( - # "Got questions? Join " - # f'the Ray Community forum ' - # "for Q&A on all things Ray, as well as to share and learn use cases " - # "and best practices with the Ray community." - # ), -} - -versionwarning_body_selector = "#main-content" - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] -# The encoding of source files. -# source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = "index" @@ -176,16 +172,16 @@ def __init__(self, version: str): copyright = str(datetime.now().year) + ", The Ray Team" author = "The Ray Team" -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the +# The version info for the project you're documenting acts as replacement for +# |version| and |release|, and is also used in various other places throughout the # built documents. Retrieve the version using `find_version` rather than importing # directly (from ray import __version__) because initializing ray will prevent # mocking of certain external dependencies. -from setup import find_version +from setup import find_version # noqa release = find_version("ray", "_version.py") -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -258,26 +254,62 @@ def __init__(self, version: str): # -- Options for HTML output ---------------------------------------------- + +def render_svg_logo(path): + with open(pathlib.Path(__file__).parent / path, "r") as f: + content = f.read() + + return content + + # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "sphinx_book_theme" +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - "repository_url": "https://github.com/ray-project/ray", - "use_repository_button": True, - "use_issues_button": True, "use_edit_page_button": True, - "path_to_docs": "doc/source", - "home_page_in_toc": True, - "show_navbar_depth": 1, - "announcement": "
    ", + "announcement": None, + "logo": { + "svg": render_svg_logo("_static/img/ray_logo.svg"), + }, + "navbar_start": ["navbar-ray-logo"], + "navbar_end": [ + "navbar-icon-links", + "navbar-anyscale", + ], + "navbar_center": ["navbar-links"], + "navbar_align": "left", + "navbar_persistent": [ + "theme-switcher", + ], + "secondary_sidebar_items": [ + "page-toc", + "edit-this-page", + ], + "content_footer_items": [ + "csat", + ], + "navigation_depth": 8, + "analytics": {"google_analytics_id": "UA-110413294-1"}, } -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] +html_context = { + "github_user": "ray-project", + "github_repo": "ray", + "github_version": "master", + "doc_path": "doc/source/", +} + +html_sidebars = { + "**": [ + "search-button-field", + "main-sidebar", + ], + "ray-overview/examples": ["examples-sidebar"], +} # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -285,9 +317,6 @@ def __init__(self, version: str): autodoc_typehints_format = "short" -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. @@ -354,21 +383,176 @@ def filter_out_undoc_class_members(member_name, class_name, module_name): FILTERS["filter_out_undoc_class_members"] = filter_out_undoc_class_members -# Add a render priority for doctest -nb_render_priority = { - "doctest": (), - "html": ( - "application/vnd.jupyter.widget-view+json", - "application/javascript", - "text/html", - "image/svg+xml", - "image/png", - "image/jpeg", - "text/markdown", - "text/latex", - "text/plain", - ), -} + +def parse_navbar_config(app: sphinx.application.Sphinx, config: sphinx.config.Config): + """Parse the navbar config file into a set of links to show in the navbar. + + Parameters + ---------- + app : sphinx.application.Sphinx + Application instance passed when the `config-inited` event is emitted + config : sphinx.config.Config + Initialized configuration to be modified + """ + if "navbar_content_path" in config: + filename = app.config["navbar_content_path"] + else: + filename = "" + + if filename: + with open(pathlib.Path(__file__).parent / filename, "r") as f: + config.navbar_content = yaml.safe_load(f) + else: + config.navbar_content = None + + +NavEntry = Dict[str, Union[str, List["NavEntry"]]] + + +def setup_context(app, pagename, templatename, context, doctree): + def render_header_nav_links() -> bs4.BeautifulSoup: + """Render external header links into the top nav bar. + The structure rendered here is defined in an external yaml file. + + Returns + ------- + str + Raw HTML to be rendered in the top nav bar + """ + if not hasattr(app.config, "navbar_content"): + raise ValueError( + "A template is attempting to call render_header_nav_links(); a " + "navbar configuration must be specified." + ) + + node = nodes.container(classes=["navbar-content"]) + node.append(render_header_nodes(app.config.navbar_content)) + header_soup = bs4.BeautifulSoup( + app.builder.render_partial(node)["fragment"], "html.parser" + ) + return add_nav_chevrons(header_soup) + + def render_header_nodes( + obj: List[NavEntry], is_top_level: bool = True + ) -> nodes.Node: + """Generate a set of header nav links with docutils nodes. + + Parameters + ---------- + is_top_level : bool + True if the call to this function is rendering the top level nodes, + False otherwise (non-top level nodes are displayed as submenus of the top + level nodes) + obj : List[NavEntry] + List of yaml config entries to render as docutils nodes + + Returns + ------- + nodes.Node + Bullet list which will be turned into header nav HTML by the sphinx builder + """ + bullet_list = nodes.bullet_list( + bullet="-", + classes=["navbar-toplevel" if is_top_level else "navbar-sublevel"], + ) + + for item in obj: + + if "file" in item: + ref_node = make_refnode( + app.builder, + context["current_page_name"], + item["file"], + None, + nodes.inline(classes=["navbar-link-title"], text=item.get("title")), + item.get("title"), + ) + elif "link" in item: + ref_node = nodes.reference("", "", internal=False) + ref_node["refuri"] = item.get("link") + ref_node["reftitle"] = item.get("title") + ref_node.append( + nodes.inline(classes=["navbar-link-title"], text=item.get("title")) + ) + + if "caption" in item: + caption = nodes.Text(item.get("caption")) + ref_node.append(caption) + + paragraph = nodes.paragraph() + paragraph.append(ref_node) + + container = nodes.container(classes=["ref-container"]) + container.append(paragraph) + + list_item = nodes.list_item( + classes=["active-link"] if item.get("file") == pagename else [] + ) + list_item.append(container) + + if "sections" in item: + wrapper = nodes.container(classes=["navbar-dropdown"]) + wrapper.append( + render_header_nodes(item["sections"], is_top_level=False) + ) + list_item.append(wrapper) + + bullet_list.append(list_item) + + return bullet_list + + context["render_header_nav_links"] = render_header_nav_links + + +def add_nav_chevrons(input_soup: bs4.BeautifulSoup) -> bs4.BeautifulSoup: + """Add dropdown chevron icons to the header nav bar. + + Parameters + ---------- + input_soup : bs4.BeautifulSoup + Soup containing rendered HTML which will be inserted into the header nav bar + + Returns + ------- + bs4.BeautifulSoup + A new BeautifulSoup instance containing chevrons on the list items that + are meant to be dropdowns. + """ + soup = copy.copy(input_soup) + + for li in soup.find_all("li", recursive=True): + divs = li.find_all("div", {"class": "navbar-dropdown"}, recursive=False) + if divs: + ref = li.find("div", {"class": "ref-container"}) + ref.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"})) + + return soup + + +def add_custom_assets( + app: sphinx.application.Sphinx, + pagename: str, + templatename: str, + context: Dict[str, Any], + doctree: nodes.Node, +): + """Add custom per-page assets. + + See documentation on Sphinx Core Events for more information: + https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx-core-events + """ + if pagename == "train/train": + app.add_css_file("css/ray-train.css") + elif pagename == "index": + # CSS for HTML part of index.html + app.add_css_file("css/splash.css") + app.add_js_file("js/splash.js") + elif pagename == "ray-overview/examples": + # Example gallery + app.add_css_file("css/examples.css") + app.add_js_file("js/examples.js") + elif pagename == "ray-overview/ray-libraries": + app.add_css_file("css/ray-libraries.css") def setup(app): @@ -378,31 +562,25 @@ def setup(app): import doctest doctest.register_optionflag("MOCK") - app.connect("html-page-context", update_context) - # Custom CSS - app.add_css_file("css/custom.css", priority=800) - app.add_css_file( - "https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" - ) - # https://github.com/ines/termynal - app.add_css_file("css/termynal.css") - - # Custom JS - app.add_js_file( - "https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js", - defer="defer", - ) - app.add_js_file("js/docsearch.js", defer="defer") - app.add_js_file("js/csat.js", defer="defer") + app.add_config_value("navbar_content_path", "navbar.yml", "env") + app.connect("config-inited", parse_navbar_config) + app.connect("html-page-context", setup_context) + app.connect("html-page-context", add_custom_assets) # https://github.com/ines/termynal app.add_js_file("js/termynal.js", defer="defer") + app.add_css_file("css/termynal.css") + app.add_js_file("js/custom.js", defer="defer") - app.add_js_file("js/assistant.js", defer="defer") + app.add_css_file("css/custom.css", priority=800) + + app.add_js_file("js/csat.js") + app.add_css_file("css/csat.css") - app.add_js_file("js/top-navigation.js", defer="defer") + app.add_js_file("js/assistant.js", defer="defer") + app.add_css_file("css/assistant.css") base_path = Path(__file__).parent github_docs = DownloadAndPreprocessEcosystemDocs(base_path) @@ -416,12 +594,6 @@ def setup(app): app.connect("builder-inited", linkcheck_summarizer.add_handler_to_linkcheck) app.connect("build-finished", linkcheck_summarizer.summarize) - # Create galleries on the fly - app.connect("builder-inited", build_gallery) - - # tag filtering system - app.add_js_file("js/tags.js") - redoc = [ { @@ -458,6 +630,7 @@ def setup(app): "numpy", "pandas", "pyarrow", + "pydantic", "pytorch_lightning", "scipy", "setproctitle", @@ -475,13 +648,15 @@ def setup(app): "watchfiles", "xgboost", "xgboost_ray", + "psutil", + "colorama", + "grpc", # Internal compiled modules "ray._raylet", "ray.core.generated", "ray.serve.generated", ] - for mock_target in autodoc_mock_imports: assert mock_target not in sys.modules, ( f"Problematic mock target ({mock_target}) found; " @@ -489,10 +664,10 @@ def setup(app): "been loaded into sys.modules when the sphinx build starts." ) -from sphinx.ext import autodoc - class MockedClassDocumenter(autodoc.ClassDocumenter): + """Remove note about base class when a class is derived from object.""" + def add_line(self, line: str, source: str, *lineno: int) -> None: if line == " Bases: :py:class:`object`": return @@ -501,6 +676,7 @@ def add_line(self, line: str, source: str, *lineno: int) -> None: autodoc.ClassDocumenter = MockedClassDocumenter + # Other sphinx docs can be linked to if the appropriate URL to the docs # is specified in the `intersphinx_mapping` - for example, types annotations # that are defined in dependencies can link to their respective documentation. @@ -520,6 +696,7 @@ def add_line(self, line: str, source: str, *lineno: int) -> None: "numpy": ("https://numpy.org/doc/stable/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), "pyarrow": ("https://arrow.apache.org/docs", None), + "pydantic": ("https://docs.pydantic.dev/latest/", None), "pymongoarrow": ("https://mongo-arrow.readthedocs.io/en/latest/", None), "pyspark": ("https://spark.apache.org/docs/latest/api/python/", None), "python": ("https://docs.python.org/3", None), diff --git a/doc/source/data/batch_inference.rst b/doc/source/data/batch_inference.rst index 32deafe2930a..92c616f57076 100644 --- a/doc/source/data/batch_inference.rst +++ b/doc/source/data/batch_inference.rst @@ -34,9 +34,10 @@ Using Ray Data for offline inference involves four basic steps: For more in-depth examples for your use case, see :ref:`the batch inference examples`. For how to configure batch inference, see :ref:`the configuration guide`. -.. tabs:: +.. tab-set:: - .. group-tab:: HuggingFace + .. tab-item:: HuggingFace + :sync: HuggingFace .. testcode:: @@ -84,7 +85,8 @@ For how to configure batch inference, see :ref:`the configuration guide`. -.. tabs:: +.. tab-set:: - .. group-tab:: HuggingFace + .. tab-item:: HuggingFace + :sync: HuggingFace .. testcode:: @@ -246,7 +250,8 @@ The remaining is the same as the :ref:`Quickstart `. {'data': 'Complete this', 'output': 'Complete this poll. Which one do you think holds the most promise for you?\n\nThank you'} - .. group-tab:: PyTorch + .. tab-item:: PyTorch + :sync: PyTorch .. testcode:: @@ -292,7 +297,8 @@ The remaining is the same as the :ref:`Quickstart `. {'output': array([0.5590901], dtype=float32)} - .. group-tab:: TensorFlow + .. tab-item:: TensorFlow + :sync: TensorFlow .. testcode:: @@ -471,7 +477,7 @@ In this case, use :meth:`XGBoostTrainer.get_model() `_. .. testcode:: - + from typing import Dict import pandas as pd import numpy as np @@ -485,18 +491,18 @@ The rest of the logic looks the same as in the `Quickstart <#quickstart>`_. class XGBoostPredictor: def __init__(self, checkpoint: Checkpoint): self.model = XGBoostTrainer.get_model(checkpoint) - + def __call__(self, data: pd.DataFrame) -> Dict[str, np.ndarray]: dmatrix = xgboost.DMatrix(data) return {"predictions": self.model.predict(dmatrix)} - - + + # Use 2 parallel actors for inference. Each actor predicts on a # different partition of data. scale = ray.data.ActorPoolStrategy(size=2) # Map the Predictor over the Dataset to get predictions. predictions = test_dataset.map_batches( - XGBoostPredictor, + XGBoostPredictor, compute=scale, batch_format="pandas", # Pass in the Checkpoint to the XGBoostPredictor constructor. diff --git a/doc/source/data/data.rst b/doc/source/data/data.rst index 737298774334..004167a23333 100644 --- a/doc/source/data/data.rst +++ b/doc/source/data/data.rst @@ -4,6 +4,16 @@ Ray Data: Scalable Datasets for ML ================================== +.. toctree:: + :hidden: + + Overview + key-concepts + user-guide + examples/index + api/api + data-internals + Ray Data is a scalable data processing library for ML workloads. It provides flexible and performant APIs for scaling :ref:`Offline batch inference ` and :ref:`Data preprocessing and ingest for ML training `. Ray Data uses `streaming execution `__ to efficiently process large datasets. .. image:: images/dataset.svg diff --git a/doc/source/data/examples/batch_training.ipynb b/doc/source/data/examples/batch_training.ipynb index c60e2dc87439..70a6b3e5f621 100644 --- a/doc/source/data/examples/batch_training.ipynb +++ b/doc/source/data/examples/batch_training.ipynb @@ -142,7 +142,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Creating a Dataset " + "(create_ds)=\n", + "## Creating a Dataset" ] }, { @@ -230,7 +231,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Filtering a Dataset on Read \n", + "(filter_ds)=\n", + "### Filtering a Dataset on Read\n", "\n", "Normally there is some last-mile data processing required before training. Let's just assume we know the data processing steps are:\n", "- Drop negative trip distances, 0 fares, 0 passengers.\n", @@ -309,7 +311,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Inspecting a Dataset \n", + "(inspect_ds)=\n", + "### Inspecting a Dataset\n", "\n", "Let's get some basic statistics about our newly created Dataset.\n", "\n", @@ -394,11 +397,11 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Transforming a Dataset in parallel using custom functions \n", + "(transform_ds)=\n", + "### Transforming a Dataset in parallel using custom functions\n", "\n", "Ray Data allows you to specify custom data transform functions. These [user defined functions (UDFs)](transforming_data) can be called using `Dataset.map_batches(my_function)`. The transformation will be conducted in parallel for each data batch.\n", "\n", @@ -496,7 +499,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Batch training with Ray Data " + "(batch_train_ds)=\n", + "## Batch training with Ray Data" ] }, { @@ -668,7 +672,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -1028,7 +1031,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Re-load a model and perform batch prediction " + "(load_model)=\n", + "## Re-load a model and perform batch prediction" ] }, { @@ -1292,7 +1296,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.10.8" }, "vscode": { "interpreter": { diff --git a/doc/source/data/key-concepts.rst b/doc/source/data/key-concepts.rst index 500714afa7f8..d1c7e78c7158 100644 --- a/doc/source/data/key-concepts.rst +++ b/doc/source/data/key-concepts.rst @@ -165,7 +165,7 @@ or remote filesystems. .. testoutput:: :options: +MOCK - + ['..._000000.parquet', '..._000001.parquet'] diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 000000000000..1a8ba42fc0f0 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,28 @@ +:html_theme.sidebar_secondary.remove: + +.. title:: Welcome to Ray! + +.. toctree:: + :hidden: + + Overview + Getting Started + Installation + Use Cases + Example Gallery + Ecosystem + Ray Core + Ray Data + Ray Train + Ray Tune + Ray Serve + Ray RLlib + More Libraries + Ray Clusters + Monitoring and Debugging + Developer Guides + Glossary + Security + +.. raw:: html + :file: splash.html diff --git a/doc/source/navbar.yml b/doc/source/navbar.yml new file mode 100644 index 000000000000..61811e302b45 --- /dev/null +++ b/doc/source/navbar.yml @@ -0,0 +1,53 @@ +- file: ray-overview/getting-started + title: "Getting Started" +- file: ray-overview/use-cases + title: "Use Cases" +- file: ray-overview/examples + title: "Example Gallery" +- file: ray-overview/installation + title: "Libraries" + sections: + - file: ray-core/walkthrough + title: Ray Core + caption: Scale general Python applications + - file: data/data + title: Ray Data + caption: Scale data ingest and preprocessing + - file: train/train + title: Ray Train + caption: Scale machine learning training + - file: tune/index + title: Ray Tune + caption: Scale hyperparameter tuning + - file: serve/index + title: Ray Serve + caption: Scale model serving + - file: rllib/index + title: Ray RLlib + caption: Scale reinforcement learning +- file: index + title: "Docs" +- link: https://discuss.ray.io + title: "Resources" + sections: + - link: https://discuss.ray.io + title: "Discussion Forum" + caption: Get your Ray questions answered + - link: https://github.com/ray-project/ray-educational-materials + title: "Training" + caption: Hands-on learning + - link: https://www.anyscale.com/blog + title: "Blog" + caption: Updates, best practices, user-stories + - link: https://www.anyscale.com/events + title: "Events" + caption: Webinars, meetups, office hours + - link: https://www.anyscale.com/blog/how-ray-and-anyscale-make-it-easy-to-do-massive-scale-machine-learning-on + title: "Success Stories" + caption: Real-world workload examples + - file: ray-overview/ray-libraries + title: "Ecosystem" + caption: Libraries integrated with Ray + - link: https://www.ray.io/community + title: "Community" + caption: Connect with us diff --git a/doc/source/ray-contribute/docs.ipynb b/doc/source/ray-contribute/docs.ipynb index 3c623f393441..82f547f48335 100644 --- a/doc/source/ray-contribute/docs.ipynb +++ b/doc/source/ray-contribute/docs.ipynb @@ -287,8 +287,7 @@ "Here's an example:\n", "\n", "````markdown\n", - "```{code-cell} python3\n", - ":tags: [hide-cell]\n", + "```python\n", "\n", "import ray\n", "import ray.rllib.agents.ppo as ppo\n", @@ -316,11 +315,7 @@ "cell_type": "code", "execution_count": null, "id": "78cac353", - "metadata": { - "tags": [ - "hide-cell" - ] - }, + "metadata": {}, "outputs": [], "source": [ "import ray\n", @@ -347,8 +342,6 @@ "id": "d716d0bd", "metadata": {}, "source": [ - "As you can see, the code block is hidden, but you can expand it by click on the \"+\" button.\n", - "\n", "### Tags for your notebook\n", "\n", "What makes this work is the `:tags: [hide-cell]` directive in the `code-cell`.\n", diff --git a/doc/source/ray-core/examples/overview.rst b/doc/source/ray-core/examples/overview.rst index 3cefef5bb6b7..0d96fee0b0e4 100644 --- a/doc/source/ray-core/examples/overview.rst +++ b/doc/source/ray-core/examples/overview.rst @@ -1,8 +1,15 @@ .. _ray-core-examples-tutorial: + Ray Tutorials and Examples ========================== +.. toctree:: + :hidden: + :glob: + + * + Machine Learning Examples ------------------------- diff --git a/doc/source/ray-core/walkthrough.rst b/doc/source/ray-core/walkthrough.rst index e10c479171bc..ebffa785d67e 100644 --- a/doc/source/ray-core/walkthrough.rst +++ b/doc/source/ray-core/walkthrough.rst @@ -3,6 +3,16 @@ What is Ray Core? ================= +.. toctree:: + :maxdepth: 1 + :hidden: + + Key Concepts + User Guides + Examples + api/index + + Ray Core provides a small number of core primitives (i.e., tasks, actors, objects) for building and scaling distributed applications. Below we'll walk through simple examples that show you how to turn your functions and classes easily into Ray tasks and actors, and how to work with Ray objects. Getting Started @@ -58,7 +68,7 @@ As seen above, Ray stores task and actor call results in its :ref:`distributed o Next Steps ---------- -.. tip:: To check how your application is doing, you can use the :ref:`Ray dashboard `. +.. tip:: To check how your application is doing, you can use the :ref:`Ray dashboard `. Ray's key primitives are simple, but can be composed together to express almost any kind of distributed computation. Learn more about Ray's :ref:`key concepts ` with the following user guides: diff --git a/doc/source/ray-more-libs/index.rst b/doc/source/ray-more-libs/index.rst index 46b2659facc7..af9f61d1d6f2 100644 --- a/doc/source/ray-more-libs/index.rst +++ b/doc/source/ray-more-libs/index.rst @@ -1,6 +1,19 @@ More Ray ML Libraries ===================== +.. toctree:: + :hidden: + + joblib + multiprocessing + ray-collective + dask-on-ray + raydp + mars-on-ray + modin/index + Ray Workflows (Alpha) <../workflows/index> + + .. TODO: we added the three Ray Core examples below, since they don't really belong there. Going forward, make sure that all "Ray Lightning" and XGBoost topics are in one document or group, and not next to each other. diff --git a/doc/source/ray-observability/index.md b/doc/source/ray-observability/index.md index ddba634d8985..e938ee5aa6a5 100644 --- a/doc/source/ray-observability/index.md +++ b/doc/source/ray-observability/index.md @@ -2,6 +2,15 @@ # Monitoring and Debugging +```{toctree} +:hidden: + +getting-started +key-concepts +User Guides +Reference +``` + This section covers how to **monitor and debug Ray applications and clusters** with Ray's Observability features. @@ -28,4 +37,3 @@ Monitoring and debugging Ray applications consist of 4 major steps: 4. Form a hypothesis, implement a fix, and validate it. The remainder of this section covers the observability tools that Ray provides to accelerate your monitoring and debugging workflow. - diff --git a/doc/source/ray-observability/reference/api.rst b/doc/source/ray-observability/reference/api.rst index db49be3d96b7..0ea155b0d9a4 100644 --- a/doc/source/ray-observability/reference/api.rst +++ b/doc/source/ray-observability/reference/api.rst @@ -23,9 +23,9 @@ Summary APIs :nosignatures: :toctree: doc/ - ray.util.state.summarize_actors - ray.util.state.summarize_objects - ray.util.state.summarize_tasks + ray.util.state.summarize_actors + ray.util.state.summarize_objects + ray.util.state.summarize_tasks List APIs ~~~~~~~~~~ diff --git a/doc/source/ray-observability/reference/index.md b/doc/source/ray-observability/reference/index.md index 06ef3bfc3449..07384079698a 100644 --- a/doc/source/ray-observability/reference/index.md +++ b/doc/source/ray-observability/reference/index.md @@ -2,9 +2,17 @@ # Reference +```{toctree} +:hidden: + +api +cli +system-metrics +``` + Monitor and debug your Ray applications and clusters using the API and CLI documented in these references. The guides include: * {ref}`state-api-ref` * {ref}`state-api-cli-ref` -* {ref}`system-metrics` \ No newline at end of file +* {ref}`system-metrics` diff --git a/doc/source/ray-observability/user-guides/cli-sdk.rst b/doc/source/ray-observability/user-guides/cli-sdk.rst index 11f1867acaeb..61f82e54b9b7 100644 --- a/doc/source/ray-observability/user-guides/cli-sdk.rst +++ b/doc/source/ray-observability/user-guides/cli-sdk.rst @@ -6,7 +6,7 @@ Monitoring with the CLI or SDK Monitoring and debugging capabilities in Ray are available through a CLI or SDK. -CLI command ``ray status`` +CLI command ``ray status`` ---------------------------- You can monitor node status and resource usage by running the CLI command, ``ray status``, on the head node. It displays @@ -102,9 +102,10 @@ This example uses the following script that runs two Tasks and creates two Actor See the summarized states of tasks. If it doesn't return the output immediately, retry the command. -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -126,7 +127,8 @@ See the summarized states of tasks. If it doesn't return the output immediately, 0 task_running_300_seconds RUNNING: 2 NORMAL_TASK 1 Actor.__init__ FINISHED: 2 ACTOR_CREATION_TASK - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -139,9 +141,10 @@ See the summarized states of tasks. If it doesn't return the output immediately, List all Actors. -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -160,7 +163,8 @@ List all Actors. 0 31405554844820381c2f0f8501000000 Actor 96956 ALIVE 1 f36758a9f8871a9ca993b1d201000000 Actor 96955 ALIVE - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -174,9 +178,10 @@ List all Actors. Get the state of a single Task using the get API. -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -196,7 +201,8 @@ Get the state of a single Task using the get API. serialized_runtime_env: '{}' state: ALIVE - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -207,9 +213,10 @@ Get the state of a single Task using the get API. Access logs through the ``ray logs`` API. -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -224,7 +231,8 @@ Access logs through the ``ray logs`` API. :actor_name:Actor Actor created - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -260,15 +268,17 @@ you can use ``list`` or ``get`` APIs to get more details for an individual abnor **Summarize all actors** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray summary actors - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -281,15 +291,17 @@ you can use ``list`` or ``get`` APIs to get more details for an individual abnor **Summarize all tasks** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray summary tasks - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -308,15 +320,17 @@ you can use ``list`` or ``get`` APIs to get more details for an individual abnor To get callsite info, set env variable `RAY_record_ref_creation_sites=1` when starting the Ray Cluster RAY_record_ref_creation_sites=1 ray start --head -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray summary objects - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -346,15 +360,17 @@ Get a list of resources. Possible resources include: **List all nodes** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list nodes - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -363,15 +379,17 @@ Get a list of resources. Possible resources include: **List all placement groups** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list placement-groups - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -383,15 +401,17 @@ Get a list of resources. Possible resources include: .. tip:: You can list resources with one or multiple filters: using `--filter` or `-f` -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list objects -f pid= -f reference_type=LOCAL_REFERENCE - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -400,15 +420,17 @@ Get a list of resources. Possible resources include: **List alive actors** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list actors -f state=ALIVE - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -417,15 +439,17 @@ Get a list of resources. Possible resources include: **List running tasks** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list tasks -f state=RUNNING - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -434,15 +458,17 @@ Get a list of resources. Possible resources include: **List non-running tasks** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list tasks -f state!=RUNNING - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -451,15 +477,17 @@ Get a list of resources. Possible resources include: **List running tasks that have a name func** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list tasks -f state=RUNNING -f name="task_running_300_seconds()" - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -470,15 +498,17 @@ Get a list of resources. Possible resources include: .. tip:: When ``--detail`` is specified, the API can query more data sources to obtain state information in details. -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray list tasks --detail - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: @@ -493,15 +523,17 @@ Get the states of a particular entity (task, actor, etc.) **Get a task's states** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray get tasks - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -511,15 +543,17 @@ Get the states of a particular entity (task, actor, etc.) **Get a node's states** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray get nodes - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -540,15 +574,17 @@ By default, the API prints logs from a head node. **Get all retrievable log file names from a head node in a cluster** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray logs cluster - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -562,9 +598,10 @@ By default, the API prints logs from a head node. **Get a particular log file from a node** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -573,7 +610,8 @@ By default, the API prints logs from a head node. # `ray logs cluster` is alias to `ray logs` when querying with globs. ray logs gcs_server.out --node-id - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -586,9 +624,10 @@ By default, the API prints logs from a head node. **Stream a log file from a node** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash @@ -598,7 +637,8 @@ By default, the API prints logs from a head node. ray logs cluster raylet.out --node-ip --follow - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -612,15 +652,17 @@ By default, the API prints logs from a head node. **Stream log from an actor with actor id** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray logs actor --id= --follow - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True @@ -634,15 +676,17 @@ By default, the API prints logs from a head node. **Stream log from a pid** -.. tabs:: +.. tab-set:: - .. group-tab:: CLI (Recommended) + .. tab-item:: CLI (Recommended) + :sync: CLI (Recommended) .. code-block:: bash ray logs worker --pid= --follow - .. group-tab:: Python SDK (Internal Developer API) + .. tab-item:: Python SDK (Internal Developer API) + :sync: Python SDK (Internal Developer API) .. testcode:: :skipif: True diff --git a/doc/source/ray-observability/user-guides/debug-apps/index.md b/doc/source/ray-observability/user-guides/debug-apps/index.md index 2a8aa767d853..8599421cda07 100644 --- a/doc/source/ray-observability/user-guides/debug-apps/index.md +++ b/doc/source/ray-observability/user-guides/debug-apps/index.md @@ -2,10 +2,21 @@ # Debugging Applications +```{toctree} +:hidden: + +general-debugging +debug-memory +debug-hangs +debug-failures +optimize-performance +ray-debugging +``` + These guides help you perform common debugging or optimization tasks for your distributed application on Ray: * {ref}`observability-general-debugging` * {ref}`ray-core-mem-profiling` * {ref}`observability-debug-hangs` * {ref}`observability-debug-failures` * {ref}`observability-optimize-performance` -* {ref}`ray-debugger` \ No newline at end of file +* {ref}`ray-debugger` diff --git a/doc/source/ray-observability/user-guides/index.md b/doc/source/ray-observability/user-guides/index.md index 1db7e61aeea0..d50e4b8b0ae8 100644 --- a/doc/source/ray-observability/user-guides/index.md +++ b/doc/source/ray-observability/user-guides/index.md @@ -2,6 +2,17 @@ # User Guides +```{toctree} +:hidden: + +Debugging Applications +cli-sdk +configure-logging +profiling +add-app-metrics +ray-tracing +``` + These guides help you monitor and debug your Ray applications and clusters. The guides include: @@ -9,4 +20,4 @@ The guides include: * {ref}`observability-programmatic` * {ref}`configure-logging` * {ref}`application-level-metrics` -* {ref}`ray-tracing` \ No newline at end of file +* {ref}`ray-tracing` diff --git a/doc/source/ray-overview/eco-gallery.yml b/doc/source/ray-overview/eco-gallery.yml deleted file mode 100644 index bbefe18f5abc..000000000000 --- a/doc/source/ray-overview/eco-gallery.yml +++ /dev/null @@ -1,190 +0,0 @@ -meta: - grid: 1 2 2 3 - gutter: 1 - class-container: container pb-3 - -classes: - class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - -projects: - - name: Classy Vision Integration - section_title: Classy Vision - description: Classy Vision is a new end-to-end, PyTorch-based framework for - large-scale training of state-of-the-art image and video classification models. - The library features a modular, flexible design that allows anyone to train - machine learning models on top of PyTorch using very simple abstractions. - website: https://github.com/facebookresearch/ClassyVision/blob/main/tutorials/ray_aws.ipynb - repo: https://github.com/facebookresearch/ClassyVision - image: ../images/classyvision.png - - name: Dask Integration - section_title: Dask - description: Dask provides advanced parallelism for analytics, enabling performance - at scale for the tools you love. Dask uses existing Python APIs and data - structures to make it easy to switch between Numpy, Pandas, - Scikit-learn to their Dask-powered equivalents. - website: dask-on-ray - repo: https://github.com/dask/dask - image: ../images/dask.png - - name: Flambé Integration - section_title: Flambé - description: Flambé is a machine learning experimentation framework built to - accelerate the entire research life cycle. Flambé’s main objective is to - provide a unified interface for prototyping models, running experiments - containing complex pipelines, monitoring those experiments in real-time, - reporting results, and deploying a final model for inference. - website: https://github.com/asappresearch/flambe - repo: https://github.com/asappresearch/flambe - image: ../images/flambe.png - - name: Flyte Integration - section_title: Flyte - description: Flyte is a Kubernetes-native workflow automation platform for complex, - mission-critical data and ML processes at scale. It has been battle-tested - at Lyft, Spotify, Freenome, and others and is truly open-source. - website: https://flyte.org/ - repo: https://github.com/flyteorg/flyte - image: ../images/flyte.png - - name: Horovod Integration - section_title: Horovod - description: Horovod is a distributed deep learning training framework for - TensorFlow, Keras, PyTorch, and Apache MXNet. The goal of - Horovod is to make distributed deep learning fast and easy to use. - website: https://horovod.readthedocs.io/en/stable/ray_include.html - repo: https://github.com/horovod/horovod - image: ../images/horovod.png - - name: Hugging Face Transformers Integration - section_title: Hugging Face Transformers - description: State-of-the-art Natural Language Processing for - Pytorch and TensorFlow 2.0. It integrates with Ray for distributed - hyperparameter tuning of transformer models. - website: https://huggingface.co/transformers/master/main_classes/trainer.html#transformers.Trainer.hyperparameter_search - repo: https://github.com/huggingface/transformers - image: ../images/hugging.png - - name: Intel Analytics Zoo Integration - section_title: Intel Analytics Zoo - description: Analytics Zoo seamlessly scales TensorFlow, Keras and PyTorch - to distributed big data (using Spark, Flink & Ray). - website: https://analytics-zoo.github.io/master/#ProgrammingGuide/rayonspark/ - repo: https://github.com/intel-analytics/analytics-zoo - image: ../images/zoo.png - - name: NLU Integration - section_title: John Snow Labs' NLU - description: The power of 350+ pre-trained NLP models, 100+ Word Embeddings, - 50+ Sentence Embeddings, and 50+ Classifiers in 46 languages - with 1 line of Python code. - website: https://nlu.johnsnowlabs.com/docs/en/predict_api#modin-dataframe - repo: https://github.com/JohnSnowLabs/nlu - image: ../images/nlu.png - - name: Ludwig Integration - section_title: Ludwig AI - description: Ludwig is a toolbox that allows users to train and test deep learning - models without the need to write code. With Ludwig, you can train a deep learning - model on Ray in zero lines of code, automatically leveraging Dask on Ray for data - preprocessing, Horovod on Ray for distributed training, and Ray Tune for - hyperparameter optimization. - website: https://medium.com/ludwig-ai/ludwig-ai-v0-4-introducing-declarative-mlops-with-ray-dask-tabnet-and-mlflow-integrations-6509c3875c2e - repo: https://github.com/ludwig-ai/ludwig - image: ../images/ludwig.png - - name: MARS Integration - section_title: MARS - description: Mars is a tensor-based unified framework for large-scale data - computation which scales Numpy, Pandas and Scikit-learn. Mars can scale in to - a single machine, and scale out to a cluster with thousands of machines. - website: mars-on-ray - repo: https://github.com/mars-project/mars - image: ../images/mars.png - - name: Modin Integration - section_title: Modin - description: Scale your pandas workflows by changing one line of code. - Modin transparently distributes the data and computation so that all you need - to do is continue using the pandas API as you were before installing Modin. - website: https://github.com/modin-project/modin - repo: https://github.com/modin-project/modin - image: ../images/modin.png - - name: Prefect Integration - section_title: Prefect - description: Prefect is an open source workflow orchestration platform in Python. - It allows you to easily define, track and schedule workflows in Python. This - integration makes it easy to run a Prefect workflow on a Ray cluster in a - distributed way. - website: https://github.com/PrefectHQ/prefect-ray - repo: https://github.com/PrefectHQ/prefect-ray - image: ../images/prefect.png - - name: PyCaret Integration - section_title: PyCaret - description: PyCaret is an open source low-code machine learning library in Python - that aims to reduce the hypothesis to insights cycle time in a ML experiment. - It enables data scientists to perform end-to-end experiments quickly - and efficiently. - website: https://github.com/pycaret/pycaret - repo: https://github.com/pycaret/pycaret - image: ../images/pycaret.png - - name: RayDP Integration - section_title: Spark on Ray (RayDP) - description: RayDP ("Spark on Ray") enables you to easily use Spark inside a - Ray program. You can use Spark to read the input data, process the data using - SQL, Spark DataFrame, or Pandas (via Koalas) API, extract and transform features - using Spark MLLib, and use RayDP Estimator API for distributed training - on the preprocessed dataset. - website: https://github.com/Intel-bigdata/oap-raydp - repo: https://github.com/Intel-bigdata/oap-raydp - image: ../images/intel.png - - name: Scikit Learn Integration - section_title: Scikit Learn - description: Scikit-learn is a free software machine learning library for - the Python programming language. It features various classification, - regression and clustering algorithms including support vector machines, - random forests, gradient boosting, k-means and DBSCAN, and is designed to - interoperate with the Python numerical and scientific libraries NumPy and SciPy. - website: https://docs.ray.io/en/master/joblib.html - repo: https://github.com/scikit-learn/scikit-learn - image: ../images/scikit.png - - name: Seldon Alibi Integration - section_title: Seldon Alibi - description: Alibi is an open source Python library aimed at machine learning model - inspection and interpretation. The focus of the library is to provide high-quality - implementations of black-box, white-box, local and global explanation methods for - classification and regression models. - website: https://github.com/SeldonIO/alibi - repo: https://github.com/SeldonIO/alibi - image: ../images/seldon.png - - name: Sematic Integration - section_title: Sematic - description: Sematic is an open-source ML pipelining tool written in Python. - It enables users to write end-to-end pipelines that can seamlessly transition between - your laptop and the cloud, with rich visualizations, traceability, - reproducibility, and usability as first-class citizens. This integration - enables dynamic allocation of Ray clusters within Sematic pipelines. - website: https://docs.sematic.dev/integrations/ray - repo: https://github.com/sematic-ai/sematic - image: ../images/sematic.png - - name: spaCy Integration - section_title: spaCy - description: spaCy is a library for advanced Natural Language Processing in Python - and Cython. It's built on the very latest research, and was designed from - day one to be used in real products. - website: https://pypi.org/project/spacy-ray/ - repo: https://github.com/explosion/spacy-ray - image: ../images/spacy.png - - name: XGBoost Integration - section_title: XGBoost - description: XGBoost is a popular gradient boosting library for classification - and regression. It is one of the most popular tools in data science and - workhorse of many top-performing Kaggle kernels. - website: https://github.com/ray-project/xgboost_ray - repo: https://github.com/ray-project/xgboost_ray - image: ../images/xgboost_logo.png - - name: LightGBM Integration - section_title: LightGBM - description: LightGBM is a high-performance gradient boosting library for - classification and regression. It is designed to be distributed and efficient. - website: https://github.com/ray-project/lightgbm_ray - repo: https://github.com/ray-project/lightgbm_ray - image: ../images/lightgbm_logo.png - - name: Volcano Integration - section_title: Volcano - description: Volcano is system for running high-performance workloads - on Kubernetes. It features powerful batch scheduling capabilities required by ML - and other data-intensive workloads. - website: https://github.com/volcano-sh/volcano/releases/tag/v1.7.0 - repo: https://github.com/volcano-sh/volcano/ - image: ./images/volcano.png diff --git a/doc/source/ray-overview/examples.rst b/doc/source/ray-overview/examples.rst index c1de39acb5ef..cd36e8158193 100644 --- a/doc/source/ray-overview/examples.rst +++ b/doc/source/ray-overview/examples.rst @@ -5,21 +5,45 @@ Ray Examples .. raw:: html - -
    - + +
    -.. margin:: +.. note:: For installation on computers running Apple Silicon (such as M1), please follow instructions `here `._ diff --git a/doc/source/rllib/rllib-cli.md b/doc/source/rllib/rllib-cli.md index a40c530ce637..6af058cec9b1 100644 --- a/doc/source/rllib/rllib-cli.md +++ b/doc/source/rllib/rllib-cli.md @@ -13,9 +13,9 @@ kernelspec: # Working with the RLlib CLI -RLlib is built in Python and if you're an advanced user, you will primarily use its -Python API to build and run your experiments. -But RLlib also comes with a command line interface (CLI)[^typer] that allows you to quickly +RLlib is built in Python and if you're an advanced user, you will primarily use its +Python API to build and run your experiments. +But RLlib also comes with a command line interface (CLI)[^typer] that allows you to quickly run and evaluate experiments without having to write any code. You can also run pre-defined RLlib examples with it. @@ -29,10 +29,10 @@ using an example built for you by the RLlib team. ### Searching for examples -The first thing you can to is filter the list of all available examples by the +The first thing you can to is filter the list of all available examples by the environment name you want to use: -```{margin} +```{note} You can run this command without the `--filter` option to see the full list of almost 100 examples that come with RLlib. ``` @@ -62,7 +62,7 @@ by typing the following CLI command: ``` -The output will be a prompt in the YAML format that contains the configuration of +The output will be a prompt in the YAML format that contains the configuration of the example.[^formats] We're not going to go into the details of the configuration here, but you should know that it's a _tuned example_, meaning that you can expect it to train well out of @@ -74,7 +74,7 @@ Here's the output of the above command (parsed as YAML for readability): :language: yaml ``` -```{margin} +```{note} Note that some of these tuned examples may require resource specifications, like multiple GPUs, that you might not have available on your machine. To "solve" some of the more advanced environments out there _efficiently_ and provide @@ -87,7 +87,7 @@ After all, running on Ray Clusters is what RLlib was built for. Let's run the example next! After showing how to start the training run, we give you some sample output of it below. Note that by default, RLlib will create an indicative experiment name for you, and logs -important metrics such as the `reward`, the `episode_reward_max`, or the +important metrics such as the `reward`, the `episode_reward_max`, or the `episode_reward_min`. ```{raw} html @@ -103,7 +103,7 @@ important metrics such as the `reward`, the `episode_reward_max`, or the | reward | episode_reward_max | episode_reward_min | ... - ... | PPO_CartPole-v0_9931e_00000 | RUNNING | ... + ... | PPO_CartPole-v0_9931e_00000 | RUNNING | ... | 23.9756 | 71 | 10 | ...
    @@ -165,7 +165,7 @@ If you want to do more than just use the examples that come with RLlib, you can also run your own algorithm configurations with `rllib`. That's what you use the `rllib train` command for. -```{margin} +```{note} Internally the `rllib example run` command uses the same functionality as `rllib train`. The only difference is the usage of pre-defined configurations in the former. @@ -173,7 +173,7 @@ The only difference is the usage of pre-defined configurations in the former. There are two basic ways to run training. You can either provide all the necessary information via the command line, or simply -point to a configuration file (like the YAML we've already seen in the +point to a configuration file (like the YAML we've already seen in the previous section). Let's briefly go over both options, starting with the recommended one. @@ -261,7 +261,7 @@ The `--config` option is used to provide a JSON string that contains the configu that you would otherwise put in your YAML or JSON config files. The `train` command has many more options that we don't discuss in detail here, -but you can use the following dropdown to read more about their definition: +but you can use the following dropdown to read more about their definition: :::{dropdown} Click here to see all `train` command line options :animate: fade-in-slide-down @@ -287,7 +287,7 @@ your Ray cluster. ::: Probably the best way to get familiar with `rllib train` is to run -`rllib train --help` and play with your own experiments. +`rllib train --help` and play with your own experiments. ## Evaluating your experiments @@ -297,8 +297,8 @@ By default, checkpoints are generated in `~/ray_results//checkpoints and you can use can provide the full path to a checkpoint as argument to the evaluate command. -```{margin} -As you've seen earlier, every successful `rllib example run` generates an +```{note} +As you've seen earlier, every successful `rllib example run` generates an `rllib evaluate` command automatically for you from a checkpoint. The same holds true for `rllib train` runs. ``` @@ -321,7 +321,7 @@ by simply omitting the checkpoint argument: ``` Note that the `evaluate` command needs to know about the algorithm and environment -you want to evaluate. +you want to evaluate. The overall structure of the command itself is slightly simpler than its `train` counterpart, as there are simply fewer options to configure. @@ -343,7 +343,7 @@ and `example`, you have all the tools you need to run simple RLlib experiments f the command line. If you want to learn more about the capabilities of RLlib, you have to learn more about -its [Python API](rllib-training-api) and how to +its [Python API](rllib-training-api) and how to [configure algorithms](rllib-algo-configuration) in the first place. @@ -356,4 +356,4 @@ its [Python API](rllib-training-api) and how to The CLI will automatically detect the format of the file you're using. [^tune]: These stopping conditions are internally picked up by Ray Tune, - which is used by RLlib under the hood. \ No newline at end of file + which is used by RLlib under the hood. diff --git a/doc/source/rllib/rllib-training.rst b/doc/source/rllib/rllib-training.rst index 1732e4281521..735a0c5ebf90 100644 --- a/doc/source/rllib/rllib-training.rst +++ b/doc/source/rllib/rllib-training.rst @@ -32,7 +32,7 @@ You can train DQN with the following commands: rllib train --algo DQN --env CartPole-v1 --stop '{"training_iteration": 30}'
    -.. margin:: +.. note:: The ``rllib train`` command (same as the ``train.py`` script in the repo) has a number of options you can show by running `rllib train --help`. @@ -85,7 +85,7 @@ Some good hyperparameters and settings are available in `the RLlib repository `__ (some of them are tuned to run on GPUs). -.. margin:: +.. note:: If you find better settings or tune an algorithm on a different domain, consider submitting a Pull Request! @@ -160,7 +160,7 @@ of the training results and retrieving the checkpoint(s) of the trained agent. :start-after: rllib-tuner-begin :end-before: rllib-tuner-end -.. margin:: +.. note:: You can find your checkpoint's version by looking into the ``rllib_checkpoint.json`` file inside your checkpoint directory. @@ -236,7 +236,7 @@ underlying neural network model being trained. For example, you may want to pre-train it separately, or otherwise update its weights outside of RLlib. This can be done by accessing the ``model`` of the policy. -.. margin:: +.. note:: To run these examples, you need to install a few extra dependencies, namely `pip install "gym[atari]" "gym[accept-rom-license]" atari_py`. @@ -324,7 +324,7 @@ Let's discuss each category one by one, starting with training options. Specifying Training Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. margin:: +.. note:: For instance, a `DQNConfig` takes a `double_q` `training` argument to specify whether to use a double-Q DQN, whereas in a `PPOConfig` this does not make sense. @@ -611,4 +611,4 @@ hangs or performance issues. Next Steps ---------- -- To check how your application is doing, you can use the :ref:`Ray dashboard `. \ No newline at end of file +- To check how your application is doing, you can use the :ref:`Ray dashboard `. diff --git a/doc/source/rllib/user-guides.rst b/doc/source/rllib/user-guides.rst index 127d0f9bd554..9725e1491b99 100644 --- a/doc/source/rllib/user-guides.rst +++ b/doc/source/rllib/user-guides.rst @@ -8,6 +8,26 @@ User Guides =========== +.. toctree:: + :hidden: + + rllib-advanced-api + rllib-models + rllib-saving-and-loading-algos-and-policies + rllib-concepts + rllib-sample-collection + rllib-replay-buffers + rllib-offline + rllib-catalogs + rllib-connector + rllib-rlmodule + rllib-learner + rllib-torch2x + rllib-fault-tolerance + rllib-dev + rllib-cli + + .. _rllib-feature-guide: RLlib Feature Guides diff --git a/doc/source/serve/advanced-guides/dev-workflow.md b/doc/source/serve/advanced-guides/dev-workflow.md index a9df66ea1c30..de8b6745a400 100644 --- a/doc/source/serve/advanced-guides/dev-workflow.md +++ b/doc/source/serve/advanced-guides/dev-workflow.md @@ -83,7 +83,7 @@ Note that rerunning `serve run` will redeploy all deployments. To prevent redepl To test on a remote cluster, you'll use `serve run` again, but this time you'll pass in an `--address` argument to specify the address of the Ray cluster to connect to. For remote clusters, this address has the form `ray://:10001`; see [Ray Client](ray-client-ref) for more information. -When making the transition from your local machine to a remote cluster, you'll need to make sure your cluster has a similar environment to your local machine--files, environment variables, and Python packages, for example. +When making the transition from your local machine to a remote cluster, you'll need to make sure your cluster has a similar environment to your local machine--files, environment variables, and Python packages, for example. Let's see a simple example that just packages the code. Run the following command on your local machine, with your remote cluster head node IP address substituted for `` in the command: @@ -106,7 +106,7 @@ For more complex dependencies, including files outside the working directory, en serve run --address=ray://:10001 --runtime-env-json='{"env_vars": {"MY_ENV_VAR": "my-value"}, "working_dir": "./project/src", "pip": ["requests", "chess"]}' local_dev:app ``` -You can also specify the `runtime_env` in a YAML file; see [serve run](serve_cli.html#serve-run) for details. +You can also specify the `runtime_env` in a YAML file; see [serve run](#serve-cli) for details. ## What's Next? diff --git a/doc/source/serve/advanced-guides/index.md b/doc/source/serve/advanced-guides/index.md index 44451f8351fb..47b2f6a0a1dd 100644 --- a/doc/source/serve/advanced-guides/index.md +++ b/doc/source/serve/advanced-guides/index.md @@ -1,6 +1,21 @@ (serve-advanced-guides)= # Advanced Guides +```{toctree} +:hidden: + +app-builder-guide +advanced-autoscaling +performance +dyn-req-batch +inplace-updates +dev-workflow +grpc-guide +deployment-graphs +managing-java-deployments +deploy-vm +``` + If you’re new to Ray Serve, we recommend starting with the [Ray Serve Quickstart](serve-getting-started). Use these advanced guides for more options and configurations: diff --git a/doc/source/serve/index.md b/doc/source/serve/index.md index b99f4a8d00b9..f014656080fb 100644 --- a/doc/source/serve/index.md +++ b/doc/source/serve/index.md @@ -2,6 +2,27 @@ # Ray Serve: Scalable and Programmable Serving +```{toctree} +:hidden: + +getting_started +key-concepts +develop-and-deploy +model_composition +multi-app +model-multiplexing +configure-serve-deployment +http-guide +Production Guide +monitoring +resource-allocation +autoscaling-guide +advanced-guides/index +architecture +tutorials/index +api/index +``` + :::{tip} [Get in touch with us](https://docs.google.com/forms/d/1l8HT35jXMPtxVUtQPeGoe09VGp5jcvSv0TqPgyz6lGU) if you're using or considering using Ray Serve. ::: @@ -17,7 +38,7 @@ Ray Serve is a scalable model serving library for building online inference APIs. Serve is framework-agnostic, so you can use a single toolkit to serve everything from deep learning models built with frameworks like PyTorch, TensorFlow, and Keras, to Scikit-Learn models, to arbitrary Python business logic. It has several features and performance optimizations for serving Large Language Models such as response streaming, dynamic request batching, multi-node/multi-GPU serving, etc. -Ray Serve is particularly well suited for [model composition](serve-model-composition) and many model serving, enabling you to build a complex inference service consisting of multiple ML models and business logic all in Python code. +Ray Serve is particularly well suited for [model composition](serve-model-composition) and many model serving, enabling you to build a complex inference service consisting of multiple ML models and business logic all in Python code. Ray Serve is built on top of Ray, so it easily scales to many machines and offers flexible scheduling support such as fractional GPUs so you can share resources and serve many machine learning models at low cost. @@ -151,7 +172,7 @@ Serve supports arbitrary Python code and therefore integrates well with the MLOp :::{dropdown} LLM developer :animate: fade-in-slide-down -Serve enables you to rapidly prototype, develop, and deploy scalable LLM applications to production. Many large language model (LLM) applications combine prompt preprocessing, vector database lookups, LLM API calls, and response validation. Because Serve supports any arbitrary Python code, you can write all these steps as a single Python module, enabling rapid development and easy testing. You can then quickly deploy your Ray Serve LLM application to production, and each application step can independently autoscale to efficiently accommodate user traffic without wasting resources. In order to improve performance of your LLM applications, Ray Serve has features for batching and can integrate with any model optimization technique. Ray Serve also supports streaming responses, a key feature for chatbot-like applications. +Serve enables you to rapidly prototype, develop, and deploy scalable LLM applications to production. Many large language model (LLM) applications combine prompt preprocessing, vector database lookups, LLM API calls, and response validation. Because Serve supports any arbitrary Python code, you can write all these steps as a single Python module, enabling rapid development and easy testing. You can then quickly deploy your Ray Serve LLM application to production, and each application step can independently autoscale to efficiently accommodate user traffic without wasting resources. In order to improve performance of your LLM applications, Ray Serve has features for batching and can integrate with any model optimization technique. Ray Serve also supports streaming responses, a key feature for chatbot-like applications. ::: @@ -222,69 +243,69 @@ or head over to the {doc}`tutorials/index` to get started building your Ray Serv .. grid-item-card:: :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - + **Getting Started** ^^^ - + Start with our quick start tutorials for :ref:`deploying a single model locally ` and how to :ref:`convert an existing model into a Ray Serve deployment ` . - + +++ .. button-ref:: serve-getting-started :color: primary :outline: :expand: - - Get Started with Ray Serve - + + Get Started with Ray Serve + .. grid-item-card:: :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - + **Key Concepts** ^^^ - + Understand the key concepts behind Ray Serve. Learn about :ref:`Deployments `, :ref:`how to query them `, and using :ref:`DeploymentHandles ` to compose multiple models and business logic together. - + +++ .. button-ref:: serve-key-concepts :color: primary :outline: :expand: - + Learn Key Concepts - + .. grid-item-card:: :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - + **Examples** ^^^ - + Follow the tutorials to learn how to integrate Ray Serve with :ref:`TensorFlow `, :ref:`Scikit-Learn `, and :ref:`RLlib `. - + +++ .. button-ref:: serve-examples :color: primary :outline: :expand: - + Serve Examples - + .. grid-item-card:: :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - + **API Reference** ^^^ - + Get more in-depth information about the Ray Serve API. - + +++ .. button-ref:: serve-api :color: primary :outline: :expand: - + Read the API Reference - + ``` For more, see the following blog posts about Ray Serve: diff --git a/doc/source/serve/production-guide/index.md b/doc/source/serve/production-guide/index.md index 62a63d84eb23..bc4c9fa08096 100644 --- a/doc/source/serve/production-guide/index.md +++ b/doc/source/serve/production-guide/index.md @@ -2,6 +2,18 @@ # Production Guide +```{toctree} +:hidden: + +config +kubernetes +docker +fault-tolerance +handling-dependencies +best-practices +``` + + The recommended way to run Ray Serve in production is on Kubernetes using the [KubeRay](kuberay-quickstart) [RayService](kuberay-rayservice-quickstart) custom resource. The RayService custom resource automatically handles important production requirements such as health checking, status reporting, failure recovery, and upgrades. If you're not running on Kubernetes, you can also run Ray Serve on a Ray cluster directly using the Serve CLI. diff --git a/doc/source/serve/tutorials/rllib.md b/doc/source/serve/tutorials/rllib.md index 047b1da1523e..a7cf21daf1a7 100644 --- a/doc/source/serve/tutorials/rllib.md +++ b/doc/source/serve/tutorials/rllib.md @@ -20,7 +20,7 @@ In particular, we show: - How to load this model from a checkpoint. - How to parse the JSON request and evaluate the payload in RLlib. -```{margin} +```{note} Check out the [Key Concepts](serve-key-concepts) page to learn more general information about Ray Serve. ``` diff --git a/doc/source/index.md b/doc/source/splash.html similarity index 74% rename from doc/source/index.md rename to doc/source/splash.html index 0018abe0e202..7122e59e5549 100644 --- a/doc/source/index.md +++ b/doc/source/splash.html @@ -1,85 +1,65 @@ -```{title} Welcome to Ray! -``` - -```{raw} html - - - - - - - - - Welcome to Ray! -
    -
    -
    -
    -

    Welcome to Ray!

    -

    Ray is an open-source unified framework for scaling AI and Python applications. - It provides the compute layer for parallel processing so that - you don’t need to be a distributed systems expert. -

    -
    - - - - - - - - - +
    +
    +
    +
    +

    Welcome to Ray!

    +

    Ray is an open-source unified framework for scaling AI and Python applications. + It provides the compute layer for parallel processing so that + you don’t need to be a distributed systems expert. +

    + +
    +
    +
    -
    -
    -
    -
    -
    -

    Scaling with Ray

    
     from ray.rllib.algorithms.ppo import PPOConfig
    @@ -274,34 +254,34 @@ 

    Scaling with Ray

    Learn more | API references
    -
    +
    - - + +
    - +
    @@ -331,7 +311,7 @@

    Beyond the basics

    Ray Libraries

    -

    Scale the entire ML pipeline from data ingest to model serving with high-level Python APIs that integrate with popular ecosystem frameworks.

    +

    Scale the entire ML pipeline from data ingest to model serving with high-level Python APIs that integrate with popular ecosystem frameworks.

    Learn more about Ray Libraries>
    @@ -340,15 +320,15 @@

    Ray Libraries

    Ray Core

    Scale generic Python code with simple, foundational primitives that enable a high degree of control for building distributed applications or custom platforms.

    - Learn more about Core > + Learn more about Core >

    Ray Clusters

    -

    Deploy a Ray cluster on AWS, GCP, Azure or kubernetes from a laptop to a large cluster to seamlessly scale workloads for production

    - Learn more about clusters > +

    Deploy a Ray cluster on AWS, GCP, Azure or kubernetes from a laptop to a large cluster to seamlessly scale workloads for production

    + Learn more about clusters >
    @@ -357,62 +337,61 @@

    Ray Clusters

    Getting involved

    Follow us on Twitter

    -
    -
    + + -

    Open an issue

    -
    +
    - -``` diff --git a/doc/source/train/deepspeed.rst b/doc/source/train/deepspeed.rst index 0cf85609c25e..6e91e7107714 100644 --- a/doc/source/train/deepspeed.rst +++ b/doc/source/train/deepspeed.rst @@ -37,7 +37,7 @@ You only need to run your existing training code with a TorchTrainer. You can ex # Start training ... - + from ray.train.torch import TorchTrainer from ray.train import ScalingConfig @@ -49,11 +49,11 @@ You only need to run your existing training code with a TorchTrainer. You can ex trainer.fit() -Below is a simple example of ZeRO-3 training with DeepSpeed only. +Below is a simple example of ZeRO-3 training with DeepSpeed only. -.. tabs:: +.. tab-set:: - .. group-tab:: Example with Ray Data + .. tab-item:: Example with Ray Data .. dropdown:: Show Code @@ -62,7 +62,7 @@ Below is a simple example of ZeRO-3 training with DeepSpeed only. :start-after: __deepspeed_torch_basic_example_start__ :end-before: __deepspeed_torch_basic_example_end__ - .. group-tab:: Example with PyTorch DataLoader + .. tab-item:: Example with PyTorch DataLoader .. dropdown:: Show Code @@ -73,9 +73,9 @@ Below is a simple example of ZeRO-3 training with DeepSpeed only. .. tip:: - To run DeepSpeed with pure PyTorch, you **don't need to** provide any additional Ray Train utilities - like :meth:`~ray.train.torch.prepare_model` or :meth:`~ray.train.torch.prepare_data_loader` in your training funciton. Instead, - keep using `deepspeed.initialize() `_ as usual to prepare everything + To run DeepSpeed with pure PyTorch, you **don't need to** provide any additional Ray Train utilities + like :meth:`~ray.train.torch.prepare_model` or :meth:`~ray.train.torch.prepare_data_loader` in your training funciton. Instead, + keep using `deepspeed.initialize() `_ as usual to prepare everything for distributed training. Run DeepSpeed with other frameworks diff --git a/doc/source/train/examples/deepspeed/gptj_deepspeed_fine_tuning.ipynb b/doc/source/train/examples/deepspeed/gptj_deepspeed_fine_tuning.ipynb index cadb0902df2d..cc74b6b0361b 100644 --- a/doc/source/train/examples/deepspeed/gptj_deepspeed_fine_tuning.ipynb +++ b/doc/source/train/examples/deepspeed/gptj_deepspeed_fine_tuning.ipynb @@ -21,11 +21,11 @@ "```\n", "\n", "This notebook has the following steps:\n", - "1. [Set up Ray](#setup)\n", - "2. [Load the dataset](#load)\n", - "3. [Preprocess the dataset with Ray Data](#preprocess)\n", - "4. [Run the training with Ray Train](#train)\n", - "5. [Generate text from prompt](#predict)" + "1. [Set up Ray](#gptj-setup)\n", + "2. [Load the dataset](#gptj-load)\n", + "3. [Preprocess the dataset with Ray Data](#gptj-preprocess)\n", + "4. [Run the training with Ray Train](#gptj-train)\n", + "5. [Generate text from prompt](#gptj-predict)" ] }, { @@ -61,7 +61,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set up Ray \n", + "(gptj-setup)=\n", + "## Set up Ray\n", "\n", "First, let's set some global variables. We will use 16 workers, each being assigned 1 GPU and 8 CPUs." ] @@ -192,7 +193,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Loading the dataset \n", + "(gptj-load)=\n", + "## Loading the dataset\n", "\n", "We will be fine-tuning the model on the [`tiny_shakespeare` dataset](https://huggingface.co/datasets/tiny_shakespeare), comprised of 40,000 lines of Shakespeare from a variety of Shakespeare's plays. The aim will be to make the GPT-J model better at generating text in the style of Shakespeare." ] @@ -253,6 +255,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "(gptj-preprocess)=\n", "Note that the dataset is represented by a single line of large string, and needs some preprocessing. To do this, use the {meth}`~ray.data.Dataset.map_batches` API to apply transformation functions to batches of data.\n", "\n", "The `split_text` function takes the single string and splits it into separate lines, removing empty lines and character names ending with ':' (eg. 'ROMEO:'). The `tokenize` function takes the lines and tokenizes them using the 🤗 Tokenizer associated with the model, ensuring each entry has the same length (`block_size`) by padding and truncating. This preprocessing is necessary for training.\n", @@ -334,7 +337,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Fine-tuning the model with Ray Train \n", + "(gptj-train)=\n", + "### Fine-tuning the model with Ray Train\n", "\n", "Configure Ray Train's {class}`~ray.train.torch.TorchTrainer` to perform distributed fine-tuning of the model. Specify a `train_loop_per_worker` function, which defines the training logic to be distributed by Ray using Distributed Data Parallelism, which uses the PyTorch Distributed backend internally. Each worker has its own copy of the model, but operates on different data. At the end of each step, all the workers sync gradients.\n", "\n", @@ -1355,6 +1359,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "(gptj-predict)=\n", "### Generate text from prompt\n", "\n", "First, download the persistent Ray Train checkpoint locally and load the fine-tuned model weights and tokenizer from the checkpoint. Then use 🤗 Transformers [`pipeline`](https://huggingface.co/docs/transformers/en/main_classes/pipelines) to generate predictions from the fine-tuned model.\n", @@ -1462,7 +1467,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.15" + "version": "3.10.8" }, "orphan": true, "vscode": { diff --git a/doc/source/train/examples/transformers/huggingface_text_classification.ipynb b/doc/source/train/examples/transformers/huggingface_text_classification.ipynb index 6f809b3712bd..2c24e48a6224 100644 --- a/doc/source/train/examples/transformers/huggingface_text_classification.ipynb +++ b/doc/source/train/examples/transformers/huggingface_text_classification.ipynb @@ -18,11 +18,11 @@ "This notebook is based on an official Hugging Face example, [How to fine-tune a model on text classification](https://github.com/huggingface/notebooks/blob/6ca682955173cc9d36ffa431ddda505a048cbe80/examples/text_classification.ipynb). This notebook shows the process of conversion from vanilla HF to Ray Train without changing the training logic unless necessary.\n", "\n", "This notebook consists of the following steps:\n", - "1. [Set up Ray](#setup)\n", - "2. [Load the dataset](#load)\n", - "3. [Preprocess the dataset with Ray Data](#preprocess)\n", - "4. [Run the training with Ray Train](#train)\n", - "5. [Optionally, share the model with the community](#share)" + "1. [Set up Ray](#hf-setup)\n", + "2. [Load the dataset](#hf-load)\n", + "3. [Preprocess the dataset with Ray Data](#hf-preprocess)\n", + "4. [Run the training with Ray Train](#hf-train)\n", + "5. [Optionally, share the model with the community](#hf-share)" ] }, { @@ -51,7 +51,8 @@ "id": "pvSRaEHChYbP" }, "source": [ - "## Set up Ray " + "(hf-setup)=\n", + "## Set up Ray" ] }, { @@ -217,7 +218,8 @@ "id": "whPRbBNbIrIl" }, "source": [ - "### Loading the dataset " + "(hf-load)=\n", + "### Loading the dataset" ] }, { @@ -290,7 +292,8 @@ "id": "n9qywopnIrJH" }, "source": [ - "### Preprocessing the data with Ray Data " + "(hf-preprocess)=\n", + "### Preprocessing the data with Ray Data" ] }, { @@ -475,7 +478,8 @@ "id": "545PP3o8IrJV" }, "source": [ - "### Fine-tuning the model with Ray Train " + "(hf-train)=\n", + "### Fine-tuning the model with Ray Train" ] }, { @@ -1054,7 +1058,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Tune hyperparameters with Ray Tune " + "(hf-predict)=\n", + "### Tune hyperparameters with Ray Tune" ] }, { @@ -1960,7 +1965,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Share the model " + "(hf-share)=\n", + "### Share the model" ] }, { @@ -2090,6 +2096,13 @@ "* {ref}`Ray Train Examples ` for more use cases\n", "* {ref}`Ray Train User Guides ` for how-to guides\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -2114,7 +2127,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.10.8" }, "orphan": true, "vscode": { diff --git a/doc/source/train/getting-started-pytorch-lightning.rst b/doc/source/train/getting-started-pytorch-lightning.rst index 68062ecad67c..61823ec0db1c 100644 --- a/doc/source/train/getting-started-pytorch-lightning.rst +++ b/doc/source/train/getting-started-pytorch-lightning.rst @@ -36,9 +36,9 @@ For reference, the final code is as follows: Compare a PyTorch Lightning training script with and without Ray Train. -.. tabs:: +.. tab-set:: - .. group-tab:: PyTorch Lightning + .. tab-item:: PyTorch Lightning .. This snippet isn't tested because it doesn't use any Ray code. @@ -59,17 +59,17 @@ Compare a PyTorch Lightning training script with and without Ray Train. self.model = resnet18(num_classes=10) self.model.conv1 = torch.nn.Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) self.criterion = torch.nn.CrossEntropyLoss() - + def forward(self, x): return self.model(x) - + def training_step(self, batch, batch_idx): x, y = batch outputs = self.forward(x) loss = self.criterion(outputs, y) self.log("loss", loss, on_step=True, prog_bar=True) return loss - + def configure_optimizers(self): return torch.optim.Adam(self.model.parameters(), lr=0.001) @@ -83,9 +83,9 @@ Compare a PyTorch Lightning training script with and without Ray Train. trainer = pl.Trainer(max_epochs=10) trainer.fit(model, train_dataloaders=train_dataloader) - - .. group-tab:: PyTorch Lightning + Ray Train + + .. tab-item:: PyTorch Lightning + Ray Train .. code-block:: python :emphasize-lines: 8-10, 34, 43, 48-50, 52, 53, 55-60 @@ -108,20 +108,20 @@ Compare a PyTorch Lightning training script with and without Ray Train. self.model = resnet18(num_classes=10) self.model.conv1 = torch.nn.Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) self.criterion = torch.nn.CrossEntropyLoss() - + def forward(self, x): return self.model(x) - + def training_step(self, batch, batch_idx): x, y = batch outputs = self.forward(x) loss = self.criterion(outputs, y) self.log("loss", loss, on_step=True, prog_bar=True) return loss - + def configure_optimizers(self): return torch.optim.Adam(self.model.parameters(), lr=0.001) - + def train_func(config): @@ -149,13 +149,13 @@ Compare a PyTorch Lightning training script with and without Ray Train. # [3] Launch distributed training job. trainer = TorchTrainer(train_func, scaling_config=scaling_config) - result = trainer.fit() + result = trainer.fit() Set up a training function -------------------------- -First, update your training code to support distributed training. +First, update your training code to support distributed training. Begin by wrapping your code in a :ref:`training function `: .. testcode:: @@ -167,7 +167,7 @@ Begin by wrapping your code in a :ref:`training function ` and :ref:`hyperparameter optimization `. +Reporting metrics and checkpoints to Ray Train enables you to support :ref:`fault-tolerant training ` and :ref:`hyperparameter optimization `. Note that the :class:`ray.train.lightning.RayTrainReportCallback` class only provides a simple implementation, and can be :ref:`further customized `. Prepare your Lightning Trainer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Finally, pass your Lightning Trainer into -:meth:`~ray.train.lightning.prepare_trainer` to validate -your configurations. +:meth:`~ray.train.lightning.prepare_trainer` to validate +your configurations. .. code-block:: diff @@ -340,7 +340,7 @@ For more details, see :ref:`train_scaling_config`. Launch a training job --------------------- -Tying this all together, you can now launch a distributed training job +Tying this all together, you can now launch a distributed training job with a :class:`~ray.train.torch.TorchTrainer`. .. testcode:: @@ -376,7 +376,7 @@ information about the training run, including the metrics and checkpoints report .. TODO: Add results guide Next steps ----------- +---------- After you have converted your PyTorch Lightning training script to use Ray Train: @@ -387,9 +387,9 @@ After you have converted your PyTorch Lightning training script to use Ray Train Version Compatibility --------------------- -Ray Train is tested with `pytorch_lightning` versions `1.6.5` and `2.0.4`. For full compatibility, use ``pytorch_lightning>=1.6.5`` . -Earlier versions aren't prohibited but may result in unexpected issues. If you run into any compatibility issues, consider upgrading your PyTorch Lightning version or -`file an issue `_. +Ray Train is tested with `pytorch_lightning` versions `1.6.5` and `2.0.4`. For full compatibility, use ``pytorch_lightning>=1.6.5`` . +Earlier versions aren't prohibited but may result in unexpected issues. If you run into any compatibility issues, consider upgrading your PyTorch Lightning version or +`file an issue `_. .. note:: @@ -400,25 +400,25 @@ Earlier versions aren't prohibited but may result in unexpected issues. If you r LightningTrainer Migration Guide -------------------------------- -Ray 2.4 introduced the `LightningTrainer`, and exposed a -`LightningConfigBuilder` to define configurations for `pl.LightningModule` -and `pl.Trainer`. +Ray 2.4 introduced the `LightningTrainer`, and exposed a +`LightningConfigBuilder` to define configurations for `pl.LightningModule` +and `pl.Trainer`. -It then instantiates the model and trainer objects and runs a pre-defined +It then instantiates the model and trainer objects and runs a pre-defined training function in a black box. -This version of the LightningTrainer API was constraining and limited +This version of the LightningTrainer API was constraining and limited your ability to manage the training functionality. -Ray 2.7 introduced the newly unified :class:`~ray.train.torch.TorchTrainer` API, which offers +Ray 2.7 introduced the newly unified :class:`~ray.train.torch.TorchTrainer` API, which offers enhanced transparency, flexibility, and simplicity. This API is more aligned -with standard PyTorch Lightning scripts, ensuring users have better +with standard PyTorch Lightning scripts, ensuring users have better control over their native Lightning code. -.. tabs:: +.. tab-set:: - .. group-tab:: (Deprecating) LightningTrainer + .. tab-item:: (Deprecating) LightningTrainer .. This snippet isn't tested because it raises a hard deprecation warning. @@ -460,24 +460,24 @@ control over their native Lightning code. ) ray_trainer.fit() - - .. group-tab:: (New API) TorchTrainer + + .. tab-item:: (New API) TorchTrainer .. This snippet isn't tested because it runs with 4 GPUs, and CI is only run with 1. .. testcode:: :skipif: True - + import lightning.pytorch as pl from ray.air import CheckpointConfig, RunConfig from ray.train.torch import TorchTrainer from ray.train.lightning import ( - RayDDPStrategy, + RayDDPStrategy, RayLightningEnvironment, RayTrainReportCallback, prepare_trainer - ) + ) def train_func(config): # [1] Create a Lightning model @@ -485,7 +485,7 @@ control over their native Lightning code. # [2] Report Checkpoint with callback ckpt_report_callback = RayTrainReportCallback() - + # [3] Create a Lighting Trainer datamodule = MNISTDataModule(batch_size=32) diff --git a/doc/source/train/getting-started-pytorch.rst b/doc/source/train/getting-started-pytorch.rst index 5cb6b70a2b3e..75f0be4cefab 100644 --- a/doc/source/train/getting-started-pytorch.rst +++ b/doc/source/train/getting-started-pytorch.rst @@ -26,7 +26,7 @@ For reference, the final code is as follows: def train_func(config): # Your PyTorch training code here. - + scaling_config = ScalingConfig(num_workers=2, use_gpu=True) trainer = TorchTrainer(train_func, scaling_config=scaling_config) result = trainer.fit() @@ -37,9 +37,9 @@ For reference, the final code is as follows: Compare a PyTorch training script with and without Ray Train. -.. tabs:: +.. tab-set:: - .. group-tab:: PyTorch + .. tab-item:: PyTorch .. This snippet isn't tested because it doesn't use any Ray code. @@ -74,14 +74,14 @@ Compare a PyTorch training script with and without Ray Train. optimizer.zero_grad() loss.backward() optimizer.step() - - checkpoint_dir = tempfile.gettempdir() + + checkpoint_dir = tempfile.gettempdir() checkpoint_path = checkpoint_dir + "/model.checkpoint" torch.save(model.state_dict(), checkpoint_path) - - .. group-tab:: PyTorch + Ray Train + + .. tab-item:: PyTorch + Ray Train .. code-block:: python :emphasize-lines: 9, 10, 12, 17, 18, 26, 27, 41, 42, 44-49 @@ -122,13 +122,13 @@ Compare a PyTorch training script with and without Ray Train. optimizer.zero_grad() loss.backward() optimizer.step() - - checkpoint_dir = tempfile.gettempdir() + + checkpoint_dir = tempfile.gettempdir() checkpoint_path = checkpoint_dir + "/model.checkpoint" torch.save(model.state_dict(), checkpoint_path) # [3] Report metrics and checkpoint. ray.train.report({"loss": loss.item()}, checkpoint=Checkpoint.from_directory(checkpoint_dir)) - + # [4] Configure scaling and resource requirements. scaling_config = ScalingConfig(num_workers=2, use_gpu=True) @@ -139,7 +139,7 @@ Compare a PyTorch training script with and without Ray Train. Set up a training function -------------------------- -First, update your training code to support distributed training. +First, update your training code to support distributed training. Begin by wrapping your code in a :ref:`training function `: .. testcode:: @@ -163,7 +163,7 @@ Use the :func:`ray.train.torch.prepare_model` utility function to: -from torch.nn.parallel import DistributedDataParallel +import ray.train.torch - def train_func(config): + def train_func(config): ... @@ -175,7 +175,7 @@ Use the :func:`ray.train.torch.prepare_model` utility function to: - model = model.to(device_id or "cpu") - model = DistributedDataParallel(model, device_ids=[device_id]) + model = ray.train.torch.prepare_model(model) - + ... Set up a dataset @@ -183,10 +183,10 @@ Set up a dataset .. TODO: Update this to use Ray Data. -Use the :func:`ray.train.torch.prepare_data_loader` utility function, which: +Use the :func:`ray.train.torch.prepare_data_loader` utility function, which: 1. Adds a ``DistributedSampler`` to your ``DataLoader``. -2. Moves the batches to the right device. +2. Moves the batches to the right device. Note that this step isn't necessary if you're passing in Ray Data to your Trainer. See :ref:`data-ingest-torch`. @@ -202,9 +202,9 @@ See :ref:`data-ingest-torch`. ... dataset = ... - + data_loader = DataLoader(dataset, batch_size=worker_batch_size) - - data_loader = DataLoader(dataset, batch_size=worker_batch_size, sampler=DistributedSampler(dataset)) + - data_loader = DataLoader(dataset, batch_size=worker_batch_size, sampler=DistributedSampler(dataset)) + data_loader = ray.train.torch.prepare_data_loader(data_loader) for X, y in data_loader: @@ -265,7 +265,7 @@ For more details, see :ref:`train_scaling_config`. Launch a training job --------------------- -Tying this all together, you can now launch a distributed training job +Tying this all together, you can now launch a distributed training job with a :class:`~ray.train.torch.TorchTrainer`. .. testcode:: @@ -305,4 +305,4 @@ After you have converted your PyTorch training script to use Ray Train: * See :ref:`User Guides ` to learn more about how to perform specific tasks. * Browse the :ref:`Examples ` for end-to-end examples of how to use Ray Train. -* Dive into the :ref:`API Reference ` for more details on the classes and methods used in this tutorial. \ No newline at end of file +* Dive into the :ref:`API Reference ` for more details on the classes and methods used in this tutorial. diff --git a/doc/source/train/getting-started-transformers.rst b/doc/source/train/getting-started-transformers.rst index 6fd223fc3a22..8739addee17a 100644 --- a/doc/source/train/getting-started-transformers.rst +++ b/doc/source/train/getting-started-transformers.rst @@ -24,7 +24,7 @@ For reference, the final code follows: def train_func(config): # Your Transformers training code here. - + scaling_config = ScalingConfig(num_workers=2, use_gpu=True) trainer = TorchTrainer(train_func, scaling_config=scaling_config) result = trainer.fit() @@ -35,9 +35,9 @@ For reference, the final code follows: Compare a Hugging Face Transformers training script with and without Ray Train. -.. tabs:: +.. tab-set:: - .. group-tab:: Hugging Face Transformers + .. tab-item:: Hugging Face Transformers .. This snippet isn't tested because it doesn't use any Ray code. @@ -52,7 +52,7 @@ Compare a Hugging Face Transformers training script with and without Ray Train. from transformers import ( Trainer, TrainingArguments, - AutoTokenizer, + AutoTokenizer, AutoModelForSequenceClassification, ) @@ -95,9 +95,9 @@ Compare a Hugging Face Transformers training script with and without Ray Train. # Start Training trainer.train() - - .. group-tab:: Hugging Face Transformers + Ray Train + + .. tab-item:: Hugging Face Transformers + Ray Train .. code-block:: python :emphasize-lines: 11-13, 15-18, 55-72 @@ -108,7 +108,7 @@ Compare a Hugging Face Transformers training script with and without Ray Train. from transformers import ( Trainer, TrainingArguments, - AutoTokenizer, + AutoTokenizer, AutoModelForSequenceClassification, ) @@ -116,7 +116,7 @@ Compare a Hugging Face Transformers training script with and without Ray Train. from ray.train import ScalingConfig from ray.train.torch import TorchTrainer - # [1] Encapsulate data preprocessing, training, and evaluation + # [1] Encapsulate data preprocessing, training, and evaluation # logic in a training function # ============================================================ def train_func(config): @@ -179,7 +179,7 @@ Compare a Hugging Face Transformers training script with and without Ray Train. Set up a training function -------------------------- -First, update your training code to support distributed training. +First, update your training code to support distributed training. You can begin by wrapping your code in a :ref:`training function `: .. testcode:: @@ -188,24 +188,24 @@ You can begin by wrapping your code in a :ref:`training function `. +Reporting metrics and checkpoints to Ray Train ensures that you can use Ray Tune and :ref:`fault-tolerant training `. Note that the :class:`ray.train.huggingface.transformers.RayTrainReportCallback` only provides a simple implementation, and you can :ref:`further customize ` it. @@ -228,8 +228,8 @@ Prepare a Transformers Trainer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Finally, pass your Transformers Trainer into -:meth:`~ray.train.huggingface.transformers.prepare_trainer` to validate -your configurations and enable Ray Data Integration. +:meth:`~ray.train.huggingface.transformers.prepare_trainer` to validate +your configurations and enable Ray Data Integration. .. code-block:: diff @@ -264,7 +264,7 @@ For more details, see :ref:`train_scaling_config`. Launch a training job --------------------- -Tying this all together, you can now launch a distributed training job +Tying this all together, you can now launch a distributed training job with a :class:`~ray.train.torch.TorchTrainer`. .. testcode:: @@ -300,7 +300,7 @@ information about the training run, including the metrics and checkpoints report .. TODO: Add results guide Next steps ----------- +---------- After you have converted your Hugging Face Transformers training script to use Ray Train: @@ -314,18 +314,18 @@ After you have converted your Hugging Face Transformers training script to use R TransformersTrainer Migration Guide ----------------------------------- -Ray 2.1 introduced the `TransformersTrainer`, which exposes a `trainer_init_per_worker` interface +Ray 2.1 introduced the `TransformersTrainer`, which exposes a `trainer_init_per_worker` interface to define `transformers.Trainer`, then runs a pre-defined training function in a black box. -Ray 2.7 introduced the newly unified :class:`~ray.train.torch.TorchTrainer` API, +Ray 2.7 introduced the newly unified :class:`~ray.train.torch.TorchTrainer` API, which offers enhanced transparency, flexibility, and simplicity. This API aligns more -with standard Hugging Face Transformers scripts, ensuring that you have better control over your +with standard Hugging Face Transformers scripts, ensuring that you have better control over your native Transformers training code. -.. tabs:: +.. tab-set:: - .. group-tab:: (Deprecating) TransformersTrainer + .. tab-item:: (Deprecating) TransformersTrainer .. This snippet isn't tested because it contains skeleton code. @@ -379,9 +379,9 @@ native Transformers training code. datasets={"train": ray_train_ds, "evaluation": ray_eval_ds}, ) result = ray_trainer.fit() - - .. group-tab:: (New API) TorchTrainer + + .. tab-item:: (New API) TorchTrainer .. This snippet isn't tested because it contains skeleton code. @@ -433,7 +433,7 @@ native Transformers training code. weight_decay=0.01, max_steps=100, ) - + trainer = transformers.Trainer( model=model, args=args, diff --git a/doc/source/train/huggingface-accelerate.rst b/doc/source/train/huggingface-accelerate.rst index 706ded67f241..4a3bfca9ec6c 100644 --- a/doc/source/train/huggingface-accelerate.rst +++ b/doc/source/train/huggingface-accelerate.rst @@ -35,7 +35,7 @@ You only need to run your existing training code with a TorchTrainer. You can ex # Start training ... - + from ray.train.torch import TorchTrainer from ray.train import ScalingConfig @@ -48,23 +48,23 @@ You only need to run your existing training code with a TorchTrainer. You can ex .. tip:: - Model and data preparation for distributed training is completely handled by the `Accelerator `_ + Model and data preparation for distributed training is completely handled by the `Accelerator `_ object and its `Accelerator.prepare() `_ method. - - Unlike with native PyTorch, PyTorch Lightning, or Hugging Face Transformers, **don't** call any additional Ray Train utilities - like :meth:`~ray.train.torch.prepare_model` or :meth:`~ray.train.torch.prepare_data_loader` in your training function. + + Unlike with native PyTorch, PyTorch Lightning, or Hugging Face Transformers, **don't** call any additional Ray Train utilities + like :meth:`~ray.train.torch.prepare_model` or :meth:`~ray.train.torch.prepare_data_loader` in your training function. Configure Accelerate -------------------- -In Ray Train, you can set configurations through the `accelerate.Accelerator `_ +In Ray Train, you can set configurations through the `accelerate.Accelerator `_ object in your training function. Below are starter examples for configuring Accelerate. -.. tabs:: +.. tab-set:: - .. group-tab:: DeepSpeed + .. tab-item:: DeepSpeed - For example, to run DeepSpeed with Accelerate, create a `DeepSpeedPlugin `_ + For example, to run DeepSpeed with Accelerate, create a `DeepSpeedPlugin `_ from a dictionary: .. testcode:: @@ -99,7 +99,7 @@ object in your training function. Below are starter examples for configuring Acc } def train_func(config): - # Create a DeepSpeedPlugin from config dict + # Create a DeepSpeedPlugin from config dict ds_plugin = DeepSpeedPlugin(hf_ds_config=DEEPSPEED_CONFIG) # Initialize Accelerator @@ -107,7 +107,7 @@ object in your training function. Below are starter examples for configuring Acc ..., deepspeed_plugin=ds_plugin, ) - + # Start training ... @@ -121,9 +121,10 @@ object in your training function. Below are starter examples for configuring Acc ) trainer.fit() - .. group-tab:: FSDP + .. tab-item:: FSDP + :sync: FSDP - For PyTorch FSDP, create a `FullyShardedDataParallelPlugin `_ + For PyTorch FSDP, create a `FullyShardedDataParallelPlugin `_ and pass it to the Accelerator. .. testcode:: @@ -135,11 +136,11 @@ object in your training function. Below are starter examples for configuring Acc def train_func(config): fsdp_plugin = FullyShardedDataParallelPlugin( state_dict_config=FullStateDictConfig( - offload_to_cpu=False, + offload_to_cpu=False, rank0_only=False ), optim_state_dict_config=FullOptimStateDictConfig( - offload_to_cpu=False, + offload_to_cpu=False, rank0_only=False ) ) @@ -163,16 +164,16 @@ object in your training function. Below are starter examples for configuring Acc ) trainer.fit() -Note that Accelerate also provides a CLI tool, `"accelerate config"`, to generate a configuration and launch your training -job with `"accelerate launch"`. However, it's not necessary here because Ray's `TorchTrainer` already sets up the Torch +Note that Accelerate also provides a CLI tool, `"accelerate config"`, to generate a configuration and launch your training +job with `"accelerate launch"`. However, it's not necessary here because Ray's `TorchTrainer` already sets up the Torch distributed environment and launches the training function on all workers. Next, see these end-to-end examples below for more details: -.. tabs:: +.. tab-set:: - .. group-tab:: Example with Ray Data + .. tab-item:: Example with Ray Data .. dropdown:: Show Code @@ -181,7 +182,7 @@ Next, see these end-to-end examples below for more details: :start-after: __accelerate_torch_basic_example_start__ :end-before: __accelerate_torch_basic_example_end__ - .. group-tab:: Example with PyTorch DataLoader + .. tab-item:: Example with PyTorch DataLoader .. dropdown:: Show Code @@ -192,8 +193,8 @@ Next, see these end-to-end examples below for more details: .. seealso:: - If you're looking for more advanced use cases, check out this Llama-2 fine-tuning example: - + If you're looking for more advanced use cases, check out this Llama-2 fine-tuning example: + - `Fine-tuning Llama-2 series models with Deepspeed, Accelerate, and Ray Train. `_ You may also find these user guides helpful: @@ -204,16 +205,14 @@ You may also find these user guides helpful: - :ref:`How to use Ray Data with Ray Train ` -AccelerateTrainer Migration Guide +AccelerateTrainer Migration Guide --------------------------------- -Before Ray 2.7, Ray Train's `AccelerateTrainer` API was the -recommended way to run Accelerate code. As a subclass of :class:`TorchTrainer `, -the AccelerateTrainer takes in a configuration file generated by ``accelerate config`` and applies it to all workers. +Before Ray 2.7, Ray Train's `AccelerateTrainer` API was the +recommended way to run Accelerate code. As a subclass of :class:`TorchTrainer `, +the AccelerateTrainer takes in a configuration file generated by ``accelerate config`` and applies it to all workers. Aside from that, the functionality of ``AccelerateTrainer`` is identical to ``TorchTrainer``. -However, this caused confusion around whether this was the *only* way to run Accelerate code. -Because you can express the full Accelerate functionality with the ``Accelerator`` and ``TorchTrainer`` combination, the plan is to deprecate the ``AccelerateTrainer`` in Ray 2.8, -and it's recommend to run your Accelerate code directly with ``TorchTrainer``. - - +However, this caused confusion around whether this was the *only* way to run Accelerate code. +Because you can express the full Accelerate functionality with the ``Accelerator`` and ``TorchTrainer`` combination, the plan is to deprecate the ``AccelerateTrainer`` in Ray 2.8, +and it's recommend to run your Accelerate code directly with ``TorchTrainer``. diff --git a/doc/source/train/images/logo.png b/doc/source/train/images/logo.png deleted file mode 100644 index 9bbedc3d99da23ea36cf2c24d2115b380b026d03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 148696 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

    APLzx{-0oB4-o&nT_p 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 lk5kFSVo(8ArLOxO1r^{ne;zR? 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

    -RZDI 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 diff --git a/doc/source/train/images/logo.svg b/doc/source/train/images/logo.svg new file mode 100644 index 000000000000..84f12c38272b --- /dev/null +++ b/doc/source/train/images/logo.svg @@ -0,0 +1,20 @@ + diff --git a/doc/source/train/more-frameworks.rst b/doc/source/train/more-frameworks.rst index dce706c1d536..e297cb4ee8d6 100644 --- a/doc/source/train/more-frameworks.rst +++ b/doc/source/train/more-frameworks.rst @@ -3,46 +3,56 @@ More Frameworks =============== +.. toctree:: + :hidden: + + Hugging Face Accelerate Guide + DeepSpeed Guide + TensorFlow and Keras Guide + XGBoost and LightGBM Guide + Horovod Guide + .. grid:: 1 2 3 4 :gutter: 1 :class-container: container pb-3 .. grid-item-card:: :img-top: /images/accelerate_logo.png - :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img + :class-img-top: mt-2 w-75 d-block mx-auto fixed-height-img + :link: huggingface-accelerate + :link-type: doc - .. button-ref:: train-hf-accelerate + Hugging Face Accelerate - Hugging Face Accelerate - .. grid-item-card:: :img-top: /images/deepspeed_logo.svg - :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - - .. button-ref:: train-deepspeed + :class-img-top: mt-2 w-75 d-block mx-auto fixed-height-img + :link: deepspeed + :link-type: doc - DeepSpeed + DeepSpeed .. grid-item-card:: :img-top: /images/tf_logo.png - :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img + :class-img-top: mt-2 w-75 d-block mx-auto fixed-height-img + :link: distributed-tensorflow-keras + :link-type: doc - .. button-ref:: distributed-tensorflow-keras - - TensorFlow and Keras + TensorFlow and Keras .. grid-item-card:: :img-top: /images/xgboost_logo.png - :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img - - .. button-ref:: distributed-xgboost-lightgbm + :class-img-top: mt-2 w-75 d-block mx-auto fixed-height-img + :link: distributed-xgboost-lightgbm + :link-type: doc - XGBoost and LightGBM + XGBoost and LightGBM .. grid-item-card:: :img-top: /images/horovod.png - :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img + :class-img-top: mt-2 w-75 d-block mx-auto fixed-height-img + :link: horovod + :link-type: doc - .. button-ref:: horovod - Horovod + Horovod diff --git a/doc/source/train/overview.rst b/doc/source/train/overview.rst index e0536b4928c6..3763f0c3f0c1 100644 --- a/doc/source/train/overview.rst +++ b/doc/source/train/overview.rst @@ -5,7 +5,7 @@ Ray Train Overview ================== - + To use Ray Train effectively, you need to understand four main concepts: #. :ref:`Training function `: A Python function that contains your model training logic. @@ -33,9 +33,9 @@ Ray Train documentation uses the following conventions: def train_func(): """User-defined training function that runs on each distributed worker process. - - This function typically contains logic for loading the model, - loading the dataset, training the model, saving checkpoints, + + This function typically contains logic for loading the model, + loading the dataset, training the model, saving checkpoints, and logging metrics. """ ... @@ -45,7 +45,7 @@ Ray Train documentation uses the following conventions: Worker ------ -Ray Train distributes model training compute to individual worker processes across the cluster. +Ray Train distributes model training compute to individual worker processes across the cluster. Each worker is a process that executes the `train_func`. The number of workers determines the parallelism of the training job and is configured in the :class:`~ray.train.ScalingConfig`. @@ -79,7 +79,7 @@ Trainer ------- The Trainer ties the previous three concepts together to launch distributed training jobs. -Ray Train provides :ref:`Trainer classes ` for different frameworks. +Ray Train provides :ref:`Trainer classes ` for different frameworks. Calling the :meth:`fit() ` method executes the training job by: #. Launching workers as defined by the :ref:`scaling_config `. @@ -97,6 +97,6 @@ Calling the :meth:`fit() ` method executes th .. testcode:: from ray.train.torch import TorchTrainer - + trainer = TorchTrainer(train_func, scaling_config=scaling_config) - trainer.fit() \ No newline at end of file + trainer.fit() diff --git a/doc/source/train/train.rst b/doc/source/train/train.rst index 2ceb336d7735..941cd71c4880 100644 --- a/doc/source/train/train.rst +++ b/doc/source/train/train.rst @@ -3,13 +3,27 @@ Ray Train: Scalable Model Training ================================== -| +.. toctree:: + :hidden: -.. figure:: images/logo.png - :align: center - :width: 50% + Overview + PyTorch Guide + PyTorch Lightning Guide + Hugging Face Transformers Guide + more-frameworks + User Guides + Examples + Benchmarks + api/api + + +.. div:: sd-d-flex-row sd-align-major-center sd-align-minor-center + + .. div:: sd-w-50 + + .. raw:: html + :file: images/logo.svg -| Ray Train is a scalable machine learning library for distributed training and fine-tuning. diff --git a/doc/source/train/user-guides.rst b/doc/source/train/user-guides.rst index 233ff8d3f7ba..64c2e6c6f654 100644 --- a/doc/source/train/user-guides.rst +++ b/doc/source/train/user-guides.rst @@ -15,4 +15,4 @@ Ray Train User Guides user-guides/results user-guides/fault-tolerance user-guides/reproducibility - Hyperparameter Optimization \ No newline at end of file + Hyperparameter Optimization diff --git a/doc/source/train/user-guides/data-loading-preprocessing.rst b/doc/source/train/user-guides/data-loading-preprocessing.rst index 79dc297c6d1d..02adfcd6d502 100644 --- a/doc/source/train/user-guides/data-loading-preprocessing.rst +++ b/doc/source/train/user-guides/data-loading-preprocessing.rst @@ -3,7 +3,7 @@ Data Loading and Preprocessing ============================== -Ray Train integrates with :ref:`Ray Data ` to offer an efficient, streaming solution for loading and preprocessing large datasets. +Ray Train integrates with :ref:`Ray Data ` to offer an efficient, streaming solution for loading and preprocessing large datasets. We recommend using Ray Data for its ability to performantly support large-scale distributed training workloads - for advantages and comparisons to alternatives, see :ref:`Ray Data Overview `. In this guide, we will cover how to incorporate Ray Data into your Ray Train script, and different ways to customize your data ingestion pipeline. @@ -29,9 +29,9 @@ Data ingestion can be set up with four basic steps: 3. Input the preprocessed Dataset into the Ray Train Trainer. 4. Consume the Ray Dataset in your training function. -.. tabs:: +.. tab-set:: - .. group-tab:: PyTorch + .. tab-item:: PyTorch .. testcode:: @@ -92,13 +92,13 @@ Data ingestion can be set up with four basic steps: ... - .. group-tab:: PyTorch Lightning + .. tab-item:: PyTorch Lightning .. code-block:: python :emphasize-lines: 9,10,13,14,25,26 from ray import train - + train_data = ray.data.read_csv("./train.csv") val_data = ray.data.read_csv("./validation.csv") @@ -120,8 +120,8 @@ Data ingestion can be set up with four basic steps: # Feed the Ray dataset iterables to ``pl.Trainer.fit``. trainer.fit( - model, - train_dataloaders=train_dataloader, + model, + train_dataloaders=train_dataloader, val_dataloaders=val_dataloader ) @@ -131,15 +131,15 @@ Data ingestion can be set up with four basic steps: scaling_config=ScalingConfig(num_workers=4), ) trainer.fit() - - .. group-tab:: HuggingFace Transformers + + .. tab-item:: HuggingFace Transformers .. code-block:: python :emphasize-lines: 12,13,16,17,24,25 import ray import ray.train - + ... train_data = ray.data.from_huggingface(hf_train_ds) @@ -207,14 +207,14 @@ Inputting and splitting data Your preprocessed datasets can be passed into a Ray Train Trainer (e.g. :class:`~ray.train.torch.TorchTrainer`) through the ``datasets`` argument. -The datasets passed into the Trainer's ``datasets`` can be accessed inside of the ``train_loop_per_worker`` run on each distributed training worker by calling :meth:`ray.train.get_dataset_shard`. +The datasets passed into the Trainer's ``datasets`` can be accessed inside of the ``train_loop_per_worker`` run on each distributed training worker by calling :meth:`ray.train.get_dataset_shard`. All datasets are split (i.e. sharded) across the training workers by default. :meth:`~ray.train.get_dataset_shard` will return ``1/n`` of the dataset, where ``n`` is the number of training workers. .. note:: - Please be aware that as the evaluation dataset is split, users have to aggregate the evaluation results across workers. - You might consider using `TorchMetrics `_ (:ref:`example `) or + Please be aware that as the evaluation dataset is split, users have to aggregate the evaluation results across workers. + You might consider using `TorchMetrics `_ (:ref:`example `) or utilities available in other frameworks that you can explore. This behavior can be overwritten by passing in the ``dataset_config`` argument. For more information on configuring splitting logic, see :ref:`Splitting datasets `. @@ -265,45 +265,45 @@ At a high level, you can compare these concepts as follows: For more details, see the following sections for each framework. -.. tabs:: +.. tab-set:: - .. tab:: PyTorch Dataset and DataLoader + .. tab-item:: PyTorch Dataset and DataLoader **Option 1 (with Ray Data):** Convert your PyTorch Dataset to a Ray Dataset and pass it into the Trainer via ``datasets`` argument. - Inside your ``train_loop_per_worker``, you can access the dataset via :meth:`ray.train.get_dataset_shard`. + Inside your ``train_loop_per_worker``, you can access the dataset via :meth:`ray.train.get_dataset_shard`. You can convert this to replace the PyTorch DataLoader via :meth:`ray.data.DataIterator.iter_torch_batches`. - + For more details, see the :ref:`Migrating from PyTorch Datasets and DataLoaders `. **Option 2 (without Ray Data):** Instantiate the Torch Dataset and DataLoader directly in the ``train_loop_per_worker``. You can use the :meth:`ray.train.torch.prepare_data_loader` utility to set up the DataLoader for distributed training. - - .. tab:: LightningDataModule + + .. tab-item:: LightningDataModule The ``LightningDataModule`` is created with PyTorch ``Dataset``\s and ``DataLoader``\s. You can apply the same logic here. - .. tab:: Hugging Face Dataset + .. tab-item:: Hugging Face Dataset **Option 1 (with Ray Data):** Convert your Hugging Face Dataset to a Ray Dataset and pass it into the Trainer via the ``datasets`` argument. - Inside your ``train_loop_per_worker``, you can access the dataset via :meth:`ray.train.get_dataset_shard`. + Inside your ``train_loop_per_worker``, you can access the dataset via :meth:`ray.train.get_dataset_shard`. For instructions, see :ref:`Ray Data for Hugging Face `. **Option 2 (without Ray Data):** Instantiate the Hugging Face Dataset directly in the ``train_loop_per_worker``. - .. tip:: +.. tip:: - When using Torch or Hugging Face Datasets directly without Ray Data, make sure to instantiate your Dataset *inside* the ``train_loop_per_worker``. - Instatiating the Dataset outside of the ``train_loop_per_worker`` and passing it in via global scope - can cause errors due to the Dataset not being serializable. + When using Torch or Hugging Face Datasets directly without Ray Data, make sure to instantiate your Dataset *inside* the ``train_loop_per_worker``. + Instatiating the Dataset outside of the ``train_loop_per_worker`` and passing it in via global scope + can cause errors due to the Dataset not being serializable. .. _train-datasets-split: Splitting datasets ------------------ -By default, Ray Train splits all datasets across workers using :meth:`Dataset.streaming_split `. Each worker sees a disjoint subset of the data, instead of iterating over the entire dataset. Unless randomly shuffled, the same splits are used for each iteration of the dataset. +By default, Ray Train splits all datasets across workers using :meth:`Dataset.streaming_split `. Each worker sees a disjoint subset of the data, instead of iterating over the entire dataset. Unless randomly shuffled, the same splits are used for each iteration of the dataset. -If want to customize which datasets are split, pass in a :class:`DataConfig ` to the Trainer constructor. +If want to customize which datasets are split, pass in a :class:`DataConfig ` to the Trainer constructor. For example, to split only the training dataset, do the following: @@ -325,7 +325,7 @@ For example, to split only the training dataset, do the following: for _ in range(2): for batch in train_ds.iter_batches(batch_size=128): print("Do some training on batch", batch) - + # Get the unsharded full validation dataset val_ds = train.get_dataset_shard("val") for _ in range(2): @@ -419,7 +419,7 @@ Ray Data has two approaches to random shuffling: 1. Shuffling data blocks and local shuffling on each training worker. This requires less communication at the cost of less randomness (i.e. rows that appear in the same data block are more likely to appear near each other in the iteration order). 2. Full global shuffle, which is more expensive. This will fully decorrelate row iteration order from the original dataset order, at the cost of significantly more computation, I/O, and communication. -For most cases, option 1 suffices. +For most cases, option 1 suffices. First, randomize each :ref:`block ` of your dataset via :meth:`randomize_block_order `. Then, when iterating over your dataset during training, enable local shuffling by specifying a ``local_shuffle_buffer_size`` to :meth:`iter_batches ` or :meth:`iter_torch_batches `. @@ -489,10 +489,10 @@ When developing or hyperparameter tuning models, reproducibility is important du "s3://anonymous@ray-example-data/sms_spam_collection_subset.txt" ) -**Step 2:** Set a seed for any shuffling operations: +**Step 2:** Set a seed for any shuffling operations: * `seed` argument to :meth:`random_shuffle ` -* `seed` argument to :meth:`randomize_block_order ` +* `seed` argument to :meth:`randomize_block_order ` * `local_shuffle_seed` argument to :meth:`iter_batches ` **Step 3:** Follow the best practices for enabling reproducibility for your training framework of choice. For example, see the `Pytorch reproducibility guide `_. diff --git a/doc/source/train/user-guides/experiment-tracking.rst b/doc/source/train/user-guides/experiment-tracking.rst index 7858c64bb536..00ab612669ea 100644 --- a/doc/source/train/user-guides/experiment-tracking.rst +++ b/doc/source/train/user-guides/experiment-tracking.rst @@ -5,17 +5,17 @@ Experiment Tracking =================== .. note:: - This guide is relevant for all trainers in which you define a custom training loop. - This includes :class:`TorchTrainer ` and + This guide is relevant for all trainers in which you define a custom training loop. + This includes :class:`TorchTrainer ` and :class:`TensorflowTrainer `. -Most experiment tracking libraries work out-of-the-box with Ray Train. -This guide provides instructions on how to set up the code so that your favorite experiment tracking libraries -can work for distributed training with Ray Train. The end of the guide has common errors to aid in debugging +Most experiment tracking libraries work out-of-the-box with Ray Train. +This guide provides instructions on how to set up the code so that your favorite experiment tracking libraries +can work for distributed training with Ray Train. The end of the guide has common errors to aid in debugging the setup. -The following pseudo code demonstrates how to use the native experiment tracking library calls -inside of Ray Train: +The following pseudo code demonstrates how to use the native experiment tracking library calls +inside of Ray Train: .. testcode:: :skipif: True @@ -30,9 +30,9 @@ inside of Ray Train: trainer = TorchTrainer(train_func, scaling_config=scaling_config) result = trainer.fit() -Ray Train lets you use native experiment tracking libraries by customizing the tracking -logic inside the :ref:`train_func` function. -In this way, you can port your experiment tracking logic to Ray Train with minimal changes. +Ray Train lets you use native experiment tracking libraries by customizing the tracking +logic inside the :ref:`train_func` function. +In this way, you can port your experiment tracking logic to Ray Train with minimal changes. Getting Started =============== @@ -41,13 +41,13 @@ Let's start by looking at some code snippets. The following examples uses Weights & Biases (W&B) and MLflow but it's adaptable to other frameworks. -.. tabs:: +.. tab-set:: - .. tab:: W&B + .. tab-item:: W&B .. testcode:: :skipif: True - + import ray from ray import train import wandb @@ -80,11 +80,11 @@ The following examples uses Weights & Biases (W&B) and MLflow but it's adaptable if train.get_context().get_world_rank() == 0: wandb.finish() - .. tab:: MLflow + .. tab-item:: MLflow .. testcode:: :skipif: True - + from ray import train import mlflow @@ -104,7 +104,7 @@ The following examples uses Weights & Biases (W&B) and MLflow but it's adaptable loss = optimize() metrics = {"loss": loss} - # Only report the results from the first worker to MLflow + # Only report the results from the first worker to MLflow to avoid duplication # Step 3 @@ -113,9 +113,9 @@ The following examples uses Weights & Biases (W&B) and MLflow but it's adaptable .. tip:: - A major difference between distributed and non-distributed training is that in distributed training, - multiple processes are running in parallel and under certain setups they have the same results. If all - of them report results to the tracking backend, you may get duplicated results. To address that, + A major difference between distributed and non-distributed training is that in distributed training, + multiple processes are running in parallel and under certain setups they have the same results. If all + of them report results to the tracking backend, you may get duplicated results. To address that, Ray Train lets you apply logging logic to only the rank 0 worker with the following method: :meth:`ray.train.get_context().get_world_rank() `. @@ -129,7 +129,7 @@ The following examples uses Weights & Biases (W&B) and MLflow but it's adaptable # Add your logging logic only for rank0 worker. ... -The interaction with the experiment tracking backend within the :ref:`train_func` +The interaction with the experiment tracking backend within the :ref:`train_func` has 4 logical steps: #. Set up the connection to a tracking backend @@ -145,60 +145,60 @@ Step 1: Connect to your tracking backend First, decide which tracking backend to use: W&B, MLflow, TensorBoard, Comet, etc. If applicable, make sure that you properly set up credentials on each training worker. -.. tabs:: +.. tab-set:: - .. tab:: W&B - - W&B offers both *online* and *offline* modes. + .. tab-item:: W&B + + W&B offers both *online* and *offline* modes. **Online** - For *online* mode, because you log to W&B's tracking service, ensure that you set the credentials - inside of :ref:`train_func`. See :ref:`Set up credentials` + For *online* mode, because you log to W&B's tracking service, ensure that you set the credentials + inside of :ref:`train_func`. See :ref:`Set up credentials` for more information. .. testcode:: :skipif: True - + # This is equivalent to `os.environ["WANDB_API_KEY"] = "your_api_key"` wandb.login(key="your_api_key") **Offline** - For *offline* mode, because you log towards a local file system, - point the offline directory to a shared storage path that all nodes can write to. + For *offline* mode, because you log towards a local file system, + point the offline directory to a shared storage path that all nodes can write to. See :ref:`Set up a shared file system` for more information. - + .. testcode:: :skipif: True os.environ["WANDB_MODE"] = "offline" - wandb.init(dir="some_shared_storage_path/wandb") + wandb.init(dir="some_shared_storage_path/wandb") + + .. tab-item:: MLflow - .. tab:: MLflow - - MLflow offers both *local* and *remote* (for example, to Databrick's MLflow service) modes. + MLflow offers both *local* and *remote* (for example, to Databrick's MLflow service) modes. **Local** - For *local* mode, because you log to a local file - system, point offline directory to a shared storage path. that all nodes can write - to. See :ref:`Set up a shared file system` for more information. - + For *local* mode, because you log to a local file + system, point offline directory to a shared storage path. that all nodes can write + to. See :ref:`Set up a shared file system` for more information. + .. testcode:: :skipif: True mlflow.start_run(tracking_uri="file:some_shared_storage_path/mlruns") **Remote, hosted by Databricks** - - Ensure that all nodes have access to the Databricks config file. + + Ensure that all nodes have access to the Databricks config file. See :ref:`Set up credentials` for more information. - + .. testcode:: :skipif: True - # The MLflow client looks for a Databricks config file + # The MLflow client looks for a Databricks config file # at the location specified by `os.environ["DATABRICKS_CONFIG_FILE"]`. os.environ["DATABRICKS_CONFIG_FILE"] = config["databricks_config_file"] mlflow.set_tracking_uri("databricks") @@ -212,7 +212,7 @@ Set up credentials Refer to each tracking library's API documentation on setting up credentials. This step usually involves setting an environment variable or accessing a config file. -The easiest way to pass an environment variable credential to training workers is through +The easiest way to pass an environment variable credential to training workers is through :ref:`runtime environments `, where you initialize with the following code: .. testcode:: @@ -230,19 +230,19 @@ One way to do this is by setting up a shared storage. Another way is to save a c Set up a shared file system ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Set up a network filesystem accessible to all nodes in the cluster. +Set up a network filesystem accessible to all nodes in the cluster. For example, AWS EFS or Google Cloud Filestore. -Step 2: Configure and start the run +Step 2: Configure and start the run ----------------------------------- This step usually involves picking an identifier for the run and associating it with a project. -Refer to the tracking libraries' documentation for semantics. +Refer to the tracking libraries' documentation for semantics. -.. To conveniently link back to Ray Train run, you may want to log the persistent storage path +.. To conveniently link back to Ray Train run, you may want to log the persistent storage path .. of the run as a config. -.. +.. .. testcode:: def train_func(config): @@ -250,10 +250,10 @@ Refer to the tracking libraries' documentation for semantics. wandb.init(..., config={"ray_train_persistent_storage_path": "TODO: fill in when API stablizes"}) .. tip:: - - When performing **fault-tolerant training** with auto-restoration, use a + + When performing **fault-tolerant training** with auto-restoration, use a consistent ID to configure all tracking runs that logically belong to the same training run. - One way to acquire an unique ID is with the following method: + One way to acquire an unique ID is with the following method: :meth:`ray.train.get_context().get_trial_id() `. .. testcode:: @@ -269,41 +269,41 @@ Refer to the tracking libraries' documentation for semantics. ... trainer = TorchTrainer( - train_func, + train_func, run_config=RunConfig(failure_config=FailureConfig(max_failures=3)) ) trainer.fit() - + Step 3: Log metrics ------------------- -You can customize how to log parameters, metrics, models, or media contents, within -:ref:`train_func`, just as in a non-distributed training script. -You can also use native integrations that a particular tracking framework has with -specific training frameworks. For example, ``mlflow.pytorch.autolog()``, -``lightning.pytorch.loggers.MLFlowLogger``, etc. +You can customize how to log parameters, metrics, models, or media contents, within +:ref:`train_func`, just as in a non-distributed training script. +You can also use native integrations that a particular tracking framework has with +specific training frameworks. For example, ``mlflow.pytorch.autolog()``, +``lightning.pytorch.loggers.MLFlowLogger``, etc. Step 4: Finish the run ---------------------- -This step ensures that all logs are synced to the tracking service. Depending on the implementation of -various tracking libraries, sometimes logs are first cached locally and only synced to the tracking -service in an asynchronous fashion. -Finishing the run makes sure that all logs are synced by the time training workers exit. +This step ensures that all logs are synced to the tracking service. Depending on the implementation of +various tracking libraries, sometimes logs are first cached locally and only synced to the tracking +service in an asynchronous fashion. +Finishing the run makes sure that all logs are synced by the time training workers exit. + +.. tab-set:: -.. tabs:: + .. tab-item:: W&B - .. tab:: W&B - .. testcode:: :skipif: True # https://docs.wandb.ai/ref/python/finish wandb.finish() - .. tab:: MLflow + .. tab-item:: MLflow .. testcode:: :skipif: True @@ -311,13 +311,13 @@ Finishing the run makes sure that all logs are synced by the time training worke # https://mlflow.org/docs/1.2.0/python_api/mlflow.html mlflow.end_run() - .. tab:: Comet + .. tab-item:: Comet .. testcode:: :skipif: True # https://www.comet.com/docs/v2/api-and-sdk/python-sdk/reference/Experiment/#experimentend - Experiment.end() + Experiment.end() Examples ======== @@ -345,10 +345,10 @@ PyTorch PyTorch Lightning ----------------- -You can use the native Logger integration in PyTorch Lightning with W&B, CometML, MLFlow, +You can use the native Logger integration in PyTorch Lightning with W&B, CometML, MLFlow, and Tensorboard, while using Ray Train's TorchTrainer. -The following example walks you through the process. The code here is runnable. +The following example walks you through the process. The code here is runnable. .. dropdown:: W&B @@ -382,7 +382,7 @@ The following example walks you through the process. The code here is runnable. :start-after: __lightning_experiment_tracking_comet_start__ .. dropdown:: TensorBoard - + .. literalinclude:: ../../../../python/ray/train/examples/experiment_tracking/lightning_exp_tracking_model_dl.py :language: python :start-after: __model_dl_start__ @@ -398,15 +398,15 @@ Common Errors Missing Credentials ------------------- -**I have already called `wandb login` cli, but am still getting** +**I have already called `wandb login` cli, but am still getting** .. code-block:: none wandb: ERROR api_key not configured (no-tty). call wandb.login(key=[your_api_key]). This is probably due to wandb credentials are not set up correctly -on worker nodes. Make sure that you run ``wandb.login`` -or pass ``WANDB_API_KEY`` to each training function. +on worker nodes. Make sure that you run ``wandb.login`` +or pass ``WANDB_API_KEY`` to each training function. See :ref:`Set up credentials ` for more details. Missing Configurations @@ -418,7 +418,7 @@ Missing Configurations databricks_cli.utils.InvalidConfigurationError: You haven't configured the CLI yet! -This is usually caused by running ``databricks configure`` which +This is usually caused by running ``databricks configure`` which generates ``~/.databrickscfg`` only on head node. Move this file to a shared location or copy it to each node. See :ref:`Set up credentials ` for more details. diff --git a/doc/source/tune/api/api.rst b/doc/source/tune/api/api.rst index 6d4618425a70..c0b66114580b 100644 --- a/doc/source/tune/api/api.rst +++ b/doc/source/tune/api/api.rst @@ -28,4 +28,4 @@ on `Github`_. integration.rst internals.rst client.rst - cli.rst \ No newline at end of file + cli.rst diff --git a/doc/source/tune/examples/batch_tuning.ipynb b/doc/source/tune/examples/batch_tuning.ipynb index 217b9fc87786..546c0df3fe54 100644 --- a/doc/source/tune/examples/batch_tuning.ipynb +++ b/doc/source/tune/examples/batch_tuning.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "id": "02d29398", "metadata": {}, @@ -12,7 +11,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "2780b3da", "metadata": {}, @@ -29,7 +27,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "c261b2bd", "metadata": { @@ -46,7 +43,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "604e8c44", "metadata": {}, @@ -62,8 +58,12 @@ { "cell_type": "code", "execution_count": null, + "id": "6160f20e", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -71,11 +71,9 @@ ] }, { - "attachments": {}, "cell_type": "markdown", - "metadata": { - "collapsed": false - }, + "id": "2147f97d", + "metadata": {}, "source": [ "Next, let's import a few required libraries, including open-source Ray itself!" ] @@ -214,18 +212,17 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "43545104", "metadata": { "tags": [] }, "source": [ - "## Define how to load and prepare Parquet data " + "(prepare_data)=\n", + "## Define how to load and prepare Parquet data" ] }, { - "attachments": {}, "cell_type": "markdown", "id": "0c5e5428", "metadata": {}, @@ -341,16 +338,15 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "55b6c727", "metadata": {}, "source": [ - "## Define a Trainable (callable) function " + "(define_trainable)=\n", + "## Define a Trainable (callable) function" ] }, { - "attachments": {}, "cell_type": "markdown", "id": "cd7ab2d0", "metadata": {}, @@ -427,16 +423,15 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "d59fbfab", "metadata": {}, "source": [ - "## Run batch training on Ray Tune " + "(run_tune_search)=\n", + "## Run batch training on Ray Tune" ] }, { - "attachments": {}, "cell_type": "markdown", "id": "4db1c6bd", "metadata": {}, @@ -658,7 +653,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "5ae0b413", "metadata": {}, @@ -919,16 +913,15 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "fbc62da1", "metadata": {}, "source": [ - "## Load a model from checkpoint and perform batch prediction " + "(load_checkpoint)=\n", + "## Load a model from checkpoint and perform batch prediction" ] }, { - "attachments": {}, "cell_type": "markdown", "id": "249bf4d3", "metadata": {}, @@ -1127,7 +1120,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "id": "ad2ef857", "metadata": {}, @@ -1205,7 +1197,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.10.8" }, "orphan": true, "vscode": { diff --git a/doc/source/tune/examples/experiment-tracking.rst b/doc/source/tune/examples/experiment-tracking.rst index 2a14d75b2301..fab518a0c8a7 100644 --- a/doc/source/tune/examples/experiment-tracking.rst +++ b/doc/source/tune/examples/experiment-tracking.rst @@ -1,6 +1,15 @@ Tune Experiment Tracking Examples --------------------------------- +.. toctree:: + :hidden: + + Weights & Biases Example + MLflow Example + Aim Example + Comet Example + + Ray Tune integrates with some popular Experiment tracking and management tools, such as CometML, or Weights & Biases. If you're interested in learning how to use Ray Tune with Tensorboard, you can find more information in our diff --git a/doc/source/tune/examples/hpo-frameworks.rst b/doc/source/tune/examples/hpo-frameworks.rst index b5f61491443b..b65ca87e5212 100644 --- a/doc/source/tune/examples/hpo-frameworks.rst +++ b/doc/source/tune/examples/hpo-frameworks.rst @@ -1,6 +1,20 @@ Tune Hyperparameter Optimization Framework Examples --------------------------------------------------- +.. toctree:: + :hidden: + + Ax Example + Dragonfly Example + HyperOpt Example + Bayesopt Example + FLAML Example + BOHB Example + Nevergrad Example + Optuna Example + SigOpt Example + + Tune integrates with a wide variety of hyperparameter optimization frameworks and their respective search algorithms. Here you can find detailed examples on each of our integrations: diff --git a/doc/source/tune/examples/index.rst b/doc/source/tune/examples/index.rst index d7636d11e2da..e96d87149135 100644 --- a/doc/source/tune/examples/index.rst +++ b/doc/source/tune/examples/index.rst @@ -4,6 +4,16 @@ Ray Tune Examples ================= +.. toctree:: + :hidden: + + ml-frameworks + experiment-tracking + hpo-frameworks + Other Examples + Exercises + + .. tip:: Check out :ref:`the Tune User Guides ` To learn more about Tune's features in depth. .. _tune-recipes: diff --git a/doc/source/tune/examples/ml-frameworks.rst b/doc/source/tune/examples/ml-frameworks.rst index 053f6295738b..a8f16f1e1488 100644 --- a/doc/source/tune/examples/ml-frameworks.rst +++ b/doc/source/tune/examples/ml-frameworks.rst @@ -1,6 +1,21 @@ Examples using Ray Tune with ML Frameworks ------------------------------------------ +.. toctree:: + :hidden: + + Scikit-Learn Example + Keras Example + PyTorch Example + PyTorch Lightning Example + Ray Serve Example + Ray RLlib Example + XGBoost Example + LightGBM Example + Horovod Example + Hugging Face Transformers Example + + Ray Tune integrates with many popular machine learning frameworks. Here you find a few practical examples showing you how to tune your models. At the end of these guides you will often find links to even more examples. @@ -88,7 +103,7 @@ At the end of these guides you will often find links to even more examples. .. button-ref:: tune-huggingface-example A Guide To Tuning Huggingface Transformers With Tune - + .. grid-item-card:: :img-top: /images/tune.png :class-img-top: pt-2 w-75 d-block mx-auto fixed-height-img diff --git a/doc/source/tune/examples/pbt_guide.ipynb b/doc/source/tune/examples/pbt_guide.ipynb index 8f0700a1d572..2629de56e5ee 100644 --- a/doc/source/tune/examples/pbt_guide.ipynb +++ b/doc/source/tune/examples/pbt_guide.ipynb @@ -10,6 +10,12 @@ "\n", "# A Guide to Population Based Training with Tune\n", "\n", + "```{toctree}\n", + ":hidden:\n", + "\n", + "Visualizing and Understanding PBT \n", + "```\n", + "\n", "Tune includes a distributed implementation of [Population Based Training (PBT)](https://www.deepmind.com/blog/population-based-training-of-neural-networks) as\n", "a [scheduler](tune-scheduler-pbt).\n", "\n", @@ -787,7 +793,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.8.9 64-bit", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -801,7 +807,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.9" + "version": "3.10.8" }, "vscode": { "interpreter": { diff --git a/doc/source/tune/index.rst b/doc/source/tune/index.rst index c6ba74ad5468..c5197427a648 100644 --- a/doc/source/tune/index.rst +++ b/doc/source/tune/index.rst @@ -3,6 +3,16 @@ Ray Tune: Hyperparameter Tuning =============================== +.. toctree:: + :hidden: + + Getting Started + Key Concepts + tutorials/overview + examples/index + faq + api/api + .. image:: images/tune_overview.png :scale: 50% :align: center diff --git a/doc/source/tune/tutorials/overview.rst b/doc/source/tune/tutorials/overview.rst index 4d4fefa45918..41704349baf7 100644 --- a/doc/source/tune/tutorials/overview.rst +++ b/doc/source/tune/tutorials/overview.rst @@ -4,6 +4,26 @@ User Guides =========== +.. toctree:: + :hidden: + + Running Basic Experiments + tune-output + Setting Trial Resources + Using Search Spaces + tune-stopping + tune-trial-checkpoints + tune-storage + tune-fault-tolerance + Using Callbacks and Metrics + tune_get_data_in_and_out + ../examples/tune_analyze_results + ../examples/pbt_guide + Deploying Tune in the Cloud + Tune Architecture + Scalability Benchmarks + + .. tip:: We'd love to hear your feedback on using Tune - `get in touch `_! In this section, you can find material on how to use Tune and its various features. diff --git a/doc/source/workflows/index.rst b/doc/source/workflows/index.rst index e76d6344711e..eaa0bdd50bf5 100644 --- a/doc/source/workflows/index.rst +++ b/doc/source/workflows/index.rst @@ -3,6 +3,18 @@ Ray Workflows: Durable Ray Task Graphs ====================================== +.. toctree:: + :hidden: + + key-concepts + basics + management + metadata + events + comparison + advanced + api/api + .. warning:: Ray Workflows is available as **alpha** in Ray 2.0+. Expect rough corners and