chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
"""APIs-tab sidebar (Pattern B): a single shared API navigation, loaded client-side.
Ray's global sidebar server-renders the whole toctree into *every* page. The symbol-
level API nav has ~3k stub pages; rendering those into every page would bloat each one
(~250 KB of sidebar) and OOM the build. Instead:
1. Capture the full ``apis/`` toctree once at ``env-updated``, *before*
``sphinx_remove_toctrees`` (priority 500) prunes the stub pages, and render it to
a single fragment written to ``_static/api-nav.html`` at ``build-finished`` (the
HTML writer isn't ready until then).
2. The per-symbol stubs are kept OUT of the global toctree by ``remove_from_toctrees``
in conf.py (Ray already does this), so the global ``main-sidebar`` stays small.
3. On API pages, swap in a tiny container that loads the shared fragment via
``_static/api-nav-loader.js`` and highlights the current page client-side.
The API reference pages keep their *original* locations (``data/api/``, ``train/api/``,
``rllib/package_ref/``, ...); they are pulled into the APIs tab purely by
``apis/index``'s toctree, with no URL change. "Is this an API page?" is therefore
answered by membership in those known source directories (``API_PATH_PREFIXES``) -- a
stateless check, so it works under parallel writing (no reliance on cross-process
state).
So each API page embeds ~no navigation HTML; the nav is one browser-cached file.
"""
import os
import re
import bs4
from sphinx.environment.adapters.toctree import global_toctree_for_doc
from sphinx.util import logging as sphinx_logging
# Reuse the in-repo copy rather than importing from ``pydata_sphinx_theme.toctree``:
# that symbol isn't part of the theme's public API, so importing it directly makes
# the docs build fragile across theme upgrades. See the docstring on the vendored copy.
from custom_directives import add_collapse_checkboxes
logger = sphinx_logging.getLogger(__name__)
APIS_PREFIX = "apis"
NAV_DOCNAME = APIS_PREFIX + "/index"
NAV_FILENAME = "api-nav.html"
# Source directories whose pages make up the APIs tab. These are aggregated by the
# toctree in apis/index.md; pages under them get the shared API sidebar. Kept in sync
# with that toctree. A page's docname starting with one of these (or being the APIs
# landing itself) marks it as API content. Stateless on purpose -- html-page-context
# can fire in worker processes under `-j`, where main-process state isn't shared.
API_PATH_PREFIXES = (
"apis/", # the APIs landing page (apis/index)
"data/api/",
"train/api/",
"tune/api/",
"serve/api/",
"ray-core/api/",
"rllib/package_ref/",
)
# Captured in the main process at env-updated and consumed in the main process at
# build-finished (same process), so it is safe under parallel read/write.
_state = {}
def _capture_apis_toctree(app, env):
"""env-updated @ priority < 500: resolve the full apis/ toctree while the stub
pages are still present (sphinx_remove_toctrees prunes them at priority 500)."""
try:
node = global_toctree_for_doc(
env, NAV_DOCNAME, app.builder,
collapse=False, maxdepth=6, includehidden=True, titles_only=True,
)
except Exception as exc:
logger.warning("[api_sidebar] could not capture apis toctree: %s", exc)
return
if node is None:
logger.warning("[api_sidebar] apis toctree resolved empty (is %s present?)", NAV_DOCNAME)
return
_state["node"] = node
def _root_relative(html):
"""global_toctree_for_doc resolves links relative to apis/index, which lives in the
apis/ directory (depth 1). The API docs themselves live at their original locations
(data/api/, train/api/, ...), so each link is ``../<path>``. Strip the single
leading ``../`` to make hrefs root-relative; the loader then resolves them against
each page's URL root."""
def repl(m):
href = m.group(1)
if re.match(r"^(https?:|/|#|mailto:)", href):
return m.group(0)
if href.startswith("../"):
href = href[3:]
return 'href="%s"' % href
return re.sub(r'href="([^"]*)"', repl, html)
def _render_and_write(app, exc):
"""build-finished (main process, writer ready): render the captured global toc,
keep ONLY the APIs section, and write it as the shared fragment.
global_toctree_for_doc returns the whole-site nav (all top-level sections), so we
isolate the API subtree here: because the toc was resolved for apis/index, the
"APIs" top-level entry is the one marked ``current`` -- we keep just its child
list (the libraries and their symbols). Leaving the API section is the top nav's
job, so nothing else belongs in this sidebar."""
if exc is not None or _state.get("node") is None:
return
try:
html = app.builder.render_partial(_state["node"])["fragment"]
except Exception as e:
logger.warning("[api_sidebar] could not render apis nav fragment: %s", e)
return
soup = bs4.BeautifulSoup(html, "html.parser")
apis_li = next(
(li for li in soup.select("li.toctree-l1") if "current" in (li.get("class") or [])),
None,
)
libs = apis_li.find("ul") if apis_li else None
if libs is None:
logger.warning("[api_sidebar] could not isolate the APIs section in the toc; "
"fragment not written")
return
libs["class"] = "nav bd-sidenav"
# render_partial emits plain nested <ul>; reuse pydata's helper to add the
# collapsible <details>/<summary> structure (closed by default) so the loader can
# expand the current path and the theme's CSS styles the chevrons natively.
frag_soup = bs4.BeautifulSoup(str(libs), "html.parser")
add_collapse_checkboxes(frag_soup)
html = _root_relative(str(frag_soup))
static_dir = os.path.join(app.outdir, "_static")
os.makedirs(static_dir, exist_ok=True)
with open(os.path.join(static_dir, NAV_FILENAME), "w", encoding="utf-8") as fh:
fh.write(html)
logger.info("[api_sidebar] wrote shared apis nav fragment (%d KB, API subtree only) "
"to _static/%s", len(html) // 1024, NAV_FILENAME)
def _on_html_page_context(app, pagename, templatename, context, doctree):
if pagename.startswith(API_PATH_PREFIXES):
context["sidebars"] = ["api-sidebar.html"]
def setup(app):
# priority < 500 so we capture the toctree BEFORE sphinx_remove_toctrees prunes it.
app.connect("env-updated", _capture_apis_toctree, priority=1)
app.connect("build-finished", _render_and_write)
app.connect("html-page-context", _on_html_page_context, priority=900)
return {"version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True}
+199
View File
@@ -0,0 +1,199 @@
from docutils import nodes
from sphinx.util.docutils import SphinxDirective
from sphinx.transforms import SphinxTransform
from docutils.nodes import Node
# BASE_NUM = 2775 # black circles, white numbers
BASE_NUM = 2459 # white circle, black numbers
class CalloutIncludePostTransform(SphinxTransform):
"""Code block post-processor for `literalinclude` blocks used in callouts."""
default_priority = 400
def apply(self, **kwargs) -> None:
visitor = LiteralIncludeVisitor(self.document)
self.document.walkabout(visitor)
class LiteralIncludeVisitor(nodes.NodeVisitor):
"""Change a literal block upon visiting it."""
def __init__(self, document: nodes.document) -> None:
super().__init__(document)
def unknown_visit(self, node: Node) -> None:
pass
def unknown_departure(self, node: Node) -> None:
pass
def visit_document(self, node: Node) -> None:
pass
def depart_document(self, node: Node) -> None:
pass
def visit_start_of_file(self, node: Node) -> None:
pass
def depart_start_of_file(self, node: Node) -> None:
pass
def visit_literal_block(self, node: nodes.literal_block) -> None:
if "<1>" in node.rawsource:
source = str(node.rawsource)
for i in range(1, 20):
source = source.replace(
f"<{i}>", chr(int(f"0x{BASE_NUM + i}", base=16))
)
node.rawsource = source
node[:] = [nodes.Text(source)]
class callout(nodes.General, nodes.Element):
"""Sphinx callout node."""
pass
def visit_callout_node(self, node):
"""We pass on node visit to prevent the
callout being treated as admonition."""
pass
def depart_callout_node(self, node):
"""Departing a callout node is a no-op, too."""
pass
class annotations(nodes.Element):
"""Sphinx annotations node."""
pass
def _replace_numbers(content: str):
"""
Replaces strings of the form <x> with circled unicode numbers (e.g. ①) as text.
Args:
content: Python str from a callout or annotations directive.
Returns: The formatted content string.
"""
for i in range(1, 20):
content.replace(f"<{i}>", chr(int(f"0x{BASE_NUM + i}", base=16)))
return content
def _parse_recursively(self, node):
"""Utility to recursively parse a node from the Sphinx AST."""
self.state.nested_parse(self.content, self.content_offset, node)
class CalloutDirective(SphinxDirective):
"""Code callout directive with annotations for Sphinx.
Use this `callout` directive by wrapping either `code-block` or `literalinclude`
directives. Each line that's supposed to be equipped with an annotation should
have an inline comment of the form "# <x>" where x is an integer.
Afterwards use the `annotations` directive to add annotations to the previously
defined code labels ("<x>") by using the syntax "<x> my annotation" to produce an
annotation "my annotation" for x.
Note that annotation lines have to be separated by a new line, i.e.
.. annotations::
<1> First comment followed by a newline,
<2> second comment after the newline.
Usage example:
-------------
.. callout::
.. code-block:: python
from ray import tune
from ray.tune.search.hyperopt import HyperOptSearch
import keras
def objective(config): # <1>
...
search_space = {"activation": tune.choice(["relu", "tanh"])} # <2>
algo = HyperOptSearch()
tuner = tune.Tuner( # <3>
...
)
results = tuner.fit()
.. annotations::
<1> Wrap a Keras model in an objective function.
<2> Define a search space and initialize the search algorithm.
<3> Start a Tune run that maximizes accuracy.
"""
has_content = True
def run(self):
self.assert_has_content()
content = self.content
content = _replace_numbers(content)
callout_node = callout("\n".join(content))
_parse_recursively(self, callout_node)
return [callout_node]
class AnnotationsDirective(SphinxDirective):
"""Annotations directive, which is only used nested within a Callout directive."""
has_content = True
def run(self):
content = self.content
content = _replace_numbers(content)
joined_content = "\n".join(content)
annotations_node = callout(joined_content)
_parse_recursively(self, annotations_node)
return [annotations_node]
def setup(app):
# Add new node types
app.add_node(
callout,
html=(visit_callout_node, depart_callout_node),
latex=(visit_callout_node, depart_callout_node),
text=(visit_callout_node, depart_callout_node),
)
app.add_node(annotations)
# Add new directives
app.add_directive("callout", CalloutDirective)
app.add_directive("annotations", AnnotationsDirective)
# Add post-processor
app.add_post_transform(CalloutIncludePostTransform)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
+122
View File
@@ -0,0 +1,122 @@
from docutils import nodes, utils, frontend
from docutils.parsers.rst import directives, Parser
from sphinx.util.docutils import SphinxDirective
class URLQueryParamRefNode(nodes.General, nodes.Element):
"""Node type for references that need URL query parameters."""
class WrapperNode(nodes.paragraph):
"""A wrapper node for URL query param references.
Sphinx will not build if you don't wrap a reference in a paragraph. And a <div>
is needed anyway for styling purposes.
"""
def visit(self, node):
if "class" not in node:
raise ValueError(
"'class' attribute must be set on the WrapperNode for a "
"URL query parameter reference."
)
self.body.append(self.starttag(node, "div", CLASS=node["class"]))
def depart(self, node):
self.body.append("</div>")
class URLQueryParamRefDirective(SphinxDirective):
"""Sphinx directive to insert a reference with query parameters."""
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
has_content = True
option_spec = {
"parameters": directives.unchanged_required,
"classes": directives.class_option,
"ref-type": (
lambda choice: directives.choice(choice, ["any", "ref", "doc", "myst"])
),
}
def run(self):
ref_type = self.options.get("ref-type", "any")
content = self.content.data
reftarget = directives.uri(self.arguments[0])
return [
URLQueryParamRefNode(
{
"docname": self.env.docname,
"parameters": self.options.get("parameters", None),
"classes": self.options.get("classes", []),
"reftarget": reftarget,
"refdocname": self.env.docname,
"refdomain": "std" if ref_type in {"ref", "doc"} else "",
"reftype": ref_type,
"refexplicit": content if content else reftarget,
"refwarn": True,
}
)
]
def on_doctree_resolved(app, doctree, docname):
"""Replace URLQueryParamRefNode instances with real references.
Any text that lives inside a URLQueryParamRefNode is parsed as usual.
Args:
app: Sphinx application
doctree: Doctree which has just been resolved
docname: Name of the document containing the reference nodes
"""
parser = Parser()
for node in doctree.traverse(URLQueryParamRefNode):
tmp_node = utils.new_document(
"Content nested under URLQueryParamRefNode",
frontend.OptionParser(components=[Parser]).get_default_values(),
)
text = "\n".join(node.rawsource["refexplicit"])
# Parse all child RST as usual, then append any parsed nodes to the
# reference node.
parser.parse(text, tmp_node)
ref_node = nodes.reference(
rawsource=text,
text="",
)
for child in tmp_node.children:
ref_node.append(child)
# Pass all URLQueryParamRefNode attributes to ref node;
# possibly not necessary
for key, value in node.rawsource.items():
ref_node[key] = value
# Need to update refuri of ref node to include URL query parameters
ref_node["refuri"] = (
app.builder.get_relative_uri(
docname,
node.rawsource["reftarget"],
)
+ node.rawsource["parameters"]
)
# Need to wrap the node in a paragraph for Sphinx to build
wrapper = WrapperNode()
wrapper["class"] = "query-param-ref-wrapper"
wrapper += ref_node
node.replace_self([wrapper])
def setup(app):
app.add_directive("query-param-ref", URLQueryParamRefDirective)
app.connect("doctree-resolved", on_doctree_resolved)
app.add_node(WrapperNode, html=(WrapperNode.visit, WrapperNode.depart))
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
+11
View File
@@ -0,0 +1,11 @@
You can post questions or issues or feedback through the following channels:
1. `Discussion Board`_: For **questions about Ray usage** or **feature requests**.
2. `GitHub Issues`_: For **bug reports**.
3. `Ray Slack`_: For **getting in touch** with Ray maintainers.
4. `StackOverflow`_: Use the [ray] tag for **questions about Ray**.
.. _`Discussion Board`: https://discuss.ray.io/
.. _`GitHub Issues`: https://github.com/ray-project/ray/issues
.. _`Ray Slack`: https://www.ray.io/join-slack
.. _`StackOverflow`: https://stackoverflow.com/questions/tagged/ray
@@ -0,0 +1,6 @@
.. admonition:: Check your version!
Things can change quickly, and so does this contributor guide.
To make sure you've got the most cutting edge version of this guide,
go check out the
`latest version <https://docs.ray.io/en/master/ray-contribute/getting-involved.html>`__.
@@ -0,0 +1,8 @@
..
.. note::
Ray 2.40 uses RLlib's new API stack by default.
The Ray team has mostly completed transitioning algorithms, example scripts, and
documentation to the new code base.
If you're still using the old API stack, see :doc:`New API stack migration guide </rllib/new-api-stack-migration-guide>` for details on how to migrate.
View File
+79
View File
@@ -0,0 +1,79 @@
/* APIs-tab sidebar loader (Pattern B).
*
* Fetches the single shared API-nav fragment (_static/api-nav.html), injects it into
* the #api-nav-mount container, highlights the current page, collapses non-current
* sections, and resolves the fragment's root-relative links against this page.
* The fragment is fetched once and reused from the browser cache across pages.
*/
(function () {
"use strict";
function init() {
var mount = document.getElementById("api-nav-mount");
if (!mount) return;
var navUrl = mount.getAttribute("data-api-nav-url");
var pagename = mount.getAttribute("data-pagename") || "";
// Path from this page back to the doc root (e.g. "../../"), derived from the
// fetch URL so we don't depend on other globals.
var root = navUrl.replace(/_static\/api-nav\.html(\?.*)?$/, "");
fetch(navUrl)
.then(function (resp) {
if (!resp.ok) throw new Error("HTTP " + resp.status);
return resp.text();
})
.then(function (html) {
mount.innerHTML = html;
var links = mount.querySelectorAll("a[href]");
// 1) Find + mark the current page (fragment hrefs are root-relative).
var currentHref = pagename + ".html";
var currentLink = null;
links.forEach(function (a) {
if (a.getAttribute("href") === currentHref) currentLink = a;
});
// 2) Collapse every section, then re-open only the current page's ancestors.
mount.querySelectorAll("details").forEach(function (d) {
d.removeAttribute("open");
});
if (currentLink) {
currentLink.classList.add("current");
var li = currentLink.closest("li");
if (li) li.classList.add("current", "active");
var el = currentLink.parentElement;
while (el && el !== mount) {
if (el.tagName === "DETAILS") el.setAttribute("open", "");
el = el.parentElement;
}
if (li) {
var own = li.querySelector(":scope > details");
if (own) own.setAttribute("open", "");
}
if (li && li.scrollIntoView) {
li.scrollIntoView({ block: "nearest" });
}
}
// 3) Resolve root-relative hrefs against this page.
links.forEach(function (a) {
var h = a.getAttribute("href");
if (h && !/^(https?:|\/|#|mailto:)/.test(h)) {
a.setAttribute("href", root + h);
}
});
})
.catch(function () {
mount.innerHTML =
'<p class="api-nav-status">API navigation failed to load.</p>';
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
+177
View File
@@ -0,0 +1,177 @@
/* Kapa Ask AI button */
#kapa-widget-container figure {
padding: 0 !important;
}
.mantine-Modal-root figure {
padding: 0 !important;
}
.assistant-title {
margin-top: 1em;
}
.container-xl.blurred {
filter: blur(5px);
}
.chat-widget {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 1000;
}
@keyframes jump {
0% {
transform: scale(1);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
100% {
transform: scale(1.05);
box-shadow: 0 4px 20px rgba(0,0,0,.1);
}
}
.search-button__wrapper.show ~ .chat-widget {
z-index: 1050;
animation: .4s jump ease infinite alternate;
filter: brightness(1.5);
}
.chat-popup {
display: none;
position: fixed;
top: 20%;
left: 50%;
transform: translate(-50%, -20%);
width: 55%;
height: 75%;
background-color: var(--pst-color-background);
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 {
border: 1px solid var(--pst-color-border);
background-color: var(--pst-color-surface);
color: var(--pst-color-text-base);
width: 70px;
height: 70px;
border-radius: 10px;
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-surface);
margin: 0em 1em 0em 0em;
border-radius: 4px;
color: var(--pst-color-text-base);
}
.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% - 100px);
overflow-y: auto;
}
.chatContentContainer input {
margin-top: 10px;
margin-bottom: 10px;
}
hr {
border: none;
height: 1px;
/* Set the hr color */
color: #333; /* old IE */
background-color: #333; /* Modern Browsers */
}
#result * {
overflow-anchor: none;
}
#anchor {
overflow-anchor: auto;
height: 1px;
}
#result {
padding: 15px;
border-radius: 10px;
margin-top: 10px;
margin-bottom: 10px;
background-color: var(--pst-color-surface);
max-height: 50vh; /* Ensure the result area does not take too much vertical space */
overflow-y: auto;
}
.chatContentContainer textarea {
flex-grow: 1;
min-width: 50px;
max-height: 40px;
resize: none;
}
#searchBtn {
white-space: nowrap;
border-radius: 4px;
}
.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;
}
+78
View File
@@ -0,0 +1,78 @@
/* 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;
color: var(--pst-color-text-base);
}
#csat-textarea::placeholder {
color: var(--pst-color-text-base);
}
.csat-icon > path {
fill: var(--pst-color-text-base);
}
+451
View File
@@ -0,0 +1,451 @@
/* 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: #0066FF;
--ray-blue: #02A0CF; /* Ray blue color - use this for all ray branding */
--pst-color-primary: 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;
--color-diff-delete-bg: rgba(212, 118, 22, 0.3);
--color-diff-insert-bg: rgba(56, 139, 253, 0.3);
--color-diff-nochange-bg: rgba(0, 0, 0, 0);
--pst-font-family-base: 'Inter', sans-serif;
--stata-dark-background: #232629;
}
html[data-theme='dark'] {
--pst-color-background: #161a1d;
--pst-color-on-background: #1d2125;
--pst-color-text-base: #f1f2f4;
--pst-color-text-muted: #b3b9c4;
--pst-color-border: #2c333a;
--bs-body-color: #f1f2f4;
--heading-color: #ffffff;
--base-pygments-code-color: #cccccc;
--pst-color-link-hover: #cce0ff;
--anyscale-border-color: #f1f2f4;
}
html[data-theme='light'] {
--pst-color-background: #ffffff;
--pst-color-on-background: #ffffff;
--pst-color-text-base: #22272b;
--pst-color-text-muted: #454f59;
--pst-color-border: #dcdfe4;
--heading-color: #161a1d;
--base-pygments-code-color: #cccccc;
--pst-color-link-hover: #09326c;
--anyscale-border-color: #161a1d;
}
nav.bd-links li > a:hover {
text-decoration: none;
}
a:hover {
text-decoration-thickness: unset;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--heading-color);
}
/* Gradient ellipse background */
.bd-sidebar-secondary {
background-color: transparent;
}
.bd-content:after {
/* Commenting the code below to make more modifications after 2.10 release */
/* background: linear-gradient(
60deg,
rgba(0, 85, 204, 0.18) 14%,
rgba(110, 93, 198, 0.18) 49.2%,
rgba(174, 71, 135, 0.18) 81.54%
); */
background-size: 746px 746px;
background-repeat: no-repeat;
background-position: center;
border-radius: 373px;
background-origin: 50%;
background-attachment: scroll;
content: '';
transform: translate(50%, 0%);
width: 746px;
height: 746px;
position: absolute;
filter: blur(100px);
z-index: -1;
}
/* Pygments diff code cell line colors; match github colorblind theme */
div.highlight > pre > span.gd {
background-color: var(--color-diff-delete-bg);
}
div.highlight > pre > span.gi {
background-color: var(--color-diff-insert-bg);
}
div.highlight > pre > span.w {
background-color: var(--color-diff-nochange-bg);
}
/* Fix some pygments styles that inadvertently get overridden by PST */
.highlight pre {
background-color: var(--stata-dark-background);
color: var(--base-pygments-code-color);
}
/* Make the article content take up all available space */
.bd-main .bd-content .bd-article-container {
max-width: 100%; /* default is 60em */
}
.bd-page-width {
max-width: 100%; /* default is 88rem */
}
/* 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;
}
ul.navbar-toplevel li:hover > div.navbar-dropdown {
display: block;
}
ul.navbar-toplevel {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin: 0;
height: 100%;
padding-left: 0;
}
.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%);
}
div.navbar-content a {
display: flex;
flex-direction: column;
align-items: start;
white-space: pre;
justify-content: center;
/* pydata-sphinx-theme 0.14's `.navbar-nav li a { height: 100% }` was
scoped to `ul.navbar-nav li a` in 0.17, which no longer matches our
`<nav class="navbar-nav">`. Without height: 100% on <a>, the <a>
shrinks to text height (~26px) inside its 64px-tall <p> parent,
leaving the link text top-aligned in the navbar and the chevron
(which is centered on the .ref-container) visually below the text.
Restoring height: 100% lets justify-content: center vertically
center the text in the full-height anchor. */
height: 100%;
}
div.navbar-content {
height: 100%;
}
nav.navbar-nav {
height: 100%;
}
.ref-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 0.5em;
height: 100%;
}
/* Highlight active nav bar link */
li.active-link {
font-weight: bold;
}
.navbar-header-items__end {
/* Prevent the anyscale button from wrapping */
flex-flow: nowrap !important;
}
.navbar {
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.08);
}
/* Set the first .navbar-persistent--mobile element to have auto left margin */
.navbar-persistent--mobile {
margin-left: auto;
}
/* Set any .navbar-persistent--mobile preceeded by a .navbar-persistent--mobile to have */
/* a 1em left margin */
.navbar-persistent--mobile ~ .navbar-persistent--mobile {
margin-left: 1em;
}
/* Disable underline for hovered links in the nav bar */
.navbar-nav li a:hover {
text-decoration: none;
}
/* pydata-sphinx-theme 0.17 stopped removing text-decoration on
<a class="reference internal"> inside the top navbar (the rule it
ships now only matches <a class="nav-link">). Re-add it for our
reference-internal anchors. */
.navbar-toplevel a.reference {
text-decoration: none;
}
/* pydata-sphinx-theme 0.17 stopped hiding the sidebar-header-items__title
element (Ray's navbar-links.html template emits "Site Navigation" as
a screen-reader heading). Hide it in the header context — it still
renders in the actual sidebar drawer where the heading is useful. */
.navbar-header-items .sidebar-header-items__title {
display: none;
}
.navbar-header-items {
padding-left: 0;
}
/* Ray logo */
.navbar-brand.logo > svg {
width: 120px;
}
.navbar-brand.logo > svg path#ray-text {
fill: var(--pst-color-text-base);
}
/* Anyscale branding */
#try-anyscale-text {
color: var(--pst-color-text-base);
border-radius: 2px;
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(--anyscale-border-color);
}
#try-anyscale-href {
text-decoration: none;
}
/* Remove margin for the release header in the sidebar, otherwise it's too much space */
#release-header {
margin: 0;
}
/* Center the Ray release header in the sidebar */
div.sidebar-primary-item:nth-child(1) {
display: flex;
flex-direction: row;
justify-content: center;
}
/* Center the search button in the sidebar */
div.sidebar-primary-item:nth-child(2) {
display: flex;
flex-direction: row;
justify-content: center;
}
/* Disable the "Back to top" button that appears if you scroll down */
button#pst-back-to-top {
display: none !important;
}
.bottom-right-promo-banner {
position: fixed;
bottom: 100px;
right: 20px;
width: 270px;
}
@media (max-width: 1500px) {
.bottom-right-promo-banner {
display: none;
}
}
/* Nav sidebar styles */
.bd-sidebar-primary {
width: 280px;
padding: 2em 2em 0em 2em;
}
/* Make sidebar take up full primary sidebar gutter, but don't wrap content */
#main-sidebar {
width: 100%;
}
nav.bd-links li > a {
color: var(--pst-color-text-base);
}
/* Sidebar checkboxes are toggled by clicking on the label; hide actual checkboxes */
.toctree-checkbox[type='checkbox']:checked ~ ul > li.current-page:before {
background-color: var(--ray-blue);
border-radius: 0.5px;
}
.toctree-checkbox[type='checkbox']:checked ~ ul > li:before {
content: '';
width: 1px;
height: 100%;
position: absolute;
background-color: var(--pst-color-border);
}
/* Highlight and bold the primary sidebar entry for the current page */
#main-sidebar li.current-page > a {
color: var(--ray-blue) !important;
font-weight: 600;
}
/* Bold the top level primary sidebar links */
#main-sidebar > .navbar-nav > .nav.bd-sidenav > li > a {
font-weight: 500;
}
/* Fix some spacing issues associated with competition with PST styles */
.sidebar-content dl {
margin-bottom: 0;
}
.sidebar-content ol li > p:first-child,
ul li > p:first-child {
margin-top: 0 !important;
}
/* 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);
}
/* Ensure that long function names get elided and show ellipses to not overflow their bounding boxes */
table.autosummary tr > td:first-child > p > a > code {
max-width: 100%;
width: fit-content;
display: block;
}
table.autosummary tr > td:first-child > p > a > code > span {
display: block;
overflow: clip;
text-overflow: ellipsis;
}
/* RTD footer container makes the parent */
/* #main-sidebar always scrollable if you don't remove negative margin. */
/* Restrict width to 30% of the window */
.bd-sidebar-primary div#rtd-footer-container {
margin: unset;
max-width: 30vw;
}
.query-param-ref-wrapper {
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--pst-color-border);
border-radius: 8px;
}
.query-param-ref-wrapper p {
margin: 0;
}
/* Styles for tables in example pages */
.table.example-table {
table-layout: fixed;
}
.table.example-table th:first-child {
width: 30%;
}
/* pydata-sphinx-theme 0.14 painted the announcement banner background via
an absolutely-positioned `::after` pseudo-element. 0.17 replaced that
with `background-color: var(--pst-color-secondary-bg)` directly on
`.bd-header-announcement`, so the `::after` rule is now a no-op and
pydata's default secondary-bg paints the banner lavender. Set the
background directly on the element to keep Ray's teal. */
.bd-header-announcement {
color: var(--pst-color-light);
background-color: var(--ray-blue);
}
.bd-header-announcement a {
color: var(--pst-color-light);
text-decoration: underline;
}
/* Prevent the PyData theme Version Switcher from getting too large */
.version-switcher__menu {
max-height: 40rem;
overflow-y: scroll;
}
/* Right align the version switcher dropdown menu to prevent it from going off screen */
.version-switcher__menu[data-bs-popper] {
right: 0;
left: unset;
}
/* Hide the RTD version switcher since we are using PyData theme one */
readthedocs-flyout {
display: none !important;
}
/* Styling the experimental Anyscale upsell CTA */
.anyscale-cta {
margin-bottom: 16px;
}
/* Prevent text wrapping around left-aligned images on ultra-wide screens */
@media (min-width: 1600px) {
.bd-content .align-left,
.bd-content .figure.align-left,
.bd-content img.align-left {
float: none !important;
display: block;
clear: both;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
@@ -0,0 +1,13 @@
#close-banner {
background: none;
border: none;
color: inherit;
font-size: 1.2em;
cursor: pointer;
padding: 0 5px;
border-radius: 3px;
transition: background-color 0.2s ease;
position: absolute;
top: 8px;
right: 12px;
}
+259
View File
@@ -0,0 +1,259 @@
html {
--remix-icon-color: #ffffff;
}
html[data-theme='dark'] {
--example-color: color-mix(in srgb, #ffffff, transparent 90%);
}
html[data-theme='light'] {
--example-color: #ffffff;
}
#examples-search-input-label {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
#examples-search-icon {
fill: var(--pst-color-text-base);
margin: 0em 1em;
}
#examples-search-input {
width: 100%;
color: var(--pst-color-text-base);
border: none;
background-color: transparent;
}
#examples-search-input:focus-visible {
outline: none;
}
#examples-search-input::placeholder {
color: var(--pst-color-text-muted);
opacity: 1;
}
#no-matches {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#no-matches-inner-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#no-matches.hidden,
.gallery-item.hidden {
display: none !important;
}
.examples-search-area {
display: flex;
flex-direction: row;
padding: 0.75em 0em;
border: 1px solid var(--pst-color-border);
border-radius: 4px;
background: var(--pst-color-background);
}
.content {
display: flex;
flex-direction: column;
width: 70%;
justify-content: center;
}
.content-wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.example-tags {
color: var(--pst-color-text-muted);
text-transform: uppercase;
}
.example-list-area {
display: grid;
grid-template-columns: 1fr;
gap: 2em;
}
@media (min-width: 960px) {
.example-list-area {
grid-template-columns: 1fr 1fr;
}
}
.example-text-area {
display: flex;
flex-direction: column;
}
#dropdown-area {
display: flex;
flex-direction: row;
gap: 2em;
margin: 1.5em 0em 2.5em 0em;
}
.filter-dropdown {
border: 1px solid var(--dropdown-border-color);
border-radius: 4px;
}
.dropdown-checkbox {
display: none;
padding: 0.5em 1.5em;
}
.dropdown-label {
user-select: none;
border: 1px solid var(--pst-color-border);
border-radius: 4px;
padding: 0.5em 1em;
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
/* When the dropdown is exposed, highlight the background of the parent div label */
.dropdown-checkbox[type='checkbox']:checked ~ .dropdown-label {
background-color: var(--pst-color-on-background);
}
.dropdown-content {
display: none;
position: absolute;
background: var(--pst-color-on-background);
padding: 0.5em 1em;
border: 1px solid var(--pst-color-border);
border-radius: 4px;
flex-direction: column;
gap: 1em;
z-index: 1;
}
/* Show dropdown when the checkbox is clicked */
.dropdown-checkbox[type='checkbox']:checked ~ .dropdown-content {
display: flex !important;
}
/* Checkboxes */
.checkbox-container {
display: block;
position: relative;
padding-left: 35px;
cursor: pointer;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
/* Color of an empty checkbox square */
background-color: var(--pst-color-surface);
}
.checkbox-container input:checked ~ .checkmark {
/* Color of checkbox when checked */
background-color: var(--pst-color-primary-highlight);
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkmark:after {
content: '';
position: absolute;
display: none;
}
.checkbox-container .checkmark:after {
left: 10px;
top: 6px;
width: 6px;
height: 12px;
border: solid var(--pst-color-border);
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
.example.hidden {
display: none;
}
.example-icon-area {
width: 70px;
height: 70px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.example-icon {
border-radius: 10px !important; /* Competes against pydata-sphinx-theme img styles */
background-color: transparent !important; /* Competes against pydata-sphinx-theme img styles */
min-width: 75px;
min-height: 75px;
}
.remix-icon {
position: absolute;
font-size: 28px;
color: var(--remix-icon-color);
}
.example-link {
color: var(--pst-color-text-base) !important;
text-decoration: none;
display: flex;
flex-direction: row;
gap: 1em;
align-items: center;
padding: 1em;
}
.example {
border-radius: 4px;
background-color: color-mix(
in srgb,
var(--pst-color-on-background) 70%,
transparent
);
box-shadow: 0px 4px 15px color-mix(in srgb, black 5%, transparent);
}
.example-other-keywords {
display: none;
}
.community-text {
color: var(--pst-color-text-muted);
}
.community-emojis {
padding-left: 0.5em;
}
+292
View File
@@ -0,0 +1,292 @@
html[data-theme='dark'] {
--community-box-color: var(--pst-color-surface);
}
html[data-theme='light'] {
--community-box-color: var(--pst-color-background);
}
.main-content {
display: flex;
flex-direction: column;
padding: 0em 5em;
}
.centered-heading {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 2em;
}
.centered-heading > p {
font-weight: 400;
}
.clicky-tab-side-by-side {
display: flex;
flex-direction: row;
gap: 1em;
}
/* Area which contains all the tab selector labels */
.tab-selector {
width: 30%;
min-width: 30%;
}
/* Area which contains all the tab panes */
.tab-area {
flex-grow: 1;
overflow-x: auto;
box-shadow: 0px 6px 30px 5px var(--pst-color-shadow);
}
.tab-pane {
border-radius: 4px;
user-select: none;
}
.tab-pane-links {
display: flex;
flex-direction: row;
gap: 1em;
padding: 1em;
background: var(--pst-color-surface);
border-radius: 0px 0px 4px 4px;
font-size: 14px;
}
.tab-pane-links > a:not(:first-child) {
border-left: 1px solid var(--pst-color-border);
padding-left: 1em;
}
.tab-pane pre {
margin: 0;
padding: 0.5em 1em;
overflow-y: auto;
animation: fadeEffect 1s; /* Fading effect takes 1 second */
border: none;
}
.tab-pane .highlight {
border-radius: 4px 4px 0px 0px;
height: 14em;
}
/* Go from zero to full opacity */
@keyframes fadeEffect {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.header-button-icon {
fill: var(--pst-color-text-base);
width: 17px;
height: 20px;
min-width: 17px;
min-height: 20px;
}
.card-row {
display: flex;
flex-direction: row;
gap: 1em;
}
.link-card {
display: flex;
flex-direction: column;
gap: 0.5em;
flex: 1;
}
.link-card-icon-label {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
}
.link-card h4 {
margin: 0;
}
.card-text-area {
display: flex;
flex-direction: column;
padding-left: 2.5em;
}
.card-icon {
width: 32px;
height: 32px;
}
.community-box {
border-radius: 4px;
display: flex;
flex-direction: row;
gap: 1em;
box-shadow: 0px 4px 10px 0px var(--pst-color-shadow);
color: var(--pst-color-text-base) !important;
align-items: center;
background: var(--community-box-color);
padding-left: 2em;
overflow: hidden;
}
/* Make remix icons larger */
.community-box i {
font-size: 25px;
}
.community-box p {
margin-top: 1rem;
text-wrap: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.community-box > img {
width: 32px;
height: 32px;
}
.nav-pills {
background-color: var(--pst-color-surface);
padding: 1em;
border-radius: 4px;
justify-content: center;
width: 30%;
gap: 0.5em;
}
.nav-pills .nav-link.active {
background-color: var(--pst-color-background) !important;
box-shadow: 0px 3px 14px 2px var(--pst-color-shadow);
border-radius: 4px;
color: var(--pst-color-text-base);
}
#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);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
#v-pills-tabContent .row {
background-color: var(--pst-color-background);
}
#v-pills-tabContent {
box-shadow: 0px 6px 30px 5px var(--pst-color-shadow);
border-radius: 4px;
}
.links-grid-wrapper {
display: flex;
flex-direction: column;
}
.links-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2em;
}
.links-grid > h4 {
margin: 0;
}
.bd-article {
padding: 0;
}
.main-content {
padding: 0em 5em;
}
.heading-buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2em;
}
.heading-buttons > a {
display: flex;
flex-direction: row;
align-items: center;
padding: 1em 1em 1em 2em;
border-radius: 4px;
background: var(--community-box-color);
color: var(--pst-color-text-base);
font-weight: 500;
text-wrap: nowrap;
box-shadow: 0px 4px 10px 0px var(--pst-color-shadow);
}
.header-button {
display: flex;
flex-direction: row;
gap: 1em;
align-items: center;
justify-content: center;
}
.header-button i {
font-size: 25px;
}
a {
text-decoration: none;
}
h1 {
font-weight: bold;
}
.card-icon svg {
width: 32px;
height: 32px;
}
.card-icon path {
stroke: var(--pst-color-link);
}
#link-card-icon-filled {
fill: var(--pst-color-link);
}
h1 {
font-weight: 600;
}
h3 {
margin-bottom: 1em;
margin-top: 4em;
}
h4 {
font-size: 16px;
font-weight: 500;
}
.links-grid b {
font-size: 18px;
font-weight: 600;
}
+12
View File
@@ -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%;
}
+14
View File
@@ -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;
}
+108
View File
@@ -0,0 +1,108 @@
/**
* termynal.js
*
* @author Ines Montani <ines@ines.io>
* @version 0.0.1
* @license MIT
*/
:root {
--color-bg: #252a33;
--color-text: #eee;
--color-text-subtle: #a2a2a2;
}
[data-termynal] {
width: auto;
max-width: 100%;
background: var(--color-bg);
color: var(--color-text);
font-size: 18px;
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
[data-termynal]:before {
content: '';
position: absolute;
top: 15px;
left: 15px;
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
/* A little hack to display the window buttons in one pseudo element. */
background: #d9515d;
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
}
[data-termynal]:after {
content: 'bash';
position: absolute;
color: var(--color-text-subtle);
top: 5px;
left: 0;
width: 100%;
text-align: center;
}
[data-ty] {
display: block;
line-height: 2;
}
[data-ty]:before {
/* Set up defaults and ensure empty lines are displayed. */
content: '';
display: inline-block;
vertical-align: middle;
}
[data-ty="input"]:before,
[data-ty-prompt]:before {
margin-right: 0.75em;
color: var(--color-text-subtle);
}
[data-ty="input"]:before {
content: '$';
}
[data-ty][data-ty-prompt]:before {
content: attr(data-ty-prompt);
}
[data-ty-cursor]:after {
content: attr(data-ty-cursor);
font-family: monospace;
margin-left: 0.5em;
-webkit-animation: blink 1s infinite;
animation: blink 1s infinite;
}
a[data-terminal-control] {
text-align: right;
display: block;
color: #aebbff;
}
/* Cursor animation */
@-webkit-keyframes blink {
50% {
opacity: 0;
}
}
@keyframes blink {
50% {
opacity: 0;
}
}
+15
View File
@@ -0,0 +1,15 @@
.example-gallery-link {
padding: 1em 2em 1em 2em;
text-decoration: none !important;
color: var(--pst-color-text-base);
display: flex;
align-items: center;
}
svg.star-icon path.star-icon-path {
fill: var(--pst-color-text-base);
}
svg.star-icon {
margin-right: 1em;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+6
View File
@@ -0,0 +1,6 @@
/* CSS for binder integration */
div.binder-badge {
margin: 1em auto;
vertical-align: middle;
}
+36
View File
@@ -0,0 +1,36 @@
/* Pandas dataframe css */
/* Taken from: https://github.com/spatialaudio/nbsphinx/blob/fb3ba670fc1ba5f54d4c487573dbc1b4ecf7e9ff/src/nbsphinx.py#L587-L619 */
table.dataframe {
border: none !important;
border-collapse: collapse;
border-spacing: 0;
border-color: transparent;
color: black;
font-size: 12px;
table-layout: fixed;
}
table.dataframe thead {
border-bottom: 1px solid black;
vertical-align: bottom;
}
table.dataframe tr,
table.dataframe th,
table.dataframe td {
text-align: right;
vertical-align: middle;
padding: 0.5em 0.5em;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
table.dataframe th {
font-weight: bold;
}
table.dataframe tbody tr:nth-child(odd) {
background: #f5f5f5;
}
table.dataframe tbody tr:hover {
background: rgba(66, 165, 245, 0.2);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="400" height="201" viewBox="0 0 400 201" xmlns="http://www.w3.org/2000/svg">
<path id="ray-text" d="M325.949 134.356V109.785L302.442 66.6406H314.244L330.495 97.3062H331.946L348.198 66.6406H360L336.493 109.785V134.356H325.949ZM253.043 134.364L272.391 66.648H290.771L310.021 134.364H299.283L294.834 118.402H268.328L263.878 134.364H253.043ZM270.94 108.728H292.222L282.354 73.1294H280.807L270.94 108.728ZM198.887 134.364V66.648H227.327C231.519 66.648 235.195 67.3896 238.355 68.8729C241.58 70.2918 244.063 72.3555 245.804 75.0641C247.61 77.7727 248.513 80.9973 248.513 84.7378V85.8019C248.513 90.0583 247.481 93.4763 245.417 96.0559C243.418 98.5711 240.967 100.345 238.065 101.376V102.924C240.516 103.053 242.483 103.892 243.966 105.439C245.449 106.923 246.191 109.083 246.191 111.921V134.364H235.647V113.372C235.647 111.631 235.195 110.244 234.292 109.212C233.39 108.18 231.938 107.664 229.939 107.664H209.334V134.364H198.887ZM209.334 98.1842H226.166C229.907 98.1842 232.809 97.249 234.873 95.3788C236.937 93.4441 237.968 90.8322 237.968 87.5431V86.7692C237.968 83.4802 236.937 80.9005 234.873 79.0303C232.874 77.0956 229.971 76.1282 226.166 76.1282H209.334V98.1842Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.63 101.311L98.3087 146.632L94.9903 143.313L140.311 97.9925L143.63 101.311ZM141.953 102.334L51.4454 102.334V97.6409L141.953 97.6409V102.334ZM94.992 55.9863L140.313 101.307L143.631 97.9886L98.3105 52.6679L94.992 55.9863Z" fill="#02A0CF"/>
<path d="M40 88.3163H62.6604V110.977H40V88.3163ZM85.3207 88.3163H107.981V110.977H85.3207V88.3163ZM85.3207 43H107.981V65.6604H85.3207V43ZM85.3207 133.645H107.981V156.306H85.3207V133.645ZM130.641 88.3163H153.301V110.977H130.641V88.3163Z" fill="#02A0CF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

@@ -0,0 +1,20 @@
<svg width="157" height="42" viewBox="0 0 157 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="157" height="41.9994" fill="#0066FF"/>
<path d="M17.204 25.5V15.896H22.258C22.9113 15.896 23.4527 16.0173 23.882 16.26C24.3207 16.4933 24.6473 16.8247 24.862 17.254C25.0767 17.674 25.184 18.1593 25.184 18.71C25.184 19.3727 25.0347 19.942 24.736 20.418C24.4373 20.894 24.0267 21.2347 23.504 21.44L25.52 25.5H24.064L22.188 21.678H18.534V25.5H17.204ZM18.534 20.544H22.132C22.664 20.544 23.0793 20.3807 23.378 20.054C23.686 19.7273 23.84 19.2793 23.84 18.71C23.84 18.3553 23.7747 18.0567 23.644 17.814C23.5133 17.5713 23.322 17.3847 23.07 17.254C22.818 17.114 22.5053 17.044 22.132 17.044H18.534V20.544ZM29.2832 25.668C28.5738 25.668 28.0045 25.486 27.5752 25.122C27.1458 24.758 26.9312 24.1233 26.9312 23.218V18.136H28.1632V23.05C28.1632 23.358 28.2005 23.61 28.2752 23.806C28.3498 24.002 28.4572 24.156 28.5972 24.268C28.7372 24.38 28.9005 24.4593 29.0872 24.506C29.2832 24.5527 29.4932 24.576 29.7172 24.576C30.0718 24.576 30.3985 24.492 30.6972 24.324C30.9958 24.156 31.2385 23.9133 31.4252 23.596C31.6212 23.2693 31.7192 22.8913 31.7192 22.462V18.136H32.9512V25.5H31.9572L31.8452 24.394H31.7472C31.5512 24.6927 31.3272 24.9353 31.0752 25.122C30.8325 25.3087 30.5572 25.444 30.2492 25.528C29.9505 25.6213 29.6285 25.668 29.2832 25.668ZM34.8625 25.5V18.136H35.8565L35.9685 19.242H36.0665C36.2625 18.9433 36.4818 18.7007 36.7245 18.514C36.9765 18.3273 37.2518 18.192 37.5505 18.108C37.8585 18.0147 38.1852 17.968 38.5305 17.968C38.9972 17.968 39.4032 18.0473 39.7485 18.206C40.1032 18.3647 40.3785 18.6213 40.5745 18.976C40.7798 19.3307 40.8825 19.8113 40.8825 20.418V25.5H39.6505V20.586C39.6505 20.278 39.6085 20.026 39.5245 19.83C39.4498 19.634 39.3425 19.48 39.2025 19.368C39.0718 19.256 38.9085 19.1767 38.7125 19.13C38.5258 19.0833 38.3158 19.06 38.0825 19.06C37.7372 19.06 37.4105 19.144 37.1025 19.312C36.7945 19.48 36.5472 19.7227 36.3605 20.04C36.1832 20.3573 36.0945 20.7353 36.0945 21.174V25.5H34.8625ZM48.7009 25.668C47.9543 25.668 47.3243 25.5327 46.8109 25.262C46.3069 24.982 45.9196 24.5573 45.6489 23.988C45.3876 23.4187 45.2569 22.6953 45.2569 21.818C45.2569 20.9313 45.3876 20.208 45.6489 19.648C45.9196 19.0787 46.3069 18.6587 46.8109 18.388C47.3243 18.108 47.9543 17.968 48.7009 17.968C49.4476 17.968 50.0729 18.108 50.5769 18.388C51.0903 18.6587 51.4776 19.0787 51.7389 19.648C52.0003 20.208 52.1309 20.9313 52.1309 21.818C52.1309 22.6953 52.0003 23.4187 51.7389 23.988C51.4776 24.5573 51.0903 24.982 50.5769 25.262C50.0729 25.5327 49.4476 25.668 48.7009 25.668ZM48.7009 24.632C49.1769 24.632 49.5736 24.5387 49.8909 24.352C50.2083 24.1653 50.4463 23.876 50.6049 23.484C50.7729 23.0827 50.8569 22.5787 50.8569 21.972V21.664C50.8569 21.048 50.7729 20.544 50.6049 20.152C50.4463 19.76 50.2083 19.4707 49.8909 19.284C49.5736 19.0973 49.1769 19.004 48.7009 19.004C48.2249 19.004 47.8236 19.0973 47.4969 19.284C47.1796 19.4707 46.9416 19.76 46.7829 20.152C46.6243 20.544 46.5449 21.048 46.5449 21.664V21.972C46.5449 22.5787 46.6243 23.0827 46.7829 23.484C46.9416 23.876 47.1796 24.1653 47.4969 24.352C47.8236 24.5387 48.2249 24.632 48.7009 24.632ZM53.6613 25.5V18.136H54.6553L54.7673 19.242H54.8653C55.0613 18.9433 55.2806 18.7007 55.5233 18.514C55.7753 18.3273 56.0506 18.192 56.3493 18.108C56.6573 18.0147 56.984 17.968 57.3293 17.968C57.796 17.968 58.202 18.0473 58.5473 18.206C58.902 18.3647 59.1773 18.6213 59.3733 18.976C59.5786 19.3307 59.6813 19.8113 59.6813 20.418V25.5H58.4493V20.586C58.4493 20.278 58.4073 20.026 58.3233 19.83C58.2486 19.634 58.1413 19.48 58.0013 19.368C57.8706 19.256 57.7073 19.1767 57.5113 19.13C57.3246 19.0833 57.1146 19.06 56.8813 19.06C56.536 19.06 56.2093 19.144 55.9013 19.312C55.5933 19.48 55.346 19.7227 55.1593 20.04C54.982 20.3573 54.8933 20.7353 54.8933 21.174V25.5H53.6613Z" fill="white"/>
<path d="M88.9463 25.4877L92.4723 16.4961H94.3464L97.885 25.4877H96.3122L95.5128 23.4033H91.2529L90.4535 25.4877H88.9463ZM91.725 22.1578H95.028L94.019 19.523C93.993 19.4448 93.9534 19.3354 93.9012 19.1957C93.8491 19.056 93.7944 18.9028 93.7371 18.7371C93.6799 18.5713 93.6235 18.403 93.5671 18.2321C93.5099 18.0613 93.4603 17.9065 93.4165 17.7668H93.3382C93.2861 17.9326 93.2204 18.1295 93.1413 18.3567C93.0631 18.5839 92.9865 18.8027 92.9116 19.0122C92.8375 19.2218 92.7744 19.3926 92.7214 19.523L91.725 22.1578Z" fill="white"/>
<path d="M98.5137 25.4903V18.5957H99.6413L99.7591 19.5786H99.8509C100.017 19.3253 100.214 19.1158 100.441 18.9491C100.668 18.7834 100.919 18.6563 101.195 18.5688C101.47 18.4813 101.774 18.4375 102.105 18.4375C102.542 18.4375 102.927 18.5141 103.259 18.6672C103.591 18.8204 103.851 19.067 104.039 19.4078C104.227 19.7486 104.321 20.2072 104.321 20.7845V25.4903H102.958V20.994C102.958 20.7323 102.925 20.5135 102.859 20.3385C102.794 20.1643 102.699 20.0238 102.577 19.9194C102.455 19.8142 102.311 19.7402 102.145 19.6964C101.979 19.6527 101.795 19.6308 101.595 19.6308C101.28 19.6308 100.994 19.7074 100.736 19.8605C100.479 20.0137 100.271 20.2257 100.113 20.4959C99.9561 20.7668 99.8778 21.0858 99.8778 21.4527V25.4894H98.5145L98.5137 25.4903Z" fill="white"/>
<path d="M114.649 25.6473C114.168 25.6473 113.744 25.5951 113.377 25.4899C113.011 25.3847 112.704 25.2408 112.46 25.0574C112.215 24.8739 112.029 24.6534 111.903 24.3959C111.776 24.1384 111.713 23.8523 111.713 23.5376C111.713 23.4938 111.715 23.4543 111.72 23.4198C111.724 23.3853 111.726 23.3541 111.726 23.328H113.076V23.4719C113.076 23.7429 113.15 23.9592 113.299 24.1207C113.447 24.2823 113.647 24.3959 113.896 24.4616C114.145 24.5272 114.418 24.56 114.715 24.56C114.977 24.56 115.22 24.5272 115.443 24.4616C115.666 24.3959 115.849 24.2975 115.993 24.167C116.137 24.0358 116.209 23.87 116.209 23.6689C116.209 23.4071 116.117 23.206 115.934 23.0663C115.751 22.9266 115.51 22.8156 115.213 22.7322C114.916 22.6489 114.605 22.5597 114.282 22.4638C113.993 22.3855 113.707 22.2997 113.424 22.208C113.139 22.1163 112.884 21.9959 112.657 21.8478C112.43 21.6997 112.246 21.5112 112.107 21.284C111.967 21.0568 111.897 20.7732 111.897 20.4324C111.897 20.1092 111.967 19.8248 112.107 19.5808C112.246 19.3359 112.441 19.1305 112.69 18.9648C112.939 18.799 113.236 18.6719 113.581 18.5844C113.926 18.4969 114.304 18.4531 114.715 18.4531C115.125 18.4531 115.523 18.4994 115.855 18.5911C116.187 18.6829 116.468 18.8116 116.701 18.9774C116.932 19.1432 117.109 19.3426 117.232 19.574C117.354 19.8054 117.415 20.052 117.415 20.3146C117.415 20.3667 117.413 20.4214 117.408 20.4787C117.404 20.5359 117.402 20.5729 117.402 20.5897H116.064V20.4719C116.064 20.2977 116.016 20.1395 115.921 19.9998C115.825 19.8601 115.673 19.7465 115.469 19.659C115.263 19.5715 114.991 19.5277 114.65 19.5277C114.423 19.5277 114.222 19.5471 114.046 19.5866C113.871 19.6262 113.728 19.6809 113.614 19.7507C113.5 19.8206 113.415 19.9039 113.358 19.9998C113.301 20.0958 113.273 20.2052 113.273 20.3272C113.273 20.5283 113.345 20.6831 113.489 20.7925C113.633 20.9019 113.823 20.9937 114.06 21.0677C114.296 21.1418 114.549 21.2226 114.82 21.3101C115.135 21.3976 115.454 21.4868 115.777 21.5785C116.1 21.6702 116.399 21.7864 116.675 21.9261C116.95 22.0658 117.173 22.2585 117.343 22.5025C117.514 22.7474 117.599 23.0747 117.599 23.4854C117.599 23.8616 117.525 24.1889 117.376 24.4683C117.227 24.7485 117.02 24.9732 116.753 25.1432C116.486 25.3132 116.174 25.4402 115.815 25.5236C115.457 25.6069 115.068 25.6481 114.649 25.6481V25.6473Z" fill="white"/>
<path d="M121.374 25.6498C120.684 25.6498 120.107 25.5211 119.644 25.2636C119.181 25.0061 118.834 24.6106 118.602 24.0771C118.371 23.5444 118.255 22.8669 118.255 22.0456C118.255 21.2243 118.373 20.5494 118.609 20.0209C118.845 19.4925 119.197 19.0969 119.664 18.8344C120.132 18.5727 120.706 18.4414 121.388 18.4414C121.86 18.4414 122.271 18.5003 122.62 18.6181C122.969 18.7359 123.266 18.911 123.511 19.1424C123.756 19.3738 123.937 19.6599 124.055 20.0007C124.173 20.3416 124.231 20.7303 124.231 21.1671H122.856C122.856 20.8002 122.803 20.4989 122.698 20.2625C122.593 20.0268 122.431 19.8493 122.213 19.7315C121.995 19.6136 121.711 19.5547 121.362 19.5547C120.995 19.5547 120.684 19.6397 120.431 19.8106C120.178 19.9805 119.985 20.2389 119.855 20.5839C119.723 20.9289 119.658 21.3817 119.658 21.9404V22.1634C119.658 22.697 119.721 23.1396 119.848 23.4939C119.974 23.8473 120.167 24.1099 120.424 24.2799C120.682 24.4507 121.012 24.5357 121.414 24.5357C121.763 24.5357 122.045 24.4726 122.26 24.3455C122.473 24.2184 122.635 24.035 122.744 23.7951C122.854 23.5553 122.909 23.2641 122.909 22.9233H124.232C124.232 23.334 124.173 23.7059 124.056 24.0375C123.938 24.3699 123.759 24.656 123.518 24.8958C123.277 25.1365 122.978 25.3217 122.62 25.4529C122.261 25.5842 121.847 25.6498 121.374 25.6498Z" fill="white"/>
<path d="M132.052 25.4877V16.4961H133.415V25.4877H132.052Z" fill="white"/>
<path d="M137.772 25.6498C137.064 25.6498 136.471 25.5211 135.995 25.2636C135.519 25.0061 135.159 24.6106 134.914 24.0771C134.669 23.5444 134.547 22.8669 134.547 22.0456C134.547 21.2243 134.669 20.5359 134.914 20.0075C135.159 19.479 135.521 19.086 136.002 18.8277C136.482 18.5702 137.081 18.4414 137.798 18.4414C138.453 18.4414 139.001 18.566 139.443 18.815C139.884 19.0641 140.219 19.442 140.446 19.9486C140.673 20.4552 140.787 21.093 140.787 21.8622V22.3864H135.95C135.967 22.8762 136.041 23.2843 136.173 23.6117C136.304 23.939 136.503 24.1797 136.769 24.3329C137.036 24.486 137.374 24.5626 137.785 24.5626C138.038 24.5626 138.266 24.5298 138.467 24.4641C138.668 24.3985 138.838 24.3026 138.978 24.1755C139.117 24.0493 139.227 23.8961 139.305 23.7169C139.383 23.5376 139.423 23.3348 139.423 23.1076H140.76C140.76 23.5183 140.69 23.881 140.55 24.1957C140.411 24.5104 140.207 24.7747 139.941 24.9884C139.674 25.203 139.358 25.3663 138.991 25.4799C138.624 25.5935 138.218 25.6507 137.772 25.6507V25.6498ZM135.976 21.3892H139.358C139.358 21.0484 139.321 20.7623 139.247 20.5309C139.173 20.2995 139.065 20.1093 138.925 19.9603C138.786 19.8122 138.619 19.7045 138.427 19.6389C138.235 19.5732 138.017 19.5404 137.772 19.5404C137.405 19.5404 137.092 19.6061 136.834 19.7373C136.577 19.8686 136.377 20.0714 136.238 20.3466C136.098 20.6218 136.01 20.9693 135.976 21.3884V21.3892Z" fill="white"/>
<path d="M130.344 19.4212C130.142 19.1065 129.843 18.8625 129.445 18.6933C129.047 18.5225 128.551 18.4375 127.957 18.4375C127.405 18.4375 126.92 18.5124 126.5 18.6605C126.08 18.8086 125.754 19.0257 125.522 19.3093C125.291 19.5946 125.175 19.9413 125.175 20.3528V20.511C125.175 20.5548 125.179 20.5985 125.187 20.6414H126.499V20.4454C126.499 20.288 126.54 20.1424 126.624 20.0052C126.707 19.8706 126.851 19.7562 127.056 19.6644C127.262 19.5727 127.548 19.5264 127.915 19.5264C128.282 19.5264 128.564 19.5744 128.761 19.6703C128.958 19.7663 129.094 19.9017 129.168 20.0768C129.242 20.2518 129.28 20.4529 129.28 20.6801V21.3626C128.632 21.3626 128.035 21.398 127.489 21.4686C126.942 21.5377 126.472 21.6605 126.078 21.8355C125.685 22.0106 125.378 22.2513 125.16 22.5559C124.941 22.8622 124.832 23.2518 124.832 23.7239C124.832 24.1262 124.902 24.4518 125.042 24.7018C125.181 24.9508 125.363 25.1452 125.586 25.2849C125.809 25.4255 126.047 25.5222 126.301 25.5744C126.554 25.6266 126.79 25.6527 127.009 25.6527C127.368 25.6527 127.692 25.6114 127.98 25.529C128.269 25.4457 128.525 25.327 128.748 25.1747C128.97 25.0224 129.161 24.8448 129.318 24.6437V25.4962H130.644V20.5632C130.644 20.1172 130.543 19.7368 130.342 19.4221L130.344 19.4212ZM129.281 22.7932C129.281 23.0465 129.233 23.2804 129.137 23.495C129.041 23.7096 128.909 23.8931 128.743 24.0462C128.578 24.2002 128.378 24.3172 128.147 24.4005C127.915 24.4838 127.664 24.525 127.393 24.525C127.157 24.525 126.951 24.4897 126.776 24.419C126.601 24.35 126.47 24.2473 126.382 24.1119C126.295 23.9764 126.251 23.8072 126.251 23.6069C126.251 23.2737 126.377 23.0145 126.631 22.8252C126.885 22.6375 127.24 22.5088 127.694 22.4389C128.149 22.3682 128.678 22.3346 129.282 22.3346V22.794L129.281 22.7932Z" fill="white"/>
<path d="M105.409 26.7656V28.0405H106.784L107.226 26.7656H105.409Z" fill="white"/>
<path d="M108.939 25.7197L111.42 18.5938H110.029L108.304 24.0645L108.229 23.8709L106.316 18.5938H104.839L107.581 25.7416L107.227 26.7657H108.577L108.939 25.7197Z" fill="white"/>
<path d="M108.578 26.7656H107.228L106.786 28.0414H107.66C107.945 28.0414 108.199 27.8604 108.292 27.5912L108.579 26.7665L108.578 26.7656Z" fill="white"/>
<path d="M79.7498 21.2812L77.3145 25.4998H82.2189C82.399 25.4998 82.5656 25.4038 82.6565 25.2473L84.9463 21.2812H79.7498Z" fill="white"/>
<path d="M84.9463 20.7185L82.6565 16.7525C82.5665 16.5959 82.3998 16.5 82.2189 16.5H77.3145L79.7498 20.7185H84.9463Z" fill="white"/>
<path d="M72.4434 16.4996H77.3142L74.8619 12.2525C74.7719 12.0959 74.6053 12 74.4243 12H69.8447L72.4425 16.4996H72.4434Z" fill="white"/>
<path d="M69.3573 12.2812L67.0675 16.2473C66.9775 16.4038 66.9775 16.5957 67.0675 16.7522L69.5198 20.9994L71.9552 16.7808L69.3573 12.2812Z" fill="white"/>
<path d="M71.956 25.2185L69.5206 21L67.0675 25.248C66.9775 25.4045 66.9775 25.5964 67.0675 25.7529L69.3573 29.719L71.9552 25.2194L71.956 25.2185Z" fill="white"/>
<path d="M69.8447 29.9996H74.4243C74.6044 29.9996 74.7711 29.9037 74.8619 29.7471L77.3142 25.5H72.4434L69.8456 29.9996H69.8447Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

+190
View File
@@ -0,0 +1,190 @@
// Create chat-widget div
var chatWidgetDiv = document.createElement('div');
chatWidgetDiv.className = 'chat-widget';
chatWidgetDiv.innerHTML = `
<button id="openChatBtn">Ask AI</button>
`;
document.body.appendChild(chatWidgetDiv);
// Create chat-popup div
const date = new Date();
var chatPopupDiv = document.createElement('div');
chatPopupDiv.className = 'chat-popup';
chatPopupDiv.id = 'chatPopup';
chatPopupDiv.innerHTML = `
<div class="chatHeader">
<div class="header-wrapper">
<h3 class="assistant-title">Ray Docs AI - Ask a question</h3>
</div>
<button id="closeChatBtn" class="btn">
<i class="fas fa-times"></i>
</button>
</div>
<div id="chatContainer" class="chatContentContainer">
<div id="result">
Please note that the results of this bot are automated and may be incorrect or contain inappropriate information.
<div id="anchor"></div>
</div>
<div class="input-group">
<textarea id="searchBar" class="input" rows="3" placeholder="Do not include any personal or confidential information."></textarea>
<button id="searchBtn" class="btn btn-primary">Ask AI</button>
</div>
</div>
<div class="chatFooter text-right p-2">
© Copyright ${date.getFullYear()}, The Ray Team.
</div>
`;
chatPopupDiv.messages = [];
document.body.appendChild(chatPopupDiv);
const anchorDiv = document.getElementById('anchor');
const chatContainerDiv = document.getElementById('chatContainer');
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.querySelector('.search-button__wrapper').classList.remove('show');
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';
blurDiv.classList.add('blurDiv-hidden');
});
// set code highlighting options
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function (code) {
return hljs.highlight('python', code).value;
},
});
function highlightCode() {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
}
function renderCopyButtons(resultDiv) {
let preElements = resultDiv.querySelectorAll('pre');
preElements.forEach((preElement, index) => {
preElement.style.position = 'relative';
let uniqueId = `button-id-${index}`;
preElement.id = uniqueId;
// Set the proper attributes to the button to make is a sphinx copy button
let copyButton = document.createElement('button');
copyButton.className = 'copybtn o-tooltip--left';
copyButton.setAttribute('data-tooltip', 'Copy');
copyButton.setAttribute('data-clipboard-target', `#${uniqueId}`);
copyButton.style.position = 'absolute';
copyButton.style.top = '10px';
copyButton.style.right = '10px';
copyButton.style.opacity = 'inherit';
let imgElement = document.createElement('img');
imgElement.src = window.data.copyIconSrc;
imgElement.alt = 'Copy to clipboard';
copyButton.appendChild(imgElement);
preElement.appendChild(copyButton);
});
}
const searchBar = document.getElementById('searchBar');
const searchBtn = document.getElementById('searchBtn');
function rayAssistant(event) {
const resultDiv = document.getElementById('result');
const searchTerm = searchBar.value;
// handle empty input
if (!searchTerm) {
return;
}
if (
event.type === 'click' ||
(event.type === 'keydown' && event.key === 'Enter')
) {
// for improved UX, we want to equate a carriage return as hitting the send button and prevent default behavior of entering a new line
if (event.type === 'keydown' && event.key === 'Enter') {
event.preventDefault();
}
// clear search bar value and change placeholder to prompt user to ask follow up question
searchBar.value = '';
searchBar.placeholder = 'Ask follow up question here';
// Add query to the list of messages
chatPopupDiv.messages.push({role: 'user', content: searchTerm});
let msgBlock = document.createElement('div');
resultDiv.insertBefore(msgBlock, anchorDiv);
let divider = document.createElement('hr');
msgBlock.appendChild(divider);
let msgHeader = document.createElement('div');
msgHeader.style.fontWeight = 'bold';
// capitalize first letter of question header
msgHeader.innerHTML =
searchTerm.charAt(0).toUpperCase() + searchTerm.slice(1);
msgBlock.appendChild(msgHeader);
async function readStream() {
try {
const response = await fetch(
'https://ray-assistant-public-bxauk.cld-kvedzwag2qa8i5bj.s.anyscaleuserdata.com/chat',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({messages: chatPopupDiv.messages}),
}
);
const reader = response.body.getReader();
let decoder = new TextDecoder('utf-8');
let msgContent = document.createElement('div');
msgBlock.appendChild(msgContent);
msgContent.innerHTML = '';
let collectChunks = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
renderCopyButtons(resultDiv);
break;
}
const chunk = decoder.decode(value, {stream: true});
collectChunks += chunk;
let html = marked.parse(collectChunks);
html = DOMPurify.sanitize(html);
msgContent.innerHTML = html;
highlightCode();
}
chatPopupDiv.messages.push({role: 'assistant', content: collectChunks});
// we want to autoscroll to the bottom of the chat container after the answer is streamed in
chatContainerDiv.scrollTo(0, chatContainerDiv.scrollHeight);
} catch (error) {
console.error('Fetch API failed:', error);
}
}
readStream().then((res) => console.log(res));
}
}
searchBtn.addEventListener('click', rayAssistant);
searchBar.addEventListener('keydown', rayAssistant);
+83
View File
@@ -0,0 +1,83 @@
// Handlers for CSAT events
/**
* Upvote or downvote the documentation via Google Analytics.
*
* @param {string} vote 'Yes' or 'No' vote to send as feedback
*/
function sendVote(vote) {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - CSAT vote not tracked');
return;
}
window.dataLayer.push({
event: 'csat_vote',
vote_type: vote,
category: 'CSAT',
page_location: window.location.href,
page_title: document.title,
value: vote === 'Yes' ? 1 : 0
});
}
/**
* Documentation feedback via Google Analytics
* @param {string} text Text to send as feedback
*/
function sendFeedback(text) {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - CSAT feedback not tracked');
return;
}
window.dataLayer.push({
event: 'csat_feedback',
feedback_text: text.substring(0, 500),
category: 'CSAT',
page_location: window.location.href,
page_title: document.title,
feedback_length: text.length
});
}
window.addEventListener("DOMContentLoaded", () => {
const yesButton = document.getElementById("csat-yes");
const noButton = document.getElementById("csat-no");
const yesIcon = document.getElementById("csat-yes-icon");
const noIcon = document.getElementById("csat-no-icon");
const text = document.getElementById("csat-textarea");
const submitButton = document.getElementById("csat-submit");
const csatGroup = document.getElementById("csat-textarea-group");
const csatFeedbackReceived = document.getElementById("csat-feedback-received");
const csatInputs = document.getElementById("csat-inputs")
yesButton.addEventListener("click", () => {
text.placeholder = "We're glad you found it helpful. If you have any additional feedback, please let us know.";
csatGroup.classList.remove("csat-hidden");
yesIcon.classList.remove("csat-hidden");
noIcon.classList.add("csat-hidden");
yesButton.classList.add("csat-button-active");
noButton.classList.remove("csat-button-active");
submitButton.scrollIntoView();
sendVote('Yes');
})
noButton.addEventListener("click", () => {
text.placeholder = "We value your feedback. Please let us know how we can improve our documentation.";
csatGroup.classList.remove("csat-hidden");
yesIcon.classList.add("csat-hidden");
noIcon.classList.remove("csat-hidden");
yesButton.classList.remove("csat-button-active");
noButton.classList.add("csat-button-active");
submitButton.scrollIntoView();
sendVote('No');
})
submitButton.addEventListener("click", () => {
csatGroup.classList.add("csat-hidden");
csatInputs.classList.add("csat-hidden");
csatFeedbackReceived.classList.remove("csat-hidden");
sendFeedback(text.value);
}, {once: true})
})
+95
View File
@@ -0,0 +1,95 @@
let new_termynals = [];
function createTermynals() {
const containers = document.getElementsByClassName("termynal");
Array.from(containers).forEach(addTermynal);
}
function addTermynal(container) {
const t = new Termynal(container, {
noInit: true,
});
new_termynals.push(t);
}
// Initialize Termynals that are visible on the page. Once initialized, remove
// the Termynal from terminals that remain to be loaded.
function loadVisibleTermynals() {
new_termynals = new_termynals.filter(termynal => {
if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
termynal.init();
return false;
}
return true;
});
}
// Store the state of the page in the browser's local storage.
// For now this includes just the sidebar scroll position.
document.addEventListener("DOMContentLoaded", () => {
const sidebar = document.getElementById("main-sidebar")
window.addEventListener("beforeunload", () => {
if (sidebar) {
localStorage.setItem("scroll", sidebar.scrollTop)
}
})
const storedScrollPosition = localStorage.getItem("scroll")
if (storedScrollPosition) {
if (sidebar) {
sidebar.scrollTop = storedScrollPosition;
}
localStorage.removeItem("scroll");
}
})
// Send GA events any time a code block is copied
document.addEventListener("DOMContentLoaded", function() {
let codeButtons = document.querySelectorAll(".copybtn");
for (let i = 0; i < codeButtons.length; i++) {
const button = codeButtons[i];
button.addEventListener("click", function() {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - code copy not tracked');
return;
}
window.dataLayer.push({
event: "code_copy_click",
category: "ray_docs_copy_code",
page_location: window.location.href,
page_title: document.title,
button_target: button.getAttribute("data-clipboard-target") || "unknown",
value: 1,
});
});
}
});
document.addEventListener("DOMContentLoaded", function() {
let anyscaleButton = document.getElementById("try-anyscale")
if (anyscaleButton) {
anyscaleButton.onclick = () => {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - try anyscale click not tracked');
return;
}
window.dataLayer.push({
event: "try_anyscale_click",
category: "TryAnyscale",
page_location: window.location.href,
page_title: document.title,
link_url: "https://www.anyscale.com",
value: 1,
});
window.open('https://www.anyscale.com', '_blank');
}
}
});
window.addEventListener("scroll", loadVisibleTermynals);
createTermynals();
loadVisibleTermynals();
@@ -0,0 +1,24 @@
// Dismissable banner functionality
document.addEventListener('DOMContentLoaded', function () {
const banner = document.querySelector('.bd-header-announcement');
const closeButton = document.getElementById('close-banner');
const bannerKey = 'ray-docs-banner-dismissed';
// Check if banner was previously dismissed
if (localStorage.getItem(bannerKey) === 'true') {
if (banner) {
banner.style.display = 'none';
}
return;
}
// Add click handler for close button
if (closeButton) {
closeButton.addEventListener('click', function () {
if (banner) {
banner.style.display = 'none';
localStorage.setItem(bannerKey, 'true');
}
});
}
});
+209
View File
@@ -0,0 +1,209 @@
/**
* Get the status (checked/unchecked) for each filter.
*
* @returns {Object} Arrays of the name and status of each filter, grouped together into filter
* groups.
*/
function getFilterStatuses() {
const useCases = Array.from(
document.querySelectorAll('#use-case-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const libraries = Array.from(
document.querySelectorAll('#library-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const frameworks = Array.from(
document.querySelectorAll('#framework-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const contributor = Array.from(
document.querySelectorAll('#all-examples-dropdown .checkbox-container'),
).map((label) => {
const inputElement = label.querySelector('input');
return {
name: inputElement.id.replace('-checkbox', ''),
isChecked: inputElement.checked,
};
});
return {
useCases,
libraries,
frameworks,
contributor,
};
}
/**
* Test whether the tags of the given example panel match the requested filters.
*
* @param {any} tags Tags of the example panel
* @param {any} filters Filter statuses for all the filter groups; this should be the output of
* getFilterStatuses.
* @returns {bool} True if the example panel matches the filters, or not.
*/
function panelMatchesFilters(tags, filters) {
return Object.entries(filters).every(([group, groupTags]) => {
// If there is no selection, consider the panel to be matched
if (groupTags.filter(({isChecked}) => isChecked).length === 0) {
return true;
}
// If "Any" is checked, consider the panel to be matched
if (
groupTags.filter(({name, isChecked}) => name === 'any' && isChecked)
.length > 0
) {
return true;
}
// Otherwise show the panel if any checked item matches the tags of the panel
return groupTags
.filter(({isChecked}) => isChecked)
.some(({name}) => tags.includes(name));
});
}
/** Apply the currently selected filters to the example gallery, showing only the relevant examples. */
function applyFilter() {
const noMatchesElement = document.getElementById('no-matches');
const panels = document.querySelectorAll('.example');
const filters = getFilterStatuses();
const searchTerm = document
.getElementById('examples-search-input')
.value.toLowerCase();
// Show all panels before hiding the ones that need to be hidden.
panels.forEach((panel) => panel.classList.remove('hidden'));
// Check the title and tags of each example panel. If the tags match and the search term matches,
// show the panel.
panels.forEach((panel) => {
const title = panel
.querySelector('.example-title')
.textContent.toLowerCase();
const tags = panel
.querySelector('.example-tags')
.textContent.toLowerCase()
.concat();
const other_keywords = panel
.querySelector('.example-other-keywords')
.textContent.toLowerCase();
const keywords = `${tags} ${other_keywords}`;
const matchesSearch =
title.includes(searchTerm) || keywords.includes(searchTerm);
const matchesTags = panelMatchesFilters(keywords, filters);
// Hide panels that have no match
if (matchesSearch && matchesTags) {
panel.classList.remove('hidden');
} else {
panel.classList.add('hidden');
}
});
// If none are shown, show the "no matches" graphic.
if (document.querySelectorAll('.example:not(.hidden)').length === 0) {
noMatchesElement.classList.remove('hidden');
} else {
noMatchesElement.classList.add('hidden');
}
// Set the URL to match the active filters using query parameters.
const selectedTags = Object.entries(filters).map(([group, groupTags]) => {
return {
group,
selected: groupTags
.filter(({isChecked}) => isChecked)
.map(({name}) => name),
};
});
const queryParam = selectedTags
.filter(({group, selected}) => selected.length > 0)
.map(({group, selected}) => `${group}=${selected.join(',')}`)
.join('&');
history.replaceState(
null,
null,
queryParam.length === 0 ? location.pathname : `?${queryParam}`,
);
}
window.addEventListener('load', () => {
// Listen for filter checkbox clicks.
document.querySelectorAll('.filter-checkbox').forEach((tag) => {
tag.addEventListener('click', () => applyFilter());
});
// Add event listener for keypresses in the search bar.
document
.getElementById('examples-search-input')
.addEventListener('keyup', (event) => {
event.preventDefault();
applyFilter();
});
// 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) {
urlParams.forEach((params) => {
params.split(',').forEach((param) => {
const element = document.getElementById(`${param}-checkbox`);
if (element) {
element.checked = true;
}
});
});
}
// Apply the filter in case there are URL query parameters.
applyFilter();
const dropdowns = Array.from(
document.querySelectorAll('.dropdown-content'),
).map((dropdown) => {
return {
dropdown,
input: dropdown.parentNode.querySelector('input'),
inputContainer: dropdown.parentNode,
};
});
document.addEventListener('click', (event) => {
let targetEl = event.target; // clicked element
do {
const unclicked = dropdowns.filter(({dropdown, inputContainer}) => {
return !(targetEl == dropdown || targetEl == inputContainer);
});
if (unclicked.length !== dropdowns.length) {
// There has been a click inside one of the dropdowns. Close unclicked dropdowns and return.
unclicked.forEach(({input}) => {
input.checked = false;
});
return;
}
// Go up the DOM.
targetEl = targetEl.parentNode;
} while (targetEl);
// This is a click outside. Close all dropdowns.
dropdowns.forEach(({dropdown, input}) => {
input.checked = false;
});
});
});
+52
View File
@@ -0,0 +1,52 @@
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');
}
});
};
});
}
function updateHighlight() {
const {theme} = document.documentElement.dataset;
['dark', 'light'].forEach((title) => {
const stylesheet = document.querySelector(`link[title="${title}"]`);
if (title === theme) {
stylesheet.removeAttribute('disabled');
} else {
stylesheet.setAttribute('disabled', 'disabled');
}
});
}
function setHighlightListener() {
const observer = new MutationObserver((mutations) => updateHighlight());
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
}
document.addEventListener('DOMContentLoaded', animateTabs);
document.addEventListener('DOMContentLoaded', () => {
hljs.highlightAll();
updateHighlight();
});
document.addEventListener('DOMContentLoaded', setHighlightListener);
+229
View File
@@ -0,0 +1,229 @@
/**
* termynal.js
* A lightweight, modern and extensible animated terminal window, using
* async/await.
*
* @author Ines Montani <ines@ines.io>
* @version 0.0.1
* @license MIT
*/
'use strict';
/** Generate a terminal widget. */
class Termynal {
/**
* Construct the widget's settings.
* @param {(string|Node)=} container - Query selector or container element.
* @param {{noInit: boolean}} options - Custom settings.
* @param {string} options.prefix - Prefix to use for data attributes.
* @param {number} options.startDelay - Delay before animation, in ms.
* @param {number} options.typeDelay - Delay between each typed character, in ms.
* @param {number} options.lineDelay - Delay between each line, in ms.
* @param {number} options.progressLength - Number of characters displayed as progress bar.
* @param {string} options.progressChar Character to use for progress bar, defaults to █.
* @param {number} options.progressPercent - Max percent of progress.
* @param {string} options.cursor Character to use for cursor, defaults to ▋.
* @param {Object[]} lineData - Dynamically loaded line data objects.
* @param {boolean} options.noInit - Don't initialise the animation.
*/
constructor(container = '#termynal', options = {}) {
this.container = (typeof container === 'string') ? document.querySelector(container) : container;
this.pfx = `data-${options.prefix || 'ty'}`;
this.startDelay = options.startDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
this.typeDelay = options.typeDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
this.lineDelay = options.lineDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
this.progressLength = options.progressLength
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
this.progressChar = options.progressChar
|| this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
this.progressPercent = options.progressPercent
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
this.cursor = options.cursor
|| this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
this.lineData = this.lineDataToElements(options.lineData || []);
this.loadLines()
if (!options.noInit) this.init()
}
loadLines() {
// Load all the lines and create the container so that the size is fixed
// Otherwise it would be changing and the user viewport would be constantly
// moving as she/he scrolls
// Appends dynamically loaded lines to existing line elements.
this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
for (let line of this.lines) {
line.style.visibility = 'hidden'
this.container.appendChild(line)
}
const restart = this.generateRestart()
restart.style.visibility = 'hidden'
this.container.appendChild(restart)
this.container.setAttribute('data-termynal', '');
}
/**
* Initialise the widget, get lines, clear container and start animation.
*/
init() {
// Appends dynamically loaded lines to existing line elements.
//this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
/**
* Calculates width and height of Termynal container.
* If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
*/
const containerStyle = getComputedStyle(this.container);
this.container.style.width = containerStyle.width !== '0px' ?
containerStyle.width : undefined;
this.container.style.minHeight = containerStyle.height !== '0px' ?
containerStyle.height : undefined;
this.container.setAttribute('data-termynal', '');
this.container.innerHTML = '';
for (let line of this.lines) {
line.style.visibility = 'visible'
}
this.start();
}
/**
* Start the animation and rener the lines depending on their data attributes.
*/
async start() {
await this._wait(this.startDelay);
for (let line of this.lines) {
const type = line.getAttribute(this.pfx);
const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
if (type == 'input') {
line.setAttribute(`${this.pfx}-cursor`, this.cursor);
await this.type(line);
await this._wait(delay);
}
else if (type == 'progress') {
await this.progress(line);
await this._wait(delay);
}
else {
this.container.appendChild(line);
await this._wait(delay);
}
line.removeAttribute(`${this.pfx}-cursor`);
}
this.addRestart()
}
/**
* Animate a typed line.
* @param {Node} line - The line element to render.
*/
async type(line) {
const chars = [...line.textContent];
const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
line.textContent = '';
this.container.appendChild(line);
for (let char of chars) {
await this._wait(delay);
line.textContent += char;
}
}
/**
* Animate a progress bar.
* @param {Node} line - The line element to render.
*/
async progress(line) {
const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
|| this.progressLength;
const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
|| this.progressChar;
const chars = progressChar.repeat(progressLength);
const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
|| this.progressPercent;
line.textContent = '';
this.container.appendChild(line);
for (let i = 1; i < chars.length + 1; i++) {
await this._wait(this.typeDelay);
const percent = Math.round(i / chars.length * 100);
line.textContent = `${chars.slice(0, i)} ${percent}%`;
if (percent > progressPercent) {
break;
}
}
}
/**
* Helper function for animation delays, called with `await`.
* @param {number} time - Timeout, in ms.
*/
_wait(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
/**
* Converts line data objects into line elements.
*
* @param {Object[]} lineData - Dynamically loaded lines.
* @param {Object} line - Line data object.
* @returns {Element[]} - Array of line elements.
*/
lineDataToElements(lineData) {
return lineData.map(line => {
let div = document.createElement('div');
div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
return div.firstElementChild;
});
}
/**
* Helper function for generating attributes string.
*
* @param {Object} line - Line data object.
* @returns {string} - String of attributes.
*/
_attributes(line) {
let attrs = '';
for (let prop in line) {
attrs += this.pfx;
if (prop === 'type') {
attrs += `="${line[prop]}" `
} else if (prop !== 'value') {
attrs += `-${prop}="${line[prop]}" `
}
}
return attrs;
}
// Taken from Typer Tiangolo
generateRestart() {
const restart = document.createElement('a')
restart.onclick = (e) => {
e.preventDefault()
this.container.innerHTML = ''
this.init()
}
restart.href = '#'
restart.setAttribute('data-terminal-control', '')
restart.innerHTML = "restart ↻"
return restart
}
addRestart() {
const restart = this.generateRestart()
this.container.appendChild(restart)
}
}
View File
+13
View File
@@ -0,0 +1,13 @@
<!-- APIs-tab sidebar (Pattern B): a tiny container that loads ONE shared API nav
fragment (_static/api-nav.html) client-side, instead of server-rendering the
nav into every page. The fragment is generated once (see _ext/api_sidebar.py)
and hydrated/highlighted by _static/api-nav-loader.js. Reuses id="main-sidebar"
so it inherits Ray's #main-sidebar CSS. -->
<nav id="main-sidebar" class="bd-docs-nav bd-links" aria-label="{{ _('APIs Navigation') }}">
<div id="api-nav-mount" class="bd-toc-item navbar-nav"
data-api-nav-url="{{ pathto('_static/api-nav.html', 1) }}"
data-pagename="{{ pagename }}">
<p class="api-nav-status">{{ _('Loading API navigation…') }}</p>
</div>
</nav>
<script src="{{ pathto('_static/api-nav-loader.js', 1) }}" defer></script>
@@ -0,0 +1,16 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autopydantic_model:: {{ fullname }}
:members:
:inherited-members: BaseModel
:exclude-members: Config
:model-show-config-summary: False
:model-show-validator-summary: False
:model-show-field-summary: False
:field-list-validators: False
:model-show-json: False
@@ -0,0 +1,17 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autopydantic_model:: {{ fullname }}
:inherited-members: BaseModel
:exclude-members: Config
:model-show-config-summary: False
:model-show-validator-summary: False
:model-show-field-summary: False
:field-list-validators: False
:model-show-json: True
:model-summary-list-order: bysource
:undoc-members:
@@ -0,0 +1,8 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline }}
.. currentmodule:: {{ module }}
.. auto{{ objtype }}:: {{ objname }}
@@ -0,0 +1,48 @@
{#
It's a known bug (https://github.com/sphinx-doc/sphinx/issues/9884)
that autosummary will generate warning for inherited instance attributes.
Those warnings will fail our build.
For now, we don't autosummary classes with inherited instance attributes.
To opt out, use `:template: autosummary/class_without_autosummary.rst`
#}
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
:show-inheritance:
{% block methods %}
{% if methods %}
.. rubric:: {{ _('Methods') }}
.. autosummary::
:nosignatures:
:toctree:
{% for item in methods %}
{{ item | filter_out_undoc_class_members(name, module) }}
{%- endfor %}
{% endif %}
{% endblock %}
{% block attributes %}
{% if attributes %}
.. rubric:: {{ _('Attributes') }}
.. autosummary::
:nosignatures:
:toctree:
{% for item in attributes %}
~{{ name }}.{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
@@ -0,0 +1,29 @@
.. currentmodule:: {{ module }}
{% if name | has_public_constructor(module) %}
{{ name }}
{{ '-' * name | length }}
.. autoclass:: {{ objname }}
{% endif %}
{% block methods %}
{% if methods %}
{% set api_groups = methods | get_api_groups(name, module) %}
{% for api_group in api_groups %}
{% if api_groups | length > 1 %}
{{ api_group }}
{{ '-' * api_group | length }}
{% endif %}
.. autosummary::
:nosignatures:
:toctree: doc
{% for method in methods | select_api_group(name, module, api_group) %}
{{ name }}.{{ method }}
{%- endfor %}
{% endfor %}
{% endif %}
{% endblock %}
@@ -0,0 +1,10 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
:members:
:show-inheritance:
@@ -0,0 +1,11 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
:members:
:noindex:
:show-inheritance:
@@ -0,0 +1,9 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
:members:
@@ -0,0 +1,9 @@
{# Short label: fullname is the fully-qualified path (e.g. ray.data.Dataset.map);
split('.')[-1] keeps just the leaf ("map") so the API-sidebar label (and page H1)
stay readable rather than repeating the full dotted path. -#}
{{ fullname.split('.')[-1] | escape | underline}}
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}()
:members:
+25
View File
@@ -0,0 +1,25 @@
<div id="csat">
<div id="csat-feedback-received" class="csat-hidden">
<span>Thanks for the feedback!</span>
</div>
<div id="csat-inputs">
<span>Was this helpful?</span>
<div id="csat-yes" class="csat-button">
<svg id="csat-yes-icon" class="csat-hidden csat-icon" width="18" height="13" viewBox="0 0 18 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00023 10.172L16.1922 0.979004L17.6072 2.393L7.00023 13L0.63623 6.636L2.05023 5.222L7.00023 10.172Z" />
</svg>
<span>Yes</span>
</div>
<div id="csat-no" class="csat-button">
<svg id="csat-no-icon" class="csat-hidden csat-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00023 5.58599L11.9502 0.635986L13.3642 2.04999L8.41423 6.99999L13.3642 11.95L11.9502 13.364L7.00023 8.41399L2.05023 13.364L0.63623 11.95L5.58623 6.99999L0.63623 2.04999L2.05023 0.635986L7.00023 5.58599Z" />
</svg>
<span>No</span>
</div>
</div>
<div id="csat-textarea-group" class="csat-hidden">
<span id="csat-feedback-label">Feedback</span>
<textarea id="csat-textarea"></textarea>
<div id="csat-submit">Submit</div>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
{# Displays a link to the edit interface of the page source in the specified
Version Control System. #} {% if sourcename is defined and
theme_use_edit_page_button==true and page_source_suffix and
show_edit_button==true %} {% set src = sourcename.split('.') %}
<div class="tocsection editthispage">
<a href="{{ get_edit_provider_and_url()[1] }}">
<i class="fa-solid fa-pencil"></i>
{% set provider = get_edit_provider_and_url()[0] %} {% block
edit_this_page_text %} {% if provider %} {% trans provider=provider %}Edit
on {{ provider }}{% endtrans %} {% else %} {% trans %}Edit{% endtrans %} {%
endif %} {% endblock %}
</a>
</div>
{% endif %}
+13
View File
@@ -0,0 +1,13 @@
{# This page is a template used for all Ray library example pages. #}
<!-- prettier-ignore -->
{% extends "!layout.html" %}
{%- block extrahead -%}
{% include 'extrahead.html' %}
{{ super() }}
{% endblock %}
<!-- prettier-ignore -->
{% block body %}
{{ render_library_examples() }}
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
<!-- Extra header to include at the top of each template.
Kept separately so that it can easily be included in any templates
that need to be overridden for individual pages; e.g. included
both in the usual template (layout.html) as well as (index.html). -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;900&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
title="light"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
disabled="disabled"
/>
<link
rel="stylesheet"
title="dark"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
disabled="disabled"
/>
<link
href="https://cdn.jsdelivr.net/npm/remixicon@4.1.0/fonts/remixicon.css"
rel="stylesheet"
/>
<!-- Used for text embedded in html on the index page -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Parser used to call hljs on responses from Ray Assistant -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Sanitizer for Ray Assistant AI -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.3/purify.min.js"></script>
<!-- Fathom - beautiful, simple website analytics -->
<script src="https://deer.ray.io/script.js" data-site="WYYANYOS" defer></script>
<!-- / Fathom -->
<!-- Google Tag Manager -->
<script>
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-N7VD67MZ');
</script>
<!-- End Google Tag Manager -->
<!-- Data to be shared with JS on every page -->
<script>
window.data = {
copyIconSrc: "{{ pathto('_static/copy-button.svg', 1) }}",
};
</script>
<!-- Herald Widget -->
<script
type="module"
id="runllm-widget-script"
src="https://widget.runllm.com"
version="stable"
crossorigin="anonymous"
runllm-keyboard-shortcut="Mod+j"
runllm-name="Ray Docs"
runllm-position="BOTTOM_RIGHT"
runllm-assistant-id="1003"
></script>
+511
View File
@@ -0,0 +1,511 @@
{% extends "!layout.html" %}
<!-- prettier-ignore -->
{%- block extrahead -%}
{% include 'extrahead.html' %}
{{ super() }}
{% endblock %}
{% block body %}
<div class="main-content">
<div class="centered-heading">
<h1>Welcome to Ray</h1>
<p>
An open source framework to build and scale your ML and Python
applications easily
</p>
</div>
<div class="heading-buttons">
<a href="{{ pathto('ray-overview/getting-started') }}">
<div class="header-button">
<i class="ri-play-line"></i>
<span>Get started with Ray</span>
</div>
</a>
<a href="{{ pathto('ray-overview/installation') }}">
<div class="header-button">
<i class="ri-install-line"></i>
<span>Install Ray</span>
</div>
</a>
<a href="{{ pathto('ray-overview/examples') }}">
<div class="header-button">
<i class="ri-code-block"></i>
<span>Ray Example Gallery</span>
</div>
</a>
</div>
<div class="clicky-tab-widget">
<h3>Scale with Ray</h3>
<div class="clicky-tab-side-by-side">
<div class="nav flex-column nav-pills" id="v-pills-tab">
<a class="nav-link active" id="v-pills-batch-tab">Batch inference</a>
<a class="nav-link" id="v-pills-training-tab">Model training</a>
<a class="nav-link" id="v-pills-tuning-tab">Hyperparameter tuning</a>
<a class="nav-link" id="v-pills-serving-tab">Model serving</a>
<a class="nav-link" id="v-pills-rl-tab">Reinforcement learning</a>
</div>
<div class="tab-area">
<div class="tab-content" id="v-pills-tabContent">
<!-- prettier-ignore -->
<div class="tab-pane fade show active no-copybutton" id="v-pills-data">
{{ pygments_highlight_python('''
from typing import Dict
import numpy as np
import ray
# Step 1: Create a Ray Dataset from in-memory Numpy arrays.
ds = ray.data.from_numpy(np.asarray(["Complete this", "for me"]))
# Step 2: Define a Predictor class for inference.
class HuggingFacePredictor:
def __init__(self):
from transformers import pipeline
# Initialize a pre-trained GPT2 Huggingface pipeline.
self.model = pipeline("text-generation", model="gpt2")
# Logic for inference on 1 batch of data.
def __call__(self, batch: Dict[str, np.ndarray]) -> Dict[str, list]:
# Get the predictions from the input batch.
predictions = self.model(
list(batch["data"]), max_length=20, num_return_sequences=1)
# `predictions` is a list of length-one lists. For example:
# [[{"generated_text": "output_1"}], ..., [{"generated_text": "output_2"}]]
# Modify the output to get it into the following format instead:
# ["output_1", "output_2"]
batch["output"] = [sequences[0]["generated_text"] for sequences in predictions]
return batch
# Use 2 parallel actors for inference. Each actor predicts on a
# different partition of data.
# Step 3: Map the Predictor over the Dataset to get predictions.
predictions = ds.map_batches(HuggingFacePredictor, compute=ray.data.ActorPoolStrategy(size=2))
# Step 4: Show one prediction output.
predictions.show(limit=1)
''') }}
<div class="tab-pane-links">
<a href="{{ pathto('data/data') }}" target="_blank">Learn more about Ray Data</a>
<a href="{{ pathto('data/examples') }}" target="_blank">Examples</a>
</div>
</div>
<!-- prettier-ignore -->
<div class="tab-pane fade no-copybutton" id="v-pills-training">
{{ pygments_highlight_python('''
from ray.train import ScalingConfig
from ray.train.torch import TorchTrainer
# Step 1: Set up PyTorch model training as you normally would.
def train_func():
model = ...
train_dataset = ...
for epoch in range(num_epochs):
... # model training logic
# Step 2: Set up Ray\'s PyTorch Trainer to run on 32 GPUs.
trainer = TorchTrainer(
train_loop_per_worker=train_func,
scaling_config=ScalingConfig(num_workers=32, use_gpu=True),
datasets={"train": train_dataset},
)
# Step 3: Run distributed model training on 32 GPUs.
result = trainer.fit()
''') }}
<div class="tab-pane-links">
<a href="{{ pathto('train/train') }}" target="_blank">Learn more about Ray Train</a>
<a href="{{ pathto('train/examples') }}" target="_blank">Examples</a>
</div>
</div>
<!-- prettier-ignore -->
<div class="tab-pane fade no-copybutton" id="v-pills-tuning">
{{ pygments_highlight_python('''
from ray import tune
# Step 1: Define an objective function to optimize.
def objective(config):
# Train model with config hyperparameters
model = train_model(
lr=config["lr"],
batch_size=config["batch_size"],
num_layers=config["num_layers"],
)
accuracy = evaluate_model(model)
# Report metrics back to Tune
tune.report(accuracy=accuracy)
# Step 2: Define hyperparameter search space.
search_space = {
"lr": tune.loguniform(1e-4, 1e-1),
"batch_size": tune.choice([32, 64, 128]),
"num_layers": tune.randint(1, 10),
}
# Step 3: Configure and run 1000 trials with Ray Tune.
tuner = tune.Tuner(
objective,
param_space=search_space,
tune_config=tune.TuneConfig(num_samples=1000),
)
result_grid = tuner.fit()
best_result = result_grid.get_best_result(metric="accuracy", mode="max")
''') }}
<div class="tab-pane-links">
<a href="{{ pathto('tune/index') }}" target="_blank">Learn more about Ray Tune</a>
<a href="{{ pathto('tune/examples/index') }}" target="_blank">Examples</a>
</div>
</div>
<!-- prettier-ignore -->
<div class="tab-pane fade no-copybutton" id="v-pills-serving">
{{ pygments_highlight_python('''
from io import BytesIO
from fastapi import FastAPI
from fastapi.responses import Response
import torch
from ray import serve
from ray.serve.handle import DeploymentHandle
app = FastAPI()
@serve.deployment(num_replicas=1)
@serve.ingress(app)
class APIIngress:
def __init__(self, diffusion_model_handle: DeploymentHandle) -> None:
self.handle = diffusion_model_handle
@app.get(
"/imagine",
responses={200: {"content": {"image/png": {}}}},
response_class=Response,
)
async def generate(self, prompt: str, img_size: int = 512):
assert len(prompt), "prompt parameter cannot be empty"
image = await self.handle.generate.remote(prompt, img_size=img_size)
file_stream = BytesIO()
image.save(file_stream, "PNG")
return Response(content=file_stream.getvalue(), media_type="image/png")
@serve.deployment(
ray_actor_options={"num_gpus": 1},
autoscaling_config={"min_replicas": 0, "max_replicas": 2},
)
class StableDiffusionV2:
def __init__(self):
from diffusers import EulerDiscreteScheduler, StableDiffusionPipeline
model_id = "stabilityai/stable-diffusion-2"
scheduler = EulerDiscreteScheduler.from_pretrained(
model_id, subfolder="scheduler"
)
self.pipe = StableDiffusionPipeline.from_pretrained(
model_id, scheduler=scheduler, revision="fp16", torch_dtype=torch.float16
)
self.pipe = self.pipe.to("cuda")
def generate(self, prompt: str, img_size: int = 512):
assert len(prompt), "prompt parameter cannot be empty"
with torch.autocast("cuda"):
image = self.pipe(prompt, height=img_size, width=img_size).images[0]
return image
entrypoint = APIIngress.bind(StableDiffusionV2.bind())
''') }}
<div class="tab-pane-links">
<a href="{{ pathto('serve/index') }}" target="_blank">Learn more about Ray Serve</a>
<a href="https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=scale_with_ray&redirectTo=/v2/template-preview/serve-stable-diffusion-v2" target="_blank">Quickstart</a>
</div>
</div>
<!-- prettier-ignore -->
<div class="tab-pane fade no-copybutton" id="v-pills-rl">
{{ pygments_highlight_python('''
from ray.rllib.algorithms.ppo import PPOConfig
# Step 1: Configure PPO to run 64 parallel workers to collect samples from the env.
ppo_config = (
PPOConfig()
.environment(env="Taxi-v3")
.rollouts(num_rollout_workers=64)
.framework("torch")
.training(model=rnn_lage)
)
# Step 2: Build the PPO algorithm.
ppo_algo = ppo_config.build()
# Step 3: Train and evaluate PPO.
for _ in range(5):
print(ppo_algo.train())
ppo_algo.evaluate()
''') }}
<div class="tab-pane-links">
<a href="{{ pathto('rllib/index') }}" target="_blank">Learn more about Ray RLlib</a>
<a href="{{ pathto('rllib/rllib-examples') }}" target="_blank">Examples</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-area">
<h3>Beyond the basics</h3>
<div class="card-row">
<div class="link-card">
<div class="link-card-icon-label">
<div class="card-icon">
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_129_1153)">
<path
d="M28 7H4C3.44772 7 3 7.44772 3 8V11C3 11.5523 3.44772 12 4 12H28C28.5523 12 29 11.5523 29 11V8C29 7.44772 28.5523 7 28 7Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M27 12V24C27 24.2652 26.8946 24.5196 26.7071 24.7071C26.5196 24.8946 26.2652 25 26 25H6C5.73478 25 5.48043 24.8946 5.29289 24.7071C5.10536 24.5196 5 24.2652 5 24V12"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M13 17H19"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_129_1153">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<h4>Ray Libraries</h4>
</div>
<div class="card-text-area">
<p>
Scale the entire ML pipeline from data ingest to model serving with
high-level Python APIs that integrate with popular ecosystem
frameworks.
</p>
<a
href="{{ pathto('ray-overview/getting-started') }}"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div class="link-card">
<div class="link-card-icon-label">
<div class="card-icon">
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_129_1163)">
<path
d="M13.0003 15.8675C12.2009 14.4208 11.8692 12.762 12.0509 11.1192C12.2325 9.47632 12.9185 7.93006 14.0147 6.69294C15.1108 5.45582 16.5632 4.58859 18.1722 4.21046C19.7812 3.83233 21.4679 3.96186 23.0003 4.58125L18.0003 10L18.7078 13.2925L22.0003 14L27.4191 9C28.0385 10.5324 28.168 12.2191 27.7899 13.8281C27.4117 15.4371 26.5445 16.8895 25.3074 17.9857C24.0703 19.0818 22.524 19.7678 20.8811 19.9495C19.2383 20.1311 17.5795 19.7994 16.1328 19L9.12532 27.125C8.56174 27.6886 7.79735 28.0052 7.00032 28.0052C6.20329 28.0052 5.43891 27.6886 4.87532 27.125C4.31174 26.5614 3.99512 25.797 3.99512 25C3.99512 24.203 4.31174 23.4386 4.87532 22.875L13.0003 15.8675Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_129_1163">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<h4>Ray Core</h4>
</div>
<div class="card-text-area">
<p>
Scale generic Python code with simple, foundational primitives that
enable a high degree of control for building distributed
applications or custom platforms.
</p>
<a href="{{ pathto('ray-core/walkthrough') }}" target="_blank">
Learn more
</a>
</div>
</div>
<div class="link-card">
<div class="link-card-icon-label">
<div class="card-icon">
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_129_1171)">
<path
d="M14 23H4C3.46957 23 2.96086 22.7893 2.58579 22.4142C2.21071 22.0391 2 21.5304 2 21V12C2 11.4696 2.21071 10.9609 2.58579 10.5858C2.96086 10.2107 3.46957 10 4 10H14"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 27H8"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M26 9H22"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M26 13H22"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M29 5H19C18.4477 5 18 5.44772 18 6V26C18 26.5523 18.4477 27 19 27H29C29.5523 27 30 26.5523 30 26V6C30 5.44772 29.5523 5 29 5Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11 23V27"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
id="link-card-icon-filled"
d="M24 24C24.8284 24 25.5 23.3284 25.5 22.5C25.5 21.6716 24.8284 21 24 21C23.1716 21 22.5 21.6716 22.5 22.5C22.5 23.3284 23.1716 24 24 24Z"
/>
</g>
<defs>
<clipPath id="clip0_129_1171">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<h4>Ray Clusters</h4>
</div>
<div class="card-text-area">
<p>
Deploy a Ray cluster on AWS, GCP, Azure, or Kubernetes to seamlessly
scale workloads for production.
</p>
<a href="{{ pathto('cluster/getting-started') }}" target="_blank">
Learn more
</a>
</div>
</div>
</div>
</div>
<div class="links-grid-wrapper">
<h3>Getting involved</h3>
<div class="links-grid">
<b>Join the community</b>
<b>Get support</b>
<b>Contribute to Ray</b>
<a
class="community-box"
href="https://www.meetup.com/Bay-Area-Ray-Meetup/"
target="_blank"
>
<i class="ri-team-line"></i>
<p>Attend community events</p>
</a>
<a
class="community-box"
href="https://www.ray.io/join-slack?utm_source=ray_docs&utm_medium=docs"
target="_blank"
>
<i class="ri-slack-line"></i>
<p>Find community on Slack</p>
</a>
<a
class="community-box"
href="./ray-contribute/getting-involved.html"
target="_blank"
>
<i class="ri-send-plane-2-line"></i>
<p>Contributor's guide</p>
</a>
<a
class="community-box"
href="https://share.hsforms.com/1Ee3Gh8c9TY69ZQib-yZJvgc7w85"
target="_blank"
>
<i class="ri-send-plane-2-line"></i>
<p>Subscribe to the newsletter</p>
</a>
<a class="community-box" href="https://discuss.ray.io/" target="_blank">
<i class="ri-chat-4-line"></i>
<p>Ask questions on the forum</p>
</a>
<a
class="community-box"
href="https://github.com/ray-project/ray/pulls"
target="_blank"
>
<i class="ri-github-line"></i>
<p>Create pull requests</p>
</a>
<a
class="community-box"
href="https://x.com/raydistributed"
target="_blank"
>
<i class="ri-twitter-line"></i>
<p>Follow us on Twitter</p>
</a>
<a
class="community-box"
href="https://github.com/ray-project/ray/issues/new/choose"
target="_blank"
>
<i class="ri-github-line"></i>
<p>Open an issue</p>
</a>
</div>
</div>
</div>
{{ super() }} {% endblock %}
+6
View File
@@ -0,0 +1,6 @@
<!-- prettier-ignore -->
{% extends "!layout.html" %}
{%- block extrahead -%}
{% include 'extrahead.html' %}
{{ super() }}
{% endblock %}
@@ -0,0 +1,3 @@
<nav id="main-sidebar" class="bd-docs-nav bd-links" aria-label="{{ _('Section Navigation') }}">
<div class="bd-toc-item navbar-nav">{{ cached_toctree }}</div>
</nav>
+3
View File
@@ -0,0 +1,3 @@
<nav id="main-sidebar" class="bd-docs-nav bd-links" aria-label="{{ _('Section Navigation') }}">
<div class="bd-toc-item navbar-nav">{{ generate_toctree_html("sidebar", show_nav_level=0, startdepth=0, maxdepth=4, collapse=False, includehidden=True, titles_only=True) }}</div>
</nav>
@@ -0,0 +1,10 @@
<a
id="try-anyscale-href"
href="https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=navbar"
target="_blank"
rel="noopener noreferrer"
>
<div id="try-anyscale-text">
<span>Try Managed Ray</span>
</div>
</a>
+9
View File
@@ -0,0 +1,9 @@
<nav class="navbar-nav">
<p class="sidebar-header-items__title"
role="heading"
aria-level="1"
aria-label="{{ _('Site Navigation') }}">
{{ _("Site Navigation") }}
</p>
{{ render_header_nav_links() }}
</nav>
@@ -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 #}
<a class="navbar-brand logo" href="{{ href }}">
{{ theme_logo["svg"] }}
</a>
@@ -0,0 +1,175 @@
<!-- prettier-ignore -->
{% extends "!layout.html" %}
{%- block extrahead -%}
{% include 'extrahead.html' %}
{{ super() }}
{% endblock %}
<!-- prettier-ignore -->
{% block body %}
{# Main example gallery for Ray. Examples here are pulled in from the individual Ray library
`examples.html` pages, which themselves are built from the `examples.yml` file for the library. #}
<div class="content-wrapper">
<div class="content">
<div class="examples-search-area">
<label id="examples-search-input-label" for="examples-search-input">
<svg
id="examples-search-icon"
width="25"
height="25"
viewBox="0 0 25 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
/>
</svg>
</label>
<input
type="text"
id="examples-search-input"
class="examples-search-term"
placeholder="Search examples"
/>
</div>
<div id="dropdown-area">
{{ render_use_cases_dropdown() }} {{ render_libraries_dropdown() }} {{
render_frameworks_dropdown() }} {{ render_contributor_dropdown() }}
</div>
<div id="no-matches" class="hidden">
<div id="no-matches-inner-content">
<svg
width="119"
height="119"
viewBox="0 0 119 119"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="59.5"
cy="59.5"
r="59.5"
fill="url(#paint0_linear_362_3841)"
fill-opacity="0.2"
/>
<path
d="M19.1665 6.66663C19.1665 5.8382 19.8381 5.16663 20.6665 5.16663H58.9165H78.0702C78.5301 5.16663 78.9647 5.37767 79.2491 5.73919L88.8648 17.9624L88.8658 17.9637L98.3484 30.0984C98.5545 30.3622 98.6665 30.6873 98.6665 31.0221V56V105.333C98.6665 106.162 97.9949 106.833 97.1665 106.833H20.6665C19.8381 106.833 19.1665 106.162 19.1665 105.333V6.66663Z"
fill="#FBFEFF"
stroke="#D0EAF9"
/>
<path
d="M77.485 29.4856C76.3051 29.4097 75.4482 28.3324 75.6397 27.1657L79.2591 5.11847C79.2874 4.94627 79.5072 4.89033 79.6143 5.02808L99.477 30.5658C99.5829 30.702 99.4784 30.8993 99.3063 30.8882L77.485 29.4856Z"
fill="#11608D"
/>
<rect
x="25.6665"
y="15.1666"
width="46.6667"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<rect
x="25.6665"
y="31.5"
width="46.6667"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<rect
x="25.6665"
y="23.3334"
width="16.3333"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<rect
x="25.6665"
y="39.6666"
width="16.3333"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<rect
x="45.5"
y="23.3334"
width="26.8333"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<rect
x="22.1665"
y="98"
width="30.3333"
height="3.5"
rx="1.75"
fill="#D6EEFC"
/>
<g clip-path="url(#clip0_362_3841)">
<path
d="M90.5158 91.8128L104.257 105.551L99.7173 110.091L85.9792 96.3494C80.8675 100.447 74.5094 102.676 67.958 102.667C52.019 102.667 39.083 89.7306 39.083 73.7916C39.083 57.8526 52.019 44.9166 67.958 44.9166C83.897 44.9166 96.833 57.8526 96.833 73.7916C96.8423 80.343 94.6135 86.7011 90.5158 91.8128ZM84.0799 89.4323C88.1516 85.245 90.4255 79.6322 90.4163 73.7916C90.4163 61.3818 80.3646 51.3333 67.958 51.3333C55.5482 51.3333 45.4997 61.3818 45.4997 73.7916C45.4997 86.1982 55.5482 96.25 67.958 96.25C73.7985 96.2592 79.4114 93.9852 83.5986 89.9135L84.0799 89.4323Z"
fill="#60ABD7"
/>
</g>
<ellipse
cx="61.8335"
cy="71.1667"
rx="3.5"
ry="4.66667"
fill="#60ABD7"
/>
<ellipse
cx="73.5"
cy="71.1667"
rx="3.5"
ry="4.66667"
fill="#60ABD7"
/>
<path
d="M60.6665 84.5834V84.5834C65.0731 82.3801 70.2599 82.3801 74.6665 84.5834V84.5834"
stroke="#60ABD7"
stroke-width="2"
/>
<defs>
<linearGradient
id="paint0_linear_362_3841"
x1="59.5"
y1="0"
x2="59.5"
y2="119"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#19B1E2" />
<stop offset="1" stop-color="#3D89E9" />
</linearGradient>
<clipPath id="clip0_362_3841">
<rect
width="77"
height="77"
fill="white"
transform="translate(32.6665 38.5)"
/>
</clipPath>
</defs>
</svg>
<h4>Sorry! We could not find an example matching that filter.</h4>
<a
id="new-example-issue-link"
href="https://github.com/ray-project/ray/issues/new?assignees=&labels=docs%2Ctriage&projects=&template=documentation-issue.yml&title=%5B%3CRay+component%3A+Core%7CRLlib%7Cetc...%3E%5D+"
>
<span id="new-example-issue-text">
Help us improve our examples by suggesting one. Tell us what example
you would like to have.
</span>
</a>
</div>
</div>
{{ render_example_gallery() }} {% endblock %}
</div>
</div>
+154
View File
@@ -0,0 +1,154 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3efe505a",
"metadata": {},
"source": [
"(document-tag-to-refer-to)=\n",
"\n",
"# Creating an Example\n",
"\n",
"This is an example template file for writing Jupyter Notebooks in markdown, using MyST.\n",
"For more information on MyST notebooks, see the\n",
"[MyST-NB documentation](https://myst-nb.readthedocs.io/en/latest/index.html).\n",
"If you want to learn more about the MyST parser, see the\n",
"[MyST documentation](https://myst-parser.readthedocs.io/en/latest/).\n",
"\n",
"MyST is CommonMark compliant, so you can use plain markdown here.\n",
"In case you need to execute restructured text (rST) directives, you can use `{eval-rst}` to execute the code.\n",
"For instance, here's a note written in rST:\n",
"\n",
"```{eval-rst}\n",
".. note::\n",
"\n",
" A note written in reStructuredText.\n",
"```\n",
"\n",
"```{margin}\n",
"You can create margins with this syntax for smaller notes that don't make it into the main\n",
"text.\n",
"```\n",
"\n",
"You can also easily define footnotes.[^example]\n",
"\n",
"[^example]: This is a footnote.\n",
"\n",
"## Adding code cells"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6b6ccdcf",
"metadata": {},
"outputs": [],
"source": [
"import ray\n",
"import ray.rllib.agents.ppo as ppo\n",
"from ray import serve\n",
"\n",
"def train_ppo_model():\n",
" trainer = ppo.PPOTrainer(\n",
" config={\"framework\": \"torch\", \"num_workers\": 0},\n",
" env=\"CartPole-v0\",\n",
" )\n",
" # Train for one iteration\n",
" trainer.train()\n",
" trainer.save(\"/tmp/rllib_checkpoint\")\n",
" return \"/tmp/rllib_checkpoint/checkpoint_000001/checkpoint-1\"\n",
"\n",
"\n",
"checkpoint_path = train_ppo_model()"
]
},
{
"cell_type": "markdown",
"id": "c21b9968",
"metadata": {},
"source": [
"## Hiding and removing cells\n",
"\n",
"You can hide cells, so that they toggle when you click the cell header.\n",
"You can use different `:tags:` like `hide-cell`, `hide-input`, or `hide-output` to hide cell content,\n",
"and you can use `remove-cell`, `remove-input`, or `remove-output` to completely remove the cell when rendered.\n",
"Those cells still show up in the notebook itself."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "607e444a",
"metadata": {
"tags": [
"hide-cell"
]
},
"outputs": [],
"source": [
"# This can be useful if you don't want to clutter the page with details.\n",
"\n",
"import ray\n",
"import ray.rllib.agents.ppo as ppo\n",
"from ray import serve"
]
},
{
"cell_type": "markdown",
"id": "0f4c428c",
"metadata": {},
"source": [
":::{tip}\n",
"Here's a quick tip.\n",
":::\n",
"\n",
"\n",
":::{note}\n",
"And this is a note.\n",
":::\n",
"\n",
"The following cell doesn't render:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f008643b",
"metadata": {
"tags": [
"remove-cell"
]
},
"outputs": [],
"source": [
"ray.shutdown()"
]
},
{
"cell_type": "markdown",
"id": "c206f666",
"metadata": {},
"source": [
"## Equations\n",
"\n",
"\\begin{equation}\n",
"\\frac {\\partial u}{\\partial x} + \\frac{\\partial v}{\\partial y} = - \\, \\frac{\\partial w}{\\partial z}\n",
"\\end{equation}\n",
"\n",
"\\begin{align*}\n",
"2x - 5y &= 8 \\\\\n",
"3x + 9y &= -12\n",
"\\end{align*}"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+98
View File
@@ -0,0 +1,98 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
kernelspec:
display_name: Python 3
language: python
name: python3
---
(document-tag-to-refer-to)=
# Creating an Example
This is an example template file for writing Jupyter Notebooks in markdown, using MyST. For more information on MyST notebooks, see the [MyST-NB documentation](https://myst-nb.readthedocs.io/en/latest/index.html). If you want to learn more about the MyST parser, see the [MyST documentation](https://myst-parser.readthedocs.io/en/latest/).
MyST is CommonMark compliant, so you can use plain markdown here. In case you need to execute restructured text (rST) directives, you can use `{eval-rst}` to execute the code. For instance, here's a note written in rST:
```{eval-rst}
.. note::
A note written in reStructuredText.
```
```{margin}
You can create margins with this syntax for smaller notes that don't make it into the main
text.
```
You can also easily define footnotes.[^example]
[^example]: This is a footnote.
## Adding code cells
```{code-cell} python3
import ray
import ray.rllib.agents.ppo as ppo
from ray import serve
def train_ppo_model():
trainer = ppo.PPOTrainer(
config={"framework": "torch", "num_workers": 0},
env="CartPole-v0",
)
# Train for one iteration
trainer.train()
trainer.save("/tmp/rllib_checkpoint")
return "/tmp/rllib_checkpoint/checkpoint_000001/checkpoint-1"
checkpoint_path = train_ppo_model()
```
## Hiding and removing cells
You can hide cells, so that they toggle when you click the cell header. You can use different `:tags:` like `hide-cell`, `hide-input`, or `hide-output` to hide cell content, and you can use `remove-cell`, `remove-input`, or `remove-output` to completely remove the cell when rendered. Those cells still show up in the notebook itself.
```{code-cell} python3
:tags: [hide-cell]
# This can be useful if you don't want to clutter the page with details.
import ray
import ray.rllib.agents.ppo as ppo
from ray import serve
```
:::{tip}
Here's a quick tip.
:::
:::{note}
And this is a note.
:::
The following cell doesn't render:
```{code-cell} python3
:tags: [remove-cell]
ray.shutdown()
```
## Equations
\begin{equation}
\frac {\partial u}{\partial x} + \frac{\partial v}{\partial y} = - \, \frac{\partial w}{\partial z}
\end{equation}
\begin{align*}
2x - 5y &= 8 \\
3x + 9y &= -12
\end{align*}
+15
View File
@@ -0,0 +1,15 @@
(ray-apis)=
# Ray APIs
API reference for Ray's libraries. Select a library below, or browse the current library's API from the sidebar.
```{toctree}
:maxdepth: 2
Ray Data </data/api/api>
Ray Train </train/api/api>
Ray Tune </tune/api/api>
Ray Serve </serve/api/index>
Ray RLlib </rllib/package_ref/index>
Ray Core </ray-core/api/index>
```
+59
View File
@@ -0,0 +1,59 @@
.. _ray-cluster-cli:
Cluster Management CLI
======================
This section contains commands for managing Ray clusters.
.. _ray-start-doc:
.. click:: ray.scripts.scripts:start
:prog: ray start
:show-nested:
.. _ray-stop-doc:
.. click:: ray.scripts.scripts:stop
:prog: ray stop
:show-nested:
.. _ray-up-doc:
.. click:: ray.scripts.scripts:up
:prog: ray up
:show-nested:
.. _ray-down-doc:
.. click:: ray.scripts.scripts:down
:prog: ray down
:show-nested:
.. _ray-exec-doc:
.. click:: ray.scripts.scripts:exec
:prog: ray exec
:show-nested:
.. _ray-submit-doc:
.. click:: ray.scripts.scripts:submit
:prog: ray submit
:show-nested:
.. _ray-attach-doc:
.. click:: ray.scripts.scripts:attach
:prog: ray attach
:show-nested:
.. _ray-get_head_ip-doc:
.. click:: ray.scripts.scripts:get_head_ip
:prog: ray get_head_ip
:show-nested:
.. _ray-monitor-doc:
.. click:: ray.scripts.scripts:monitor
:prog: ray monitor
:show-nested:
@@ -0,0 +1,284 @@
(observability-configure-manage-dashboard)=
# Configuring and Managing Ray Dashboard
{ref}`Ray Dashboard<observability-getting-started>` is one of the most important tools to monitor and debug Ray applications and Clusters. This page describes how to configure Ray Dashboard on your Clusters.
Dashboard configurations may differ depending on how you launch Ray Clusters (e.g., local Ray Cluster vs. KubeRay). Integrations with Prometheus and Grafana are optional for enhanced Dashboard experience.
:::{note}
Ray Dashboard is useful for interactive development and debugging because when clusters terminate, the dashboard UI and the underlying data are no longer accessible. For production monitoring and debugging, you should rely on [persisted logs](../cluster/kubernetes/user-guides/persist-kuberay-custom-resource-logs.md), [persisted metrics](./metrics.md), [persisted Ray states](../ray-observability/user-guides/cli-sdk.rst), and other observability tools.
:::
## Changing the Ray Dashboard port
Ray Dashboard runs on port `8265` of the head node. Follow the instructions below to customize the port if needed.
::::{tab-set}
:::{tab-item} Single-node local cluster
**Start the cluster explicitly with CLI** <br/> Pass the ``--dashboard-port`` argument with ``ray start`` in the command line.
**Start the cluster implicitly with `ray.init`** <br/> Pass the keyword argument ``dashboard_port`` in your call to ``ray.init()``.
:::
:::{tab-item} VM Cluster Launcher
Include the ``--dashboard-port`` argument in the `head_start_ray_commands` section of the [Cluster Launcher's YAML file](https://github.com/ray-project/ray/blob/0574620d454952556fa1befc7694353d68c72049/python/ray/autoscaler/aws/example-full.yaml#L172).
```yaml
head_start_ray_commands:
- ray stop
# Replace ${YOUR_PORT} with the port number you need.
- ulimit -n 65536; ray start --head --dashboard-port=${YOUR_PORT} --port=6379 --object-manager-port=8076 --autoscaling-config=~/ray_bootstrap_config.yaml
```
:::
:::{tab-item} KubeRay
View the [specifying non-default ports](https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/config.html#specifying-non-default-ports) page for details.
:::
::::
(dashboard-in-browser)=
## Viewing Ray Dashboard in browsers
When you start a single-node Ray cluster on your laptop, you can access the dashboard through a URL printed when Ray is initialized (the default URL is `http://localhost:8265`).
When you start a remote Ray cluster with the {ref}`VM cluster launcher <vm-cluster-quick-start>`, {ref}`KubeRay operator <kuberay-quickstart>`, or manual configuration, the Ray Dashboard launches on the head node but the dashboard port may not be publicly exposed. You need an additional setup to access the Ray Dashboard from outside the head node.
:::{danger}
For security purposes, do not expose Ray Dashboard publicly without proper authentication in place.
:::
::::{tab-set}
:::{tab-item} VM Cluster Launcher
**Port forwarding** <br/> You can securely port-forward local traffic to the dashboard with the ``ray dashboard`` command.
```shell
$ ray dashboard [-p <port, 8265 by default>] <cluster config file>
```
The dashboard is now visible at ``http://localhost:8265``.
:::
:::{tab-item} KubeRay
The KubeRay operator makes Dashboard available via a Service targeting the Ray head pod, named ``<RayCluster name>-head-svc``. Access Dashboard from within the Kubernetes cluster at ``http://<RayCluster name>-head-svc:8265``.
There are two ways to expose Dashboard outside the Cluster:
**1. Setting up ingress** <br/> Follow the [instructions](kuberay-ingress) to set up ingress to access Ray Dashboard. **The Ingress must only allows access from trusted sources.**
**2. Port forwarding** <br/> You can also view the dashboard from outside the Kubernetes cluster by using port-forwarding:
```shell
$ kubectl port-forward service/${RAYCLUSTER_NAME}-head-svc 8265:8265
# Visit ${YOUR_IP}:8265 for the Dashboard (e.g. 127.0.0.1:8265 or ${YOUR_VM_IP}:8265)
```
```{admonition} Note
:class: note
Do not use port forwarding for production environment. Follow the instructions above to expose the Dashboard with Ingress.
```
For more information about configuring network access to a Ray cluster on Kubernetes, see the {ref}`networking notes <kuberay-networking>`.
:::
::::
## Running behind a reverse proxy
Ray Dashboard should work out-of-the-box when accessed via a reverse proxy. API requests don't need to be proxied individually.
Always access the dashboard with a trailing ``/`` at the end of the URL. For example, if your proxy is set up to handle requests to ``/ray/dashboard``, view the dashboard at ``www.my-website.com/ray/dashboard/``.
The dashboard sends HTTP requests with relative URL paths. Browsers handle these requests as expected when the ``window.location.href`` ends in a trailing ``/``.
This is a peculiarity of how many browsers handle requests with relative URLs, despite what [MDN](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL#examples_of_relative_urls) defines as the expected behavior.
Make your dashboard visible without a trailing ``/`` by including a rule in your reverse proxy that redirects the user's browser to ``/``, i.e. ``/ray/dashboard`` --> ``/ray/dashboard/``.
Below is an example with a [traefik](https://doc.traefik.io/traefik/getting-started/quick-start/) TOML file that accomplishes this:
```yaml
[http]
[http.routers]
[http.routers.to-dashboard]
rule = "PathPrefix(`/ray/dashboard`)"
middlewares = ["test-redirectregex", "strip"]
service = "dashboard"
[http.middlewares]
[http.middlewares.test-redirectregex.redirectRegex]
regex = "^(.*)/ray/dashboard$"
replacement = "${1}/ray/dashboard/"
[http.middlewares.strip.stripPrefix]
prefixes = ["/ray/dashboard"]
[http.services]
[http.services.dashboard.loadBalancer]
[[http.services.dashboard.loadBalancer.servers]]
url = "http://localhost:8265"
```
```{admonition} Warning
:class: warning
The Ray Dashboard provides read **and write** access to the Ray Cluster. The reverse proxy must provide authentication or network ingress controls to prevent unauthorized access to the Cluster.
```
## Disabling the Dashboard
Dashboard is included if you use `ray[default]` or {ref}`other installation commands <installation>` and automatically started.
To disable the Dashboard, use the `--include-dashboard` argument.
::::{tab-set}
:::{tab-item} Single-node local cluster
**Start the cluster explicitly with CLI** <br/>
```bash
ray start --include-dashboard=False
```
**Start the cluster implicitly with `ray.init`** <br/>
```{testcode}
:hide:
import ray
ray.shutdown()
```
```{testcode}
import ray
ray.init(include_dashboard=False)
```
:::
:::{tab-item} VM Cluster Launcher
Include the `ray start --head --include-dashboard=False` argument in the `head_start_ray_commands` section of the [Cluster Launcher's YAML file](https://github.com/ray-project/ray/blob/0574620d454952556fa1befc7694353d68c72049/python/ray/autoscaler/aws/example-full.yaml#L172).
:::
:::{tab-item} KubeRay
```{admonition} Warning
:class: warning
It's not recommended to disable Dashboard because several KubeRay features like `RayJob` and `RayService` depend on it.
```
Set `spec.headGroupSpec.rayStartParams.include-dashboard` to `False`. Check out this [example YAML file](https://gist.github.com/kevin85421/0e6a8dd02c056704327d949b9ec96ef9).
:::
::::
(observability-visualization-setup)=
## Embed Grafana visualizations into Ray Dashboard
For the enhanced Ray Dashboard experience, like {ref}`viewing time-series metrics<dash-metrics-view>` together with logs, Job info, etc., set up Prometheus and Grafana and integrate them with Ray Dashboard.
### Setting up Prometheus
To render Grafana visualizations, you need Prometheus to scrape metrics from Ray Clusters. Follow {ref}`the instructions <prometheus-setup>` to set up your Prometheus server and start to scrape system and application metrics from Ray Clusters.
### Setting up Grafana
Grafana is a tool that supports advanced visualizations of Prometheus metrics and allows you to create custom dashboards with your favorite metrics. Follow {ref}`the instructions <grafana>` to set up Grafana.
(embed-grafana-in-dashboard)=
### Embedding Grafana visualizations into Ray Dashboard
To view embedded time-series visualizations in Ray Dashboard, the following must be set up:
1. The head node of the cluster is able to access Prometheus and Grafana.
2. The browser of the dashboard user is able to access Grafana.
Configure these settings using the `RAY_GRAFANA_HOST`, `RAY_PROMETHEUS_HOST`, `RAY_PROMETHEUS_NAME`, and `RAY_GRAFANA_IFRAME_HOST` environment variables when you start the Ray Clusters.
* Set `RAY_GRAFANA_HOST` to an address that the head node can use to access Grafana. Head node does health checks on Grafana on the backend.
* Set `RAY_GRAFANA_ORG_ID` to the organization ID you use in Grafana. Default is "1".
* Set `RAY_PROMETHEUS_HOST` to an address the head node can use to access Prometheus.
* Set `RAY_PROMETHEUS_NAME` to select a different data source to use for the Grafana dashboard panels to use. Default is "Prometheus".
* Set `RAY_GRAFANA_IFRAME_HOST` to an address that the user's browsers can use to access Grafana and embed visualizations. If `RAY_GRAFANA_IFRAME_HOST` is not set, Ray Dashboard uses the value of `RAY_GRAFANA_HOST`.
For example, if the IP of the head node is 55.66.77.88 and Grafana is hosted on port 3000. Set the value to `RAY_GRAFANA_HOST=http://55.66.77.88:3000`.
* If you start a single-node Ray Cluster manually, make sure these environment variables are set and accessible before you start the cluster or as a prefix to the `ray start ...` command, e.g., `RAY_GRAFANA_HOST=http://55.66.77.88:3000 ray start ...`
* If you start a Ray Cluster with {ref}`VM Cluster Launcher <cloud-vm-index>`, the environment variables should be set under `head_start_ray_commands` as a prefix to the `ray start ...` command.
* If you start a Ray Cluster with {ref}`KubeRay <kuberay-index>`, refer to this {ref}`tutorial <kuberay-prometheus-grafana>`.
If all the environment variables are set properly, you should see time-series metrics in {ref}`Ray Dashboard <observability-getting-started>`.
:::{note}
If you use a different Prometheus server for each Ray Cluster and use the same Grafana server for all Clusters, set the `RAY_PROMETHEUS_NAME` environment variable to different values for each Ray Cluster and add these datasources in Grafana. Follow {ref}`these instructions <grafana>` to set up Grafana.
:::
#### Alternate Prometheus host location
By default, Ray Dashboard assumes Prometheus is hosted at `localhost:9090`. You can choose to run Prometheus on a non-default port or on a different machine. In this case, make sure that Prometheus can scrape the metrics from your Ray nodes following instructions {ref}`here <scrape-metrics>`.
Then, configure `RAY_PROMETHEUS_HOST` environment variable properly as stated above. For example, if Prometheus is hosted at port 9000 on a node with ip 55.66.77.88, set `RAY_PROMETHEUS_HOST=http://55.66.77.88:9000`.
#### Customize headers for requests from the Ray dashboard to Prometheus
If Prometheus requires additional headers for authentication, set `RAY_PROMETHEUS_HEADERS` in one of the following JSON formats for Ray dashboard to send them to Prometheus:
1. `{"Header1": "Value1", "Header2": "Value2"}`
2. `[["Header1", "Value1"], ["Header2", "Value2"], ["Header2", "Value3"]]`
#### Alternate Grafana host location
By default, Ray Dashboard assumes Grafana is hosted at `localhost:3000`. You can choose to run Grafana on a non-default port or on a different machine as long as the head node and the dashboard browsers of can access it.
If Grafana is exposed with NGINX ingress on a Kubernetes cluster, the following line should be present in the Grafana ingress annotation:
```yaml
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Frame-Options SAMEORIGIN always;
```
When both Grafana and the Ray Cluster are on the same Kubernetes cluster, set `RAY_GRAFANA_HOST` to the external URL of the Grafana ingress.
#### User authentication for Grafana
When the Grafana instance requires user authentication, the following settings have to be in its [configuration file](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/) to correctly embed in Ray Dashboard:
```ini
[security]
allow_embedding = true
cookie_secure = true
cookie_samesite = none
```
#### Troubleshooting
##### Dashboard message: either Prometheus or Grafana server is not detected
If you have followed the instructions above to set up everything, run the connection checks below in your browser:
* check Head Node connection to Prometheus server: add `api/prometheus_health` to the end of Ray Dashboard URL (for example: http://127.0.0.1:8265/api/prometheus_health)and visit it.
* check Head Node connection to Grafana server: add `api/grafana_health` to the end of Ray Dashboard URL (for example: http://127.0.0.1:8265/api/grafana_health) and visit it.
* check browser connection to Grafana server: visit the URL used in `RAY_GRAFANA_IFRAME_HOST`.
##### Getting an error that says `RAY_GRAFANA_HOST` is not setup
If you have set up Grafana, check that:
* You've included the protocol in the URL (e.g., `http://your-grafana-url.com` instead of `your-grafana-url.com`).
* The URL doesn't have a trailing slash (e.g., `http://your-grafana-url.com` instead of `http://your-grafana-url.com/`).
##### Certificate Authority (CA error)
You may see a CA error if your Grafana instance is hosted behind HTTPS. Contact the Grafana service owner to properly enable HTTPS traffic.
## Viewing built-in Dashboard API metrics
Dashboard is powered by a server that serves both the UI code and the data about the cluster via API endpoints. Ray emits basic Prometheus metrics for each API endpoint:
`ray_dashboard_api_requests_count_requests_total`: Collects the total count of requests. This is tagged by endpoint, method, and http_status.
`ray_dashboard_api_requests_duration_seconds_bucket`: Collects the duration of requests. This is tagged by endpoint and method.
For example, you can view the p95 duration of all requests with this query:
```text
histogram_quantile(0.95, sum(rate(ray_dashboard_api_requests_duration_seconds_bucket[5m])) by (le))
```
You can query these metrics from the Prometheus or Grafana UI. Find instructions above for how to set these tools up.
@@ -0,0 +1,32 @@
import os
import socket
import sys
import time
# trainer.py
from collections import Counter
import ray
num_cpus = int(sys.argv[1])
ray.init(address=os.environ["ip_head"])
print("Nodes in the Ray cluster:")
print(ray.nodes())
@ray.remote
def f():
time.sleep(1)
return socket.gethostbyname("localhost")
# The following takes one second (assuming that
# ray was able to access all of the allocated nodes).
for i in range(60):
start = time.time()
ip_addresses = ray.get([f.remote() for _ in range(num_cpus)])
print(Counter(ip_addresses))
end = time.time()
print(end - start)
@@ -0,0 +1,45 @@
#!/bin/bash
# shellcheck disable=SC2206
#SBATCH --job-name=test
#SBATCH --cpus-per-task=5
#SBATCH --mem-per-cpu=1GB
#SBATCH --nodes=4
#SBATCH --tasks-per-node=1
#SBATCH --time=00:30:00
set -x
# __doc_head_address_start__
# Getting the node names
nodes=$(scontrol show hostnames "$SLURM_JOB_NODELIST")
nodes_array=($nodes)
head_node=${nodes_array[0]}
port=6379
ip_head=$head_node:$port
export ip_head
echo "IP Head: $ip_head"
# __doc_head_address_end__
# __doc_symmetric_run_start__
# Start Ray cluster using symmetric_run.py on all nodes.
# Symmetric run will automatically start Ray on all nodes and run the script ONLY the head node.
# Use the '--' separator to separate Ray arguments and the entrypoint command.
# The --min-nodes argument ensures all nodes join before running the script.
# All nodes (including head and workers) will execute this block.
# The entrypoint (simple-trainer.py) will only run on the head node.
srun --nodes="$SLURM_JOB_NUM_NODES" --ntasks="$SLURM_JOB_NUM_NODES" \
ray symmetric-run \
--address "$ip_head" \
--min-nodes "$SLURM_JOB_NUM_NODES" \
--num-cpus="${SLURM_CPUS_PER_TASK}" \
--num-gpus="${SLURM_GPUS_PER_TASK}" \
-- \
python -u simple-trainer.py "$SLURM_CPUS_PER_TASK"
# __doc_symmetric_run_end__
# __doc_script_start__
# The entrypoint script (simple-trainer.py) will be run on the head node by symmetric_run.
+109
View File
@@ -0,0 +1,109 @@
# slurm-launch.py
# Usage:
# python slurm-launch.py --exp-name test \
# --command "rllib train --run PPO --env CartPole-v0"
import argparse
import subprocess
import sys
import time
from pathlib import Path
template_file = Path(__file__) / "slurm-template.sh"
JOB_NAME = "${JOB_NAME}"
NUM_NODES = "${NUM_NODES}"
NUM_GPUS_PER_NODE = "${NUM_GPUS_PER_NODE}"
PARTITION_OPTION = "${PARTITION_OPTION}"
COMMAND_PLACEHOLDER = "${COMMAND_PLACEHOLDER}"
GIVEN_NODE = "${GIVEN_NODE}"
LOAD_ENV = "${LOAD_ENV}"
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--exp-name",
type=str,
required=True,
help="The job name and path to logging file (exp_name.log).",
)
parser.add_argument(
"--num-nodes", "-n", type=int, default=1, help="Number of nodes to use."
)
parser.add_argument(
"--node",
"-w",
type=str,
help="The specified nodes to use. Same format as the "
"return of 'sinfo'. Default: ''.",
)
parser.add_argument(
"--num-gpus",
type=int,
default=0,
help="Number of GPUs to use in each node. (Default: 0)",
)
parser.add_argument(
"--partition",
"-p",
type=str,
)
parser.add_argument(
"--load-env",
type=str,
help="The script to load your environment ('module load cuda/10.1')",
default="",
)
parser.add_argument(
"--command",
type=str,
required=True,
help="The command you wish to execute. For example: "
" --command 'python test.py'. "
"Note that the command must be a string.",
)
args = parser.parse_args()
if args.node:
# assert args.num_nodes == 1
node_info = "#SBATCH -w {}".format(args.node)
else:
node_info = ""
job_name = "{}_{}".format(
args.exp_name, time.strftime("%m%d-%H%M", time.localtime())
)
partition_option = (
"#SBATCH --partition={}".format(args.partition) if args.partition else ""
)
# ===== Modified the template script =====
with open(template_file, "r") as f:
text = f.read()
text = text.replace(JOB_NAME, job_name)
text = text.replace(NUM_NODES, str(args.num_nodes))
text = text.replace(NUM_GPUS_PER_NODE, str(args.num_gpus))
text = text.replace(PARTITION_OPTION, partition_option)
text = text.replace(COMMAND_PLACEHOLDER, str(args.command))
text = text.replace(LOAD_ENV, str(args.load_env))
text = text.replace(GIVEN_NODE, node_info)
text = text.replace(
"# THIS FILE IS A TEMPLATE AND IT SHOULD NOT BE DEPLOYED TO PRODUCTION!",
"# THIS FILE IS MODIFIED AUTOMATICALLY FROM TEMPLATE AND SHOULD BE "
"RUNNABLE!",
)
# ===== Save the script =====
script_file = "{}.sh".format(job_name)
with open(script_file, "w") as f:
f.write(text)
# ===== Submit the job =====
print("Starting to submit job!")
subprocess.Popen(["sbatch", script_file])
print(
"Job submitted! Script file is at: <{}>. Log file is at: <{}>".format(
script_file, "{}.log".format(job_name)
)
)
sys.exit(0)
@@ -0,0 +1,64 @@
#!/bin/bash
# shellcheck disable=SC2206
# THIS FILE IS GENERATED BY AUTOMATION SCRIPT! PLEASE REFER TO ORIGINAL SCRIPT!
# THIS FILE IS A TEMPLATE AND IT SHOULD NOT BE DEPLOYED TO PRODUCTION!
${PARTITION_OPTION}
#SBATCH --job-name=${JOB_NAME}
#SBATCH --output=${JOB_NAME}.log
${GIVEN_NODE}
### This script works for any number of nodes, Ray will find and manage all resources
#SBATCH --nodes=${NUM_NODES}
#SBATCH --exclusive
### Give all resources to a single Ray task, ray can manage the resources internally
#SBATCH --ntasks-per-node=1
#SBATCH --gpus-per-task=${NUM_GPUS_PER_NODE}
# Load modules or your own conda environment here
# module load pytorch/v1.4.0-gpu
# conda activate ${CONDA_ENV}
${LOAD_ENV}
# ===== DO NOT CHANGE THINGS HERE UNLESS YOU KNOW WHAT YOU ARE DOING =====
# This script is a modification to the implementation suggest by gregSchwartz18 here:
# https://github.com/ray-project/ray/issues/826#issuecomment-522116599
redis_password=$(uuidgen)
export redis_password
nodes=$(scontrol show hostnames "$SLURM_JOB_NODELIST") # Getting the node names
nodes_array=($nodes)
node_1=${nodes_array[0]}
ip=$(srun --nodes=1 --ntasks=1 -w "$node_1" hostname --ip-address) # making redis-address
# if we detect a space character in the head node IP, we'll
# convert it to an ipv4 address. This step is optional.
if [[ "$ip" == *" "* ]]; then
IFS=' ' read -ra ADDR <<< "$ip"
if [[ ${#ADDR[0]} -gt 16 ]]; then
ip=${ADDR[1]}
else
ip=${ADDR[0]}
fi
echo "IPV6 address detected. We split the IPV4 address as $ip"
fi
port=6379
ip_head=$ip:$port
export ip_head
echo "IP Head: $ip_head"
echo "STARTING HEAD at $node_1"
srun --nodes=1 --ntasks=1 -w "$node_1" \
ray start --head --node-ip-address="$ip" --port=$port --redis-password="$redis_password" --block &
sleep 30
worker_num=$((SLURM_JOB_NUM_NODES - 1)) #number of nodes other than the head node
for ((i = 1; i <= worker_num; i++)); do
node_i=${nodes_array[$i]}
echo "STARTING WORKER $i at $node_i"
srun --nodes=1 --ntasks=1 -w "$node_i" ray start --address "$ip_head" --redis-password="$redis_password" --block &
sleep 5
done
# ===== Call your code below =====
${COMMAND_PLACEHOLDER}
@@ -0,0 +1,19 @@
from ray.job_submission import JobSubmissionClient
client = JobSubmissionClient("http://127.0.0.1:8265")
kick_off_xgboost_benchmark = (
# Clone ray. If ray is already present, don't clone again.
"git clone https://github.com/ray-project/ray || true; "
# Run the benchmark.
"python ray/release/train_tests/xgboost_lightgbm/train_batch_inference_benchmark.py"
" xgboost --size=100G --disable-check"
)
submission_id = client.submit_job(
entrypoint=kick_off_xgboost_benchmark,
)
print("Use the following command to follow this Job's logs:")
print(f"ray job logs '{submission_id}' --follow")
@@ -0,0 +1,18 @@
import skein
import sys
from urllib.parse import urlparse
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python dashboard.py <dashboard-address>")
sys.exit(1)
address = sys.argv[1]
# Check if the address is a valid URL
result = urlparse(address)
if not all([result.scheme, result.netloc]):
print("Error: Invalid dashboard address. Please provide a valid URL.")
sys.exit(1)
print("Registering dashboard " + address + " on skein.")
app = skein.ApplicationClient.from_current()
app.ui.add_page("ray-dashboard", address, "Ray Dashboard")
@@ -0,0 +1,50 @@
import sys
import time
from collections import Counter
import ray
@ray.remote
def get_host_name(x):
import platform
import time
time.sleep(0.01)
return x + (platform.node(),)
def wait_for_nodes(expected):
# Wait for all nodes to join the cluster.
while True:
num_nodes = len(ray.nodes())
if num_nodes < expected:
print(
"{} nodes have joined so far, waiting for {} more.".format(
num_nodes, expected - num_nodes
)
)
sys.stdout.flush()
time.sleep(1)
else:
break
def main():
wait_for_nodes(4)
# Check that objects can be transferred from each node to each other node.
for i in range(10):
print("Iteration {}".format(i))
results = [get_host_name.remote(get_host_name.remote(())) for _ in range(100)]
print(Counter(ray.get(results)))
sys.stdout.flush()
print("Success!")
sys.stdout.flush()
time.sleep(20)
if __name__ == "__main__":
ray.init(address="localhost:6379")
main()
@@ -0,0 +1,71 @@
name: ray
services:
# Head service.
ray-head:
# There should only be one instance of the head node per cluster.
instances: 1
resources:
# The resources for the head node.
vcores: 1
memory: 2048
files:
# ray/doc/source/cluster/doc_code/yarn/example.py
example.py: example.py
# ray/doc/source/cluster/doc_code/yarn/dashboard.py
dashboard.py: dashboard.py
# # A packaged python environment using `conda-pack`. Note that Skein
# # doesn't require any specific way of distributing files, but this
# # is a good one for python projects. This is optional.
# # See https://jcrist.github.io/skein/distributing-files.html
# environment: environment.tar.gz
script: |
# Activate the packaged conda environment
# - source environment/bin/activate
# This gets the IP address of the head node.
RAY_HEAD_ADDRESS=$(hostname -i)
# This stores the Ray head address in the Skein key-value store so that the workers can retrieve it later.
skein kv put current --key=RAY_HEAD_ADDRESS --value=$RAY_HEAD_ADDRESS
# This command starts all the processes needed on the ray head node.
# By default, we set object store memory and heap memory to roughly 200 MB. This is conservative
# and should be set according to application needs.
#
ray start --head --port=6379 --object-store-memory=200000000 --memory 200000000 --num-cpus=1 --dashboard-host=$RAY_HEAD_ADDRESS
# This registers the Ray dashboard on Skein, which can be accessed on Skein's web UI.
python dashboard.py "http://$RAY_HEAD_ADDRESS:8265"
# This executes the user script.
python example.py
# After the user script has executed, all started processes should also die.
ray stop
skein application shutdown current
# Worker service.
ray-worker:
# The number of instances to start initially. This can be scaled
# dynamically later.
instances: 4
resources:
# The resources for the worker node
vcores: 1
memory: 2048
# files:
# environment: environment.tar.gz
depends:
# Don't start any worker nodes until the head node is started
- ray-head
script: |
# Activate the packaged conda environment
# - source environment/bin/activate
# This command gets any addresses it needs (e.g. the head node) from
# the skein key-value store.
RAY_HEAD_ADDRESS=$(skein kv get --key=RAY_HEAD_ADDRESS current)
# The below command starts all the processes needed on a ray worker node, blocking until killed with sigterm.
# After sigterm, all started processes should also die (ray stop).
ray start --object-store-memory=200000000 --memory 200000000 --num-cpus=1 --address=$RAY_HEAD_ADDRESS:6379 --block; ray stop
+103
View File
@@ -0,0 +1,103 @@
.. _cluster-FAQ:
===
FAQ
===
These are some Frequently Asked Questions for Ray clusters.
If you still have questions after reading this FAQ, reach out on the
`Ray Discourse forum <https://discuss.ray.io/>`__.
Do Ray clusters support multi-tenancy?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yes, you can run multiple :ref:`jobs <jobs-overview>` from different users simultaneously in a Ray cluster
but it's not recommended in production.
Some Ray features are still missing for multi-tenancy in production:
* Ray doesn't provide strong resource isolation:
Ray :ref:`resources <core-resources>` are logical and they don't limit the physical resources a task or actor can use while running.
This means simultaneous jobs can interfere with each other and makes them less reliable to run in production.
* Ray doesn't support priorities: All jobs, tasks and actors have the same priority so there is no way to prioritize important jobs under load.
* Ray doesn't support access control: Jobs have full access to a Ray cluster and all of the resources within it.
On the other hand, you can run the same job multiple times using the same cluster to save the cluster startup time.
.. note::
A Ray :ref:`namespace <namespaces-guide>` is just a logical grouping of jobs and named actors. Unlike a Kubernetes namespace, it doesn't provide any other multi-tenancy functions like resource quotas.
I have multiple Ray users. What's the right way to deploy Ray for them?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Start a Ray cluster for each user to isolate their workloads.
What's the difference between ``--node-ip-address`` and ``--address``?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When starting a head node on a machine with more than one network address, you
may need to specify the externally available address so worker nodes can
connect. Use this command:
.. code:: bash
ray start --head --node-ip-address xx.xx.xx.xx --port nnnn
Then when starting the worker node, use this command to connect to the head node:
.. code:: bash
ray start --address xx.xx.xx.xx:nnnn
What does a worker node failure to connect look like?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the worker node can't connect to the head node, you should see this error:
Unable to connect to GCS at xx.xx.xx.xx:nnnn. Check that (1) Ray GCS with
matching version started successfully at the specified address, and (2)
there is no firewall setting preventing access.
The most likely cause is that the worker node can't access the IP address
given. You can use ``ip route get xx.xx.xx.xx`` on the worker node to start
debugging routing issues.
You may also see failures in the log like:
This node has an IP address of xx.xx.xx.xx, while we cannot find the
matched Raylet address. This may come from when you connect the Ray
cluster with a different IP address or connect a container.
The cause of this error may be the head node overloading with too many simultaneous
connections. The solution for this problem is to start the worker nodes more slowly.
Problems getting a SLURM cluster to work
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A class of issues exist with starting Ray on SLURM clusters. While the exact causes aren't understood, (as of June 2023), some Ray
improvements mitigate some of the resource contention. Some of the issues
reported are as follows:
* Using a machine with a large number of CPUs, and starting one worker per CPU
together with OpenBLAS (as used in NumPy) may allocate too many threads. This
issue is a `known OpenBLAS limitation`_. You can mitigate it by limiting OpenBLAS
to one thread per process as explained in the link.
* Resource allocation isn't as expected: usually the configuration has too many CPUs allocated per node. The best practice is to verify the SLURM configuration without
starting Ray to verify that the allocations are as expected. For more
detailed information see :ref:`ray-slurm-deploy`.
.. _`known OpenBLAS limitation`: http://www.openmathlib.org/OpenBLAS/docs/faq/#how-can-i-use-openblas-in-multi-threaded-applications
Where does my Ray Job entrypoint script run? On the head node or worker nodes?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, jobs submitted using the :ref:`Ray Job API <jobs-quickstart>` run
their `entrypoint` script on the head node. You can change this by specifying
any of the options `--entrypoint-num-cpus`, `--entrypoint-num-gpus`,
`--entrypoint-resources` or `--entrypoint-memory` to `ray job submit`, or the
corresponding arguments if using the Python SDK. If these are specified, the
job entrypoint will be scheduled on a node that has the requested resources
available.
+109
View File
@@ -0,0 +1,109 @@
.. _cluster-index:
Ray Clusters Overview
=====================
.. toctree::
:hidden:
Key Concepts <key-concepts>
Deploying on Kubernetes <kubernetes/index>
Deploying on VMs <vms/index>
metrics
configure-manage-dashboard
Applications Guide <running-applications/index>
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*.
A Ray cluster is a set of worker nodes connected to a common :ref:`Ray head node <cluster-head-node>`.
Ray clusters can be fixed-size, or they may :ref:`autoscale up and down <cluster-autoscaler>` according
to the resources requested by applications running on the cluster.
Where can I deploy Ray clusters?
--------------------------------
Ray provides native cluster deployment support on the following technology stacks:
* On :ref:`AWS, GCP, and Azure <cloud-vm-index>`. Community-supported Aliyun and vSphere integrations also exist.
* On :ref:`Kubernetes <kuberay-index>`, via the officially supported KubeRay project.
* On `Anyscale <https://console.anyscale.com/register/ha?render_flow=ray&utm_source=ray_docs&utm_medium=docs&utm_campaign=ray-doc-upsell&utm_content=ray-cluster-deployment>`_, a fully managed Ray platform by the creators of Ray. You can either bring an existing AWS, GCP, Azure and Kubernetes clusters, or use the Anyscale hosted compute layer.
Advanced users may want to :ref:`deploy Ray manually <on-prem>`
or onto :ref:`platforms not listed here <ref-cluster-setup>`.
.. note::
Multi-node Ray clusters are only supported on Linux. At your own risk, you
may deploy Windows and OSX clusters by setting the environment variable
``RAY_ENABLE_WINDOWS_OR_OSX_CLUSTER=1`` during deployment.
What's next?
------------
.. grid:: 1 2 2 2
:gutter: 1
:class-container: container pb-3
.. grid-item-card::
**I want to learn key Ray cluster concepts**
^^^
Understand the key concepts and main ways of interacting with a Ray cluster.
+++
.. button-ref:: cluster-key-concepts
:color: primary
:outline:
:expand:
Learn Key Concepts
.. grid-item-card::
**I want to run Ray on Kubernetes**
^^^
Deploy a Ray application to a Kubernetes cluster. You can run the tutorial on a
Kubernetes cluster or on your laptop via Kind.
+++
.. button-ref:: kuberay-quickstart
:color: primary
:outline:
:expand:
Get Started with Ray on Kubernetes
.. grid-item-card::
**I want to run Ray on a cloud provider**
^^^
Take a sample application designed to run on a laptop and scale it up in the
cloud. Access to an AWS or GCP account is required.
+++
.. button-ref:: vm-cluster-quick-start
:color: primary
:outline:
:expand:
Get Started with Ray on VMs
.. grid-item-card::
**I want to run my application on an existing Ray cluster**
^^^
Guide to submitting applications as Jobs to existing Ray clusters.
+++
.. button-ref:: jobs-quickstart
:color: primary
:outline:
:expand:
Job Submission
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

+87
View File
@@ -0,0 +1,87 @@
Key Concepts
============
.. _cluster-key-concepts:
This page introduces key concepts for Ray clusters:
.. contents::
:local:
Ray Cluster
-----------
A Ray cluster consists of a single :ref:`head node <cluster-head-node>`
and any number of connected :ref:`worker nodes <cluster-worker-nodes>`:
.. figure:: images/ray-cluster.svg
:align: center
:width: 600px
*A Ray cluster with two worker nodes. Each node runs Ray helper processes to
facilitate distributed scheduling and memory management. The head node runs
additional control processes (highlighted in blue).*
The number of worker nodes may be *autoscaled* with application demand as specified
by your Ray cluster configuration. The head node runs the :ref:`autoscaler <cluster-autoscaler>`.
.. note::
Ray nodes are implemented as pods when :ref:`running on Kubernetes <kuberay-index>`.
Users can submit jobs for execution on the Ray cluster, or can interactively use the
cluster by connecting to the head node and running `ray.init`. See
:ref:`Ray Jobs <jobs-quickstart>` for more information.
.. _cluster-head-node:
Head Node
---------
Every Ray cluster has one node which is designated as the *head node* of the cluster.
The head node is identical to other worker nodes, except that it also runs singleton processes responsible for cluster management such as the
:ref:`autoscaler <cluster-autoscaler>`, :term:`GCS <GCS / Global Control Service>` and the Ray driver processes
which run :ref:`Ray jobs <cluster-clients-and-jobs>`. Ray may schedule
tasks and actors on the head node just like any other worker node, which is not desired in large-scale clusters.
See :ref:`vms-large-cluster-configure-head-node` for the best practice in large-scale clusters.
.. _cluster-worker-nodes:
Worker Node
------------
*Worker nodes* do not run any head node management processes, and serve only to run user code in Ray tasks and actors. They participate in distributed scheduling, as well as the storage and distribution of Ray objects in :ref:`cluster memory <objects-in-ray>`.
.. _cluster-autoscaler:
Autoscaler
----------
The *Ray autoscaler* is a process that runs on the :ref:`head node <cluster-head-node>` (or as a sidecar container in the head pod if :ref:`using Kubernetes <kuberay-index>`).
When the resource demands of the Ray workload exceed the
current capacity of the cluster, the autoscaler will try to increase the number of worker nodes. When worker nodes
sit idle, the autoscaler will remove worker nodes from the cluster.
It is important to understand that the autoscaler only reacts to task and actor resource requests, and not application metrics or physical resource utilization.
To learn more about autoscaling, refer to the user guides for Ray clusters on :ref:`VMs <cloud-vm-index>` and :ref:`Kubernetes <kuberay-index>`.
.. note::
Version 2.10.0 introduces the alpha release of Autoscaling V2 on KubeRay. Discover the enhancements and configuration details :ref:`here <kuberay-autoscaler-v2>`.
.. _cluster-clients-and-jobs:
Ray Jobs
--------
A Ray job is a single application: it is the collection of Ray tasks, objects, and actors that originate from the same script.
The worker that runs the Python script is known as the *driver* of the job.
There are two ways to run a Ray job on a Ray cluster:
1. (Recommended) Submit the job using the :ref:`Ray Jobs API <jobs-overview>`.
2. Run the driver script directly on the Ray cluster, for interactive development.
For details on these workflows, refer to the :ref:`Ray Jobs API guide <jobs-overview>`.
.. figure:: images/ray-job-diagram.png
:align: center
:width: 650px
*Two ways of running a job on a Ray cluster.*
@@ -0,0 +1,11 @@
(kuberay-benchmarks)=
# KubeRay Benchmarks
```{toctree}
:hidden:
benchmarks/memory-scalability-benchmark
```
- {ref}`kuberay-mem-scalability`
@@ -0,0 +1,81 @@
(kuberay-mem-scalability)=
# KubeRay memory and scalability benchmark
## Architecture
![benchmark architecture](../images/benchmark_architecture.png)
This architecture is not a good practice, but it can fulfill the current requirements.
## Preparation
Clone the [KubeRay repository](https://github.com/ray-project/kuberay) and checkout the `master` branch. This tutorial requires several files in the repository.
## Step 1: Create a new Kubernetes cluster
Create a GKE cluster with autoscaling enabled. The following command creates a Kubernetes cluster named `kuberay-benchmark-cluster` on Google GKE. The cluster can scale up to 16 nodes, and each node of type `e2-highcpu-16` has 16 CPUs and 16 GB of memory. The following experiments may create up to ~150 Pods in the Kubernetes cluster, and each Ray Pod requires 1 CPU and 1 GB of memory.
```sh
gcloud container clusters create kuberay-benchmark-cluster \
--num-nodes=1 --min-nodes 0 --max-nodes 16 --enable-autoscaling \
--zone=us-west1-b --machine-type e2-highcpu-16
```
## Step 2: Install Prometheus and Grafana
```sh
# Path: kuberay/
./install/prometheus/install.sh
```
Follow "Step 2: Install Kubernetes Prometheus Stack via Helm chart" in [prometheus-grafana.md](kuberay-prometheus-grafana) to install the [kube-prometheus-stack v48.2.1](https://github.com/prometheus-community/helm-charts/tree/kube-prometheus-stack-48.2.1/charts/kube-prometheus-stack) chart and related custom resources.
## Step 3: Install a KubeRay operator
Follow [this document](kuberay-operator-deploy) to install the latest stable KubeRay operator via Helm repository.
## Step 4: Run experiments
* Step 4.1: Make sure the `kubectl` CLI can connect to your GKE cluster. If not, run `gcloud auth login`.
* Step 4.2: Run an experiment.
```sh
# You can modify `memory_benchmark_utils` to run the experiment you want to run.
# (path: benchmark/memory_benchmark/scripts)
python3 memory_benchmark_utils.py | tee benchmark_log
```
* Step 4.3: Follow [prometheus-grafana.md](kuberay-prometheus-grafana) to access Grafana's dashboard.
* Sign into the Grafana dashboard.
* Click on "Dashboards".
* Select "Kubernetes / Compute Resources / Pod".
* Locate the "Memory Usage" panel for the KubeRay operator Pod.
* Select the time range, then click on "Inspect" followed by "Data" to download the memory usage data of the KubeRay operator Pod.
* Step 4.4: Delete all RayCluster custom resources.
```sh
kubectl delete --all rayclusters.ray.io --namespace=default
```
* Step 4.5: Repeat Step 4.2 to Step 4.4 for other experiments.
# Experiments
This benchmark is based on three benchmark experiments:
* Experiment 1: Launch a RayCluster with 1 head and no workers. A new cluster is initiated every 20 seconds until there are a total of 150 RayCluster custom resources.
* Experiment 2: Create a Kubernetes cluster, with only 1 RayCluster. Add 5 new worker Pods to this RayCluster every 60 seconds until the total reaches 150 Pods.
* Experiment 3: Create a 5-node (1 head + 4 workers) RayCluster every 60 seconds up to 30 RayCluster custom resources.
Based on [the survey](https://forms.gle/KtMLzjXcKoeSTj359) for KubeRay users, the benchmark target is set at 150 Ray Pods to cover most use cases.
## Experiment results (KubeRay v0.6.0)
![benchmark result](../images/benchmark_result.png)
* You can generate the above figure by running:
```sh
# (path: benchmark/memory_benchmark/scripts)
python3 experiment_figures.py
# The output image `benchmark_result.png` will be stored in `scripts/`.
```
* As shown in the figure, the memory usage of the KubeRay operator Pod is highly and positively correlated to the number of Pods in the Kubernetes cluster. In addition, the number of custom resources in the Kubernetes cluster does not have a significant impact on the memory usage.
* Note that the x-axis "Number of Pods" is the number of Pods that are created rather than running. If the Kubernetes cluster does not have enough computing resources, the GKE Autopilot adds a new Kubernetes node into the cluster. This process may take a few minutes, so some Pods may be pending in the process. This lag may can explain why the memory usage is somewhat throttled.
@@ -0,0 +1,46 @@
# Fluent Bit Config
config:
inputs: |
[INPUT]
Name tail
Path /var/log/containers/*.log
multiline.parser docker, cri
Tag kube.*
Mem_Buf_Limit 5MB
Skip_Long_Lines On
filters: |
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
K8S-Logging.Parser On
K8S-Logging.Exclude On
outputs: |
[OUTPUT]
Name loki
Match *
Host loki-gateway
Port 80
Labels job=fluent-bit,namespace=$kubernetes['namespace_name'],pod=$kubernetes['pod_name'],container=$kubernetes['container_name']
Auto_Kubernetes_Labels Off
tenant_id test
---
# Grafana Datasource Config
datasources:
datasources.yaml:
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
editable: true
url: http://loki-gateway.default
jsonData:
timeout: 60
maxLines: 1000
httpHeaderName1: "X-Scope-OrgID"
secureJsonData:
httpHeaderValue1: "test"
@@ -0,0 +1,383 @@
apiVersion: v1
kind: Secret
metadata:
name: ca-tls
data:
# output from cat ca.crt | base64
ca.crt: |
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM3RENDQWRRQ0NRQ05Yck8zQTAwbWRqQU5CZ2txaGtpRzl3MEJBUXNGQURBNE1SRXdEd1lEVlFRRERBZ3EKTG5KaGVTNXBiekVMTUFrR0ExVUVCaE1DVlZNeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh3SGhjTgpNak13TXpJM01EZ3dNVFF4V2hjTk16TXdNekkwTURnd01UUXhXakE0TVJFd0R3WURWUVFEREFncUxuSmhlUzVwCmJ6RUxNQWtHQTFVRUJoTUNWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHdnZ0VpTUEwR0NTcUcKU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3ZJbGNGSmZxaFNidWowQ3ZpalA0c2xXN3I3Qk1kYVJOeAp5aDhJMGNaSU5QcjQ5Rjg1dXNrY0pxbnFHNC9LeThBYnlacURBUUxsalFUa0Exb3FxVHhGdTZMSm5LOGJHN012Cm90dStjVlZLWW5SeDlLWVoyWi90THRPdzhjZHFzOURuNXVERVh0L0loZzBRc0tVRDNJN3U3QjF5bVpxTjQwWEgKWDVMRUJkN1llSm5XZExqOStLOTl6ZVR0aHlUMWtsRGsySVp2ZjVsa2xjT2hHRzA5RmNtZlF5REFlM2VvTm1IWQpVaUhVU0NORGtnWTV3U3A4V3R6RXEydHBhZEQ2eTVCNVRMS2kvV1l4ZTJLM2tXbTZnUytwQTIvdkZIaU93RHNaClNqb1ZncUtMZ0lNSnZMOGR0bitaWjNLbDlMRkZNY0JiMWJ1NCtKN2U1bno3RTRVSG4wN0pBZ01CQUFFd0RRWUoKS29aSWh2Y05BUUVMQlFBRGdnRUJBQWhSY3g2NzVJbjJVaERhMzArTkZ0UlNTcUJwK1E2WTl3VGNTL0NqM1J3MgpLSnkzUVhBU0xJUW1ESWdrVlBJeEY0V1VYUFdGdmxUL0taQ2JRejRvN2M3ck9DWEVEWnVhbExUSHRrTHVSZFNWClVHSTVSWTJXNUx6UXM2MnNtUG13OWVQYnNLek5kOEpjWkwvNndHZnNsZVQyY1RLTjliZVE2ZWdiQmdEcy91d0sKeVdOREtnaE4vaE16YmRSaFh2SFNiTW8rUkgvRG1Va1VhTXZZc3NNbzFYQkwzRXZwbmpnZXI1ZWQ5ZDVjQWYvUQpuU0VCMk13Z08rWHEwKy9sWmpiUFNWOVdWQnY1YjZlc1ZPcnZrV2o2TUFKcjUwb3BwT09KUy9TbTNEU3F5aDRBClR5c1BOblQxYStxWDRVZXljZ05VbXRoOXdONFBnc3B6ZEpORWtVdTVSSmM9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
# output from cat ca.key | base64
ca.key: |
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3ZJbGNGSmZxaFNidWoKMEN2aWpQNHNsVzdyN0JNZGFSTnh5aDhJMGNaSU5QcjQ5Rjg1dXNrY0pxbnFHNC9LeThBYnlacURBUUxsalFUawpBMW9xcVR4RnU2TEpuSzhiRzdNdm90dStjVlZLWW5SeDlLWVoyWi90THRPdzhjZHFzOURuNXVERVh0L0loZzBRCnNLVUQzSTd1N0IxeW1acU40MFhIWDVMRUJkN1llSm5XZExqOStLOTl6ZVR0aHlUMWtsRGsySVp2ZjVsa2xjT2gKR0cwOUZjbWZReURBZTNlb05tSFlVaUhVU0NORGtnWTV3U3A4V3R6RXEydHBhZEQ2eTVCNVRMS2kvV1l4ZTJLMwprV202Z1MrcEEyL3ZGSGlPd0RzWlNqb1ZncUtMZ0lNSnZMOGR0bitaWjNLbDlMRkZNY0JiMWJ1NCtKN2U1bno3CkU0VUhuMDdKQWdNQkFBRUNnZ0VCQUpYbG9XK2hveE83UlNRZmdBQkhSeUdud1NtaWhIWE93cnJKRWFqOXkyVncKRzBOTC9ka3Vld1ZpUGxwR3Z0c0hhMlVkTitkYXpUem1aMEkxY0U1RlRYWXQ5RlgxaXBaOExmRGV4cEFJOXNSVQo0bS9Ld3dRckZVdnZvWGE0YWtOMHBxQm1Kd2xNWHVPRmdOZEJLZXZWTW0xaW9JMisxTjhPb0dIVjlvdGFydks5ClUzY09CbmVBSjZmamF6ODd4RG1NY0dBcG82ZWdMOG0xaWJ1NUNwcFo2L2J2YVZYbHhFdXRtUjZYR2VKczdBRzMKVEtFYVhzTU1qdFdaM3ZXUDArMFJIMGpzRVI2a0ZMeEI3KzRHRWdPSk1WblZqbjlzT3FhVW41KzJ1REkrdkFkbAo0K2Fya3dwQnpzbGlaUVJLVW95aGwvMTRRZW9pcXpwVk9oVVpheFJOTnpVQ2dZRUE0RC83TmRudGFxa0JSdEdiClZUQTE0clA3Vy90THZSQnpBNWtLc3Q4V3crWFVGeTcvVEY2NkxpMVVtKzhhK2ttY1pMTm9mamNZc1pTMExkVXMKMlR4dk1IRWplcmdNUm5oWmUrOGJBZlZkd1RTcCtpdUpMKzRZWW1JRUZWUWlsSXhtRURzMkZQSnVZMHRDZW9ETAprVEFSeUNtMENPYUR1VXdLZjlMY3h3SFR4M3NDZ1lFQXgrNGpyOHV3aXh3WmwwUFBJb1Z1by9wSHZMT0ZxNXNBCmIrVEZnMEhFTVdIK1JKclhLRjA1YTRGNS9zc3pLZ09ZMGFZVUxlWnp3V1dJZElId0pzQnhGWktOdHRYTkhRbS8KOEFlVGRENnZ1OXlmN0tFZjhRNnFmaDRPRExvVDg0UTFWbGs4ek5ZN0FNUWZwN2p5RnpFOStvSm9tdlM0Snc1SApCZUNLZGZGR1RZc0NnWUVBaitkL0JhZTd1MTZJK3pFM1JRdVRDTkFHMVpnRm1tWWI2SXNsV25QZTRBZDBld3dsCnVKUnhWWUN4Y3YrVmlGZ0VqSHEwNjRuZnh0VnVhcHNLRkwyN2ZKS2QrZnB4cGlkRkJVc0RRZFo3TzZqWUN6bzAKNXhVYmdNYjFaOXA5OW1YQ2VWZ0Y5SnMrUzJuWVYxU2ZUYVJUUk9lK0tKZ0VuN3cwWUtLb0d1MEpRbEVDZ1lCZApZdXJnYm5Ca1NoZmFCQjU0cllMa3JUOWM4UzM2M2tmeC9CWVdIVjRiQXY3VjVNMmpXUWc5SXhsczNsVmp4cEpYCk94QXA4SDhaVXVmT0kvT2M1ajdzS0t4eFBxUzBiNTFyN04zL2FsaURrNlpQeldNeUlmdVpOVWl5d1NnWWt5U20KMU1BRm5mdXBlL0tkVVZJamF5amNIcFhsNjNFcExRNFh2SzV3TU9iNXlRS0JnR0kzSTAwSTlnbURzS1JrOFkxdQpId1l0dVdrNjFvWEhUTHorR3d6RUNCQ0VnNkZxMjZVeDZmVzBySlVwV3pOVURCNkRRRGxCTGx3S1M4Z1R3eGtGCkRkY3VrbzFHekdlQWYvazEwWktTZmFXNVcwVlloVGVjSDhyZXpReWxwUk5YT3ZNZkFwWUplcnhBZ09yK3hFajUKK2wwalU0MDBTMUx0cWhLVzZMK3kxRVd5Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tls
data:
gencert_head.sh: |
#!/bin/sh
## Create tls.key
openssl genrsa -out /etc/ray/tls/tls.key 2048
## Write CSR Config
cat > /etc/ray/tls/csr.conf <<EOF
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = US
ST = California
L = San Fransisco
O = ray
OU = ray
CN = *.ray.io
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
DNS.2 = service-ray-head.default.svc.cluster.local
IP.1 = 127.0.0.1
IP.2 = $POD_IP
EOF
## Create CSR using tls.key
openssl req -new -key /etc/ray/tls/tls.key -out /etc/ray/tls/ca.csr -config /etc/ray/tls/csr.conf
## Write cert config
cat > /etc/ray/tls/cert.conf <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = service-ray-head.default.svc.cluster.local
IP.1 = 127.0.0.1
IP.2 = $POD_IP
EOF
## Generate tls.cert
openssl x509 -req \
-in /etc/ray/tls/ca.csr \
-CA /etc/ray/tls/ca.crt -CAkey /etc/ray/tls/ca.key \
-CAcreateserial -out /etc/ray/tls/tls.crt \
-days 365 \
-sha256 -extfile /etc/ray/tls/cert.conf
gencert_worker.sh: |
#!/bin/sh
## Create tls.key
openssl genrsa -out /etc/ray/tls/tls.key 2048
## Write CSR Config
cat > /etc/ray/tls/csr.conf <<EOF
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = US
ST = California
L = San Fransisco
O = ray
OU = ray
CN = *.ray.io
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = $POD_IP
EOF
## Create CSR using tls.key
openssl req -new -key /etc/ray/tls/tls.key -out /etc/ray/tls/ca.csr -config /etc/ray/tls/csr.conf
## Write cert config
cat > /etc/ray/tls/cert.conf <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = $POD_IP
EOF
## Generate tls.cert
openssl x509 -req \
-in /etc/ray/tls/ca.csr \
-CA /etc/ray/tls/ca.crt -CAkey /etc/ray/tls/ca.key \
-CAcreateserial -out /etc/ray/tls/tls.crt \
-days 365 \
-sha256 -extfile /etc/ray/tls/cert.conf
---
# Ray head node service, allowing worker pods to discover the head node to perform the bidirectional communication.
# More contexts can be found at [the Ports configurations doc](https://docs.ray.io/en/latest/ray-core/configure.html#ports-configurations).
apiVersion: v1
kind: Service
metadata:
name: service-ray-head
labels:
app: ray-head
spec:
clusterIP: None
ports:
- name: client
protocol: TCP
port: 10001
targetPort: 10001
- name: dashboard
protocol: TCP
port: 8265
targetPort: 8265
- name: gcs-server
protocol: TCP
port: 6379
targetPort: 6379
selector:
app: ray-head
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-ray-head
labels:
app: ray-head
spec:
# Do not change this - Ray currently only supports one head node per cluster.
replicas: 1
selector:
matchLabels:
app: ray-head
template:
metadata:
labels:
app: ray-head
spec:
# If the head node goes down, the entire cluster (including all worker
# nodes) goes down as well. If you want Kubernetes to bring up a new
# head node in this case, set this to "Always," else set it to "Never."
restartPolicy: Always
terminationGracePeriodSeconds: 60
# This volume allocates shared memory for Ray to use for its plasma
# object store. If you do not provide this, Ray falls back to
# /tmp which cause slowdowns if it's not a shared memory volume.
volumes:
- name: dshm
emptyDir:
medium: Memory
- name: ca-tls
secret:
secretName: ca-tls
- name: ray-tls
emptyDir: {}
# The gencert_head.sh can be prebaked into the docker container so the configMap is optional
- name: gen-tls-script
configMap:
name: tls
defaultMode: 0777
# An array of keys from the ConfigMap to create as files
items:
- key: gencert_head.sh
path: gencert_head.sh
initContainers:
- name: ray-head-tls
image: rayproject/ray:2.3.0
command: ["/bin/sh", "-c", "cp -R /etc/ca/tls /etc/ray && /etc/gen/tls/gencert_head.sh"]
volumeMounts:
- mountPath: /etc/ca/tls
name: ca-tls
readOnly: true
- mountPath: /etc/ray/tls
name: ray-tls
- mountPath: /etc/gen/tls
name: gen-tls-script
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
containers:
- name: ray-head
image: rayproject/ray:2.3.0
imagePullPolicy: IfNotPresent
command: [ "/bin/sh", "-c", "--" ]
args:
- "ray start --head --port=6379 --num-cpus=$MY_CPU_REQUEST --dashboard-host=127.0.0.1 --object-manager-port=8076 --node-manager-port=8077 --dashboard-agent-grpc-port=8078 --dashboard-agent-listen-port=52365 --block"
ports:
- containerPort: 6379 # GCS server
- containerPort: 10001 # Used by Ray Client
- containerPort: 8265 # Used by Ray Dashboard
# This volume allocates shared memory for Ray to use for its plasma
# object store. If you do not provide this, Ray falls back to
# /tmp which cause slowdowns if it's not a shared memory volume.
volumeMounts:
- mountPath: /dev/shm
name: dshm
- mountPath: /etc/ca/tls
name: ca-tls
readOnly: true
- mountPath: /etc/ray/tls
name: ray-tls
env:
- name: RAY_USE_TLS
value: "1"
- name: RAY_TLS_SERVER_CERT
value: "/etc/ray/tls/tls.crt"
- name: RAY_TLS_SERVER_KEY
value: "/etc/ray/tls/tls.key"
- name: RAY_TLS_CA_CERT
value: "/etc/ca/tls/ca.crt"
- name: RAY_BACKEND_LOG_LEVEL
value: warning
# This is used in the ray start command so that Ray can spawn the
# correct number of processes. Omitting this may lead to degraded
# performance.
- name: MY_CPU_REQUEST
valueFrom:
resourceFieldRef:
resource: requests.cpu
resources:
limits:
cpu: "2"
memory: "4G"
requests:
# For production use-cases, we recommend specifying integer CPU requests and limits.
# We also recommend setting requests equal to limits for both CPU and memory.
# For this example, we use a 500m CPU request to accommodate resource-constrained local
# Kubernetes testing environments such as Kind and minikube.
cpu: "2"
# The rest state memory usage of the Ray head node is around 1Gb. We do not
# recommend allocating less than 2Gb memory for the Ray head pod.
# For production use-cases, we recommend allocating at least 8Gb memory for each Ray container.
memory: "4G"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-ray-worker
labels:
app: ray-worker
spec:
# Change this to scale the number of worker nodes started in the Ray cluster.
replicas: 2
selector:
matchLabels:
app: ray-worker
template:
metadata:
labels:
app: ray-worker
spec:
restartPolicy: Always
volumes:
- name: dshm
emptyDir:
medium: Memory
- name: ca-tls
secret:
secretName: ca-tls
- name: ray-tls
emptyDir: {}
# The gencert_worker.sh can be prebaked into the docker container so the configMap is optional
- name: gen-tls-script
configMap:
name: tls
defaultMode: 0777
# An array of keys from the ConfigMap to create as files
items:
- key: gencert_worker.sh
path: gencert_worker.sh
initContainers:
- name: ray-worker-tls
image: rayproject/ray:2.3.0
command: ["/bin/sh", "-c", "cp -R /etc/ca/tls /etc/ray && /etc/gen/tls/gencert_worker.sh"]
volumeMounts:
- mountPath: /etc/ca/tls
name: ca-tls
readOnly: true
- mountPath: /etc/ray/tls
name: ray-tls
- mountPath: /etc/gen/tls
name: gen-tls-script
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
containers:
- name: ray-worker
image: rayproject/ray:2.3.0
imagePullPolicy: IfNotPresent
command: ["/bin/bash", "-c", "--"]
args:
- "ray start --num-cpus=$MY_CPU_REQUEST --address=service-ray-head.default.svc.cluster.local:6379 --object-manager-port=8076 --node-manager-port=8077 --dashboard-agent-grpc-port=8078 --dashboard-agent-listen-port=52365 --block"
ports:
- containerPort: 6379 # GCS server
# This volume allocates shared memory for Ray to use for its plasma
# object store. If you do not provide this, Ray falls back to
# /tmp which cause slowdowns if it's not a shared memory volume.
volumeMounts:
- mountPath: /dev/shm
name: dshm
- mountPath: /etc/ca/tls
name: ca-tls
readOnly: true
- mountPath: /etc/ray/tls
name: ray-tls
env:
- name: RAY_USE_TLS
value: "1"
- name: RAY_TLS_SERVER_CERT
value: "/etc/ray/tls/tls.crt"
- name: RAY_TLS_SERVER_KEY
value: "/etc/ray/tls/tls.key"
- name: RAY_TLS_CA_CERT
value: "/etc/ca/tls/ca.crt"
- name: RAY_BACKEND_LOG_LEVEL
value: warning
# This is used in the ray start command so that Ray can spawn the
# correct number of processes. Omitting this may lead to degraded
# performance.
- name: MY_CPU_REQUEST
valueFrom:
resourceFieldRef:
resource: requests.cpu
# The resource requests and limits in this config are too small for production!
# It is better to use a few large Ray pods than many small ones.
# For production, it is ideal to size each Ray pod to take up the
# entire Kubernetes node on which it is scheduled.
resources:
limits:
cpu: "1"
memory: "1G"
# For production use-cases, we recommend specifying integer CPU requests and limits.
# We also recommend setting requests equal to limits for both CPU and memory.
# For this example, we use a 500m CPU request to accommodate resource-constrained local
# Kubernetes testing environments such as Kind and minikube.
requests:
cpu: "500m"
memory: "1G"
+39
View File
@@ -0,0 +1,39 @@
(kuberay-examples)=
# Examples
```{toctree}
:hidden:
examples/mnist-training-example
examples/stable-diffusion-rayservice
examples/tpu-serve-stable-diffusion
examples/mobilenet-rayservice
examples/text-summarizer-rayservice
examples/rayjob-batch-inference-example
examples/rayjob-kueue-priority-scheduling
examples/rayjob-kueue-gang-scheduling
examples/distributed-checkpointing-with-gcsfuse
examples/rayserve-llm-example
examples/rayserve-deepseek-example
examples/verl-post-training
examples/argocd
examples/rayjob-agent-sandbox
```
This section presents example Ray workloads to try out on your Kubernetes cluster.
- {ref}`kuberay-mnist-training-example` (CPU-only)
- {ref}`kuberay-mobilenet-rayservice-example` (CPU-only)
- {ref}`kuberay-stable-diffusion-rayservice-example`
- {ref}`kuberay-tpu-stable-diffusion-example`
- {ref}`kuberay-text-summarizer-rayservice-example`
- {ref}`kuberay-batch-inference-example`
- {ref}`kuberay-kueue-priority-scheduling-example`
- {ref}`kuberay-kueue-gang-scheduling-example`
- {ref}`kuberay-distributed-checkpointing-gcsfuse`
- {ref}`kuberay-rayservice-llm-example`
- {ref}`kuberay-rayservice-deepseek-example`
- {ref}`kuberay-verl`
- {ref}`kuberay-agent-sandbox`

Some files were not shown because too many files have changed in this diff Show More