diff --git a/lib/python3.12/site-packages/nvidia/cufft/include/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/nvidia/cufft/include/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1635bc96f51bf93ee4f910b674d48286e89182da Binary files /dev/null and b/lib/python3.12/site-packages/nvidia/cufft/include/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/nvidia/cufft/include/cufftXt.h b/lib/python3.12/site-packages/nvidia/cufft/include/cufftXt.h new file mode 100644 index 0000000000000000000000000000000000000000..6a22724261d27ae0e5e894e4e2f60ef484b52bf2 --- /dev/null +++ b/lib/python3.12/site-packages/nvidia/cufft/include/cufftXt.h @@ -0,0 +1,259 @@ + + /* Copyright 2005-2021 NVIDIA Corporation. All rights reserved. + * + * NOTICE TO LICENSEE: + * + * The source code and/or documentation ("Licensed Deliverables") are + * subject to NVIDIA intellectual property rights under U.S. and + * international Copyright laws. + * + * The Licensed Deliverables contained herein are PROPRIETARY and + * CONFIDENTIAL to NVIDIA and are being provided under the terms and + * conditions of a form of NVIDIA software license agreement by and + * between NVIDIA and Licensee ("License Agreement") or electronically + * accepted by Licensee. Notwithstanding any terms or conditions to + * the contrary in the License Agreement, reproduction or disclosure + * of the Licensed Deliverables to any third party without the express + * written consent of NVIDIA is prohibited. + * + * NOTWITHSTANDING ANY TERMS OR CONDITIONS TO THE CONTRARY IN THE + * LICENSE AGREEMENT, NVIDIA MAKES NO REPRESENTATION ABOUT THE + * SUITABILITY OF THESE LICENSED DELIVERABLES FOR ANY PURPOSE. THEY ARE + * PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND. + * NVIDIA DISCLAIMS ALL WARRANTIES WITH REGARD TO THESE LICENSED + * DELIVERABLES, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY, + * NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. + * NOTWITHSTANDING ANY TERMS OR CONDITIONS TO THE CONTRARY IN THE + * LICENSE AGREEMENT, IN NO EVENT SHALL NVIDIA BE LIABLE FOR ANY + * SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, OR ANY + * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + * OF THESE LICENSED DELIVERABLES. + * + * U.S. Government End Users. These Licensed Deliverables are a + * "commercial item" as that term is defined at 48 C.F.R. 2.101 (OCT + * 1995), consisting of "commercial computer software" and "commercial + * computer software documentation" as such terms are used in 48 + * C.F.R. 12.212 (SEPT 1995) and are provided to the U.S. Government + * only as a commercial end item. Consistent with 48 C.F.R.12.212 and + * 48 C.F.R. 227.7202-1 through 227.7202-4 (JUNE 1995), all + * U.S. Government End Users acquire the Licensed Deliverables with + * only those rights set forth herein. + * + * Any use of the Licensed Deliverables in individual and commercial + * software must include, in the user documentation and internal + * comments to the code, the above Disclaimer and U.S. Government End + * Users Notice. + */ + +/*! +* \file cufftXt.h +* \brief Public header file for the NVIDIA CUDA FFT library (CUFFT) +*/ + +#ifndef _CUFFTXT_H_ +#define _CUFFTXT_H_ +#include "cudalibxt.h" +#include "cufft.h" + + +#ifndef CUFFTAPI +#ifdef _WIN32 +#define CUFFTAPI __stdcall +#else +#define CUFFTAPI +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// +// cufftXtSubFormat identifies the data layout of +// a memory descriptor owned by cufft. +// note that multi GPU cufft does not yet support out-of-place transforms +// + +typedef enum cufftXtSubFormat_t { + CUFFT_XT_FORMAT_INPUT = 0x00, //by default input is in linear order across GPUs + CUFFT_XT_FORMAT_OUTPUT = 0x01, //by default output is in scrambled order depending on transform + CUFFT_XT_FORMAT_INPLACE = 0x02, //by default inplace is input order, which is linear across GPUs + CUFFT_XT_FORMAT_INPLACE_SHUFFLED = 0x03, //shuffled output order after execution of the transform + CUFFT_XT_FORMAT_1D_INPUT_SHUFFLED = 0x04, //shuffled input order prior to execution of 1D transforms + CUFFT_XT_FORMAT_DISTRIBUTED_INPUT = 0x05, + CUFFT_XT_FORMAT_DISTRIBUTED_OUTPUT = 0x06, + CUFFT_FORMAT_UNDEFINED = 0x07 +} cufftXtSubFormat; + +// +// cufftXtCopyType specifies the type of copy for cufftXtMemcpy +// +typedef enum cufftXtCopyType_t { + CUFFT_COPY_HOST_TO_DEVICE = 0x00, + CUFFT_COPY_DEVICE_TO_HOST = 0x01, + CUFFT_COPY_DEVICE_TO_DEVICE = 0x02, + CUFFT_COPY_UNDEFINED = 0x03 +} cufftXtCopyType; + +// +// cufftXtQueryType specifies the type of query for cufftXtQueryPlan +// +typedef enum cufftXtQueryType_t { + CUFFT_QUERY_1D_FACTORS = 0x00, + CUFFT_QUERY_UNDEFINED = 0x01 +} cufftXtQueryType; + +typedef struct cufftXt1dFactors_t { + long long int size; + long long int stringCount; + long long int stringLength; + long long int substringLength; + long long int factor1; + long long int factor2; + long long int stringMask; + long long int substringMask; + long long int factor1Mask; + long long int factor2Mask; + int stringShift; + int substringShift; + int factor1Shift; + int factor2Shift; +} cufftXt1dFactors; + +// +// cufftXtWorkAreaPolicy specifies policy for cufftXtSetWorkAreaPolicy +// +typedef enum cufftXtWorkAreaPolicy_t { + CUFFT_WORKAREA_MINIMAL = 0, /* maximum reduction */ + CUFFT_WORKAREA_USER = 1, /* use workSize parameter as limit */ + CUFFT_WORKAREA_PERFORMANCE = 2, /* default - 1x overhead or more, maximum performance */ +} cufftXtWorkAreaPolicy; + +// multi-GPU routines +cufftResult CUFFTAPI cufftXtSetGPUs(cufftHandle handle, int nGPUs, int *whichGPUs); + +cufftResult CUFFTAPI cufftXtMalloc(cufftHandle plan, + cudaLibXtDesc ** descriptor, + cufftXtSubFormat format); + +cufftResult CUFFTAPI cufftXtMemcpy(cufftHandle plan, + void *dstPointer, + void *srcPointer, + cufftXtCopyType type); + +cufftResult CUFFTAPI cufftXtFree(cudaLibXtDesc *descriptor); + +cufftResult CUFFTAPI cufftXtSetWorkArea(cufftHandle plan, void **workArea); + +cufftResult CUFFTAPI cufftXtExecDescriptorC2C(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output, + int direction); + +cufftResult CUFFTAPI cufftXtExecDescriptorR2C(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output); + +cufftResult CUFFTAPI cufftXtExecDescriptorC2R(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output); + +cufftResult CUFFTAPI cufftXtExecDescriptorZ2Z(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output, + int direction); + +cufftResult CUFFTAPI cufftXtExecDescriptorD2Z(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output); + +cufftResult CUFFTAPI cufftXtExecDescriptorZ2D(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output); + +// Utility functions + +cufftResult CUFFTAPI cufftXtQueryPlan(cufftHandle plan, void *queryStruct, cufftXtQueryType queryType); + + +// callbacks + + +typedef enum cufftXtCallbackType_t { + CUFFT_CB_LD_COMPLEX = 0x0, + CUFFT_CB_LD_COMPLEX_DOUBLE = 0x1, + CUFFT_CB_LD_REAL = 0x2, + CUFFT_CB_LD_REAL_DOUBLE = 0x3, + CUFFT_CB_ST_COMPLEX = 0x4, + CUFFT_CB_ST_COMPLEX_DOUBLE = 0x5, + CUFFT_CB_ST_REAL = 0x6, + CUFFT_CB_ST_REAL_DOUBLE = 0x7, + CUFFT_CB_UNDEFINED = 0x8 + +} cufftXtCallbackType; + +typedef cufftComplex (*cufftCallbackLoadC)(void *dataIn, size_t offset, void *callerInfo, void *sharedPointer); +typedef cufftDoubleComplex (*cufftCallbackLoadZ)(void *dataIn, size_t offset, void *callerInfo, void *sharedPointer); +typedef cufftReal (*cufftCallbackLoadR)(void *dataIn, size_t offset, void *callerInfo, void *sharedPointer); +typedef cufftDoubleReal(*cufftCallbackLoadD)(void *dataIn, size_t offset, void *callerInfo, void *sharedPointer); + +typedef void (*cufftCallbackStoreC)(void *dataOut, size_t offset, cufftComplex element, void *callerInfo, void *sharedPointer); +typedef void (*cufftCallbackStoreZ)(void *dataOut, size_t offset, cufftDoubleComplex element, void *callerInfo, void *sharedPointer); +typedef void (*cufftCallbackStoreR)(void *dataOut, size_t offset, cufftReal element, void *callerInfo, void *sharedPointer); +typedef void (*cufftCallbackStoreD)(void *dataOut, size_t offset, cufftDoubleReal element, void *callerInfo, void *sharedPointer); + + +cufftResult CUFFTAPI cufftXtSetCallback(cufftHandle plan, void **callback_routine, cufftXtCallbackType cbType, void **caller_info); +cufftResult CUFFTAPI cufftXtClearCallback(cufftHandle plan, cufftXtCallbackType cbType); +cufftResult CUFFTAPI cufftXtSetCallbackSharedSize(cufftHandle plan, cufftXtCallbackType cbType, size_t sharedSize); + +cufftResult CUFFTAPI cufftXtMakePlanMany(cufftHandle plan, + int rank, + long long int *n, + long long int *inembed, + long long int istride, + long long int idist, + cudaDataType inputtype, + long long int *onembed, + long long int ostride, + long long int odist, + cudaDataType outputtype, + long long int batch, + size_t *workSize, + cudaDataType executiontype); + +cufftResult CUFFTAPI cufftXtGetSizeMany(cufftHandle plan, + int rank, + long long int *n, + long long int *inembed, + long long int istride, + long long int idist, + cudaDataType inputtype, + long long int *onembed, + long long int ostride, + long long int odist, + cudaDataType outputtype, + long long int batch, + size_t *workSize, + cudaDataType executiontype); + + +cufftResult CUFFTAPI cufftXtExec(cufftHandle plan, + void *input, + void *output, + int direction); + +cufftResult CUFFTAPI cufftXtExecDescriptor(cufftHandle plan, + cudaLibXtDesc *input, + cudaLibXtDesc *output, + int direction); + +cufftResult CUFFTAPI cufftXtSetWorkAreaPolicy(cufftHandle plan, cufftXtWorkAreaPolicy policy, size_t *workSize); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/python3.12/site-packages/nvidia/cufft/lib/__init__.py b/lib/python3.12/site-packages/nvidia/cufft/lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/nvidia/cufft/lib/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/nvidia/cufft/lib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c983b85c38a8923a7caff6543d7c788a3f35d207 Binary files /dev/null and b/lib/python3.12/site-packages/nvidia/cufft/lib/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__init__.py b/lib/python3.12/site-packages/ray/_common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21910318d6c3aef7faf246dfdec9c95be1b0ca7d Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/constants.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9a5c354fedeb40853f615478d3f287a6ddc0769 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/constants.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/deprecation.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/deprecation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93e8cb98b07292fe2ce4899f4b2f1377a9917684 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/deprecation.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/filters.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49fdc2d32eae9a88f32a808c1566496ce235e562 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/filters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/formatters.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/formatters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8265a5beffe2b8e5500f5aeb44d99b6190d04f1b Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/formatters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/network_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/network_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62187743da4d062484e122b77bf8d8c4a12d7256 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/network_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/pydantic_compat.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/pydantic_compat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcb67f992fd04adb5ab6fcc5c577f77a20494ac3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/pydantic_compat.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/ray_constants.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/ray_constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8401d86a1c111d8c05d36aa4d099687328e774d Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/ray_constants.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/ray_option_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/ray_option_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7ef363120993118a6ef38abaead3b180993cafa Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/ray_option_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/retry.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/retry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc46c7f193c6ae0930e5eb09c7c20bdae11844c4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/retry.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/serialization.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/serialization.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aeb1b9dc58e19635c2b85f27d92320200922e541 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/serialization.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/signature.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/signature.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa02eaa29a0b5106d64c4c2b4cf9ab5639cbb168 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/signature.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/test_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/test_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbd8587f32893fbb5e46ccc3f0b212245940f445 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/test_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8ebee22824ed7d3f7c0a9c41769a635aab79d46 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/constants.py b/lib/python3.12/site-packages/ray/_common/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..c9ca78de99f5493adcf42653908413d5f8e26a1d --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/constants.py @@ -0,0 +1,7 @@ +# Prefix for the node id resource that is automatically added to each node. +# For example, a node may have id `node:172.23.42.1`. +NODE_ID_PREFIX = "node:" +# The system resource that head node has. +HEAD_NODE_RESOURCE_NAME = NODE_ID_PREFIX + "__internal_head__" + +RAY_WARN_BLOCKING_GET_INSIDE_ASYNC_ENV_VAR = "RAY_WARN_BLOCKING_GET_INSIDE_ASYNC" diff --git a/lib/python3.12/site-packages/ray/_common/deprecation.py b/lib/python3.12/site-packages/ray/_common/deprecation.py new file mode 100644 index 0000000000000000000000000000000000000000..01c0bd62467c5745c3c9890061abdeaef2c9099f --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/deprecation.py @@ -0,0 +1,141 @@ +import inspect +import logging +from typing import Optional, Union + +from ray.util import log_once +from ray.util.annotations import _mark_annotated + +logger = logging.getLogger(__name__) + +# A constant to use for any configuration that should be deprecated +# (to check, whether this config has actually been assigned a proper value or +# not). +DEPRECATED_VALUE = -1 + + +def deprecation_warning( + old: str, + new: Optional[str] = None, + *, + help: Optional[str] = None, + error: Optional[Union[bool, Exception]] = None, + stacklevel: int = 2, +) -> None: + """Warns (via the `logger` object) or throws a deprecation warning/error. + + Args: + old: A description of the "thing" that is to be deprecated. + new: A description of the new "thing" that replaces it. + help: An optional help text to tell the user, what to + do instead of using `old`. + error: Whether or which exception to raise. If True, raise ValueError. + If False, just warn. If `error` is-a subclass of Exception, + raise that Exception. + stacklevel: The stacklevel to use for the warning message. + Use 2 to point to where this function is called, 3+ to point + further up the stack. + + Raises: + ValueError: If `error=True`. + Exception: Of type `error`, iff `error` is a sub-class of `Exception`. + """ + msg = "`{}` has been deprecated.{}".format( + old, (" Use `{}` instead.".format(new) if new else f" {help}" if help else "") + ) + + if error: + if not isinstance(error, bool) and issubclass(error, Exception): + # error is an Exception + raise error(msg) + else: + # error is a boolean, construct ValueError ourselves + raise ValueError(msg) + else: + logger.warning( + "DeprecationWarning: " + msg + " This will raise an error in the future!", + stacklevel=stacklevel, + ) + + +def Deprecated(old=None, *, new=None, help=None, error): + """Decorator for documenting a deprecated class, method, or function. + + Automatically adds a `deprecation.deprecation_warning(old=..., + error=False)` to not break existing code at this point to the decorated + class' constructor, method, or function. + + In a next major release, this warning should then be made an error + (by setting error=True), which means at this point that the + class/method/function is no longer supported, but will still inform + the user about the deprecation event. + + In a further major release, the class, method, function should be erased + entirely from the codebase. + + + .. testcode:: + :skipif: True + + from ray._common.deprecation import Deprecated + # Deprecated class: Patches the constructor to warn if the class is + # used. + @Deprecated(new="NewAndMuchCoolerClass", error=False) + class OldAndUncoolClass: + ... + + # Deprecated class method: Patches the method to warn if called. + class StillCoolClass: + ... + @Deprecated(new="StillCoolClass.new_and_much_cooler_method()", + error=False) + def old_and_uncool_method(self, uncool_arg): + ... + + # Deprecated function: Patches the function to warn if called. + @Deprecated(new="new_and_much_cooler_function", error=False) + def old_and_uncool_function(*uncool_args): + ... + """ + + def _inner(obj): + # A deprecated class. + if inspect.isclass(obj): + # Patch the class' init method to raise the warning/error. + obj_init = obj.__init__ + + def patched_init(*args, **kwargs): + if log_once(old or obj.__name__): + deprecation_warning( + old=old or obj.__name__, + new=new, + help=help, + error=error, + stacklevel=3, + ) + return obj_init(*args, **kwargs) + + obj.__init__ = patched_init + _mark_annotated(obj) + # Return the patched class (with the warning/error when + # instantiated). + return obj + + # A deprecated class method or function. + # Patch with the warning/error at the beginning. + def _ctor(*args, **kwargs): + if log_once(old or obj.__name__): + deprecation_warning( + old=old or obj.__name__, + new=new, + help=help, + error=error, + stacklevel=3, + ) + # Call the deprecated method/function. + return obj(*args, **kwargs) + + # Return the patched class method/function. + return _ctor + + # Return the prepared decorator. + return _inner diff --git a/lib/python3.12/site-packages/ray/_common/filters.py b/lib/python3.12/site-packages/ray/_common/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..f3c8219b540997239e4c925dda7189a3f214e9ca --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/filters.py @@ -0,0 +1,55 @@ +import logging +from typing import Any, Dict + +import ray +from ray._private.ray_logging.constants import LogKey + + +class CoreContextFilter(logging.Filter): + TASK_LEVEL_LOG_KEYS = [ + LogKey.TASK_ID.value, + LogKey.TASK_NAME.value, + LogKey.TASK_FUNCTION_NAME.value, + ] + + @classmethod + def get_ray_core_logging_context(cls) -> Dict[str, Any]: + """ + Get the ray core logging context as a dict. + Only use this function if you need include the attributes to the log record + yourself by bypassing the filter. + """ + if not ray.is_initialized(): + # There is no additional context if ray is not initialized + return {} + + runtime_context = ray.get_runtime_context() + ray_core_logging_context = { + LogKey.JOB_ID.value: runtime_context.get_job_id(), + LogKey.WORKER_ID.value: runtime_context.get_worker_id(), + LogKey.NODE_ID.value: runtime_context.get_node_id(), + } + if runtime_context.worker.mode == ray.WORKER_MODE: + ray_core_logging_context[ + LogKey.ACTOR_ID.value + ] = runtime_context.get_actor_id() + ray_core_logging_context[ + LogKey.TASK_ID.value + ] = runtime_context.get_task_id() + ray_core_logging_context[ + LogKey.TASK_NAME.value + ] = runtime_context.get_task_name() + ray_core_logging_context[ + LogKey.TASK_FUNCTION_NAME.value + ] = runtime_context.get_task_function_name() + ray_core_logging_context[ + LogKey.ACTOR_NAME.value + ] = runtime_context.get_actor_name() + return ray_core_logging_context + + def filter(self, record): + context = self.get_ray_core_logging_context() + for key, value in context.items(): + if value is not None: + setattr(record, key, value) + return True diff --git a/lib/python3.12/site-packages/ray/_common/formatters.py b/lib/python3.12/site-packages/ray/_common/formatters.py new file mode 100644 index 0000000000000000000000000000000000000000..bf67a309bb5c5d33bb7836f36a8a1ac15120bd85 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/formatters.py @@ -0,0 +1,120 @@ +import json +import logging +from abc import ABC, abstractmethod +from typing import Any, Dict, List + +from ray._private.log import INTERNAL_TIMESTAMP_LOG_KEY +from ray._private.ray_constants import LOGGER_FORMAT +from ray._private.ray_logging.constants import ( + LOGGER_FLATTEN_KEYS, + LOGRECORD_STANDARD_ATTRS, + LogKey, +) + + +def _append_flatten_attributes(formatted_attrs: Dict[str, Any], key: str, value: Any): + """Flatten the dictionary values for special keys and append the values in place. + + If the key is in `LOGGER_FLATTEN_KEYS`, the value will be flattened and appended + to the `formatted_attrs` dictionary. Otherwise, the key-value pair will be appended + directly. + """ + if key in LOGGER_FLATTEN_KEYS: + if not isinstance(value, dict): + raise ValueError( + f"Expected a dictionary passing into {key}, but got {type(value)}" + ) + for k, v in value.items(): + if k in formatted_attrs: + raise KeyError(f"Found duplicated key in the log record: {k}") + formatted_attrs[k] = v + else: + formatted_attrs[key] = value + + +class AbstractFormatter(logging.Formatter, ABC): + def __init__(self, fmt=None, datefmt=None, style="%", validate=True) -> None: + super().__init__(fmt, datefmt, style, validate) + self._additional_log_standard_attrs = [] + + def set_additional_log_standard_attrs( + self, additional_log_standard_attrs: List[str] + ) -> None: + self._additional_log_standard_attrs = additional_log_standard_attrs + + @property + def additional_log_standard_attrs(self) -> List[str]: + return self._additional_log_standard_attrs + + def generate_record_format_attrs( + self, + record: logging.LogRecord, + exclude_default_standard_attrs, + ) -> dict: + record_format_attrs = {} + + # If `exclude_default_standard_attrs` is False, include the standard attributes. + # Otherwise, include only Ray and user-provided context. + if not exclude_default_standard_attrs: + record_format_attrs.update( + { + LogKey.ASCTIME.value: self.formatTime(record), + LogKey.LEVELNAME.value: record.levelname, + LogKey.MESSAGE.value: record.getMessage(), + LogKey.FILENAME.value: record.filename, + LogKey.LINENO.value: record.lineno, + LogKey.PROCESS.value: record.process, + } + ) + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + record_format_attrs[LogKey.EXC_TEXT.value] = record.exc_text + + # Add the user specified additional standard attributes. + for key in self._additional_log_standard_attrs: + _append_flatten_attributes( + record_format_attrs, key, getattr(record, key, None) + ) + + for key, value in record.__dict__.items(): + # Both Ray and user-provided context are stored in `record_format`. + if key not in LOGRECORD_STANDARD_ATTRS: + _append_flatten_attributes(record_format_attrs, key, value) + + # Format the internal timestamp to the standardized `timestamp_ns` key. + if INTERNAL_TIMESTAMP_LOG_KEY in record_format_attrs: + record_format_attrs[LogKey.TIMESTAMP_NS.value] = record_format_attrs.pop( + INTERNAL_TIMESTAMP_LOG_KEY + ) + + return record_format_attrs + + @abstractmethod + def format(self, record: logging.LogRecord) -> str: + pass + + +class JSONFormatter(AbstractFormatter): + def format(self, record: logging.LogRecord) -> str: + record_format_attrs = self.generate_record_format_attrs( + record, exclude_default_standard_attrs=False + ) + return json.dumps(record_format_attrs) + + +class TextFormatter(AbstractFormatter): + def __init__(self, fmt=None, datefmt=None, style="%", validate=True) -> None: + super().__init__(fmt, datefmt, style, validate) + self._inner_formatter = logging.Formatter(LOGGER_FORMAT) + + def format(self, record: logging.LogRecord) -> str: + s = self._inner_formatter.format(record) + record_format_attrs = self.generate_record_format_attrs( + record, exclude_default_standard_attrs=True + ) + + additional_attrs = " ".join( + [f"{key}={value}" for key, value in record_format_attrs.items()] + ) + return f"{s} {additional_attrs}" diff --git a/lib/python3.12/site-packages/ray/_common/network_utils.py b/lib/python3.12/site-packages/ray/_common/network_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..593407b5c3775ef06d55b2a812f9c7ad63fb211d --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/network_utils.py @@ -0,0 +1,111 @@ +import socket +from contextlib import closing +from functools import lru_cache +from typing import Optional, Tuple, Union + +from ray._raylet import ( + build_address as _build_address, + is_ipv6 as _is_ipv6, + node_ip_address_from_perspective as _node_ip_address_from_perspective, + parse_address as _parse_address, +) + + +def parse_address(address: str) -> Optional[Tuple[str, str]]: + """Parse a network address string into host and port. + + Args: + address: The address string to parse (e.g., "localhost:8000", "[::1]:8000"). + + Returns: + Tuple with (host, port) if port found, None if no colon separator. + """ + return _parse_address(address) + + +def build_address(host: str, port: Union[int, str]) -> str: + """Build a network address string from host and port. + + Args: + host: The hostname or IP address. + port: The port number (int or string). + + Returns: + Formatted address string (e.g., "localhost:8000" or "[::1]:8000"). + """ + return _build_address(host, port) + + +def node_ip_address_from_perspective(address: Optional[str] = None) -> str: + """IP address by which the local node can be reached *from* the `address`. + + If no address is given, defaults to public DNS servers for detection. + + Args: + address: The IP address and port of any known live service on the + network you care about. + + Returns: + The IP address by which the local node can be reached from the address. + """ + return _node_ip_address_from_perspective(address) + + +def is_ipv6(host: str) -> bool: + """Check if a host is resolved to IPv6. + + Args: + host: The IP or domain name to check (must be without port). + + Returns: + True if the host is resolved to IPv6, False if IPv4. + """ + return _is_ipv6(host) + + +@lru_cache(maxsize=1) +def get_localhost_ip() -> str: + """Get localhost loopback ip with IPv4/IPv6 support. + + Returns: + The localhost loopback IP. + """ + # Try IPv4 first, then IPv6 localhost resolution + for family in [socket.AF_INET, socket.AF_INET6]: + try: + dns_result = socket.getaddrinfo( + "localhost", None, family, socket.SOCK_STREAM + ) + return dns_result[0][4][0] + except Exception: + continue + + # Final fallback to IPv4 loopback + return "127.0.0.1" + + +def is_localhost(host: str) -> bool: + """Check if the given host string represents a localhost address. + + Args: + host: The hostname or IP address to check. + + Returns: + True if the host is a localhost address, False otherwise. + """ + return host in ("localhost", "127.0.0.1", "::1") + + +def find_free_port(family: socket.AddressFamily = socket.AF_INET) -> int: + """Find a free port on the local machine. + + Args: + family: The socket address family (AF_INET for IPv4, AF_INET6 for IPv6). + Defaults to AF_INET. + + Returns: + An available port number. + """ + with closing(socket.socket(family, socket.SOCK_STREAM)) as s: + s.bind(("", 0)) + return s.getsockname()[1] diff --git a/lib/python3.12/site-packages/ray/_common/pydantic_compat.py b/lib/python3.12/site-packages/ray/_common/pydantic_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..b405e64ffa8ff77aa3eeb694961bbe141e3a6dd3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/pydantic_compat.py @@ -0,0 +1,108 @@ +# ruff: noqa +import packaging.version + +# Pydantic is a dependency of `ray["default"]` but not the minimal installation, +# so handle the case where it isn't installed. +try: + import pydantic + + PYDANTIC_INSTALLED = True +except ImportError: + pydantic = None + PYDANTIC_INSTALLED = False + + +if not PYDANTIC_INSTALLED: + IS_PYDANTIC_2 = False + BaseModel = None + Extra = None + Field = None + NonNegativeFloat = None + NonNegativeInt = None + PositiveFloat = None + PositiveInt = None + PrivateAttr = None + StrictInt = None + ValidationError = None + root_validator = None + validator = None + is_subclass_of_base_model = lambda obj: False +# In pydantic <1.9.0, __version__ attribute is missing, issue ref: +# https://github.com/pydantic/pydantic/issues/2572, so we need to check +# the existence prior to comparison. +elif not hasattr(pydantic, "__version__") or packaging.version.parse( + pydantic.__version__ +) < packaging.version.parse("2.0"): + IS_PYDANTIC_2 = False + from pydantic import ( + BaseModel, + Extra, + Field, + NonNegativeFloat, + NonNegativeInt, + PositiveFloat, + PositiveInt, + PrivateAttr, + StrictInt, + ValidationError, + root_validator, + validator, + ) + + def is_subclass_of_base_model(obj): + return issubclass(obj, BaseModel) + +else: + IS_PYDANTIC_2 = True + from pydantic.v1 import ( + BaseModel, + Extra, + Field, + NonNegativeFloat, + NonNegativeInt, + PositiveFloat, + PositiveInt, + PrivateAttr, + StrictInt, + ValidationError, + root_validator, + validator, + ) + + def is_subclass_of_base_model(obj): + from pydantic import BaseModel as BaseModelV2 + from pydantic.v1 import BaseModel as BaseModelV1 + + return issubclass(obj, BaseModelV1) or issubclass(obj, BaseModelV2) + + +def register_pydantic_serializers(serialization_context): + if not PYDANTIC_INSTALLED: + return + + if IS_PYDANTIC_2: + # TODO(edoakes): compare against the version that has the fixes. + from pydantic.v1.fields import ModelField + else: + from pydantic.fields import ModelField + + # Pydantic's Cython validators are not serializable. + # https://github.com/cloudpipe/cloudpickle/issues/408 + serialization_context._register_cloudpickle_serializer( + ModelField, + custom_serializer=lambda o: { + "name": o.name, + # outer_type_ is the original type for ModelFields, + # while type_ can be updated later with the nested type + # like int for List[int]. + "type_": o.outer_type_, + "class_validators": o.class_validators, + "model_config": o.model_config, + "default": o.default, + "default_factory": o.default_factory, + "required": o.required, + "alias": o.alias, + "field_info": o.field_info, + }, + custom_deserializer=lambda kwargs: ModelField(**kwargs), + ) diff --git a/lib/python3.12/site-packages/ray/_common/ray_constants.py b/lib/python3.12/site-packages/ray/_common/ray_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..0c8c65269bb41d99ce17f6cf093fca27e47c197e --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/ray_constants.py @@ -0,0 +1,5 @@ +# Default max_concurrency option in @ray.remote for async actors. +DEFAULT_MAX_CONCURRENCY_ASYNC = 1000 + +LOGGING_ROTATE_BYTES = 512 * 1024 * 1024 # 512MB. +LOGGING_ROTATE_BACKUP_COUNT = 5 # 5 Backup files at max. diff --git a/lib/python3.12/site-packages/ray/_common/ray_option_utils.py b/lib/python3.12/site-packages/ray/_common/ray_option_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..85a972881d639f07c01f247cf0a65bc55d63b494 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/ray_option_utils.py @@ -0,0 +1,384 @@ +"""Manage, parse and validate options for Ray tasks, actors and actor methods.""" +import warnings +from dataclasses import dataclass +from typing import Any, Callable, Dict, Optional, Tuple, Union + +import ray +from ray._private import ray_constants +from ray._private.label_utils import ( + validate_fallback_strategy, + validate_label_selector, +) +from ray._private.utils import get_ray_doc_version +from ray.util.placement_group import PlacementGroup +from ray.util.scheduling_strategies import ( + NodeAffinitySchedulingStrategy, + NodeLabelSchedulingStrategy, + PlacementGroupSchedulingStrategy, +) + + +@dataclass +class Option: + # Type constraint of an option. + type_constraint: Optional[Union[type, Tuple[type]]] = None + # Value constraint of an option. + # The callable should return None if there is no error. + # Otherwise, return the error message. + value_constraint: Optional[Callable[[Any], Optional[str]]] = None + # Default value. + default_value: Any = None + + def validate(self, keyword: str, value: Any): + """Validate the option.""" + if self.type_constraint is not None: + if not isinstance(value, self.type_constraint): + raise TypeError( + f"The type of keyword '{keyword}' must be {self.type_constraint}, " + f"but received type {type(value)}" + ) + if self.value_constraint is not None: + possible_error_message = self.value_constraint(value) + if possible_error_message: + raise ValueError(possible_error_message) + + +def _counting_option(name: str, infinite: bool = True, default_value: Any = None): + """This is used for positive and discrete options. + + Args: + name: The name of the option keyword. + infinite: If True, user could use -1 to represent infinity. + default_value: The default value for this option. + + Returns: + An Option object. + """ + if infinite: + return Option( + (int, type(None)), + lambda x: None + if (x is None or x >= -1) + else f"The keyword '{name}' only accepts None, 0, -1" + " or a positive integer, where -1 represents infinity.", + default_value=default_value, + ) + return Option( + (int, type(None)), + lambda x: None + if (x is None or x >= 0) + else f"The keyword '{name}' only accepts None, 0 or a positive integer.", + default_value=default_value, + ) + + +def _validate_resource_quantity(name, quantity): + if quantity < 0: + return f"The quantity of resource {name} cannot be negative" + if ( + isinstance(quantity, float) + and quantity != 0.0 + and int(quantity * ray._raylet.RESOURCE_UNIT_SCALING) == 0 + ): + return ( + f"The precision of the fractional quantity of resource {name}" + " cannot go beyond 0.0001" + ) + resource_name = "GPU" if name == "num_gpus" else name + if resource_name in ray._private.accelerators.get_all_accelerator_resource_names(): + ( + valid, + error_message, + ) = ray._private.accelerators.get_accelerator_manager_for_resource( + resource_name + ).validate_resource_request_quantity( + quantity + ) + if not valid: + return error_message + return None + + +def _resource_option(name: str, default_value: Any = None): + """This is used for resource related options.""" + return Option( + (float, int, type(None)), + lambda x: None if (x is None) else _validate_resource_quantity(name, x), + default_value=default_value, + ) + + +def _validate_resources(resources: Optional[Dict[str, float]]) -> Optional[str]: + if resources is None: + return None + + if "CPU" in resources or "GPU" in resources: + return ( + "Use the 'num_cpus' and 'num_gpus' keyword instead of 'CPU' and 'GPU' " + "in 'resources' keyword" + ) + + for name, quantity in resources.items(): + possible_error_message = _validate_resource_quantity(name, quantity) + if possible_error_message: + return possible_error_message + + return None + + +_common_options = { + "label_selector": Option((dict, type(None)), lambda x: validate_label_selector(x)), + "fallback_strategy": Option( + (list, type(None)), lambda x: validate_fallback_strategy(x) + ), + "accelerator_type": Option((str, type(None))), + "memory": _resource_option("memory"), + "name": Option((str, type(None))), + "num_cpus": _resource_option("num_cpus"), + "num_gpus": _resource_option("num_gpus"), + "object_store_memory": _counting_option("object_store_memory", False), + # TODO(suquark): "placement_group", "placement_group_bundle_index" + # and "placement_group_capture_child_tasks" are deprecated, + # use "scheduling_strategy" instead. + "placement_group": Option( + (type(None), str, PlacementGroup), default_value="default" + ), + "placement_group_bundle_index": Option(int, default_value=-1), + "placement_group_capture_child_tasks": Option((bool, type(None))), + "resources": Option((dict, type(None)), lambda x: _validate_resources(x)), + "runtime_env": Option((dict, type(None))), + "scheduling_strategy": Option( + ( + type(None), + str, + PlacementGroupSchedulingStrategy, + NodeAffinitySchedulingStrategy, + NodeLabelSchedulingStrategy, + ) + ), + "enable_task_events": Option(bool, default_value=True), + "_labels": Option((dict, type(None))), +} + + +def issubclass_safe(obj: Any, cls_: type) -> bool: + try: + return issubclass(obj, cls_) + except TypeError: + return False + + +_task_only_options = { + "max_calls": _counting_option("max_calls", False, default_value=0), + # Normal tasks may be retried on failure this many times. + # TODO(swang): Allow this to be set globally for an application. + "max_retries": _counting_option( + "max_retries", default_value=ray_constants.DEFAULT_TASK_MAX_RETRIES + ), + # override "_common_options" + "num_cpus": _resource_option("num_cpus", default_value=1), + "num_returns": Option( + (int, str, type(None)), + lambda x: None + if (x is None or x == "dynamic" or x == "streaming" or x >= 0) + else "Default None. When None is passed, " + "The default value is 1 for a task and actor task, and " + "'streaming' for generator tasks and generator actor tasks. " + "The keyword 'num_returns' only accepts None, " + "a non-negative integer, " + "'streaming' (for generators), or 'dynamic'. 'dynamic' flag " + "will be deprecated in the future, and it is recommended to use " + "'streaming' instead.", + default_value=None, + ), + "object_store_memory": Option( # override "_common_options" + (int, type(None)), + lambda x: None + if (x is None) + else "Setting 'object_store_memory' is not implemented for tasks", + ), + "retry_exceptions": Option( + (bool, list, tuple), + lambda x: None + if ( + isinstance(x, bool) + or ( + isinstance(x, (list, tuple)) + and all(issubclass_safe(x_, Exception) for x_ in x) + ) + ) + else "retry_exceptions must be either a boolean or a list of exceptions", + default_value=False, + ), + "_generator_backpressure_num_objects": Option( + (int, type(None)), + lambda x: None + if x != 0 + else ( + "_generator_backpressure_num_objects=0 is not allowed. " + "Use a value > 0. If the value is equal to 1, the behavior " + "is identical to Python generator (generator 1 object " + "whenever `next` is called). Use -1 to disable this feature. " + ), + ), +} + +_actor_only_options = { + "concurrency_groups": Option((list, dict, type(None))), + "enable_tensor_transport": Option((bool, type(None)), default_value=None), + "lifetime": Option( + (str, type(None)), + lambda x: None + if x in (None, "detached", "non_detached") + else "actor `lifetime` argument must be one of 'detached', " + "'non_detached' and 'None'.", + ), + "max_concurrency": _counting_option("max_concurrency", False), + "max_restarts": _counting_option("max_restarts", default_value=0), + "max_task_retries": _counting_option("max_task_retries", default_value=0), + "max_pending_calls": _counting_option("max_pending_calls", default_value=-1), + "namespace": Option((str, type(None))), + "get_if_exists": Option(bool, default_value=False), + "allow_out_of_order_execution": Option((bool, type(None))), +} + +# Priority is important here because during dictionary update, same key with higher +# priority overrides the same key with lower priority. We make use of priority +# to set the correct default value for tasks / actors. + +# priority: _common_options > _actor_only_options > _task_only_options +valid_options: Dict[str, Option] = { + **_task_only_options, + **_actor_only_options, + **_common_options, +} +# priority: _task_only_options > _common_options +task_options: Dict[str, Option] = {**_common_options, **_task_only_options} +# priority: _actor_only_options > _common_options +actor_options: Dict[str, Option] = {**_common_options, **_actor_only_options} + +remote_args_error_string = ( + "The @ray.remote decorator must be applied either with no arguments and no " + "parentheses, for example '@ray.remote', or it must be applied using some of " + f"the arguments in the list {list(valid_options.keys())}, for example " + "'@ray.remote(num_returns=2, resources={\"CustomResource\": 1})'." +) + + +def _check_deprecate_placement_group(options: Dict[str, Any]): + """Check if deprecated placement group option exists.""" + placement_group = options.get("placement_group", "default") + scheduling_strategy = options.get("scheduling_strategy") + # TODO(suquark): @ray.remote(placement_group=None) is used in + # "python/ray.data._internal/remote_fn.py" and many other places, + # while "ray.data.read_api.read_datasource" set "scheduling_strategy=SPREAD". + # This might be a bug, but it is also ok to allow them co-exist. + if (placement_group not in ("default", None)) and (scheduling_strategy is not None): + raise ValueError( + "Placement groups should be specified via the " + "scheduling_strategy option. " + "The placement_group option is deprecated." + ) + + +def _warn_if_using_deprecated_placement_group( + options: Dict[str, Any], caller_stacklevel: int +): + placement_group = options["placement_group"] + placement_group_bundle_index = options["placement_group_bundle_index"] + placement_group_capture_child_tasks = options["placement_group_capture_child_tasks"] + if placement_group != "default": + warnings.warn( + "placement_group parameter is deprecated. Use " + "scheduling_strategy=PlacementGroupSchedulingStrategy(...) " + "instead, see the usage at " + f"https://docs.ray.io/en/{get_ray_doc_version()}/ray-core/package-ref.html#ray-remote.", # noqa: E501 + DeprecationWarning, + stacklevel=caller_stacklevel + 1, + ) + if placement_group_bundle_index != -1: + warnings.warn( + "placement_group_bundle_index parameter is deprecated. Use " + "scheduling_strategy=PlacementGroupSchedulingStrategy(...) " + "instead, see the usage at " + f"https://docs.ray.io/en/{get_ray_doc_version()}/ray-core/package-ref.html#ray-remote.", # noqa: E501 + DeprecationWarning, + stacklevel=caller_stacklevel + 1, + ) + if placement_group_capture_child_tasks: + warnings.warn( + "placement_group_capture_child_tasks parameter is deprecated. Use " + "scheduling_strategy=PlacementGroupSchedulingStrategy(...) " + "instead, see the usage at " + f"https://docs.ray.io/en/{get_ray_doc_version()}/ray-core/package-ref.html#ray-remote.", # noqa: E501 + DeprecationWarning, + stacklevel=caller_stacklevel + 1, + ) + + +def validate_task_options(options: Dict[str, Any], in_options: bool): + """Options check for Ray tasks. + + Args: + options: Options for Ray tasks. + in_options: If True, we are checking the options under the context of + ".options()". + """ + for k, v in options.items(): + if k not in task_options: + raise ValueError( + f"Invalid option keyword {k} for remote functions. " + f"Valid ones are {list(task_options)}." + ) + task_options[k].validate(k, v) + if in_options and "max_calls" in options: + raise ValueError("Setting 'max_calls' is not supported in '.options()'.") + _check_deprecate_placement_group(options) + + +def validate_actor_options(options: Dict[str, Any], in_options: bool): + """Options check for Ray actors. + + Args: + options: Options for Ray actors. + in_options: If True, we are checking the options under the context of + ".options()". + """ + for k, v in options.items(): + if k not in actor_options: + raise ValueError( + f"Invalid option keyword {k} for actors. " + f"Valid ones are {list(actor_options)}." + ) + actor_options[k].validate(k, v) + + if in_options and "concurrency_groups" in options: + raise ValueError( + "Setting 'concurrency_groups' is not supported in '.options()'." + ) + + if options.get("get_if_exists") and not options.get("name"): + raise ValueError("The actor name must be specified to use `get_if_exists`.") + + if "object_store_memory" in options: + warnings.warn( + "Setting 'object_store_memory'" + " for actors is deprecated since it doesn't actually" + " reserve the required object store memory." + f" Use object spilling that's enabled by default (https://docs.ray.io/en/{get_ray_doc_version()}/ray-core/objects/object-spilling.html) " # noqa: E501 + "instead to bypass the object store memory size limitation.", + DeprecationWarning, + stacklevel=1, + ) + + _check_deprecate_placement_group(options) + + +def update_options( + original_options: Dict[str, Any], new_options: Dict[str, Any] +) -> Dict[str, Any]: + """Update original options with new options and return. + The returned updated options contain shallow copy of original options. + """ + + return {**original_options, **new_options} diff --git a/lib/python3.12/site-packages/ray/_common/retry.py b/lib/python3.12/site-packages/ray/_common/retry.py new file mode 100644 index 0000000000000000000000000000000000000000..947a6360f53fa22e7953d144f00f3fbcee398027 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/retry.py @@ -0,0 +1,82 @@ +import functools +import logging +import random +import time +from typing import Any, Callable, List, Optional + +logger = logging.getLogger(__name__) + + +def call_with_retry( + f: Callable, + description: str, + match: Optional[List[str]] = None, + max_attempts: int = 10, + max_backoff_s: int = 32, + *args, + **kwargs, +) -> Any: + """Retry a function with exponential backoff. + + Args: + f: The function to retry. + description: An imperative description of the function being retried. For + example, "open the file". + match: A list of strings to match in the exception message. If ``None``, any + error is retried. + max_attempts: The maximum number of attempts to retry. + max_backoff_s: The maximum number of seconds to backoff. + *args: Arguments to pass to the function. + **kwargs: Keyword arguments to pass to the function. + + Returns: + The result of the function. + """ + # TODO: consider inverse match and matching exception type + assert max_attempts >= 1, f"`max_attempts` must be positive. Got {max_attempts}." + + for i in range(max_attempts): + try: + return f(*args, **kwargs) + except Exception as e: + exception_str = str(e) + is_retryable = match is None or any( + pattern in exception_str for pattern in match + ) + if is_retryable and i + 1 < max_attempts: + # Retry with binary exponential backoff with 20% random jitter. + backoff = min(2**i, max_backoff_s) * (random.uniform(0.8, 1.2)) + logger.debug( + f"Retrying {i+1} attempts to {description} after {backoff} seconds." + ) + time.sleep(backoff) + else: + if is_retryable: + logger.debug( + f"Failed to {description} after {max_attempts} attempts. Raising." + ) + else: + logger.debug( + f"Did not find a match for {exception_str}. Raising after {i+1} attempts." + ) + raise e from None + + +def retry( + description: str, + match: Optional[List[str]] = None, + max_attempts: int = 10, + max_backoff_s: int = 32, +) -> Callable: + """Decorator-based version of call_with_retry.""" + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def inner(*args, **kwargs): + return call_with_retry( + func, description, match, max_attempts, max_backoff_s, *args, **kwargs + ) + + return inner + + return decorator diff --git a/lib/python3.12/site-packages/ray/_common/serialization.py b/lib/python3.12/site-packages/ray/_common/serialization.py new file mode 100644 index 0000000000000000000000000000000000000000..051f155d38747c1888d4fde849cb4dbdf6e5101d --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/serialization.py @@ -0,0 +1,34 @@ +import io +import logging +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + pass + + +import ray._private.utils +import ray.cloudpickle as pickle +import ray.exceptions +from ray._private import ray_constants +from ray.util import inspect_serializability + +logger = logging.getLogger(__name__) +ALLOW_OUT_OF_BAND_OBJECT_REF_SERIALIZATION = ray_constants.env_bool( + "RAY_allow_out_of_band_object_ref_serialization", True +) + + +def pickle_dumps(obj: Any, error_msg: str): + """Wrap cloudpickle.dumps to provide better error message + when the object is not serializable. + """ + try: + return pickle.dumps(obj) + except (TypeError, ray.exceptions.OufOfBandObjectRefSerializationException) as e: + sio = io.StringIO() + inspect_serializability(obj, print_file=sio) + msg = f"{error_msg}:\n{sio.getvalue()}" + if isinstance(e, TypeError): + raise TypeError(msg) from e + else: + raise ray.exceptions.OufOfBandObjectRefSerializationException(msg) diff --git a/lib/python3.12/site-packages/ray/_common/signature.py b/lib/python3.12/site-packages/ray/_common/signature.py new file mode 100644 index 0000000000000000000000000000000000000000..190d18c139062e3ae014bf9b5c61e484a3a101d7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/signature.py @@ -0,0 +1,163 @@ +import inspect +import logging +from inspect import Parameter +from typing import Any, Dict, List, Tuple + +from ray._private.inspect_util import is_cython + +# Logger for this module. It should be configured at the entry point +# into the program using Ray. Ray provides a default configuration at +# entry/init points. +logger = logging.getLogger(__name__) + +# This dummy type is also defined in ArgumentsBuilder.java. Please keep it +# synced. +DUMMY_TYPE = b"__RAY_DUMMY__" + + +def get_signature(func: Any) -> inspect.Signature: + """Get signature parameters. + + Support Cython functions by grabbing relevant attributes from the Cython + function and attaching to a no-op function. This is somewhat brittle, since + inspect may change, but given that inspect is written to a PEP, we hope + it is relatively stable. Future versions of Python may allow overloading + the inspect 'isfunction' and 'ismethod' functions / create ABC for Python + functions. Until then, it appears that Cython won't do anything about + compatability with the inspect module. + + Args: + func: The function whose signature should be checked. + + Returns: + A function signature object, which includes the names of the keyword + arguments as well as their default values. + + Raises: + TypeError: A type error if the signature is not supported + """ + # The first condition for Cython functions, the latter for Cython instance + # methods + if is_cython(func): + attrs = ["__code__", "__annotations__", "__defaults__", "__kwdefaults__"] + + if all(hasattr(func, attr) for attr in attrs): + original_func = func + + def func(): + return + + for attr in attrs: + setattr(func, attr, getattr(original_func, attr)) + else: + raise TypeError(f"{func!r} is not a Python function we can process") + + return inspect.signature(func) + + +def extract_signature(func: Any, ignore_first: bool = False) -> List[Parameter]: + """Extract the function signature from the function. + + Args: + func: The function whose signature should be extracted. + ignore_first: True if the first argument should be ignored. This should + be used when func is a method of a class. + + Returns: + List of Parameter objects representing the function signature. + """ + signature_parameters = list(get_signature(func).parameters.values()) + + if ignore_first: + if len(signature_parameters) == 0: + raise ValueError( + "Methods must take a 'self' argument, but the " + f"method '{func.__name__}' does not have one." + ) + signature_parameters = signature_parameters[1:] + + return signature_parameters + + +def validate_args( + signature_parameters: List[Parameter], args: Tuple[Any, ...], kwargs: Dict[str, Any] +) -> None: + """Validates the arguments against the signature. + + Args: + signature_parameters: The list of Parameter objects + representing the function signature, obtained from + `extract_signature`. + args: The positional arguments passed into the function. + kwargs: The keyword arguments passed into the function. + + Raises: + TypeError: Raised if arguments do not fit in the function signature. + """ + reconstructed_signature = inspect.Signature(parameters=signature_parameters) + try: + reconstructed_signature.bind(*args, **kwargs) + except TypeError as exc: # capture a friendlier stacktrace + raise TypeError(str(exc)) from None + + +def flatten_args( + signature_parameters: List[Parameter], args: Tuple[Any, ...], kwargs: Dict[str, Any] +) -> List[Any]: + """Validates the arguments against the signature and flattens them. + + The flat list representation is a serializable format for arguments. + Since the flatbuffer representation of function arguments is a list, we + combine both keyword arguments and positional arguments. We represent + this with two entries per argument value - [DUMMY_TYPE, x] for positional + arguments and [KEY, VALUE] for keyword arguments. See the below example. + See `recover_args` for logic restoring the flat list back to args/kwargs. + + Args: + signature_parameters: The list of Parameter objects + representing the function signature, obtained from + `extract_signature`. + args: The positional arguments passed into the function. + kwargs: The keyword arguments passed into the function. + + Returns: + List of args and kwargs. Non-keyword arguments are prefixed + by internal enum DUMMY_TYPE. + + Raises: + TypeError: Raised if arguments do not fit in the function signature. + """ + validate_args(signature_parameters, args, kwargs) + list_args = [] + for arg in args: + list_args += [DUMMY_TYPE, arg] + + for keyword, arg in kwargs.items(): + list_args += [keyword, arg] + return list_args + + +def recover_args(flattened_args: List[Any]) -> Tuple[List[Any], Dict[str, Any]]: + """Recreates `args` and `kwargs` from the flattened arg list. + + Args: + flattened_args: List of args and kwargs. This should be the output of + `flatten_args`. + + Returns: + args: The non-keyword arguments passed into the function. + kwargs: The keyword arguments passed into the function. + """ + assert ( + len(flattened_args) % 2 == 0 + ), "Flattened arguments need to be even-numbered. See `flatten_args`." + args = [] + kwargs = {} + for name_index in range(0, len(flattened_args), 2): + name, arg = flattened_args[name_index], flattened_args[name_index + 1] + if name == DUMMY_TYPE: + args.append(arg) + else: + kwargs[name] = arg + + return args, kwargs diff --git a/lib/python3.12/site-packages/ray/_common/test_utils.py b/lib/python3.12/site-packages/ray/_common/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c5e6020b1c98f548056e5f35abb22e210d1720eb --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/test_utils.py @@ -0,0 +1,249 @@ +"""Test utilities for Ray. + +This module contains test utility classes that are distributed with the Ray package +and can be used by external libraries and tests. These utilities must remain in +_common/ (not in tests/) to be accessible in the Ray package distribution. +""" + +import asyncio +import inspect +import os +import time +import traceback +import uuid +from collections.abc import Awaitable +from contextlib import contextmanager +from enum import Enum +from typing import Any, Callable, Dict, Iterator, List, Optional, Set + +import ray +import ray._common.usage.usage_lib as ray_usage_lib +import ray._private.utils +from ray._common.network_utils import build_address + + +@ray.remote(num_cpus=0) +class SignalActor: + """A Ray actor for coordinating test execution through signals. + + Useful for testing async coordination, waiting for specific states, + and synchronizing multiple actors or tasks in tests. + """ + + def __init__(self): + self.ready_event = asyncio.Event() + self.num_waiters = 0 + + def send(self, clear: bool = False): + self.ready_event.set() + if clear: + self.ready_event.clear() + + async def wait(self, should_wait: bool = True): + if should_wait: + self.num_waiters += 1 + await self.ready_event.wait() + self.num_waiters -= 1 + + async def cur_num_waiters(self) -> int: + return self.num_waiters + + +@ray.remote(num_cpus=0) +class Semaphore: + """A Ray actor implementing a semaphore for test coordination. + + Useful for testing resource limiting, concurrency control, + and coordination between multiple actors or tasks. + """ + + def __init__(self, value: int = 1): + self._sema = asyncio.Semaphore(value=value) + + async def acquire(self): + await self._sema.acquire() + + async def release(self): + self._sema.release() + + async def locked(self) -> bool: + return self._sema.locked() + + +__all__ = ["SignalActor", "Semaphore"] + + +def wait_for_condition( + condition_predictor: Callable[..., bool], + timeout: float = 10, + retry_interval_ms: float = 100, + raise_exceptions: bool = False, + **kwargs: Any, +): + """Wait until a condition is met or time out with an exception. + + Args: + condition_predictor: A function that predicts the condition. + timeout: Maximum timeout in seconds. + retry_interval_ms: Retry interval in milliseconds. + raise_exceptions: If true, exceptions that occur while executing + condition_predictor won't be caught and instead will be raised. + **kwargs: Arguments to pass to the condition_predictor. + + Returns: + None: Returns when the condition is met. + + Raises: + RuntimeError: If the condition is not met before the timeout expires. + """ + start = time.time() + last_ex = None + while time.time() - start <= timeout: + try: + if condition_predictor(**kwargs): + return + except Exception: + if raise_exceptions: + raise + last_ex = ray._private.utils.format_error_message(traceback.format_exc()) + time.sleep(retry_interval_ms / 1000.0) + message = "The condition wasn't met before the timeout expired." + if last_ex is not None: + message += f" Last exception: {last_ex}" + raise RuntimeError(message) + + +async def async_wait_for_condition( + condition_predictor: Callable[..., Awaitable[bool]], + timeout: float = 10, + retry_interval_ms: float = 100, + **kwargs: Any, +): + """Wait until a condition is met or time out with an exception. + + Args: + condition_predictor: A function that predicts the condition. + timeout: Maximum timeout in seconds. + retry_interval_ms: Retry interval in milliseconds. + **kwargs: Arguments to pass to the condition_predictor. + + Returns: + None: Returns when the condition is met. + + Raises: + RuntimeError: If the condition is not met before the timeout expires. + """ + start = time.time() + last_ex = None + while time.time() - start <= timeout: + try: + if inspect.iscoroutinefunction(condition_predictor): + if await condition_predictor(**kwargs): + return + else: + if condition_predictor(**kwargs): + return + except Exception as ex: + last_ex = ex + await asyncio.sleep(retry_interval_ms / 1000.0) + message = "The condition wasn't met before the timeout expired." + if last_ex is not None: + message += f" Last exception: {last_ex}" + raise RuntimeError(message) + + +@contextmanager +def simulate_s3_bucket( + port: int = 5002, + region: str = "us-west-2", +) -> Iterator[str]: + """Context manager that simulates an S3 bucket and yields the URI. + + Args: + port: The port of the localhost endpoint where S3 is being served. + region: The S3 region. + + Yields: + str: URI for the simulated S3 bucket. + """ + from moto.server import ThreadedMotoServer + + old_env = os.environ + os.environ["AWS_ACCESS_KEY_ID"] = "testing" + os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" + os.environ["AWS_SECURITY_TOKEN"] = "testing" + os.environ["AWS_SESSION_TOKEN"] = "testing" + + s3_server = f"http://{build_address('localhost', port)}" + server = ThreadedMotoServer(port=port) + server.start() + url = f"s3://{uuid.uuid4().hex}?region={region}&endpoint_override={s3_server}" + yield url + server.stop() + os.environ = old_env + + +class TelemetryCallsite(Enum): + DRIVER = "driver" + ACTOR = "actor" + TASK = "task" + + +def _get_library_usages() -> Set[str]: + return set( + ray_usage_lib.get_library_usages_to_report( + ray.experimental.internal_kv.internal_kv_get_gcs_client() + ) + ) + + +def _get_extra_usage_tags() -> Dict[str, str]: + return ray_usage_lib.get_extra_usage_tags_to_report( + ray.experimental.internal_kv.internal_kv_get_gcs_client() + ) + + +def check_library_usage_telemetry( + use_lib_fn: Callable[[], None], + *, + callsite: TelemetryCallsite, + expected_library_usages: List[Set[str]], + expected_extra_usage_tags: Optional[Dict[str, str]] = None, +): + """Helper for writing tests to validate library usage telemetry. + + `use_lib_fn` is a callable that will be called from the provided callsite. + After calling it, the telemetry data to export will be validated against + expected_library_usages and expected_extra_usage_tags. + """ + assert len(_get_library_usages()) == 0, _get_library_usages() + + if callsite == TelemetryCallsite.DRIVER: + use_lib_fn() + elif callsite == TelemetryCallsite.ACTOR: + + @ray.remote + class A: + def __init__(self): + use_lib_fn() + + a = A.remote() + ray.get(a.__ray_ready__.remote()) + elif callsite == TelemetryCallsite.TASK: + + @ray.remote + def f(): + use_lib_fn() + + ray.get(f.remote()) + else: + assert False, f"Unrecognized callsite: {callsite}" + + library_usages = _get_library_usages() + extra_usage_tags = _get_extra_usage_tags() + + assert library_usages in expected_library_usages, library_usages + if expected_extra_usage_tags: + assert all( + [extra_usage_tags[k] == v for k, v in expected_extra_usage_tags.items()] + ), extra_usage_tags diff --git a/lib/python3.12/site-packages/ray/_common/usage/__init__.py b/lib/python3.12/site-packages/ray/_common/usage/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/_common/usage/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8deeb24da58c631936d9ae15019cc1292b79e82b Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_constants.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fac5dadcb1b7f044fa8e4fcb549892a564fdfaf Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_constants.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_lib.cpython-312.pyc b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_lib.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..475f0cc0192a1b577a0c3a66fb65511b1033385d Binary files /dev/null and b/lib/python3.12/site-packages/ray/_common/usage/__pycache__/usage_lib.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_common/usage/usage_constants.py b/lib/python3.12/site-packages/ray/_common/usage/usage_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5b97ad175e334aa963e2f02629329830c57ff8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/usage/usage_constants.py @@ -0,0 +1,63 @@ +SCHEMA_VERSION = "0.1" + +# The key to store / obtain cluster metadata. +CLUSTER_METADATA_KEY = b"CLUSTER_METADATA" + +# The name of a json file where usage stats will be written. +USAGE_STATS_FILE = "usage_stats.json" + +USAGE_STATS_ENABLED_ENV_VAR = "RAY_USAGE_STATS_ENABLED" + +USAGE_STATS_SOURCE_ENV_VAR = "RAY_USAGE_STATS_SOURCE" + +USAGE_STATS_SOURCE_OSS = "OSS" + +USAGE_STATS_ENABLED_FOR_CLI_MESSAGE = ( + "Usage stats collection is enabled. To disable this, add `--disable-usage-stats` " + "to the command that starts the cluster, or run the following command:" + " `ray disable-usage-stats` before starting the cluster. " + "See https://docs.ray.io/en/master/cluster/usage-stats.html for more details." +) + +USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE = ( + "Usage stats collection is enabled. To disable this, run the following command:" + " `ray disable-usage-stats` before starting Ray. " + "See https://docs.ray.io/en/master/cluster/usage-stats.html for more details." +) + +USAGE_STATS_DISABLED_MESSAGE = "Usage stats collection is disabled." + +USAGE_STATS_ENABLED_BY_DEFAULT_FOR_CLI_MESSAGE = ( + "Usage stats collection is enabled by default without user confirmation " + "because this terminal is detected to be non-interactive. " + "To disable this, add `--disable-usage-stats` to the command that starts " + "the cluster, or run the following command:" + " `ray disable-usage-stats` before starting the cluster. " + "See https://docs.ray.io/en/master/cluster/usage-stats.html for more details." +) + +USAGE_STATS_ENABLED_BY_DEFAULT_FOR_RAY_INIT_MESSAGE = ( + "Usage stats collection is enabled by default for nightly wheels. " + "To disable this, run the following command:" + " `ray disable-usage-stats` before starting Ray. " + "See https://docs.ray.io/en/master/cluster/usage-stats.html for more details." +) + +USAGE_STATS_CONFIRMATION_MESSAGE = ( + "Enable usage stats collection? " + "This prompt will auto-proceed in 10 seconds to avoid blocking cluster startup." +) + +LIBRARY_USAGE_SET_NAME = "library_usage_" + +HARDWARE_USAGE_SET_NAME = "hardware_usage_" + +# Keep in-sync with the same constants defined in usage_stats_client.h +EXTRA_USAGE_TAG_PREFIX = "extra_usage_tag_" +USAGE_STATS_NAMESPACE = "usage_stats" + +KUBERNETES_SERVICE_HOST_ENV = "KUBERNETES_SERVICE_HOST" +KUBERAY_ENV = "RAY_USAGE_STATS_KUBERAY_IN_USE" + +PROVIDER_KUBERNETES_GENERIC = "kubernetes" +PROVIDER_KUBERAY = "kuberay" diff --git a/lib/python3.12/site-packages/ray/_common/usage/usage_lib.py b/lib/python3.12/site-packages/ray/_common/usage/usage_lib.py new file mode 100644 index 0000000000000000000000000000000000000000..085c0c4f9fa9070718e2e3af6bdefa8370725492 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/usage/usage_lib.py @@ -0,0 +1,1016 @@ +"""This is the module that is in charge of Ray usage report (telemetry) APIs. + +NOTE: Ray's usage report is currently "on by default". + One could opt-out, see details at https://docs.ray.io/en/master/cluster/usage-stats.html. # noqa + +Ray usage report follows the specification from +https://docs.ray.io/en/master/cluster/usage-stats.html#usage-stats-collection # noqa + +# Module + +The module consists of 2 parts. + +## Public API +It contains public APIs to obtain usage report information. +APIs will be added before the usage report becomes opt-in by default. + +## Internal APIs for usage processing/report +The telemetry report consists of 5 components. This module is in charge of the top 2 layers. + +Report -> usage_lib +--------------------- +Usage data processing -> usage_lib +--------------------- +Data storage -> Ray API server +--------------------- +Aggregation -> Ray API server (currently a dashboard server) +--------------------- +Usage data collection -> Various components (Ray agent, GCS, etc.) + usage_lib (cluster metadata). + +Usage report is currently "off by default". You can enable the report by setting an environment variable +RAY_USAGE_STATS_ENABLED=1. For example, `RAY_USAGE_STATS_ENABLED=1 ray start --head`. +Or `RAY_USAGE_STATS_ENABLED=1 python [drivers with ray.init()]`. + +"Ray API server (currently a dashboard server)" reports the usage data to https://usage-stats.ray.io/. + +Data is reported every hour by default. + +Note that it is also possible to configure the interval using the environment variable, +`RAY_USAGE_STATS_REPORT_INTERVAL_S`. + +To see collected/reported data, see `usage_stats.json` inside a temp +folder (e.g., /tmp/ray/session_[id]/*). +""" +import json +import logging +import os +import platform +import sys +import threading +import time +from dataclasses import asdict, dataclass +from enum import Enum, auto +from pathlib import Path +from typing import Dict, List, Optional, Set + +import requests +import yaml + +import ray +import ray._common.usage.usage_constants as usage_constant +import ray._private.ray_constants as ray_constants +from ray._raylet import GcsClient +from ray.core.generated import gcs_pb2, usage_pb2 +from ray.experimental.internal_kv import ( + _internal_kv_initialized, + _internal_kv_put, +) + +logger = logging.getLogger(__name__) +TagKey = usage_pb2.TagKey + +################# +# Internal APIs # +################# + + +@dataclass(init=True) +class ClusterConfigToReport: + cloud_provider: Optional[str] = None + min_workers: Optional[int] = None + max_workers: Optional[int] = None + head_node_instance_type: Optional[str] = None + worker_node_instance_types: Optional[List[str]] = None + + +@dataclass(init=True) +class ClusterStatusToReport: + total_num_cpus: Optional[int] = None + total_num_gpus: Optional[int] = None + total_memory_gb: Optional[float] = None + total_object_store_memory_gb: Optional[float] = None + + +@dataclass(init=True) +class UsageStatsToReport: + """Usage stats to report""" + + #: The schema version of the report. + schema_version: str + #: The source of the data (i.e. OSS). + source: str + #: When the data is collected and reported. + collect_timestamp_ms: int + #: The total number of successful reports for the lifetime of the cluster. + total_success: Optional[int] = None + #: The total number of failed reports for the lifetime of the cluster. + total_failed: Optional[int] = None + #: The sequence number of the report. + seq_number: Optional[int] = None + #: The Ray version in use. + ray_version: Optional[str] = None + #: The Python version in use. + python_version: Optional[str] = None + #: A random id of the cluster session. + session_id: Optional[str] = None + #: The git commit hash of Ray (i.e. ray.__commit__). + git_commit: Optional[str] = None + #: The operating system in use. + os: Optional[str] = None + #: When the cluster is started. + session_start_timestamp_ms: Optional[int] = None + #: The cloud provider found in the cluster.yaml file (e.g., aws). + cloud_provider: Optional[str] = None + #: The min_workers found in the cluster.yaml file. + min_workers: Optional[int] = None + #: The max_workers found in the cluster.yaml file. + max_workers: Optional[int] = None + #: The head node instance type found in the cluster.yaml file (e.g., i3.8xlarge). + head_node_instance_type: Optional[str] = None + #: The worker node instance types found in the cluster.yaml file (e.g., i3.8xlarge). + worker_node_instance_types: Optional[List[str]] = None + #: The total num of cpus in the cluster. + total_num_cpus: Optional[int] = None + #: The total num of gpus in the cluster. + total_num_gpus: Optional[int] = None + #: The total size of memory in the cluster. + total_memory_gb: Optional[float] = None + #: The total size of object store memory in the cluster. + total_object_store_memory_gb: Optional[float] = None + #: The Ray libraries that are used (e.g., rllib). + library_usages: Optional[List[str]] = None + #: The extra tags to report when specified by an + # environment variable RAY_USAGE_STATS_EXTRA_TAGS + extra_usage_tags: Optional[Dict[str, str]] = None + #: The number of alive nodes when the report is generated. + total_num_nodes: Optional[int] = None + #: The total number of running jobs excluding internal ones + # when the report is generated. + total_num_running_jobs: Optional[int] = None + #: The libc version in the OS. + libc_version: Optional[str] = None + #: The hardwares that are used (e.g. Intel Xeon). + hardware_usages: Optional[List[str]] = None + + +@dataclass(init=True) +class UsageStatsToWrite: + """Usage stats to write to `USAGE_STATS_FILE` + + We are writing extra metadata such as the status of report + to this file. + """ + + usage_stats: UsageStatsToReport + # Whether or not the last report succeeded. + success: bool + # The error message of the last report if it happens. + error: str + + +class UsageStatsEnabledness(Enum): + ENABLED_EXPLICITLY = auto() + DISABLED_EXPLICITLY = auto() + ENABLED_BY_DEFAULT = auto() + + +_recorded_library_usages = set() +_recorded_library_usages_lock = threading.Lock() +_recorded_extra_usage_tags = dict() +_recorded_extra_usage_tags_lock = threading.Lock() + + +def _add_to_usage_set(set_name: str, value: str): + assert _internal_kv_initialized() + try: + _internal_kv_put( + f"{set_name}{value}".encode(), + b"", + namespace=usage_constant.USAGE_STATS_NAMESPACE.encode(), + ) + except Exception as e: + logger.debug(f"Failed to add {value} to usage set {set_name}, {e}") + + +def _get_usage_set(gcs_client, set_name: str) -> Set[str]: + try: + result = set() + usages = gcs_client.internal_kv_keys( + set_name.encode(), + namespace=usage_constant.USAGE_STATS_NAMESPACE.encode(), + ) + for usage in usages: + usage = usage.decode("utf-8") + result.add(usage[len(set_name) :]) + + return result + except Exception as e: + logger.debug(f"Failed to get usage set {set_name}, {e}") + return set() + + +def _put_library_usage(library_usage: str): + _add_to_usage_set(usage_constant.LIBRARY_USAGE_SET_NAME, library_usage) + + +def _put_hardware_usage(hardware_usage: str): + _add_to_usage_set(usage_constant.HARDWARE_USAGE_SET_NAME, hardware_usage) + + +def record_extra_usage_tag( + key: TagKey, value: str, gcs_client: Optional[GcsClient] = None +): + """Record extra kv usage tag. + + If the key already exists, the value will be overwritten. + + To record an extra tag, first add the key to the TagKey enum and + then call this function. + It will make a synchronous call to the internal kv store if the tag is updated. + + Args: + key: The key of the tag. + value: The value of the tag. + gcs_client: The GCS client to perform KV operation PUT. Defaults to None. + When None, it will try to get the global client from the internal_kv. + + Returns: + None + """ + key = TagKey.Name(key).lower() + with _recorded_extra_usage_tags_lock: + if _recorded_extra_usage_tags.get(key) == value: + return + _recorded_extra_usage_tags[key] = value + + if not _internal_kv_initialized() and gcs_client is None: + # This happens if the record is before ray.init and + # no GCS client is used for recording explicitly. + return + + _put_extra_usage_tag(key, value, gcs_client) + + +def _put_extra_usage_tag(key: str, value: str, gcs_client: Optional[GcsClient] = None): + try: + key = f"{usage_constant.EXTRA_USAGE_TAG_PREFIX}{key}".encode() + val = value.encode() + namespace = usage_constant.USAGE_STATS_NAMESPACE.encode() + if gcs_client is not None: + # Use the GCS client. + gcs_client.internal_kv_put(key, val, namespace=namespace) + else: + # Use internal kv. + assert _internal_kv_initialized() + _internal_kv_put(key, val, namespace=namespace) + except Exception as e: + logger.debug(f"Failed to put extra usage tag, {e}") + + +def record_hardware_usage(hardware_usage: str): + """Record hardware usage (e.g. which CPU model is used)""" + assert _internal_kv_initialized() + _put_hardware_usage(hardware_usage) + + +def record_library_usage(library_usage: str): + """Record library usage (e.g. which library is used)""" + with _recorded_library_usages_lock: + if library_usage in _recorded_library_usages: + return + _recorded_library_usages.add(library_usage) + + if not _internal_kv_initialized(): + # This happens if the library is imported before ray.init + return + + # Only report lib usage for driver / ray client / workers. Otherwise, + # it can be reported if the library is imported from + # e.g., API server. + if ( + ray._private.worker.global_worker.mode == ray.SCRIPT_MODE + or ray._private.worker.global_worker.mode == ray.WORKER_MODE + or ray.util.client.ray.is_connected() + ): + _put_library_usage(library_usage) + + +def _put_pre_init_library_usages(): + assert _internal_kv_initialized() + # NOTE: When the lib is imported from a worker, ray should + # always be initialized, so there's no need to register the + # pre init hook. + if not ( + ray._private.worker.global_worker.mode == ray.SCRIPT_MODE + or ray.util.client.ray.is_connected() + ): + return + + for library_usage in _recorded_library_usages: + _put_library_usage(library_usage) + + +def _put_pre_init_extra_usage_tags(): + assert _internal_kv_initialized() + for k, v in _recorded_extra_usage_tags.items(): + _put_extra_usage_tag(k, v) + + +def put_pre_init_usage_stats(): + _put_pre_init_library_usages() + _put_pre_init_extra_usage_tags() + + +def reset_global_state(): + global _recorded_library_usages, _recorded_extra_usage_tags + + with _recorded_library_usages_lock: + _recorded_library_usages = set() + with _recorded_extra_usage_tags_lock: + _recorded_extra_usage_tags = dict() + + +ray._private.worker._post_init_hooks.append(put_pre_init_usage_stats) + + +def _usage_stats_report_url(): + # The usage collection server URL. + # The environment variable is testing-purpose only. + return os.getenv("RAY_USAGE_STATS_REPORT_URL", "https://usage-stats.ray.io/") + + +def _usage_stats_report_interval_s(): + return int(os.getenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", 3600)) + + +def _usage_stats_config_path(): + return os.getenv( + "RAY_USAGE_STATS_CONFIG_PATH", os.path.expanduser("~/.ray/config.json") + ) + + +def _usage_stats_enabledness() -> UsageStatsEnabledness: + # Env var has higher priority than config file. + usage_stats_enabled_env_var = os.getenv(usage_constant.USAGE_STATS_ENABLED_ENV_VAR) + if usage_stats_enabled_env_var == "0": + return UsageStatsEnabledness.DISABLED_EXPLICITLY + elif usage_stats_enabled_env_var == "1": + return UsageStatsEnabledness.ENABLED_EXPLICITLY + elif usage_stats_enabled_env_var is not None: + raise ValueError( + f"Valid value for {usage_constant.USAGE_STATS_ENABLED_ENV_VAR} " + f"env var is 0 or 1, but got {usage_stats_enabled_env_var}" + ) + + usage_stats_enabled_config_var = None + try: + with open(_usage_stats_config_path()) as f: + config = json.load(f) + usage_stats_enabled_config_var = config.get("usage_stats") + except FileNotFoundError: + pass + except Exception as e: + logger.debug(f"Failed to load usage stats config {e}") + + if usage_stats_enabled_config_var is False: + return UsageStatsEnabledness.DISABLED_EXPLICITLY + elif usage_stats_enabled_config_var is True: + return UsageStatsEnabledness.ENABLED_EXPLICITLY + elif usage_stats_enabled_config_var is not None: + raise ValueError( + f"Valid value for 'usage_stats' in {_usage_stats_config_path()}" + f" is true or false, but got {usage_stats_enabled_config_var}" + ) + + # Usage stats is enabled by default. + return UsageStatsEnabledness.ENABLED_BY_DEFAULT + + +def is_nightly_wheel() -> bool: + return ray.__commit__ != "{{RAY_COMMIT_SHA}}" and "dev" in ray.__version__ + + +def usage_stats_enabled() -> bool: + return _usage_stats_enabledness() is not UsageStatsEnabledness.DISABLED_EXPLICITLY + + +def usage_stats_prompt_enabled(): + return int(os.getenv("RAY_USAGE_STATS_PROMPT_ENABLED", "1")) == 1 + + +def _generate_cluster_metadata(*, ray_init_cluster: bool): + """Return a dictionary of cluster metadata. + + Params: + ray_init_cluster: Whether the cluster is started by ray.init() + + Returns: + A dictionary of cluster metadata. + """ + ray_version, python_version = ray._private.utils.compute_version_info() + # These two metadata is necessary although usage report is not enabled + # to check version compatibility. + metadata = { + "ray_version": ray_version, + "python_version": python_version, + "ray_init_cluster": ray_init_cluster, + } + # Additional metadata is recorded only when usage stats are enabled. + if usage_stats_enabled(): + metadata.update( + { + "git_commit": ray.__commit__, + "os": sys.platform, + "session_start_timestamp_ms": int(time.time() * 1000), + } + ) + if sys.platform == "linux": + # Record llibc version + (lib, ver) = platform.libc_ver() + if not lib: + metadata.update({"libc_version": "NA"}) + else: + metadata.update({"libc_version": f"{lib}:{ver}"}) + return metadata + + +def show_usage_stats_prompt(cli: bool) -> None: + if not usage_stats_prompt_enabled(): + return + + from ray.autoscaler._private.cli_logger import cli_logger + + prompt_print = cli_logger.print if cli else print + + usage_stats_enabledness = _usage_stats_enabledness() + if usage_stats_enabledness is UsageStatsEnabledness.DISABLED_EXPLICITLY: + prompt_print(usage_constant.USAGE_STATS_DISABLED_MESSAGE) + elif usage_stats_enabledness is UsageStatsEnabledness.ENABLED_BY_DEFAULT: + if not cli: + prompt_print( + usage_constant.USAGE_STATS_ENABLED_BY_DEFAULT_FOR_RAY_INIT_MESSAGE + ) + elif cli_logger.interactive: + enabled = cli_logger.confirm( + False, + usage_constant.USAGE_STATS_CONFIRMATION_MESSAGE, + _default=True, + _timeout_s=10, + ) + set_usage_stats_enabled_via_env_var(enabled) + # Remember user's choice. + try: + set_usage_stats_enabled_via_config(enabled) + except Exception as e: + logger.debug( + f"Failed to persist usage stats choice for future clusters: {e}" + ) + if enabled: + prompt_print(usage_constant.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE) + else: + prompt_print(usage_constant.USAGE_STATS_DISABLED_MESSAGE) + else: + prompt_print( + usage_constant.USAGE_STATS_ENABLED_BY_DEFAULT_FOR_CLI_MESSAGE, + ) + else: + assert usage_stats_enabledness is UsageStatsEnabledness.ENABLED_EXPLICITLY + prompt_print( + usage_constant.USAGE_STATS_ENABLED_FOR_CLI_MESSAGE + if cli + else usage_constant.USAGE_STATS_ENABLED_FOR_RAY_INIT_MESSAGE + ) + + +def set_usage_stats_enabled_via_config(enabled) -> None: + config = {} + try: + with open(_usage_stats_config_path()) as f: + config = json.load(f) + if not isinstance(config, dict): + logger.debug( + f"Invalid ray config file, should be a json dict but got {type(config)}" + ) + config = {} + except FileNotFoundError: + pass + except Exception as e: + logger.debug(f"Failed to load ray config file {e}") + + config["usage_stats"] = enabled + + try: + os.makedirs(os.path.dirname(_usage_stats_config_path()), exist_ok=True) + with open(_usage_stats_config_path(), "w") as f: + json.dump(config, f) + except Exception as e: + raise Exception( + "Failed to " + f'{"enable" if enabled else "disable"}' + ' usage stats by writing {"usage_stats": ' + f'{"true" if enabled else "false"}' + "} to " + f"{_usage_stats_config_path()}" + ) from e + + +def set_usage_stats_enabled_via_env_var(enabled) -> None: + os.environ[usage_constant.USAGE_STATS_ENABLED_ENV_VAR] = "1" if enabled else "0" + + +def put_cluster_metadata(gcs_client: GcsClient, *, ray_init_cluster: bool) -> dict: + """Generate the cluster metadata and store it to GCS. + + It is a blocking API. + + Params: + gcs_client: The GCS client to perform KV operation PUT. + ray_init_cluster: Whether the cluster is started by ray.init() + + Raises: + gRPC exceptions: If PUT fails. + + Returns: + The cluster metadata. + """ + metadata = _generate_cluster_metadata(ray_init_cluster=ray_init_cluster) + gcs_client.internal_kv_put( + usage_constant.CLUSTER_METADATA_KEY, + json.dumps(metadata).encode(), + overwrite=True, + namespace=ray_constants.KV_NAMESPACE_CLUSTER, + ) + return metadata + + +def get_total_num_running_jobs_to_report(gcs_client) -> Optional[int]: + """Return the total number of running jobs in the cluster excluding internal ones""" + try: + result = gcs_client.get_all_job_info( + skip_submission_job_info_field=True, skip_is_running_tasks_field=True + ) + total_num_running_jobs = 0 + for job_info in result.values(): + if not job_info.is_dead and not job_info.config.ray_namespace.startswith( + "_ray_internal" + ): + total_num_running_jobs += 1 + return total_num_running_jobs + except Exception as e: + logger.info(f"Failed to query number of running jobs in the cluster: {e}") + return None + + +def get_total_num_alive_nodes_to_report(gcs_client, timeout=None) -> Optional[int]: + """Return the total number of alive nodes in the cluster""" + try: + result = gcs_client.get_all_node_info( + timeout=timeout, state_filter=gcs_pb2.GcsNodeInfo.GcsNodeState.ALIVE + ) + return len(result.items()) + except Exception as e: + logger.info(f"Failed to query number of nodes in the cluster: {e}") + return None + + +def get_library_usages_to_report(gcs_client) -> List[str]: + return list(_get_usage_set(gcs_client, usage_constant.LIBRARY_USAGE_SET_NAME)) + + +def get_hardware_usages_to_report(gcs_client) -> List[str]: + return list(_get_usage_set(gcs_client, usage_constant.HARDWARE_USAGE_SET_NAME)) + + +def get_extra_usage_tags_to_report(gcs_client: GcsClient) -> Dict[str, str]: + """Get the extra usage tags from env var and gcs kv store. + + The env var should be given this way; key=value;key=value. + If parsing is failed, it will return the empty data. + + Params: + gcs_client: The GCS client. + + Returns: + Extra usage tags as kv pairs. + """ + extra_usage_tags = dict() + + extra_usage_tags_env_var = os.getenv("RAY_USAGE_STATS_EXTRA_TAGS", None) + if extra_usage_tags_env_var: + try: + kvs = extra_usage_tags_env_var.strip(";").split(";") + for kv in kvs: + k, v = kv.split("=") + extra_usage_tags[k] = v + except Exception as e: + logger.info(f"Failed to parse extra usage tags env var: {e}") + + valid_tag_keys = [tag_key.lower() for tag_key in TagKey.keys()] + try: + keys = gcs_client.internal_kv_keys( + usage_constant.EXTRA_USAGE_TAG_PREFIX.encode(), + namespace=usage_constant.USAGE_STATS_NAMESPACE.encode(), + ) + kv = gcs_client.internal_kv_multi_get( + keys, namespace=usage_constant.USAGE_STATS_NAMESPACE.encode() + ) + for key, value in kv.items(): + key = key.decode("utf-8") + key = key[len(usage_constant.EXTRA_USAGE_TAG_PREFIX) :] + assert key in valid_tag_keys + extra_usage_tags[key] = value.decode("utf-8") + except Exception as e: + logger.info(f"Failed to get extra usage tags from kv store: {e}") + return extra_usage_tags + + +def _get_cluster_status_to_report_v2(gcs_client: GcsClient) -> ClusterStatusToReport: + """ + Get the current status of this cluster. A temporary proxy for the + autoscaler v2 API. + + It is a blocking API. + + Params: + gcs_client: The GCS client. + + Returns: + The current cluster status or empty ClusterStatusToReport + if it fails to get that information. + """ + from ray.autoscaler.v2.sdk import get_cluster_status + + result = ClusterStatusToReport() + try: + cluster_status = get_cluster_status(gcs_client.address) + total_resources = cluster_status.total_resources() + result.total_num_cpus = int(total_resources.get("CPU", 0)) + result.total_num_gpus = int(total_resources.get("GPU", 0)) + + to_GiB = 1 / 2**30 + result.total_memory_gb = total_resources.get("memory", 0) * to_GiB + result.total_object_store_memory_gb = ( + total_resources.get("object_store_memory", 0) * to_GiB + ) + except Exception as e: + logger.info(f"Failed to get cluster status to report {e}") + finally: + return result + + +def get_cluster_status_to_report(gcs_client: GcsClient) -> ClusterStatusToReport: + """Get the current status of this cluster. + + It is a blocking API. + + Params: + gcs_client: The GCS client to perform KV operation GET. + + Returns: + The current cluster status or empty if it fails to get that information. + """ + try: + + from ray.autoscaler.v2.utils import is_autoscaler_v2 + + if is_autoscaler_v2(): + return _get_cluster_status_to_report_v2(gcs_client) + + cluster_status = gcs_client.internal_kv_get( + ray._private.ray_constants.DEBUG_AUTOSCALING_STATUS.encode(), + namespace=None, + ) + if not cluster_status: + return ClusterStatusToReport() + + result = ClusterStatusToReport() + to_GiB = 1 / 2**30 + cluster_status = json.loads(cluster_status.decode("utf-8")) + if ( + "load_metrics_report" not in cluster_status + or "usage" not in cluster_status["load_metrics_report"] + ): + return ClusterStatusToReport() + + usage = cluster_status["load_metrics_report"]["usage"] + # usage is a map from resource to (used, total) pair + if "CPU" in usage: + result.total_num_cpus = int(usage["CPU"][1]) + if "GPU" in usage: + result.total_num_gpus = int(usage["GPU"][1]) + if "memory" in usage: + result.total_memory_gb = usage["memory"][1] * to_GiB + if "object_store_memory" in usage: + result.total_object_store_memory_gb = ( + usage["object_store_memory"][1] * to_GiB + ) + return result + except Exception as e: + logger.info(f"Failed to get cluster status to report {e}") + return ClusterStatusToReport() + + +def get_cloud_from_metadata_requests() -> str: + def cloud_metadata_request(url: str, headers: Optional[Dict[str, str]]) -> bool: + try: + res = requests.get(url, headers=headers, timeout=1) + # Only accept successful responses (200 OK) to avoid false positives like 400 - Bad Request + # when multiple cloud providers use the same IP (169.254.169.254) + if res.status_code == 200: + return True + # ConnectionError is a superclass of ConnectTimeout + except requests.exceptions.ConnectionError: + pass + except Exception as e: + logger.info( + f"Unexpected exception when making cloud provider metadata request: {e}" + ) + return False + + AZURE_METADATA_URL = ( + "http://169.254.169.254/metadata/instance?api-version=2021-12-13" + ) + AZURE_METADATA_HEADERS = {"Metadata": "true"} + GCP_METADATA_URL = "http://metadata.google.internal/computeMetadata/v1" + GCP_METADATA_HEADERS = {"Metadata-Flavor": "Google"} + AWS_METADATA_URL = "http://169.254.169.254/latest/meta-data/" + AWS_METADATA_HEADERS = None + + if cloud_metadata_request(AZURE_METADATA_URL, AZURE_METADATA_HEADERS): + return "azure" + elif cloud_metadata_request(GCP_METADATA_URL, GCP_METADATA_HEADERS): + return "gcp" + elif cloud_metadata_request(AWS_METADATA_URL, AWS_METADATA_HEADERS): + return "aws" + else: + return "unknown" + + +def get_cluster_config_to_report( + cluster_config_file_path: str, +) -> ClusterConfigToReport: + """Get the static cluster (autoscaler) config used to launch this cluster. + + Params: + cluster_config_file_path: The file path to the cluster config file. + + Returns: + The cluster (autoscaler) config or empty if it fails to get that information. + """ + + def get_instance_type(node_config): + if not node_config: + return None + if "InstanceType" in node_config: + # aws + return node_config["InstanceType"] + if "machineType" in node_config: + # gcp + return node_config["machineType"] + if ( + "azure_arm_parameters" in node_config + and "vmSize" in node_config["azure_arm_parameters"] + ): + return node_config["azure_arm_parameters"]["vmSize"] + return None + + try: + with open(cluster_config_file_path) as f: + config = yaml.safe_load(f) + result = ClusterConfigToReport() + if "min_workers" in config: + result.min_workers = config["min_workers"] + if "max_workers" in config: + result.max_workers = config["max_workers"] + + if "provider" in config and "type" in config["provider"]: + result.cloud_provider = config["provider"]["type"] + + if "head_node_type" not in config: + return result + if "available_node_types" not in config: + return result + head_node_type = config["head_node_type"] + available_node_types = config["available_node_types"] + for available_node_type in available_node_types: + if available_node_type == head_node_type: + head_node_instance_type = get_instance_type( + available_node_types[available_node_type].get("node_config") + ) + if head_node_instance_type: + result.head_node_instance_type = head_node_instance_type + else: + worker_node_instance_type = get_instance_type( + available_node_types[available_node_type].get("node_config") + ) + if worker_node_instance_type: + result.worker_node_instance_types = ( + result.worker_node_instance_types or set() + ) + result.worker_node_instance_types.add(worker_node_instance_type) + if result.worker_node_instance_types: + result.worker_node_instance_types = list( + result.worker_node_instance_types + ) + return result + except FileNotFoundError: + # It's a manually started cluster or k8s cluster + result = ClusterConfigToReport() + + # Check if we're on Kubernetes + if usage_constant.KUBERNETES_SERVICE_HOST_ENV in os.environ: + # Check if we're using KubeRay >= 0.4.0. + if usage_constant.KUBERAY_ENV in os.environ: + result.cloud_provider = usage_constant.PROVIDER_KUBERAY + # Else, we're on Kubernetes but not in either of the above categories. + else: + result.cloud_provider = usage_constant.PROVIDER_KUBERNETES_GENERIC + + # if kubernetes was not set as cloud_provider vs. was set before + if result.cloud_provider is None: + result.cloud_provider = get_cloud_from_metadata_requests() + else: + result.cloud_provider += f"_${get_cloud_from_metadata_requests()}" + + return result + except Exception as e: + logger.info(f"Failed to get cluster config to report {e}") + return ClusterConfigToReport() + + +def get_cluster_metadata(gcs_client: GcsClient) -> dict: + """Get the cluster metadata from GCS. + + It is a blocking API. + + This will return None if `put_cluster_metadata` was never called. + + Params: + gcs_client: The GCS client to perform KV operation GET. + + Returns: + The cluster metadata in a dictionary. + + Raises: + RuntimeError: If it fails to obtain cluster metadata from GCS. + """ + return json.loads( + gcs_client.internal_kv_get( + usage_constant.CLUSTER_METADATA_KEY, + namespace=ray_constants.KV_NAMESPACE_CLUSTER, + ).decode("utf-8") + ) + + +def is_ray_init_cluster(gcs_client: ray._raylet.GcsClient) -> bool: + """Return whether the cluster is started by ray.init()""" + cluster_metadata = get_cluster_metadata(gcs_client) + return cluster_metadata["ray_init_cluster"] + + +def generate_disabled_report_data() -> UsageStatsToReport: + """Generate the report data indicating usage stats is disabled""" + data = UsageStatsToReport( + schema_version=usage_constant.SCHEMA_VERSION, + source=os.getenv( + usage_constant.USAGE_STATS_SOURCE_ENV_VAR, + usage_constant.USAGE_STATS_SOURCE_OSS, + ), + collect_timestamp_ms=int(time.time() * 1000), + ) + return data + + +def generate_report_data( + cluster_config_to_report: ClusterConfigToReport, + total_success: int, + total_failed: int, + seq_number: int, + gcs_address: str, + cluster_id: str, +) -> UsageStatsToReport: + """Generate the report data. + + Params: + cluster_config_to_report: The cluster (autoscaler) + config generated by `get_cluster_config_to_report`. + total_success: The total number of successful report + for the lifetime of the cluster. + total_failed: The total number of failed report + for the lifetime of the cluster. + seq_number: The sequence number that's incremented whenever + a new report is sent. + gcs_address: the address of gcs to get data to report. + cluster_id: hex id of the cluster. + + Returns: + UsageStats + """ + assert cluster_id + + gcs_client = ray._raylet.GcsClient(address=gcs_address, cluster_id=cluster_id) + + cluster_metadata = get_cluster_metadata(gcs_client) + cluster_status_to_report = get_cluster_status_to_report(gcs_client) + + data = UsageStatsToReport( + schema_version=usage_constant.SCHEMA_VERSION, + source=os.getenv( + usage_constant.USAGE_STATS_SOURCE_ENV_VAR, + usage_constant.USAGE_STATS_SOURCE_OSS, + ), + collect_timestamp_ms=int(time.time() * 1000), + total_success=total_success, + total_failed=total_failed, + seq_number=seq_number, + ray_version=cluster_metadata["ray_version"], + python_version=cluster_metadata["python_version"], + session_id=cluster_id, + git_commit=cluster_metadata["git_commit"], + os=cluster_metadata["os"], + session_start_timestamp_ms=cluster_metadata["session_start_timestamp_ms"], + cloud_provider=cluster_config_to_report.cloud_provider, + min_workers=cluster_config_to_report.min_workers, + max_workers=cluster_config_to_report.max_workers, + head_node_instance_type=cluster_config_to_report.head_node_instance_type, + worker_node_instance_types=cluster_config_to_report.worker_node_instance_types, + total_num_cpus=cluster_status_to_report.total_num_cpus, + total_num_gpus=cluster_status_to_report.total_num_gpus, + total_memory_gb=cluster_status_to_report.total_memory_gb, + total_object_store_memory_gb=cluster_status_to_report.total_object_store_memory_gb, # noqa: E501 + library_usages=get_library_usages_to_report(gcs_client), + extra_usage_tags=get_extra_usage_tags_to_report(gcs_client), + total_num_nodes=get_total_num_alive_nodes_to_report(gcs_client), + total_num_running_jobs=get_total_num_running_jobs_to_report(gcs_client), + libc_version=cluster_metadata.get("libc_version"), + hardware_usages=get_hardware_usages_to_report(gcs_client), + ) + return data + + +def generate_write_data( + usage_stats: UsageStatsToReport, + error: str, +) -> UsageStatsToWrite: + """Generate the report data. + + Params: + usage_stats: The usage stats that were reported. + error: The error message of failed reports. + + Returns: + UsageStatsToWrite + """ + data = UsageStatsToWrite( + usage_stats=usage_stats, + success=error is None, + error=error, + ) + return data + + +class UsageReportClient: + """The client implementation for usage report. + + It is in charge of writing usage stats to the directory + and report usage stats. + """ + + def write_usage_data(self, data: UsageStatsToWrite, dir_path: str) -> None: + """Write the usage data to the directory. + + Params: + data: Data to report + dir_path: The path to the directory to write usage data. + """ + # Atomically update the file. + dir_path = Path(dir_path) + destination = dir_path / usage_constant.USAGE_STATS_FILE + temp = dir_path / f"{usage_constant.USAGE_STATS_FILE}.tmp" + with temp.open(mode="w") as json_file: + json_file.write(json.dumps(asdict(data))) + if sys.platform == "win32": + # Windows 32 doesn't support atomic renaming, so we should delete + # the file first. + destination.unlink(missing_ok=True) + temp.rename(destination) + + def report_usage_data(self, url: str, data: UsageStatsToReport) -> None: + """Report the usage data to the usage server. + + Params: + url: The URL to update resource usage. + data: Data to report. + + Raises: + requests.HTTPError: If requests fails. + """ + r = requests.request( + "POST", + url, + headers={"Content-Type": "application/json"}, + json=asdict(data), + timeout=10, + ) + r.raise_for_status() + return r diff --git a/lib/python3.12/site-packages/ray/_common/utils.py b/lib/python3.12/site-packages/ray/_common/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..32842113479c5303454ba453a256aea7db35d8ba --- /dev/null +++ b/lib/python3.12/site-packages/ray/_common/utils.py @@ -0,0 +1,368 @@ +import asyncio +import binascii +import errno +import importlib +import inspect +import os +import random +import string +import sys +import tempfile +from inspect import signature +from types import ModuleType +from typing import Any, Coroutine, Dict, Optional, Tuple + +import psutil + + +def import_module_and_attr( + full_path: str, *, reload_module: bool = False +) -> Tuple[ModuleType, Any]: + """Given a full import path to a module attr, return the imported module and attr. + + If `reload_module` is set, the module will be reloaded using `importlib.reload`. + + Args: + full_path: The full import path to the module and attr. + reload_module: Whether to reload the module. + + Returns: + A tuple of the imported module and attr. + """ + if ":" in full_path: + if full_path.count(":") > 1: + raise ValueError( + f'Got invalid import path "{full_path}". An ' + "import path may have at most one colon." + ) + module_name, attr_name = full_path.split(":") + else: + last_period_idx = full_path.rfind(".") + module_name = full_path[:last_period_idx] + attr_name = full_path[last_period_idx + 1 :] + module = importlib.import_module(module_name) + if reload_module: + importlib.reload(module) + return module, getattr(module, attr_name) + + +def import_attr(full_path: str, *, reload_module: bool = False) -> Any: + """Given a full import path to a module attr, return the imported attr. + + If `reload_module` is set, the module will be reloaded using `importlib.reload`. + + For example, the following are equivalent: + MyClass = import_attr("module.submodule:MyClass") + MyClass = import_attr("module.submodule.MyClass") + from module.submodule import MyClass + + Returns: + Imported attr + """ + return import_module_and_attr(full_path, reload_module=reload_module)[1] + + +def get_or_create_event_loop() -> asyncio.AbstractEventLoop: + """Get a running async event loop if one exists, otherwise create one. + + This function serves as a proxy for the deprecating get_event_loop(). + It tries to get the running loop first, and if no running loop + could be retrieved: + - For python version <3.10: it falls back to the get_event_loop + call. + - For python version >= 3.10: it uses the same python implementation + of _get_event_loop() at asyncio/events.py. + + Ideally, one should use high level APIs like asyncio.run() with python + version >= 3.7, if not possible, one should create and manage the event + loops explicitly. + """ + vers_info = sys.version_info + if vers_info.major >= 3 and vers_info.minor >= 10: + # This follows the implementation of the deprecating `get_event_loop` + # in python3.10's asyncio. See python3.10/asyncio/events.py + # _get_event_loop() + try: + loop = asyncio.get_running_loop() + assert loop is not None + return loop + except RuntimeError as e: + # No running loop, relying on the error message as for now to + # differentiate runtime errors. + assert "no running event loop" in str(e) + return asyncio.get_event_loop_policy().get_event_loop() + + return asyncio.get_event_loop() + + +_BACKGROUND_TASKS = set() + + +def run_background_task(coroutine: Coroutine) -> asyncio.Task: + """Schedule a task reliably to the event loop. + + This API is used when you don't want to cache the reference of `asyncio.Task`. + For example, + + ``` + get_event_loop().create_task(coroutine(*args)) + ``` + + The above code doesn't guarantee to schedule the coroutine to the event loops + + When using create_task in a "fire and forget" way, we should keep the references + alive for the reliable execution. This API is used to fire and forget + asynchronous execution. + + https://docs.python.org/3/library/asyncio-task.html#creating-tasks + """ + task = get_or_create_event_loop().create_task(coroutine) + # Add task to the set. This creates a strong reference. + _BACKGROUND_TASKS.add(task) + + # To prevent keeping references to finished tasks forever, + # make each task remove its own reference from the set after + # completion: + task.add_done_callback(_BACKGROUND_TASKS.discard) + return task + + +# Used in gpu detection +RESOURCE_CONSTRAINT_PREFIX = "accelerator_type:" +PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME = "bundle" + + +def resources_from_ray_options(options_dict: Dict[str, Any]) -> Dict[str, Any]: + """Determine a task's resource requirements. + + Args: + options_dict: The dictionary that contains resources requirements. + + Returns: + A dictionary of the resource requirements for the task. + """ + resources = (options_dict.get("resources") or {}).copy() + + if "CPU" in resources or "GPU" in resources: + raise ValueError( + "The resources dictionary must not contain the key 'CPU' or 'GPU'" + ) + elif "memory" in resources or "object_store_memory" in resources: + raise ValueError( + "The resources dictionary must not " + "contain the key 'memory' or 'object_store_memory'" + ) + elif PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME in resources: + raise ValueError( + "The resource should not include `bundle` which " + f"is reserved for Ray. resources: {resources}" + ) + + num_cpus = options_dict.get("num_cpus") + num_gpus = options_dict.get("num_gpus") + memory = options_dict.get("memory") + object_store_memory = options_dict.get("object_store_memory") + accelerator_type = options_dict.get("accelerator_type") + + if num_cpus is not None: + resources["CPU"] = num_cpus + if num_gpus is not None: + resources["GPU"] = num_gpus + if memory is not None: + resources["memory"] = int(memory) + if object_store_memory is not None: + resources["object_store_memory"] = object_store_memory + if accelerator_type is not None: + resources[f"{RESOURCE_CONSTRAINT_PREFIX}{accelerator_type}"] = 0.001 + + return resources + + +# Match the standard alphabet used for UUIDs. +RANDOM_STRING_ALPHABET = string.ascii_lowercase + string.digits + + +def get_random_alphanumeric_string(length: int): + """Generates random string of length consisting exclusively of + - Lower-case ASCII chars + - Digits + """ + return "".join(random.choices(RANDOM_STRING_ALPHABET, k=length)) + + +_PRINTED_WARNING = set() + + +def get_call_location(back: int = 1): + """ + Get the location (filename and line number) of a function caller, `back` + frames up the stack. + + Args: + back: The number of frames to go up the stack, not including this + function. + + Returns: + A string with the filename and line number of the caller. + For example, "myfile.py:123". + """ + stack = inspect.stack() + try: + frame = stack[back + 1] + return f"{frame.filename}:{frame.lineno}" + except IndexError: + return "UNKNOWN" + + +def get_user_temp_dir(): + if "RAY_TMPDIR" in os.environ: + return os.environ["RAY_TMPDIR"] + elif sys.platform.startswith("linux") and "TMPDIR" in os.environ: + return os.environ["TMPDIR"] + elif sys.platform.startswith("darwin") or sys.platform.startswith("linux"): + # Ideally we wouldn't need this fallback, but keep it for now for + # for compatibility + tempdir = os.path.join(os.sep, "tmp") + else: + tempdir = tempfile.gettempdir() + return tempdir + + +def get_ray_temp_dir(): + return os.path.join(get_user_temp_dir(), "ray") + + +def get_ray_address_file(temp_dir: Optional[str]): + if temp_dir is None: + temp_dir = get_ray_temp_dir() + return os.path.join(temp_dir, "ray_current_cluster") + + +def reset_ray_address(temp_dir: Optional[str] = None): + address_file = get_ray_address_file(temp_dir) + if os.path.exists(address_file): + try: + os.remove(address_file) + except OSError: + pass + + +def load_class(path): + """Load a class at runtime given a full path. + + Example of the path: mypkg.mysubpkg.myclass + """ + class_data = path.split(".") + if len(class_data) < 2: + raise ValueError("You need to pass a valid path like mymodule.provider_class") + module_path = ".".join(class_data[:-1]) + class_str = class_data[-1] + module = importlib.import_module(module_path) + return getattr(module, class_str) + + +def get_system_memory( + # For cgroups v1: + memory_limit_filename: str = "/sys/fs/cgroup/memory/memory.limit_in_bytes", + # For cgroups v2: + memory_limit_filename_v2: str = "/sys/fs/cgroup/memory.max", +): + """Return the total amount of system memory in bytes. + + Args: + memory_limit_filename: The path to the file that contains the memory + limit for the Docker container. Defaults to + /sys/fs/cgroup/memory/memory.limit_in_bytes. + memory_limit_filename_v2: The path to the file that contains the memory + limit for the Docker container in cgroups v2. Defaults to + /sys/fs/cgroup/memory.max. + + Returns: + The total amount of system memory in bytes. + """ + # Try to accurately figure out the memory limit if we are in a docker + # container. Note that this file is not specific to Docker and its value is + # often much larger than the actual amount of memory. + docker_limit = None + if os.path.exists(memory_limit_filename): + with open(memory_limit_filename, "r") as f: + docker_limit = int(f.read().strip()) + elif os.path.exists(memory_limit_filename_v2): + with open(memory_limit_filename_v2, "r") as f: + # Don't forget to strip() the newline: + max_file = f.read().strip() + if max_file.isnumeric(): + docker_limit = int(max_file) + else: + # max_file is "max", i.e. is unset. + docker_limit = None + + # Use psutil if it is available. + psutil_memory_in_bytes = psutil.virtual_memory().total + + if docker_limit is not None: + # We take the min because the cgroup limit is very large if we aren't + # in Docker. + return min(docker_limit, psutil_memory_in_bytes) + + return psutil_memory_in_bytes + + +def binary_to_hex(identifier): + hex_identifier = binascii.hexlify(identifier) + hex_identifier = hex_identifier.decode() + return hex_identifier + + +def hex_to_binary(hex_identifier): + return binascii.unhexlify(hex_identifier) + + +def try_make_directory_shared(directory_path): + try: + os.chmod(directory_path, 0o0777) + except OSError as e: + # Silently suppress the PermissionError that is thrown by the chmod. + # This is done because the user attempting to change the permissions + # on a directory may not own it. The chmod is attempted whether the + # directory is new or not to avoid race conditions. + # ray-project/ray/#3591 + if e.errno in [errno.EACCES, errno.EPERM]: + pass + else: + raise + + +def try_to_create_directory(directory_path): + # Attempt to create a directory that is globally readable/writable. + directory_path = os.path.expanduser(directory_path) + os.makedirs(directory_path, exist_ok=True) + # Change the log directory permissions so others can use it. This is + # important when multiple people are using the same machine. + try_make_directory_shared(directory_path) + + +def get_function_args(callable): + all_parameters = frozenset(signature(callable).parameters) + return list(all_parameters) + + +def decode(byte_str: str, allow_none: bool = False, encode_type: str = "utf-8"): + """Make this unicode in Python 3, otherwise leave it as bytes. + + Args: + byte_str: The byte string to decode. + allow_none: If true, then we will allow byte_str to be None in which + case we will return an empty string. TODO(rkn): Remove this flag. + This is only here to simplify upgrading to flatbuffers 1.10.0. + encode_type: The encoding type to use for decoding. Defaults to "utf-8". + + Returns: + A byte string in Python 2 and a unicode string in Python 3. + """ + if byte_str is None and allow_none: + return "" + + if not isinstance(byte_str, bytes): + raise ValueError(f"The argument {byte_str} must be a bytes object.") + return byte_str.decode(encode_type) diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4ddc25c5129d5ea55f2aab6b77d941f770d692e Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/_clonevirtualenv.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/_clonevirtualenv.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5deb6903e7b496641f82087ad11d21ee736ccb6f Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/_clonevirtualenv.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..889f098fec9d318eda327442b47175557e1b7cac Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c35949241e240bb43c5830148c5d00887ddbbe0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/conda_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/constants.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7412bb53347ec56420975dbd93743b707c43bc3c Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/constants.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/context.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/context.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44c3f0839e9edf972e3e8c56553a0ac7c5ff37c1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/context.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/default_impl.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/default_impl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01d53acefebedff9cd2489d9ac297738e8b1e4f5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/default_impl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/dependency_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/dependency_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c53ecb19e7aa6b31e8e52850728884c7278c71a Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/dependency_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/image_uri.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/image_uri.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84f1108b85a6dfd0dd652514dfdfa3b57e269330 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/image_uri.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/java_jars.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/java_jars.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f97dd7b6090fecee339dff5866230c6aa1a4382 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/java_jars.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/nsight.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/nsight.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b5e7e924238f89d235973b1bd2b4d67ee3da762 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/nsight.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/packaging.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/packaging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bdebe60209a56844edc1faba04600cc53846d06 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/packaging.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/pip.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/pip.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f450107131cbf1ca77031e864a9d21244e30c32c Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/pip.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72a91eda9e512250f43b39dc08eb3be7b6f97fe5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin_schema_manager.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin_schema_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7bfd5b85f1c7623ab9bb722d67cd3d971362ca55 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/plugin_schema_manager.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/protocol.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2425e4ebb91e4ba38951132665ccf32b026ac5e4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/protocol.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_executable.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_executable.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0282ad9dce3f78dabb4e370ab39e497255c954bf Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_executable.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_modules.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_modules.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6fd10f101e7ca9bd93e0e6c93aa36c0138e8665 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/py_modules.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/rocprof_sys.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/rocprof_sys.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..226dc34c017f0ee220f4d4d0e878812493d364cd Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/rocprof_sys.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/setup_hook.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/setup_hook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7567a7bb6d953d425ec7cb0c2a30e94942b2094e Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/setup_hook.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uri_cache.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uri_cache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a3aa77a20ce6b4814178d095e817151a2e7e549 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uri_cache.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ce15bc706783dc1189948135707a1751e9df5ed Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5672c7d5a5edce66fdb68fc0bcf368114ecbb15 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv_runtime_env_hook.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv_runtime_env_hook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..047733b4992302190a1761ca58a2ed0ba3976476 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/uv_runtime_env_hook.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/validation.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/validation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef0e5d3afb0a31057fa1763fb60eb28d431e7864 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/validation.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/virtualenv_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/virtualenv_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b22dc5636fd1d8629eb07fdf4c1bb3aeb5407ccd Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/virtualenv_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/working_dir.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/working_dir.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91709f6192984e20eaa52599800fc2493b59bc87 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/__pycache__/working_dir.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..694f72b836333743a6416b372fe8a7230566c876 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/main.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31030a1e9df5b918f2b98b6e39b9ee5cc8427027 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/main.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_agent.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a361a8aa3b6247cd08d72fce8049b70e1671652f Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_agent.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_consts.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_consts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62b35f4e253267290b009e9046f711f1b4bdca53 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/__pycache__/runtime_env_consts.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/INSTALLER b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/LICENSE b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f26bcf4d2de6eb136e31006ca3ab447d5e488adf --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/LICENSE @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/METADATA b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..c632040d66bf120a377fc3785940934361273a66 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/METADATA @@ -0,0 +1,123 @@ +Metadata-Version: 2.3 +Name: aiohappyeyeballs +Version: 2.6.1 +Summary: Happy Eyeballs for asyncio +License: PSF-2.0 +Author: J. Nick Koston +Author-email: nick@koston.org +Requires-Python: >=3.9 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Topic :: Software Development :: Libraries +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: License :: OSI Approved :: Python Software Foundation License +Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues +Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md +Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io +Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs +Description-Content-Type: text/markdown + +# aiohappyeyeballs + +

+ + CI Status + + + Documentation Status + + + Test coverage percentage + +

+

+ + Poetry + + + Ruff + + + pre-commit + +

+

+ + PyPI Version + + Supported Python versions + License +

+ +--- + +**Documentation**: https://aiohappyeyeballs.readthedocs.io + +**Source Code**: https://github.com/aio-libs/aiohappyeyeballs + +--- + +[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) +([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html)) + +## Use case + +This library exists to allow connecting with +[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) +([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html)) +when you +already have a list of addrinfo and not a DNS name. + +The stdlib version of `loop.create_connection()` +will only work when you pass in an unresolved name which +is not a good fit when using DNS caching or resolving +names via another method such as `zeroconf`. + +## Installation + +Install this via pip (or your favourite package manager): + +`pip install aiohappyeyeballs` + +## License + +[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE) + +## Example usage + +```python + +addr_infos = await loop.getaddrinfo("example.org", 80) + +socket = await start_connection(addr_infos) +socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2) + +transport, protocol = await loop.create_connection( + MyProtocol, sock=socket, ...) + +# Remove the first address for each family from addr_info +pop_addr_infos_interleave(addr_info, 1) + +# Remove all matching address from addr_info +remove_addr_infos(addr_info, "dead::beef::") + +# Convert a local_addr to local_addr_infos +local_addr_infos = addr_to_addr_infos(("127.0.0.1",0)) +``` + +## Credits + +This package contains code from cpython and is licensed under the same terms as cpython itself. + +This package was created with +[Copier](https://copier.readthedocs.io/) and the +[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template) +project template. + diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/RECORD b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..9e0e2023f21e2df03b9b2d75aae5b116887feede --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/RECORD @@ -0,0 +1,16 @@ +aiohappyeyeballs-2.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiohappyeyeballs-2.6.1.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936 +aiohappyeyeballs-2.6.1.dist-info/METADATA,sha256=NSXlhJwAfi380eEjAo7BQ4P_TVal9xi0qkyZWibMsVM,5915 +aiohappyeyeballs-2.6.1.dist-info/RECORD,, +aiohappyeyeballs-2.6.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88 +aiohappyeyeballs/__init__.py,sha256=x7kktHEtaD9quBcWDJPuLeKyjuVAI-Jj14S9B_5hcTs,361 +aiohappyeyeballs/__pycache__/__init__.cpython-312.pyc,, +aiohappyeyeballs/__pycache__/_staggered.cpython-312.pyc,, +aiohappyeyeballs/__pycache__/impl.cpython-312.pyc,, +aiohappyeyeballs/__pycache__/types.cpython-312.pyc,, +aiohappyeyeballs/__pycache__/utils.cpython-312.pyc,, +aiohappyeyeballs/_staggered.py,sha256=edfVowFx-P-ywJjIEF3MdPtEMVODujV6CeMYr65otac,6900 +aiohappyeyeballs/impl.py,sha256=Dlcm2mTJ28ucrGnxkb_fo9CZzLAkOOBizOt7dreBbXE,9681 +aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiohappyeyeballs/types.py,sha256=YZJIAnyoV4Dz0WFtlaf_OyE4EW7Xus1z7aIfNI6tDDQ,425 +aiohappyeyeballs/utils.py,sha256=on9GxIR0LhEfZu8P6Twi9hepX9zDanuZM20MWsb3xlQ,3028 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/WHEEL b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..0582547b15f02d3a51659106262832565d5dc5ea --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohappyeyeballs-2.6.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 2.1.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/INSTALLER b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/METADATA b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..ba4116f99ff6066129469d5ec0a8f596fde9a7c7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/METADATA @@ -0,0 +1,262 @@ +Metadata-Version: 2.4 +Name: aiohttp +Version: 3.13.2 +Summary: Async http client/server framework (asyncio) +Maintainer-email: aiohttp team +License: Apache-2.0 AND MIT +Project-URL: Homepage, https://github.com/aio-libs/aiohttp +Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org +Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org +Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI +Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp +Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html +Project-URL: Docs: RTD, https://docs.aiohttp.org +Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues +Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: AsyncIO +Classifier: Intended Audience :: Developers +Classifier: Operating System :: POSIX +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE.txt +License-File: vendor/llhttp/LICENSE +Requires-Dist: aiohappyeyeballs>=2.5.0 +Requires-Dist: aiosignal>=1.4.0 +Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11" +Requires-Dist: attrs>=17.3.0 +Requires-Dist: frozenlist>=1.1.1 +Requires-Dist: multidict<7.0,>=4.5 +Requires-Dist: propcache>=0.2.0 +Requires-Dist: yarl<2.0,>=1.17.0 +Provides-Extra: speedups +Requires-Dist: aiodns>=3.3.0; extra == "speedups" +Requires-Dist: Brotli; platform_python_implementation == "CPython" and extra == "speedups" +Requires-Dist: brotlicffi; platform_python_implementation != "CPython" and extra == "speedups" +Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups" +Dynamic: license-file + +================================== +Async http client/server framework +================================== + +.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg + :height: 64px + :width: 64px + :alt: aiohttp logo + +| + +.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg + :target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI + :alt: GitHub Actions status for master branch + +.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/aiohttp + :alt: codecov.io status for master branch + +.. image:: https://badge.fury.io/py/aiohttp.svg + :target: https://pypi.org/project/aiohttp + :alt: Latest PyPI package version + +.. image:: https://img.shields.io/pypi/dm/aiohttp + :target: https://pypistats.org/packages/aiohttp + :alt: Downloads count + +.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest + :target: https://docs.aiohttp.org/ + :alt: Latest Read The Docs + +.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json + :target: https://codspeed.io/aio-libs/aiohttp + :alt: Codspeed.io status for aiohttp + + +Key Features +============ + +- Supports both client and server side of HTTP protocol. +- Supports both client and server Web-Sockets out-of-the-box and avoids + Callback Hell. +- Provides Web-server with middleware and pluggable routing. + + +Getting started +=============== + +Client +------ + +To get something from the web: + +.. code-block:: python + + import aiohttp + import asyncio + + async def main(): + + async with aiohttp.ClientSession() as session: + async with session.get('http://python.org') as response: + + print("Status:", response.status) + print("Content-type:", response.headers['content-type']) + + html = await response.text() + print("Body:", html[:15], "...") + + asyncio.run(main()) + +This prints: + +.. code-block:: + + Status: 200 + Content-type: text/html; charset=utf-8 + Body: ... + +Coming from `requests `_ ? Read `why we need so many lines `_. + +Server +------ + +An example using a simple server: + +.. code-block:: python + + # examples/server_simple.py + from aiohttp import web + + async def handle(request): + name = request.match_info.get('name', "Anonymous") + text = "Hello, " + name + return web.Response(text=text) + + async def wshandle(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + async for msg in ws: + if msg.type == web.WSMsgType.text: + await ws.send_str("Hello, {}".format(msg.data)) + elif msg.type == web.WSMsgType.binary: + await ws.send_bytes(msg.data) + elif msg.type == web.WSMsgType.close: + break + + return ws + + + app = web.Application() + app.add_routes([web.get('/', handle), + web.get('/echo', wshandle), + web.get('/{name}', handle)]) + + if __name__ == '__main__': + web.run_app(app) + + +Documentation +============= + +https://aiohttp.readthedocs.io/ + + +Demos +===== + +https://github.com/aio-libs/aiohttp-demos + + +External links +============== + +* `Third party libraries + `_ +* `Built with aiohttp + `_ +* `Powered by aiohttp + `_ + +Feel free to make a Pull Request for adding your link to these pages! + + +Communication channels +====================== + +*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions + +*Matrix*: `#aio-libs:matrix.org `_ + +We support `Stack Overflow +`_. +Please add *aiohttp* tag to your question there. + +Requirements +============ + +- attrs_ +- multidict_ +- yarl_ +- frozenlist_ + +Optionally you may install the aiodns_ library (highly recommended for sake of speed). + +.. _aiodns: https://pypi.python.org/pypi/aiodns +.. _attrs: https://github.com/python-attrs/attrs +.. _multidict: https://pypi.python.org/pypi/multidict +.. _frozenlist: https://pypi.org/project/frozenlist/ +.. _yarl: https://pypi.python.org/pypi/yarl +.. _async-timeout: https://pypi.python.org/pypi/async_timeout + +License +======= + +``aiohttp`` is offered under the Apache 2 license. + + +Keepsafe +======== + +The aiohttp community would like to thank Keepsafe +(https://www.getkeepsafe.com) for its support in the early days of +the project. + + +Source code +=========== + +The latest developer version is available in a GitHub repository: +https://github.com/aio-libs/aiohttp + +Benchmarks +========== + +If you are interested in efficiency, the AsyncIO community maintains a +list of benchmarks on the official wiki: +https://github.com/python/asyncio/wiki/Benchmarks + +-------- + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp + :target: https://insights.linuxfoundation.org/project/aiohttp + :alt: LFX Health Score diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/RECORD b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..edd2864e9e76b5e1773e2dd2a4cbfbf53e5597fc --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/RECORD @@ -0,0 +1,139 @@ +aiohttp-3.13.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiohttp-3.13.2.dist-info/METADATA,sha256=3xr8ZyYTInh909TqCdZhKIC37g5nTgyP-Nj_yglCs5A,8135 +aiohttp-3.13.2.dist-info/RECORD,, +aiohttp-3.13.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiohttp-3.13.2.dist-info/WHEEL,sha256=DxRnWQz-Kp9-4a4hdDHsSv0KUC3H7sN9Nbef3-8RjXU,190 +aiohttp-3.13.2.dist-info/licenses/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588 +aiohttp-3.13.2.dist-info/licenses/vendor/llhttp/LICENSE,sha256=68qFTgE0zSVtZzYnwgSZ9CV363S6zwi58ltianPJEnc,1105 +aiohttp-3.13.2.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8 +aiohttp/.hash/_cparser.pxd.hash,sha256=pjs-sEXNw_eijXGAedwG-BHnlFp8B7sOCgUagIWaU2A,121 +aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125 +aiohttp/.hash/_http_parser.pyx.hash,sha256=ju4DG_uNv8rTD6pu3IunE1ysx3ZbH4OjiQHUb_URSoA,125 +aiohttp/.hash/_http_writer.pyx.hash,sha256=9txOh7t7c3y-vLmiuEY5dltmXvEo0CYyU4U853yyv9E,125 +aiohttp/.hash/hdrs.py.hash,sha256=v6IaKbsxjsdQxBzhb5AjP0x_9G3rUe84D7avf7AI4cs,116 +aiohttp/__init__.py,sha256=YJ2jOOSU0hSTbloGbi5-jtcDfSmpBp2RTQEQAt0ccOA,8302 +aiohttp/__pycache__/__init__.cpython-312.pyc,, +aiohttp/__pycache__/_cookie_helpers.cpython-312.pyc,, +aiohttp/__pycache__/abc.cpython-312.pyc,, +aiohttp/__pycache__/base_protocol.cpython-312.pyc,, +aiohttp/__pycache__/client.cpython-312.pyc,, +aiohttp/__pycache__/client_exceptions.cpython-312.pyc,, +aiohttp/__pycache__/client_middleware_digest_auth.cpython-312.pyc,, +aiohttp/__pycache__/client_middlewares.cpython-312.pyc,, +aiohttp/__pycache__/client_proto.cpython-312.pyc,, +aiohttp/__pycache__/client_reqrep.cpython-312.pyc,, +aiohttp/__pycache__/client_ws.cpython-312.pyc,, +aiohttp/__pycache__/compression_utils.cpython-312.pyc,, +aiohttp/__pycache__/connector.cpython-312.pyc,, +aiohttp/__pycache__/cookiejar.cpython-312.pyc,, +aiohttp/__pycache__/formdata.cpython-312.pyc,, +aiohttp/__pycache__/hdrs.cpython-312.pyc,, +aiohttp/__pycache__/helpers.cpython-312.pyc,, +aiohttp/__pycache__/http.cpython-312.pyc,, +aiohttp/__pycache__/http_exceptions.cpython-312.pyc,, +aiohttp/__pycache__/http_parser.cpython-312.pyc,, +aiohttp/__pycache__/http_websocket.cpython-312.pyc,, +aiohttp/__pycache__/http_writer.cpython-312.pyc,, +aiohttp/__pycache__/log.cpython-312.pyc,, +aiohttp/__pycache__/multipart.cpython-312.pyc,, +aiohttp/__pycache__/payload.cpython-312.pyc,, +aiohttp/__pycache__/payload_streamer.cpython-312.pyc,, +aiohttp/__pycache__/pytest_plugin.cpython-312.pyc,, +aiohttp/__pycache__/resolver.cpython-312.pyc,, +aiohttp/__pycache__/streams.cpython-312.pyc,, +aiohttp/__pycache__/tcp_helpers.cpython-312.pyc,, +aiohttp/__pycache__/test_utils.cpython-312.pyc,, +aiohttp/__pycache__/tracing.cpython-312.pyc,, +aiohttp/__pycache__/typedefs.cpython-312.pyc,, +aiohttp/__pycache__/web.cpython-312.pyc,, +aiohttp/__pycache__/web_app.cpython-312.pyc,, +aiohttp/__pycache__/web_exceptions.cpython-312.pyc,, +aiohttp/__pycache__/web_fileresponse.cpython-312.pyc,, +aiohttp/__pycache__/web_log.cpython-312.pyc,, +aiohttp/__pycache__/web_middlewares.cpython-312.pyc,, +aiohttp/__pycache__/web_protocol.cpython-312.pyc,, +aiohttp/__pycache__/web_request.cpython-312.pyc,, +aiohttp/__pycache__/web_response.cpython-312.pyc,, +aiohttp/__pycache__/web_routedef.cpython-312.pyc,, +aiohttp/__pycache__/web_runner.cpython-312.pyc,, +aiohttp/__pycache__/web_server.cpython-312.pyc,, +aiohttp/__pycache__/web_urldispatcher.cpython-312.pyc,, +aiohttp/__pycache__/web_ws.cpython-312.pyc,, +aiohttp/__pycache__/worker.cpython-312.pyc,, +aiohttp/_cookie_helpers.py,sha256=INC-1MTQU7yJqBVmV48Fw30kzZH47KdVUrP_bbfpGvs,13647 +aiohttp/_cparser.pxd,sha256=UnbUYCHg4NdXfgyRVYAMv2KTLWClB4P-xCrvtj_r7ew,4295 +aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68 +aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007 +aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=3YrWjkYJoctR2UCDpht9IcnT4PE18AnSoHlgI11fN5k,2824440 +aiohttp/_http_parser.pyx,sha256=tmA1PaJn7H8U1nyXtoHJV44pxYVzqXAf1UgJaYPaw28,28219 +aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=mWC-4rsbntVD1V5ZEKEpSW_sm63V0XRe1fDR0lygipo,539144 +aiohttp/_http_writer.pyx,sha256=VlFEBM6HoVv8a0AAJtc6JwFlsv2-cDE8-gB94p3dfhQ,4664 +aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128 +aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128 +aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=5xf3oobk6vx4xbJm-xtZ1_QufB8fYFtLQV2MNdqUc1w,132 +aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44 +aiohttp/_websocket/__pycache__/__init__.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/helpers.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/models.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/reader.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/reader_c.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/reader_py.cpython-312.pyc,, +aiohttp/_websocket/__pycache__/writer.cpython-312.pyc,, +aiohttp/_websocket/helpers.py,sha256=P-XLv8IUaihKzDenVUqfKU5DJbWE5HvG8uhvUZK8Ic4,5038 +aiohttp/_websocket/mask.cpython-312-x86_64-linux-gnu.so,sha256=EpRwPJm1K1yavMCd9llAWdT4AsqKx_QEN0rb0eJH_Kc,263512 +aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112 +aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397 +aiohttp/_websocket/models.py,sha256=XAzjs_8JYszWXIgZ6R3ZRrF-tX9Q_6LiD49WRYojopM,2121 +aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030 +aiohttp/_websocket/reader_c.cpython-312-x86_64-linux-gnu.so,sha256=G-fK_d5U4UcA5kBo53v_5AYeOxksxvuy5b3y_XrZD04,1824528 +aiohttp/_websocket/reader_c.pxd,sha256=nl_njtDrzlQU0rjgGGjZDB-swguE0tX_bCPobkShVa4,2625 +aiohttp/_websocket/reader_c.py,sha256=gSsE_iSBr7-ORvOmgkCT7Jpj4_j3854i_Cp88Se1_6E,18791 +aiohttp/_websocket/reader_py.py,sha256=gSsE_iSBr7-ORvOmgkCT7Jpj4_j3854i_Cp88Se1_6E,18791 +aiohttp/_websocket/writer.py,sha256=2OvSktPmNh_g20h1cXJt2Xu8u6IvswnPjdur7OwBbJk,11261 +aiohttp/abc.py,sha256=M66F4S6m00bIEn7y4ha_XLTMDmVQ9dPihfOVB0pGfOo,7149 +aiohttp/base_protocol.py,sha256=Tp8cxUPQvv9kUPk3w6lAzk6d2MAzV3scwI_3Go3C47c,3025 +aiohttp/client.py,sha256=fOQfwcIUL1NGAVRV4DDj6-wipBzeD8KZpmzhO-LLKp4,58357 +aiohttp/client_exceptions.py,sha256=uyKbxI2peZhKl7lELBMx3UeusNkfpemPWpGFq0r6JeM,11367 +aiohttp/client_middleware_digest_auth.py,sha256=BIoQJ5eWL5NNkPOmezTGrceWIho8ETDvS8NKvX-3Xdw,17088 +aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918 +aiohttp/client_proto.py,sha256=56_WtLStZGBFPYKzgEgY6v24JkhV1y6JEmmuxeJT2So,12110 +aiohttp/client_reqrep.py,sha256=eEREDrZ0M8ZFTt1wjHduR-P8_sm40K65gNz-iMGYask,53391 +aiohttp/client_ws.py,sha256=1CIjIXwyzOMIYw6AjUES4-qUwbyVHW1seJKQfg_Rta8,15109 +aiohttp/compression_utils.py,sha256=Cmn4bim6iDYUST1Fp66EBRDzIz_3gUQBLg4HkbEljrc,10408 +aiohttp/connector.py,sha256=WQetKoSW7XnHA9r4o9OWwO3-n7ymOwBd2Tg_xHNw0Bs,68456 +aiohttp/cookiejar.py,sha256=e28ZMQwJ5P0vbPX1OX4Se7-k3zeGvocFEqzGhwpG53k,18922 +aiohttp/formdata.py,sha256=xqYMbUo1qoLYPuzY92XeR4pyEe-w-DNcToARDF3GUhA,6384 +aiohttp/hdrs.py,sha256=2rj5MyA-6yRdYPhW5UKkW4iNWhEAlGIOSBH5D4FmKNE,5111 +aiohttp/helpers.py,sha256=Q1307PCEnWz4RP8crUw8dk58c0YF2Ei3JywkKfRxz5E,30629 +aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842 +aiohttp/http_exceptions.py,sha256=AZafFHgtAkAgrKZf8zYPU8VX2dq32-VAoP-UZxBLU0c,2960 +aiohttp/http_parser.py,sha256=fACBNI47n9hnVPWfm5AJufuezsoYOF_VLp4bptYjvQI,37377 +aiohttp/http_websocket.py,sha256=8VXFKw6KQUEmPg48GtRMB37v0gTK7A0inoxXuDxMZEc,842 +aiohttp/http_writer.py,sha256=fbRtKPYSqRbtAdr_gqpjF2-4sI1ESL8dPDF-xY_mAMY,12446 +aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325 +aiohttp/multipart.py,sha256=6q6QRjKFVqaWzTbc7bkuBtXsTaQq5b2BhHxLBvAElac,40040 +aiohttp/payload.py,sha256=O6nsYNULL7AeM2cyJ6TYX73ncVnL5xJwt5AegxwMKqw,40874 +aiohttp/payload_streamer.py,sha256=ZzEYyfzcjGWkVkK3XR2pBthSCSIykYvY3Wr5cGQ2eTc,2211 +aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 +aiohttp/pytest_plugin.py,sha256=z4XwqmsKdyJCKxbGiA5kFf90zcedvomqk4RqjZbhKNk,12901 +aiohttp/resolver.py,sha256=gsrfUpFf8iHlcHfJvY-1fiBHW3PRvRVNb5lNZBg3zlY,10031 +aiohttp/streams.py,sha256=cQxo6Fyu_HDWDpbezGRVPIVYtVtTbSLRF7g511DNmSs,22601 +aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961 +aiohttp/test_utils.py,sha256=ZJSzZWjC76KSbtwddTKcP6vHpUl_ozfAf3F93ewmHRU,23016 +aiohttp/tracing.py,sha256=-6aaW6l0J9uJD45LzR4cijYH0j62pt0U_nn_aVzFku4,14558 +aiohttp/typedefs.py,sha256=wUlqwe9Mw9W8jT3HsYJcYk00qP3EMPz3nTkYXmeNN48,1657 +aiohttp/web.py,sha256=JzSNmejg5G6YeFAnkIgZfytqbU86sNu844yYKmoUpqs,17852 +aiohttp/web_app.py,sha256=lGU_aAMN-h3wy-LTTHi6SeKH8ydt1G51BXcCspgD5ZA,19452 +aiohttp/web_exceptions.py,sha256=7nIuiwhZ39vJJ9KrWqArA5QcWbUdqkz2CLwEpJapeN8,10360 +aiohttp/web_fileresponse.py,sha256=Xzau8EMrWNrFg3u46h4UEteg93G4zYq94CU6vy0HiqE,16362 +aiohttp/web_log.py,sha256=rX5D7xLOX2B6BMdiZ-chme_KfJfW5IXEoFwLfkfkajs,7865 +aiohttp/web_middlewares.py,sha256=sFI0AgeNjdyAjuz92QtMIpngmJSOxrqe2Jfbs4BNUu0,4165 +aiohttp/web_protocol.py,sha256=c8a0PKGqfhIAiq2RboMsy1NRza4dnj6gnXIWvJUeCF0,27015 +aiohttp/web_request.py,sha256=zN96OlMRlrCFOMRpdh7y9rvHP0Hm8zavC0OFCj0wlSg,29833 +aiohttp/web_response.py,sha256=PKcziNU4LmftXqKVvoRMrAbOeVClpSN-iznHsiWezmU,29341 +aiohttp/web_routedef.py,sha256=VT1GAx6BrawoDh5RwBwBu5wSABSqgWwAe74AUCyZAEo,6110 +aiohttp/web_runner.py,sha256=v1G1nKiOOQgFnTSR4IMc6I9ReEFDMaHtMLvO_roDM-A,11786 +aiohttp/web_server.py,sha256=-9WDKUAiR9ll-rSdwXSqG6YjaoW79d1R4y0BGSqgUMA,2888 +aiohttp/web_urldispatcher.py,sha256=3ryu1ZOpcq79IYNMd6EjYWmQ_i6JbsJzS_IaV0yoYBg,44203 +aiohttp/web_ws.py,sha256=lItgmyatkXh0M6EY7JoZnSZkUl6R0wv8B88X4ILqQbU,22739 +aiohttp/worker.py,sha256=zT0iWN5Xze194bO6_VjHou0x7lR_k0MviN6Kadnk22g,8152 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/REQUESTED b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/WHEEL b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..f3e8a970f16adf4526f1722547053522d94bf860 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp312-cp312-manylinux_2_17_x86_64 +Tag: cp312-cp312-manylinux2014_x86_64 +Tag: cp312-cp312-manylinux_2_28_x86_64 + diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/LICENSE.txt b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..e497a322f2091d022983b9c5c043082ab61d1a8c --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/LICENSE.txt @@ -0,0 +1,13 @@ + Copyright aio-libs contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/vendor/llhttp/LICENSE b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/vendor/llhttp/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6c1512dd6bcd6de314673f8c2c7ff3fd6139f28e --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/licenses/vendor/llhttp/LICENSE @@ -0,0 +1,22 @@ +This software is licensed under the MIT License. + +Copyright Fedor Indutny, 2018. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/top_level.txt b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..ee4ba4f3d739e094878215c84eb41ba85c80e4a8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiohttp-3.13.2.dist-info/top_level.txt @@ -0,0 +1 @@ +aiohttp diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__init__.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5ede0090ad93cacd642b11645c1c71bcd3021e2b --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__init__.py @@ -0,0 +1,59 @@ +import sys +from typing import Any, Awaitable, Callable, TypeVar + +from frozenlist import FrozenList + +if sys.version_info >= (3, 11): + from typing import Unpack +else: + from typing_extensions import Unpack + +if sys.version_info >= (3, 13): + from typing import TypeVarTuple +else: + from typing_extensions import TypeVarTuple + +_T = TypeVar("_T") +_Ts = TypeVarTuple("_Ts", default=Unpack[tuple[()]]) + +__version__ = "1.4.0" + +__all__ = ("Signal",) + + +class Signal(FrozenList[Callable[[Unpack[_Ts]], Awaitable[object]]]): + """Coroutine-based signal implementation. + + To connect a callback to a signal, use any list method. + + Signals are fired using the send() coroutine, which takes named + arguments. + """ + + __slots__ = ("_owner",) + + def __init__(self, owner: object): + super().__init__() + self._owner = owner + + def __repr__(self) -> str: + return "".format( + self._owner, self.frozen, list(self) + ) + + async def send(self, *args: Unpack[_Ts], **kwargs: Any) -> None: + """ + Sends data to all registered receivers. + """ + if not self.frozen: + raise RuntimeError("Cannot send non-frozen signal.") + + for receiver in self: + await receiver(*args, **kwargs) + + def __call__( + self, func: Callable[[Unpack[_Ts]], Awaitable[_T]] + ) -> Callable[[Unpack[_Ts]], Awaitable[_T]]: + """Decorator to add a function to this Signal.""" + self.append(func) + return func diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a45ca2c26c7ddee1c45f1c2893cb8ae933eb5f1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/py.typed b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/aiosignal/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/INSTALLER b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/METADATA b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..7a4a4b7a7e9adbe4542d98f6b2f40e65ee64b182 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/METADATA @@ -0,0 +1,209 @@ +Metadata-Version: 2.4 +Name: idna +Version: 3.11 +Summary: Internationalized Domain Names in Applications (IDNA) +Author-email: Kim Davies +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Utilities +License-File: LICENSE.md +Requires-Dist: ruff >= 0.6.2 ; extra == "all" +Requires-Dist: mypy >= 1.11.2 ; extra == "all" +Requires-Dist: pytest >= 8.3.2 ; extra == "all" +Requires-Dist: flake8 >= 7.1.1 ; extra == "all" +Project-URL: Changelog, https://github.com/kjd/idna/blob/master/HISTORY.rst +Project-URL: Issue tracker, https://github.com/kjd/idna/issues +Project-URL: Source, https://github.com/kjd/idna +Provides-Extra: all + +Internationalized Domain Names in Applications (IDNA) +===================================================== + +Support for `Internationalized Domain Names in +Applications (IDNA) `_ +and `Unicode IDNA Compatibility Processing +`_. + +The latest versions of these standards supplied here provide +more comprehensive language coverage and reduce the potential of +allowing domains with known security vulnerabilities. This library +is a suitable replacement for the “encodings.idna” +module that comes with the Python standard library, but which +only supports an older superseded IDNA specification from 2003. + +Basic functions are simply executed: + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('ドメイン.テスト') + b'xn--eckwd4c7c.xn--zckzah' + >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) + ドメイン.テスト + + +Installation +------------ + +This package is available for installation from PyPI via the +typical mechanisms, such as: + +.. code-block:: bash + + $ python3 -m pip install idna + + +Usage +----- + +For typical usage, the ``encode`` and ``decode`` functions will take a +domain name argument and perform a conversion to ASCII compatible encoding +(known as A-labels), or to Unicode strings (known as U-labels) +respectively. + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('ドメイン.テスト') + b'xn--eckwd4c7c.xn--zckzah' + >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) + ドメイン.テスト + +Conversions can be applied at a per-label basis using the ``ulabel`` or +``alabel`` functions if necessary: + +.. code-block:: pycon + + >>> idna.alabel('测试') + b'xn--0zwm56d' + + +Compatibility Mapping (UTS #46) ++++++++++++++++++++++++++++++++ + +This library provides support for `Unicode IDNA Compatibility +Processing `_ which normalizes input from +different potential ways a user may input a domain prior to performing the IDNA +conversion operations. This functionality, known as a +`mapping `_, is considered by the +specification to be a local user-interface issue distinct from IDNA +conversion functionality. + +For example, “Königsgäßchen” is not a permissible label as *LATIN +CAPITAL LETTER K* is not allowed (nor are capital letters in general). +UTS 46 will convert this into lower case prior to applying the IDNA +conversion. + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('Königsgäßchen') + ... + idna.core.InvalidCodepoint: Codepoint U+004B at position 1 of 'Königsgäßchen' not allowed + >>> idna.encode('Königsgäßchen', uts46=True) + b'xn--knigsgchen-b4a3dun' + >>> print(idna.decode('xn--knigsgchen-b4a3dun')) + königsgäßchen + + +Exceptions +---------- + +All errors raised during the conversion following the specification +should raise an exception derived from the ``idna.IDNAError`` base +class. + +More specific exceptions that may be generated as ``idna.IDNABidiError`` +when the error reflects an illegal combination of left-to-right and +right-to-left characters in a label; ``idna.InvalidCodepoint`` when +a specific codepoint is an illegal character in an IDN label (i.e. +INVALID); and ``idna.InvalidCodepointContext`` when the codepoint is +illegal based on its position in the string (i.e. it is CONTEXTO or CONTEXTJ +but the contextual requirements are not satisfied.) + +Building and Diagnostics +------------------------ + +The IDNA and UTS 46 functionality relies upon pre-calculated lookup +tables for performance. These tables are derived from computing against +eligibility criteria in the respective standards using the command-line +script ``tools/idna-data``. + +This tool will fetch relevant codepoint data from the Unicode repository +and perform the required calculations to identify eligibility. There are +three main modes: + +* ``idna-data make-libdata``. Generates ``idnadata.py`` and + ``uts46data.py``, the pre-calculated lookup tables used for IDNA and + UTS 46 conversions. Implementers who wish to track this library against + a different Unicode version may use this tool to manually generate a + different version of the ``idnadata.py`` and ``uts46data.py`` files. + +* ``idna-data make-table``. Generate a table of the IDNA disposition + (e.g. PVALID, CONTEXTJ, CONTEXTO) in the format found in Appendix + B.1 of RFC 5892 and the pre-computed tables published by `IANA + `_. + +* ``idna-data U+0061``. Prints debugging output on the various + properties associated with an individual Unicode codepoint (in this + case, U+0061), that are used to assess the IDNA and UTS 46 status of a + codepoint. This is helpful in debugging or analysis. + +The tool accepts a number of arguments, described using ``idna-data +-h``. Most notably, the ``--version`` argument allows the specification +of the version of Unicode to be used in computing the table data. For +example, ``idna-data --version 9.0.0 make-libdata`` will generate +library data against Unicode 9.0.0. + + +Additional Notes +---------------- + +* **Packages**. The latest tagged release version is published in the + `Python Package Index `_. + +* **Version support**. This library supports Python 3.8 and higher. + As this library serves as a low-level toolkit for a variety of + applications, many of which strive for broad compatibility with older + Python versions, there is no rush to remove older interpreter support. + Support for older versions are likely to be removed from new releases + as automated tests can no longer easily be run, i.e. once the Python + version is officially end-of-life. + +* **Testing**. The library has a test suite based on each rule of the + IDNA specification, as well as tests that are provided as part of the + Unicode Technical Standard 46, `Unicode IDNA Compatibility Processing + `_. + +* **Emoji**. It is an occasional request to support emoji domains in + this library. Encoding of symbols like emoji is expressly prohibited by + the technical standard IDNA 2008 and emoji domains are broadly phased + out across the domain industry due to associated security risks. For + now, applications that need to support these non-compliant labels + may wish to consider trying the encode/decode operation in this library + first, and then falling back to using `encodings.idna`. See `the Github + project `_ for more discussion. + +* **Transitional processing**. Unicode 16.0.0 removed transitional + processing so the `transitional` argument for the encode() method + no longer has any effect and will be removed at a later date. + diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/RECORD b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..8525b6daf9b59dd5e02fdf5797c63da32f780ee8 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/RECORD @@ -0,0 +1,22 @@ +idna-3.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +idna-3.11.dist-info/METADATA,sha256=fCwSww9SuiN8TIHllFSASUQCW55hAs8dzKnr9RaEEbA,8378 +idna-3.11.dist-info/RECORD,, +idna-3.11.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +idna-3.11.dist-info/licenses/LICENSE.md,sha256=t6M2q_OwThgOwGXN0W5wXQeeHMehT5EKpukYfza5zYc,1541 +idna/__init__.py,sha256=MPqNDLZbXqGaNdXxAFhiqFPKEQXju2jNQhCey6-5eJM,868 +idna/__pycache__/__init__.cpython-312.pyc,, +idna/__pycache__/codec.cpython-312.pyc,, +idna/__pycache__/compat.cpython-312.pyc,, +idna/__pycache__/core.cpython-312.pyc,, +idna/__pycache__/idnadata.cpython-312.pyc,, +idna/__pycache__/intranges.cpython-312.pyc,, +idna/__pycache__/package_data.cpython-312.pyc,, +idna/__pycache__/uts46data.cpython-312.pyc,, +idna/codec.py,sha256=M2SGWN7cs_6B32QmKTyTN6xQGZeYQgQ2wiX3_DR6loE,3438 +idna/compat.py,sha256=RzLy6QQCdl9784aFhb2EX9EKGCJjg0P3PilGdeXXcx8,316 +idna/core.py,sha256=P26_XVycuMTZ1R2mNK1ZREVzM5mvTzdabBXfyZVU1Lc,13246 +idna/idnadata.py,sha256=SG8jhaGE53iiD6B49pt2pwTv_UvClciWE-N54oR2p4U,79623 +idna/intranges.py,sha256=amUtkdhYcQG8Zr-CoMM_kVRacxkivC1WgxN1b63KKdU,1898 +idna/package_data.py,sha256=_CUavOxobnbyNG2FLyHoN8QHP3QM9W1tKuw7eq9QwBk,21 +idna/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +idna/uts46data.py,sha256=H9J35VkD0F9L9mKOqjeNGd2A-Va6FlPoz6Jz4K7h-ps,243725 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/WHEEL b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..d8b9936dad9ab2513fa6979f411560d3b6b57e37 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/licenses/LICENSE.md b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..256ba90cd91190a6c980bd44663dc51c201c14d3 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna-3.11.dist-info/licenses/LICENSE.md @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2013-2025, Kim Davies and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__init__.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cfdc030a751b089fc7e38fc88093b791605d501d --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__init__.py @@ -0,0 +1,45 @@ +from .core import ( + IDNABidiError, + IDNAError, + InvalidCodepoint, + InvalidCodepointContext, + alabel, + check_bidi, + check_hyphen_ok, + check_initial_combiner, + check_label, + check_nfc, + decode, + encode, + ulabel, + uts46_remap, + valid_contextj, + valid_contexto, + valid_label_length, + valid_string_length, +) +from .intranges import intranges_contain +from .package_data import __version__ + +__all__ = [ + "__version__", + "IDNABidiError", + "IDNAError", + "InvalidCodepoint", + "InvalidCodepointContext", + "alabel", + "check_bidi", + "check_hyphen_ok", + "check_initial_combiner", + "check_label", + "check_nfc", + "decode", + "encode", + "intranges_contain", + "ulabel", + "uts46_remap", + "valid_contextj", + "valid_contexto", + "valid_label_length", + "valid_string_length", +] diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4133a9d2a342c9ed6dea71ae6b57e7123992ac30 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/codec.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/codec.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2fb64f9775441d7dc7cbae563418d325c9cb5cd Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/codec.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/compat.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/compat.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98bb1b1d100deddecf4c2d1f9b8be283aad78401 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/compat.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/core.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/core.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f63750b57f1c49dc22a069efd2bbaa6b6d1dacc Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/core.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/intranges.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/intranges.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..490bb32474c13c0ebe6a95a8748310e2d083286b Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/intranges.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/package_data.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/package_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23e18862541c7334b11accb06c149bb8f13264f0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/__pycache__/package_data.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/codec.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/codec.py new file mode 100644 index 0000000000000000000000000000000000000000..cbc2e4ff4ec3e2318d47615bab44ea0ca3dba978 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/codec.py @@ -0,0 +1,122 @@ +import codecs +import re +from typing import Any, Optional, Tuple + +from .core import IDNAError, alabel, decode, encode, ulabel + +_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") + + +class Codec(codecs.Codec): + def encode(self, data: str, errors: str = "strict") -> Tuple[bytes, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return b"", 0 + + return encode(data), len(data) + + def decode(self, data: bytes, errors: str = "strict") -> Tuple[str, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return "", 0 + + return decode(data), len(data) + + +class IncrementalEncoder(codecs.BufferedIncrementalEncoder): + def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return b"", 0 + + labels = _unicode_dots_re.split(data) + trailing_dot = b"" + if labels: + if not labels[-1]: + trailing_dot = b"." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = b"." + + result = [] + size = 0 + for label in labels: + result.append(alabel(label)) + if size: + size += 1 + size += len(label) + + # Join with U+002E + result_bytes = b".".join(result) + trailing_dot + size += len(trailing_dot) + return result_bytes, size + + +class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return ("", 0) + + if not isinstance(data, str): + data = str(data, "ascii") + + labels = _unicode_dots_re.split(data) + trailing_dot = "" + if labels: + if not labels[-1]: + trailing_dot = "." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = "." + + result = [] + size = 0 + for label in labels: + result.append(ulabel(label)) + if size: + size += 1 + size += len(label) + + result_str = ".".join(result) + trailing_dot + size += len(trailing_dot) + return (result_str, size) + + +class StreamWriter(Codec, codecs.StreamWriter): + pass + + +class StreamReader(Codec, codecs.StreamReader): + pass + + +def search_function(name: str) -> Optional[codecs.CodecInfo]: + if name != "idna2008": + return None + return codecs.CodecInfo( + name=name, + encode=Codec().encode, + decode=Codec().decode, # type: ignore + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) + + +codecs.register(search_function) diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/compat.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/compat.py new file mode 100644 index 0000000000000000000000000000000000000000..1df9f2a70e6815908f2784e88897a9a359eef84c --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/compat.py @@ -0,0 +1,15 @@ +from typing import Any, Union + +from .core import decode, encode + + +def ToASCII(label: str) -> bytes: + return encode(label) + + +def ToUnicode(label: Union[bytes, bytearray]) -> str: + return decode(label) + + +def nameprep(s: Any) -> None: + raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/core.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/core.py new file mode 100644 index 0000000000000000000000000000000000000000..8177bf7a324f9f54a29e41e867f5d56f2dd0a924 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/core.py @@ -0,0 +1,437 @@ +import bisect +import re +import unicodedata +from typing import Optional, Union + +from . import idnadata +from .intranges import intranges_contain + +_virama_combining_class = 9 +_alabel_prefix = b"xn--" +_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") + + +class IDNAError(UnicodeError): + """Base exception for all IDNA-encoding related problems""" + + pass + + +class IDNABidiError(IDNAError): + """Exception when bidirectional requirements are not satisfied""" + + pass + + +class InvalidCodepoint(IDNAError): + """Exception when a disallowed or unallocated codepoint is used""" + + pass + + +class InvalidCodepointContext(IDNAError): + """Exception when the codepoint is not valid in the context it is used""" + + pass + + +def _combining_class(cp: int) -> int: + v = unicodedata.combining(chr(cp)) + if v == 0: + if not unicodedata.name(chr(cp)): + raise ValueError("Unknown character in unicodedata") + return v + + +def _is_script(cp: str, script: str) -> bool: + return intranges_contain(ord(cp), idnadata.scripts[script]) + + +def _punycode(s: str) -> bytes: + return s.encode("punycode") + + +def _unot(s: int) -> str: + return "U+{:04X}".format(s) + + +def valid_label_length(label: Union[bytes, str]) -> bool: + if len(label) > 63: + return False + return True + + +def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool: + if len(label) > (254 if trailing_dot else 253): + return False + return True + + +def check_bidi(label: str, check_ltr: bool = False) -> bool: + # Bidi rules should only be applied if string contains RTL characters + bidi_label = False + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + if direction == "": + # String likely comes from a newer version of Unicode + raise IDNABidiError("Unknown directionality in label {} at position {}".format(repr(label), idx)) + if direction in ["R", "AL", "AN"]: + bidi_label = True + if not bidi_label and not check_ltr: + return True + + # Bidi rule 1 + direction = unicodedata.bidirectional(label[0]) + if direction in ["R", "AL"]: + rtl = True + elif direction == "L": + rtl = False + else: + raise IDNABidiError("First codepoint in label {} must be directionality L, R or AL".format(repr(label))) + + valid_ending = False + number_type: Optional[str] = None + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + + if rtl: + # Bidi rule 2 + if direction not in [ + "R", + "AL", + "AN", + "EN", + "ES", + "CS", + "ET", + "ON", + "BN", + "NSM", + ]: + raise IDNABidiError("Invalid direction for codepoint at position {} in a right-to-left label".format(idx)) + # Bidi rule 3 + if direction in ["R", "AL", "EN", "AN"]: + valid_ending = True + elif direction != "NSM": + valid_ending = False + # Bidi rule 4 + if direction in ["AN", "EN"]: + if not number_type: + number_type = direction + else: + if number_type != direction: + raise IDNABidiError("Can not mix numeral types in a right-to-left label") + else: + # Bidi rule 5 + if direction not in ["L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"]: + raise IDNABidiError("Invalid direction for codepoint at position {} in a left-to-right label".format(idx)) + # Bidi rule 6 + if direction in ["L", "EN"]: + valid_ending = True + elif direction != "NSM": + valid_ending = False + + if not valid_ending: + raise IDNABidiError("Label ends with illegal codepoint directionality") + + return True + + +def check_initial_combiner(label: str) -> bool: + if unicodedata.category(label[0])[0] == "M": + raise IDNAError("Label begins with an illegal combining character") + return True + + +def check_hyphen_ok(label: str) -> bool: + if label[2:4] == "--": + raise IDNAError("Label has disallowed hyphens in 3rd and 4th position") + if label[0] == "-" or label[-1] == "-": + raise IDNAError("Label must not start or end with a hyphen") + return True + + +def check_nfc(label: str) -> None: + if unicodedata.normalize("NFC", label) != label: + raise IDNAError("Label must be in Normalization Form C") + + +def valid_contextj(label: str, pos: int) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x200C: + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + + ok = False + for i in range(pos - 1, -1, -1): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord("T"): + continue + elif joining_type in [ord("L"), ord("D")]: + ok = True + break + else: + break + + if not ok: + return False + + ok = False + for i in range(pos + 1, len(label)): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord("T"): + continue + elif joining_type in [ord("R"), ord("D")]: + ok = True + break + else: + break + return ok + + if cp_value == 0x200D: + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + return False + + else: + return False + + +def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x00B7: + if 0 < pos < len(label) - 1: + if ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C: + return True + return False + + elif cp_value == 0x0375: + if pos < len(label) - 1 and len(label) > 1: + return _is_script(label[pos + 1], "Greek") + return False + + elif cp_value == 0x05F3 or cp_value == 0x05F4: + if pos > 0: + return _is_script(label[pos - 1], "Hebrew") + return False + + elif cp_value == 0x30FB: + for cp in label: + if cp == "\u30fb": + continue + if _is_script(cp, "Hiragana") or _is_script(cp, "Katakana") or _is_script(cp, "Han"): + return True + return False + + elif 0x660 <= cp_value <= 0x669: + for cp in label: + if 0x6F0 <= ord(cp) <= 0x06F9: + return False + return True + + elif 0x6F0 <= cp_value <= 0x6F9: + for cp in label: + if 0x660 <= ord(cp) <= 0x0669: + return False + return True + + return False + + +def check_label(label: Union[str, bytes, bytearray]) -> None: + if isinstance(label, (bytes, bytearray)): + label = label.decode("utf-8") + if len(label) == 0: + raise IDNAError("Empty Label") + + check_nfc(label) + check_hyphen_ok(label) + check_initial_combiner(label) + + for pos, cp in enumerate(label): + cp_value = ord(cp) + if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]): + continue + elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]): + try: + if not valid_contextj(label, pos): + raise InvalidCodepointContext( + "Joiner {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) + ) + except ValueError: + raise IDNAError( + "Unknown codepoint adjacent to joiner {} at position {} in {}".format( + _unot(cp_value), pos + 1, repr(label) + ) + ) + elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]): + if not valid_contexto(label, pos): + raise InvalidCodepointContext( + "Codepoint {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) + ) + else: + raise InvalidCodepoint( + "Codepoint {} at position {} of {} not allowed".format(_unot(cp_value), pos + 1, repr(label)) + ) + + check_bidi(label) + + +def alabel(label: str) -> bytes: + try: + label_bytes = label.encode("ascii") + ulabel(label_bytes) + if not valid_label_length(label_bytes): + raise IDNAError("Label too long") + return label_bytes + except UnicodeEncodeError: + pass + + check_label(label) + label_bytes = _alabel_prefix + _punycode(label) + + if not valid_label_length(label_bytes): + raise IDNAError("Label too long") + + return label_bytes + + +def ulabel(label: Union[str, bytes, bytearray]) -> str: + if not isinstance(label, (bytes, bytearray)): + try: + label_bytes = label.encode("ascii") + except UnicodeEncodeError: + check_label(label) + return label + else: + label_bytes = bytes(label) + + label_bytes = label_bytes.lower() + if label_bytes.startswith(_alabel_prefix): + label_bytes = label_bytes[len(_alabel_prefix) :] + if not label_bytes: + raise IDNAError("Malformed A-label, no Punycode eligible content found") + if label_bytes.decode("ascii")[-1] == "-": + raise IDNAError("A-label must not end with a hyphen") + else: + check_label(label_bytes) + return label_bytes.decode("ascii") + + try: + label = label_bytes.decode("punycode") + except UnicodeError: + raise IDNAError("Invalid A-label") + check_label(label) + return label + + +def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str: + """Re-map the characters in the string according to UTS46 processing.""" + from .uts46data import uts46data + + output = "" + + for pos, char in enumerate(domain): + code_point = ord(char) + try: + uts46row = uts46data[code_point if code_point < 256 else bisect.bisect_left(uts46data, (code_point, "Z")) - 1] + status = uts46row[1] + replacement: Optional[str] = None + if len(uts46row) == 3: + replacement = uts46row[2] + if ( + status == "V" + or (status == "D" and not transitional) + or (status == "3" and not std3_rules and replacement is None) + ): + output += char + elif replacement is not None and ( + status == "M" or (status == "3" and not std3_rules) or (status == "D" and transitional) + ): + output += replacement + elif status != "I": + raise IndexError() + except IndexError: + raise InvalidCodepoint( + "Codepoint {} not allowed at position {} in {}".format(_unot(code_point), pos + 1, repr(domain)) + ) + + return unicodedata.normalize("NFC", output) + + +def encode( + s: Union[str, bytes, bytearray], + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, + transitional: bool = False, +) -> bytes: + if not isinstance(s, str): + try: + s = str(s, "ascii") + except UnicodeDecodeError: + raise IDNAError("should pass a unicode string to the function rather than a byte string.") + if uts46: + s = uts46_remap(s, std3_rules, transitional) + trailing_dot = False + result = [] + if strict: + labels = s.split(".") + else: + labels = _unicode_dots_re.split(s) + if not labels or labels == [""]: + raise IDNAError("Empty domain") + if labels[-1] == "": + del labels[-1] + trailing_dot = True + for label in labels: + s = alabel(label) + if s: + result.append(s) + else: + raise IDNAError("Empty label") + if trailing_dot: + result.append(b"") + s = b".".join(result) + if not valid_string_length(s, trailing_dot): + raise IDNAError("Domain too long") + return s + + +def decode( + s: Union[str, bytes, bytearray], + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, +) -> str: + try: + if not isinstance(s, str): + s = str(s, "ascii") + except UnicodeDecodeError: + raise IDNAError("Invalid ASCII in A-label") + if uts46: + s = uts46_remap(s, std3_rules, False) + trailing_dot = False + result = [] + if not strict: + labels = _unicode_dots_re.split(s) + else: + labels = s.split(".") + if not labels or labels == [""]: + raise IDNAError("Empty domain") + if not labels[-1]: + del labels[-1] + trailing_dot = True + for label in labels: + s = ulabel(label) + if s: + result.append(s) + else: + raise IDNAError("Empty label") + if trailing_dot: + result.append("") + return ".".join(result) diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/idnadata.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/idnadata.py new file mode 100644 index 0000000000000000000000000000000000000000..ded47cae0b16977aae69f3895ecfe8b8980f58d0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/idnadata.py @@ -0,0 +1,4309 @@ +# This file is automatically generated by tools/idna-data + +__version__ = "16.0.0" + +scripts = { + "Greek": ( + 0x37000000374, + 0x37500000378, + 0x37A0000037E, + 0x37F00000380, + 0x38400000385, + 0x38600000387, + 0x3880000038B, + 0x38C0000038D, + 0x38E000003A2, + 0x3A3000003E2, + 0x3F000000400, + 0x1D2600001D2B, + 0x1D5D00001D62, + 0x1D6600001D6B, + 0x1DBF00001DC0, + 0x1F0000001F16, + 0x1F1800001F1E, + 0x1F2000001F46, + 0x1F4800001F4E, + 0x1F5000001F58, + 0x1F5900001F5A, + 0x1F5B00001F5C, + 0x1F5D00001F5E, + 0x1F5F00001F7E, + 0x1F8000001FB5, + 0x1FB600001FC5, + 0x1FC600001FD4, + 0x1FD600001FDC, + 0x1FDD00001FF0, + 0x1FF200001FF5, + 0x1FF600001FFF, + 0x212600002127, + 0xAB650000AB66, + 0x101400001018F, + 0x101A0000101A1, + 0x1D2000001D246, + ), + "Han": ( + 0x2E8000002E9A, + 0x2E9B00002EF4, + 0x2F0000002FD6, + 0x300500003006, + 0x300700003008, + 0x30210000302A, + 0x30380000303C, + 0x340000004DC0, + 0x4E000000A000, + 0xF9000000FA6E, + 0xFA700000FADA, + 0x16FE200016FE4, + 0x16FF000016FF2, + 0x200000002A6E0, + 0x2A7000002B73A, + 0x2B7400002B81E, + 0x2B8200002CEA2, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x2F8000002FA1E, + 0x300000003134B, + 0x31350000323B0, + ), + "Hebrew": ( + 0x591000005C8, + 0x5D0000005EB, + 0x5EF000005F5, + 0xFB1D0000FB37, + 0xFB380000FB3D, + 0xFB3E0000FB3F, + 0xFB400000FB42, + 0xFB430000FB45, + 0xFB460000FB50, + ), + "Hiragana": ( + 0x304100003097, + 0x309D000030A0, + 0x1B0010001B120, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1F2000001F201, + ), + "Katakana": ( + 0x30A1000030FB, + 0x30FD00003100, + 0x31F000003200, + 0x32D0000032FF, + 0x330000003358, + 0xFF660000FF70, + 0xFF710000FF9E, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B001, + 0x1B1200001B123, + 0x1B1550001B156, + 0x1B1640001B168, + ), +} +joining_types = { + 0xAD: 84, + 0x300: 84, + 0x301: 84, + 0x302: 84, + 0x303: 84, + 0x304: 84, + 0x305: 84, + 0x306: 84, + 0x307: 84, + 0x308: 84, + 0x309: 84, + 0x30A: 84, + 0x30B: 84, + 0x30C: 84, + 0x30D: 84, + 0x30E: 84, + 0x30F: 84, + 0x310: 84, + 0x311: 84, + 0x312: 84, + 0x313: 84, + 0x314: 84, + 0x315: 84, + 0x316: 84, + 0x317: 84, + 0x318: 84, + 0x319: 84, + 0x31A: 84, + 0x31B: 84, + 0x31C: 84, + 0x31D: 84, + 0x31E: 84, + 0x31F: 84, + 0x320: 84, + 0x321: 84, + 0x322: 84, + 0x323: 84, + 0x324: 84, + 0x325: 84, + 0x326: 84, + 0x327: 84, + 0x328: 84, + 0x329: 84, + 0x32A: 84, + 0x32B: 84, + 0x32C: 84, + 0x32D: 84, + 0x32E: 84, + 0x32F: 84, + 0x330: 84, + 0x331: 84, + 0x332: 84, + 0x333: 84, + 0x334: 84, + 0x335: 84, + 0x336: 84, + 0x337: 84, + 0x338: 84, + 0x339: 84, + 0x33A: 84, + 0x33B: 84, + 0x33C: 84, + 0x33D: 84, + 0x33E: 84, + 0x33F: 84, + 0x340: 84, + 0x341: 84, + 0x342: 84, + 0x343: 84, + 0x344: 84, + 0x345: 84, + 0x346: 84, + 0x347: 84, + 0x348: 84, + 0x349: 84, + 0x34A: 84, + 0x34B: 84, + 0x34C: 84, + 0x34D: 84, + 0x34E: 84, + 0x34F: 84, + 0x350: 84, + 0x351: 84, + 0x352: 84, + 0x353: 84, + 0x354: 84, + 0x355: 84, + 0x356: 84, + 0x357: 84, + 0x358: 84, + 0x359: 84, + 0x35A: 84, + 0x35B: 84, + 0x35C: 84, + 0x35D: 84, + 0x35E: 84, + 0x35F: 84, + 0x360: 84, + 0x361: 84, + 0x362: 84, + 0x363: 84, + 0x364: 84, + 0x365: 84, + 0x366: 84, + 0x367: 84, + 0x368: 84, + 0x369: 84, + 0x36A: 84, + 0x36B: 84, + 0x36C: 84, + 0x36D: 84, + 0x36E: 84, + 0x36F: 84, + 0x483: 84, + 0x484: 84, + 0x485: 84, + 0x486: 84, + 0x487: 84, + 0x488: 84, + 0x489: 84, + 0x591: 84, + 0x592: 84, + 0x593: 84, + 0x594: 84, + 0x595: 84, + 0x596: 84, + 0x597: 84, + 0x598: 84, + 0x599: 84, + 0x59A: 84, + 0x59B: 84, + 0x59C: 84, + 0x59D: 84, + 0x59E: 84, + 0x59F: 84, + 0x5A0: 84, + 0x5A1: 84, + 0x5A2: 84, + 0x5A3: 84, + 0x5A4: 84, + 0x5A5: 84, + 0x5A6: 84, + 0x5A7: 84, + 0x5A8: 84, + 0x5A9: 84, + 0x5AA: 84, + 0x5AB: 84, + 0x5AC: 84, + 0x5AD: 84, + 0x5AE: 84, + 0x5AF: 84, + 0x5B0: 84, + 0x5B1: 84, + 0x5B2: 84, + 0x5B3: 84, + 0x5B4: 84, + 0x5B5: 84, + 0x5B6: 84, + 0x5B7: 84, + 0x5B8: 84, + 0x5B9: 84, + 0x5BA: 84, + 0x5BB: 84, + 0x5BC: 84, + 0x5BD: 84, + 0x5BF: 84, + 0x5C1: 84, + 0x5C2: 84, + 0x5C4: 84, + 0x5C5: 84, + 0x5C7: 84, + 0x610: 84, + 0x611: 84, + 0x612: 84, + 0x613: 84, + 0x614: 84, + 0x615: 84, + 0x616: 84, + 0x617: 84, + 0x618: 84, + 0x619: 84, + 0x61A: 84, + 0x61C: 84, + 0x620: 68, + 0x622: 82, + 0x623: 82, + 0x624: 82, + 0x625: 82, + 0x626: 68, + 0x627: 82, + 0x628: 68, + 0x629: 82, + 0x62A: 68, + 0x62B: 68, + 0x62C: 68, + 0x62D: 68, + 0x62E: 68, + 0x62F: 82, + 0x630: 82, + 0x631: 82, + 0x632: 82, + 0x633: 68, + 0x634: 68, + 0x635: 68, + 0x636: 68, + 0x637: 68, + 0x638: 68, + 0x639: 68, + 0x63A: 68, + 0x63B: 68, + 0x63C: 68, + 0x63D: 68, + 0x63E: 68, + 0x63F: 68, + 0x640: 67, + 0x641: 68, + 0x642: 68, + 0x643: 68, + 0x644: 68, + 0x645: 68, + 0x646: 68, + 0x647: 68, + 0x648: 82, + 0x649: 68, + 0x64A: 68, + 0x64B: 84, + 0x64C: 84, + 0x64D: 84, + 0x64E: 84, + 0x64F: 84, + 0x650: 84, + 0x651: 84, + 0x652: 84, + 0x653: 84, + 0x654: 84, + 0x655: 84, + 0x656: 84, + 0x657: 84, + 0x658: 84, + 0x659: 84, + 0x65A: 84, + 0x65B: 84, + 0x65C: 84, + 0x65D: 84, + 0x65E: 84, + 0x65F: 84, + 0x66E: 68, + 0x66F: 68, + 0x670: 84, + 0x671: 82, + 0x672: 82, + 0x673: 82, + 0x675: 82, + 0x676: 82, + 0x677: 82, + 0x678: 68, + 0x679: 68, + 0x67A: 68, + 0x67B: 68, + 0x67C: 68, + 0x67D: 68, + 0x67E: 68, + 0x67F: 68, + 0x680: 68, + 0x681: 68, + 0x682: 68, + 0x683: 68, + 0x684: 68, + 0x685: 68, + 0x686: 68, + 0x687: 68, + 0x688: 82, + 0x689: 82, + 0x68A: 82, + 0x68B: 82, + 0x68C: 82, + 0x68D: 82, + 0x68E: 82, + 0x68F: 82, + 0x690: 82, + 0x691: 82, + 0x692: 82, + 0x693: 82, + 0x694: 82, + 0x695: 82, + 0x696: 82, + 0x697: 82, + 0x698: 82, + 0x699: 82, + 0x69A: 68, + 0x69B: 68, + 0x69C: 68, + 0x69D: 68, + 0x69E: 68, + 0x69F: 68, + 0x6A0: 68, + 0x6A1: 68, + 0x6A2: 68, + 0x6A3: 68, + 0x6A4: 68, + 0x6A5: 68, + 0x6A6: 68, + 0x6A7: 68, + 0x6A8: 68, + 0x6A9: 68, + 0x6AA: 68, + 0x6AB: 68, + 0x6AC: 68, + 0x6AD: 68, + 0x6AE: 68, + 0x6AF: 68, + 0x6B0: 68, + 0x6B1: 68, + 0x6B2: 68, + 0x6B3: 68, + 0x6B4: 68, + 0x6B5: 68, + 0x6B6: 68, + 0x6B7: 68, + 0x6B8: 68, + 0x6B9: 68, + 0x6BA: 68, + 0x6BB: 68, + 0x6BC: 68, + 0x6BD: 68, + 0x6BE: 68, + 0x6BF: 68, + 0x6C0: 82, + 0x6C1: 68, + 0x6C2: 68, + 0x6C3: 82, + 0x6C4: 82, + 0x6C5: 82, + 0x6C6: 82, + 0x6C7: 82, + 0x6C8: 82, + 0x6C9: 82, + 0x6CA: 82, + 0x6CB: 82, + 0x6CC: 68, + 0x6CD: 82, + 0x6CE: 68, + 0x6CF: 82, + 0x6D0: 68, + 0x6D1: 68, + 0x6D2: 82, + 0x6D3: 82, + 0x6D5: 82, + 0x6D6: 84, + 0x6D7: 84, + 0x6D8: 84, + 0x6D9: 84, + 0x6DA: 84, + 0x6DB: 84, + 0x6DC: 84, + 0x6DF: 84, + 0x6E0: 84, + 0x6E1: 84, + 0x6E2: 84, + 0x6E3: 84, + 0x6E4: 84, + 0x6E7: 84, + 0x6E8: 84, + 0x6EA: 84, + 0x6EB: 84, + 0x6EC: 84, + 0x6ED: 84, + 0x6EE: 82, + 0x6EF: 82, + 0x6FA: 68, + 0x6FB: 68, + 0x6FC: 68, + 0x6FF: 68, + 0x70F: 84, + 0x710: 82, + 0x711: 84, + 0x712: 68, + 0x713: 68, + 0x714: 68, + 0x715: 82, + 0x716: 82, + 0x717: 82, + 0x718: 82, + 0x719: 82, + 0x71A: 68, + 0x71B: 68, + 0x71C: 68, + 0x71D: 68, + 0x71E: 82, + 0x71F: 68, + 0x720: 68, + 0x721: 68, + 0x722: 68, + 0x723: 68, + 0x724: 68, + 0x725: 68, + 0x726: 68, + 0x727: 68, + 0x728: 82, + 0x729: 68, + 0x72A: 82, + 0x72B: 68, + 0x72C: 82, + 0x72D: 68, + 0x72E: 68, + 0x72F: 82, + 0x730: 84, + 0x731: 84, + 0x732: 84, + 0x733: 84, + 0x734: 84, + 0x735: 84, + 0x736: 84, + 0x737: 84, + 0x738: 84, + 0x739: 84, + 0x73A: 84, + 0x73B: 84, + 0x73C: 84, + 0x73D: 84, + 0x73E: 84, + 0x73F: 84, + 0x740: 84, + 0x741: 84, + 0x742: 84, + 0x743: 84, + 0x744: 84, + 0x745: 84, + 0x746: 84, + 0x747: 84, + 0x748: 84, + 0x749: 84, + 0x74A: 84, + 0x74D: 82, + 0x74E: 68, + 0x74F: 68, + 0x750: 68, + 0x751: 68, + 0x752: 68, + 0x753: 68, + 0x754: 68, + 0x755: 68, + 0x756: 68, + 0x757: 68, + 0x758: 68, + 0x759: 82, + 0x75A: 82, + 0x75B: 82, + 0x75C: 68, + 0x75D: 68, + 0x75E: 68, + 0x75F: 68, + 0x760: 68, + 0x761: 68, + 0x762: 68, + 0x763: 68, + 0x764: 68, + 0x765: 68, + 0x766: 68, + 0x767: 68, + 0x768: 68, + 0x769: 68, + 0x76A: 68, + 0x76B: 82, + 0x76C: 82, + 0x76D: 68, + 0x76E: 68, + 0x76F: 68, + 0x770: 68, + 0x771: 82, + 0x772: 68, + 0x773: 82, + 0x774: 82, + 0x775: 68, + 0x776: 68, + 0x777: 68, + 0x778: 82, + 0x779: 82, + 0x77A: 68, + 0x77B: 68, + 0x77C: 68, + 0x77D: 68, + 0x77E: 68, + 0x77F: 68, + 0x7A6: 84, + 0x7A7: 84, + 0x7A8: 84, + 0x7A9: 84, + 0x7AA: 84, + 0x7AB: 84, + 0x7AC: 84, + 0x7AD: 84, + 0x7AE: 84, + 0x7AF: 84, + 0x7B0: 84, + 0x7CA: 68, + 0x7CB: 68, + 0x7CC: 68, + 0x7CD: 68, + 0x7CE: 68, + 0x7CF: 68, + 0x7D0: 68, + 0x7D1: 68, + 0x7D2: 68, + 0x7D3: 68, + 0x7D4: 68, + 0x7D5: 68, + 0x7D6: 68, + 0x7D7: 68, + 0x7D8: 68, + 0x7D9: 68, + 0x7DA: 68, + 0x7DB: 68, + 0x7DC: 68, + 0x7DD: 68, + 0x7DE: 68, + 0x7DF: 68, + 0x7E0: 68, + 0x7E1: 68, + 0x7E2: 68, + 0x7E3: 68, + 0x7E4: 68, + 0x7E5: 68, + 0x7E6: 68, + 0x7E7: 68, + 0x7E8: 68, + 0x7E9: 68, + 0x7EA: 68, + 0x7EB: 84, + 0x7EC: 84, + 0x7ED: 84, + 0x7EE: 84, + 0x7EF: 84, + 0x7F0: 84, + 0x7F1: 84, + 0x7F2: 84, + 0x7F3: 84, + 0x7FA: 67, + 0x7FD: 84, + 0x816: 84, + 0x817: 84, + 0x818: 84, + 0x819: 84, + 0x81B: 84, + 0x81C: 84, + 0x81D: 84, + 0x81E: 84, + 0x81F: 84, + 0x820: 84, + 0x821: 84, + 0x822: 84, + 0x823: 84, + 0x825: 84, + 0x826: 84, + 0x827: 84, + 0x829: 84, + 0x82A: 84, + 0x82B: 84, + 0x82C: 84, + 0x82D: 84, + 0x840: 82, + 0x841: 68, + 0x842: 68, + 0x843: 68, + 0x844: 68, + 0x845: 68, + 0x846: 82, + 0x847: 82, + 0x848: 68, + 0x849: 82, + 0x84A: 68, + 0x84B: 68, + 0x84C: 68, + 0x84D: 68, + 0x84E: 68, + 0x84F: 68, + 0x850: 68, + 0x851: 68, + 0x852: 68, + 0x853: 68, + 0x854: 82, + 0x855: 68, + 0x856: 82, + 0x857: 82, + 0x858: 82, + 0x859: 84, + 0x85A: 84, + 0x85B: 84, + 0x860: 68, + 0x862: 68, + 0x863: 68, + 0x864: 68, + 0x865: 68, + 0x867: 82, + 0x868: 68, + 0x869: 82, + 0x86A: 82, + 0x870: 82, + 0x871: 82, + 0x872: 82, + 0x873: 82, + 0x874: 82, + 0x875: 82, + 0x876: 82, + 0x877: 82, + 0x878: 82, + 0x879: 82, + 0x87A: 82, + 0x87B: 82, + 0x87C: 82, + 0x87D: 82, + 0x87E: 82, + 0x87F: 82, + 0x880: 82, + 0x881: 82, + 0x882: 82, + 0x883: 67, + 0x884: 67, + 0x885: 67, + 0x886: 68, + 0x889: 68, + 0x88A: 68, + 0x88B: 68, + 0x88C: 68, + 0x88D: 68, + 0x88E: 82, + 0x897: 84, + 0x898: 84, + 0x899: 84, + 0x89A: 84, + 0x89B: 84, + 0x89C: 84, + 0x89D: 84, + 0x89E: 84, + 0x89F: 84, + 0x8A0: 68, + 0x8A1: 68, + 0x8A2: 68, + 0x8A3: 68, + 0x8A4: 68, + 0x8A5: 68, + 0x8A6: 68, + 0x8A7: 68, + 0x8A8: 68, + 0x8A9: 68, + 0x8AA: 82, + 0x8AB: 82, + 0x8AC: 82, + 0x8AE: 82, + 0x8AF: 68, + 0x8B0: 68, + 0x8B1: 82, + 0x8B2: 82, + 0x8B3: 68, + 0x8B4: 68, + 0x8B5: 68, + 0x8B6: 68, + 0x8B7: 68, + 0x8B8: 68, + 0x8B9: 82, + 0x8BA: 68, + 0x8BB: 68, + 0x8BC: 68, + 0x8BD: 68, + 0x8BE: 68, + 0x8BF: 68, + 0x8C0: 68, + 0x8C1: 68, + 0x8C2: 68, + 0x8C3: 68, + 0x8C4: 68, + 0x8C5: 68, + 0x8C6: 68, + 0x8C7: 68, + 0x8C8: 68, + 0x8CA: 84, + 0x8CB: 84, + 0x8CC: 84, + 0x8CD: 84, + 0x8CE: 84, + 0x8CF: 84, + 0x8D0: 84, + 0x8D1: 84, + 0x8D2: 84, + 0x8D3: 84, + 0x8D4: 84, + 0x8D5: 84, + 0x8D6: 84, + 0x8D7: 84, + 0x8D8: 84, + 0x8D9: 84, + 0x8DA: 84, + 0x8DB: 84, + 0x8DC: 84, + 0x8DD: 84, + 0x8DE: 84, + 0x8DF: 84, + 0x8E0: 84, + 0x8E1: 84, + 0x8E3: 84, + 0x8E4: 84, + 0x8E5: 84, + 0x8E6: 84, + 0x8E7: 84, + 0x8E8: 84, + 0x8E9: 84, + 0x8EA: 84, + 0x8EB: 84, + 0x8EC: 84, + 0x8ED: 84, + 0x8EE: 84, + 0x8EF: 84, + 0x8F0: 84, + 0x8F1: 84, + 0x8F2: 84, + 0x8F3: 84, + 0x8F4: 84, + 0x8F5: 84, + 0x8F6: 84, + 0x8F7: 84, + 0x8F8: 84, + 0x8F9: 84, + 0x8FA: 84, + 0x8FB: 84, + 0x8FC: 84, + 0x8FD: 84, + 0x8FE: 84, + 0x8FF: 84, + 0x900: 84, + 0x901: 84, + 0x902: 84, + 0x93A: 84, + 0x93C: 84, + 0x941: 84, + 0x942: 84, + 0x943: 84, + 0x944: 84, + 0x945: 84, + 0x946: 84, + 0x947: 84, + 0x948: 84, + 0x94D: 84, + 0x951: 84, + 0x952: 84, + 0x953: 84, + 0x954: 84, + 0x955: 84, + 0x956: 84, + 0x957: 84, + 0x962: 84, + 0x963: 84, + 0x981: 84, + 0x9BC: 84, + 0x9C1: 84, + 0x9C2: 84, + 0x9C3: 84, + 0x9C4: 84, + 0x9CD: 84, + 0x9E2: 84, + 0x9E3: 84, + 0x9FE: 84, + 0xA01: 84, + 0xA02: 84, + 0xA3C: 84, + 0xA41: 84, + 0xA42: 84, + 0xA47: 84, + 0xA48: 84, + 0xA4B: 84, + 0xA4C: 84, + 0xA4D: 84, + 0xA51: 84, + 0xA70: 84, + 0xA71: 84, + 0xA75: 84, + 0xA81: 84, + 0xA82: 84, + 0xABC: 84, + 0xAC1: 84, + 0xAC2: 84, + 0xAC3: 84, + 0xAC4: 84, + 0xAC5: 84, + 0xAC7: 84, + 0xAC8: 84, + 0xACD: 84, + 0xAE2: 84, + 0xAE3: 84, + 0xAFA: 84, + 0xAFB: 84, + 0xAFC: 84, + 0xAFD: 84, + 0xAFE: 84, + 0xAFF: 84, + 0xB01: 84, + 0xB3C: 84, + 0xB3F: 84, + 0xB41: 84, + 0xB42: 84, + 0xB43: 84, + 0xB44: 84, + 0xB4D: 84, + 0xB55: 84, + 0xB56: 84, + 0xB62: 84, + 0xB63: 84, + 0xB82: 84, + 0xBC0: 84, + 0xBCD: 84, + 0xC00: 84, + 0xC04: 84, + 0xC3C: 84, + 0xC3E: 84, + 0xC3F: 84, + 0xC40: 84, + 0xC46: 84, + 0xC47: 84, + 0xC48: 84, + 0xC4A: 84, + 0xC4B: 84, + 0xC4C: 84, + 0xC4D: 84, + 0xC55: 84, + 0xC56: 84, + 0xC62: 84, + 0xC63: 84, + 0xC81: 84, + 0xCBC: 84, + 0xCBF: 84, + 0xCC6: 84, + 0xCCC: 84, + 0xCCD: 84, + 0xCE2: 84, + 0xCE3: 84, + 0xD00: 84, + 0xD01: 84, + 0xD3B: 84, + 0xD3C: 84, + 0xD41: 84, + 0xD42: 84, + 0xD43: 84, + 0xD44: 84, + 0xD4D: 84, + 0xD62: 84, + 0xD63: 84, + 0xD81: 84, + 0xDCA: 84, + 0xDD2: 84, + 0xDD3: 84, + 0xDD4: 84, + 0xDD6: 84, + 0xE31: 84, + 0xE34: 84, + 0xE35: 84, + 0xE36: 84, + 0xE37: 84, + 0xE38: 84, + 0xE39: 84, + 0xE3A: 84, + 0xE47: 84, + 0xE48: 84, + 0xE49: 84, + 0xE4A: 84, + 0xE4B: 84, + 0xE4C: 84, + 0xE4D: 84, + 0xE4E: 84, + 0xEB1: 84, + 0xEB4: 84, + 0xEB5: 84, + 0xEB6: 84, + 0xEB7: 84, + 0xEB8: 84, + 0xEB9: 84, + 0xEBA: 84, + 0xEBB: 84, + 0xEBC: 84, + 0xEC8: 84, + 0xEC9: 84, + 0xECA: 84, + 0xECB: 84, + 0xECC: 84, + 0xECD: 84, + 0xECE: 84, + 0xF18: 84, + 0xF19: 84, + 0xF35: 84, + 0xF37: 84, + 0xF39: 84, + 0xF71: 84, + 0xF72: 84, + 0xF73: 84, + 0xF74: 84, + 0xF75: 84, + 0xF76: 84, + 0xF77: 84, + 0xF78: 84, + 0xF79: 84, + 0xF7A: 84, + 0xF7B: 84, + 0xF7C: 84, + 0xF7D: 84, + 0xF7E: 84, + 0xF80: 84, + 0xF81: 84, + 0xF82: 84, + 0xF83: 84, + 0xF84: 84, + 0xF86: 84, + 0xF87: 84, + 0xF8D: 84, + 0xF8E: 84, + 0xF8F: 84, + 0xF90: 84, + 0xF91: 84, + 0xF92: 84, + 0xF93: 84, + 0xF94: 84, + 0xF95: 84, + 0xF96: 84, + 0xF97: 84, + 0xF99: 84, + 0xF9A: 84, + 0xF9B: 84, + 0xF9C: 84, + 0xF9D: 84, + 0xF9E: 84, + 0xF9F: 84, + 0xFA0: 84, + 0xFA1: 84, + 0xFA2: 84, + 0xFA3: 84, + 0xFA4: 84, + 0xFA5: 84, + 0xFA6: 84, + 0xFA7: 84, + 0xFA8: 84, + 0xFA9: 84, + 0xFAA: 84, + 0xFAB: 84, + 0xFAC: 84, + 0xFAD: 84, + 0xFAE: 84, + 0xFAF: 84, + 0xFB0: 84, + 0xFB1: 84, + 0xFB2: 84, + 0xFB3: 84, + 0xFB4: 84, + 0xFB5: 84, + 0xFB6: 84, + 0xFB7: 84, + 0xFB8: 84, + 0xFB9: 84, + 0xFBA: 84, + 0xFBB: 84, + 0xFBC: 84, + 0xFC6: 84, + 0x102D: 84, + 0x102E: 84, + 0x102F: 84, + 0x1030: 84, + 0x1032: 84, + 0x1033: 84, + 0x1034: 84, + 0x1035: 84, + 0x1036: 84, + 0x1037: 84, + 0x1039: 84, + 0x103A: 84, + 0x103D: 84, + 0x103E: 84, + 0x1058: 84, + 0x1059: 84, + 0x105E: 84, + 0x105F: 84, + 0x1060: 84, + 0x1071: 84, + 0x1072: 84, + 0x1073: 84, + 0x1074: 84, + 0x1082: 84, + 0x1085: 84, + 0x1086: 84, + 0x108D: 84, + 0x109D: 84, + 0x135D: 84, + 0x135E: 84, + 0x135F: 84, + 0x1712: 84, + 0x1713: 84, + 0x1714: 84, + 0x1732: 84, + 0x1733: 84, + 0x1752: 84, + 0x1753: 84, + 0x1772: 84, + 0x1773: 84, + 0x17B4: 84, + 0x17B5: 84, + 0x17B7: 84, + 0x17B8: 84, + 0x17B9: 84, + 0x17BA: 84, + 0x17BB: 84, + 0x17BC: 84, + 0x17BD: 84, + 0x17C6: 84, + 0x17C9: 84, + 0x17CA: 84, + 0x17CB: 84, + 0x17CC: 84, + 0x17CD: 84, + 0x17CE: 84, + 0x17CF: 84, + 0x17D0: 84, + 0x17D1: 84, + 0x17D2: 84, + 0x17D3: 84, + 0x17DD: 84, + 0x1807: 68, + 0x180A: 67, + 0x180B: 84, + 0x180C: 84, + 0x180D: 84, + 0x180F: 84, + 0x1820: 68, + 0x1821: 68, + 0x1822: 68, + 0x1823: 68, + 0x1824: 68, + 0x1825: 68, + 0x1826: 68, + 0x1827: 68, + 0x1828: 68, + 0x1829: 68, + 0x182A: 68, + 0x182B: 68, + 0x182C: 68, + 0x182D: 68, + 0x182E: 68, + 0x182F: 68, + 0x1830: 68, + 0x1831: 68, + 0x1832: 68, + 0x1833: 68, + 0x1834: 68, + 0x1835: 68, + 0x1836: 68, + 0x1837: 68, + 0x1838: 68, + 0x1839: 68, + 0x183A: 68, + 0x183B: 68, + 0x183C: 68, + 0x183D: 68, + 0x183E: 68, + 0x183F: 68, + 0x1840: 68, + 0x1841: 68, + 0x1842: 68, + 0x1843: 68, + 0x1844: 68, + 0x1845: 68, + 0x1846: 68, + 0x1847: 68, + 0x1848: 68, + 0x1849: 68, + 0x184A: 68, + 0x184B: 68, + 0x184C: 68, + 0x184D: 68, + 0x184E: 68, + 0x184F: 68, + 0x1850: 68, + 0x1851: 68, + 0x1852: 68, + 0x1853: 68, + 0x1854: 68, + 0x1855: 68, + 0x1856: 68, + 0x1857: 68, + 0x1858: 68, + 0x1859: 68, + 0x185A: 68, + 0x185B: 68, + 0x185C: 68, + 0x185D: 68, + 0x185E: 68, + 0x185F: 68, + 0x1860: 68, + 0x1861: 68, + 0x1862: 68, + 0x1863: 68, + 0x1864: 68, + 0x1865: 68, + 0x1866: 68, + 0x1867: 68, + 0x1868: 68, + 0x1869: 68, + 0x186A: 68, + 0x186B: 68, + 0x186C: 68, + 0x186D: 68, + 0x186E: 68, + 0x186F: 68, + 0x1870: 68, + 0x1871: 68, + 0x1872: 68, + 0x1873: 68, + 0x1874: 68, + 0x1875: 68, + 0x1876: 68, + 0x1877: 68, + 0x1878: 68, + 0x1885: 84, + 0x1886: 84, + 0x1887: 68, + 0x1888: 68, + 0x1889: 68, + 0x188A: 68, + 0x188B: 68, + 0x188C: 68, + 0x188D: 68, + 0x188E: 68, + 0x188F: 68, + 0x1890: 68, + 0x1891: 68, + 0x1892: 68, + 0x1893: 68, + 0x1894: 68, + 0x1895: 68, + 0x1896: 68, + 0x1897: 68, + 0x1898: 68, + 0x1899: 68, + 0x189A: 68, + 0x189B: 68, + 0x189C: 68, + 0x189D: 68, + 0x189E: 68, + 0x189F: 68, + 0x18A0: 68, + 0x18A1: 68, + 0x18A2: 68, + 0x18A3: 68, + 0x18A4: 68, + 0x18A5: 68, + 0x18A6: 68, + 0x18A7: 68, + 0x18A8: 68, + 0x18A9: 84, + 0x18AA: 68, + 0x1920: 84, + 0x1921: 84, + 0x1922: 84, + 0x1927: 84, + 0x1928: 84, + 0x1932: 84, + 0x1939: 84, + 0x193A: 84, + 0x193B: 84, + 0x1A17: 84, + 0x1A18: 84, + 0x1A1B: 84, + 0x1A56: 84, + 0x1A58: 84, + 0x1A59: 84, + 0x1A5A: 84, + 0x1A5B: 84, + 0x1A5C: 84, + 0x1A5D: 84, + 0x1A5E: 84, + 0x1A60: 84, + 0x1A62: 84, + 0x1A65: 84, + 0x1A66: 84, + 0x1A67: 84, + 0x1A68: 84, + 0x1A69: 84, + 0x1A6A: 84, + 0x1A6B: 84, + 0x1A6C: 84, + 0x1A73: 84, + 0x1A74: 84, + 0x1A75: 84, + 0x1A76: 84, + 0x1A77: 84, + 0x1A78: 84, + 0x1A79: 84, + 0x1A7A: 84, + 0x1A7B: 84, + 0x1A7C: 84, + 0x1A7F: 84, + 0x1AB0: 84, + 0x1AB1: 84, + 0x1AB2: 84, + 0x1AB3: 84, + 0x1AB4: 84, + 0x1AB5: 84, + 0x1AB6: 84, + 0x1AB7: 84, + 0x1AB8: 84, + 0x1AB9: 84, + 0x1ABA: 84, + 0x1ABB: 84, + 0x1ABC: 84, + 0x1ABD: 84, + 0x1ABE: 84, + 0x1ABF: 84, + 0x1AC0: 84, + 0x1AC1: 84, + 0x1AC2: 84, + 0x1AC3: 84, + 0x1AC4: 84, + 0x1AC5: 84, + 0x1AC6: 84, + 0x1AC7: 84, + 0x1AC8: 84, + 0x1AC9: 84, + 0x1ACA: 84, + 0x1ACB: 84, + 0x1ACC: 84, + 0x1ACD: 84, + 0x1ACE: 84, + 0x1B00: 84, + 0x1B01: 84, + 0x1B02: 84, + 0x1B03: 84, + 0x1B34: 84, + 0x1B36: 84, + 0x1B37: 84, + 0x1B38: 84, + 0x1B39: 84, + 0x1B3A: 84, + 0x1B3C: 84, + 0x1B42: 84, + 0x1B6B: 84, + 0x1B6C: 84, + 0x1B6D: 84, + 0x1B6E: 84, + 0x1B6F: 84, + 0x1B70: 84, + 0x1B71: 84, + 0x1B72: 84, + 0x1B73: 84, + 0x1B80: 84, + 0x1B81: 84, + 0x1BA2: 84, + 0x1BA3: 84, + 0x1BA4: 84, + 0x1BA5: 84, + 0x1BA8: 84, + 0x1BA9: 84, + 0x1BAB: 84, + 0x1BAC: 84, + 0x1BAD: 84, + 0x1BE6: 84, + 0x1BE8: 84, + 0x1BE9: 84, + 0x1BED: 84, + 0x1BEF: 84, + 0x1BF0: 84, + 0x1BF1: 84, + 0x1C2C: 84, + 0x1C2D: 84, + 0x1C2E: 84, + 0x1C2F: 84, + 0x1C30: 84, + 0x1C31: 84, + 0x1C32: 84, + 0x1C33: 84, + 0x1C36: 84, + 0x1C37: 84, + 0x1CD0: 84, + 0x1CD1: 84, + 0x1CD2: 84, + 0x1CD4: 84, + 0x1CD5: 84, + 0x1CD6: 84, + 0x1CD7: 84, + 0x1CD8: 84, + 0x1CD9: 84, + 0x1CDA: 84, + 0x1CDB: 84, + 0x1CDC: 84, + 0x1CDD: 84, + 0x1CDE: 84, + 0x1CDF: 84, + 0x1CE0: 84, + 0x1CE2: 84, + 0x1CE3: 84, + 0x1CE4: 84, + 0x1CE5: 84, + 0x1CE6: 84, + 0x1CE7: 84, + 0x1CE8: 84, + 0x1CED: 84, + 0x1CF4: 84, + 0x1CF8: 84, + 0x1CF9: 84, + 0x1DC0: 84, + 0x1DC1: 84, + 0x1DC2: 84, + 0x1DC3: 84, + 0x1DC4: 84, + 0x1DC5: 84, + 0x1DC6: 84, + 0x1DC7: 84, + 0x1DC8: 84, + 0x1DC9: 84, + 0x1DCA: 84, + 0x1DCB: 84, + 0x1DCC: 84, + 0x1DCD: 84, + 0x1DCE: 84, + 0x1DCF: 84, + 0x1DD0: 84, + 0x1DD1: 84, + 0x1DD2: 84, + 0x1DD3: 84, + 0x1DD4: 84, + 0x1DD5: 84, + 0x1DD6: 84, + 0x1DD7: 84, + 0x1DD8: 84, + 0x1DD9: 84, + 0x1DDA: 84, + 0x1DDB: 84, + 0x1DDC: 84, + 0x1DDD: 84, + 0x1DDE: 84, + 0x1DDF: 84, + 0x1DE0: 84, + 0x1DE1: 84, + 0x1DE2: 84, + 0x1DE3: 84, + 0x1DE4: 84, + 0x1DE5: 84, + 0x1DE6: 84, + 0x1DE7: 84, + 0x1DE8: 84, + 0x1DE9: 84, + 0x1DEA: 84, + 0x1DEB: 84, + 0x1DEC: 84, + 0x1DED: 84, + 0x1DEE: 84, + 0x1DEF: 84, + 0x1DF0: 84, + 0x1DF1: 84, + 0x1DF2: 84, + 0x1DF3: 84, + 0x1DF4: 84, + 0x1DF5: 84, + 0x1DF6: 84, + 0x1DF7: 84, + 0x1DF8: 84, + 0x1DF9: 84, + 0x1DFA: 84, + 0x1DFB: 84, + 0x1DFC: 84, + 0x1DFD: 84, + 0x1DFE: 84, + 0x1DFF: 84, + 0x200B: 84, + 0x200D: 67, + 0x200E: 84, + 0x200F: 84, + 0x202A: 84, + 0x202B: 84, + 0x202C: 84, + 0x202D: 84, + 0x202E: 84, + 0x2060: 84, + 0x2061: 84, + 0x2062: 84, + 0x2063: 84, + 0x2064: 84, + 0x206A: 84, + 0x206B: 84, + 0x206C: 84, + 0x206D: 84, + 0x206E: 84, + 0x206F: 84, + 0x20D0: 84, + 0x20D1: 84, + 0x20D2: 84, + 0x20D3: 84, + 0x20D4: 84, + 0x20D5: 84, + 0x20D6: 84, + 0x20D7: 84, + 0x20D8: 84, + 0x20D9: 84, + 0x20DA: 84, + 0x20DB: 84, + 0x20DC: 84, + 0x20DD: 84, + 0x20DE: 84, + 0x20DF: 84, + 0x20E0: 84, + 0x20E1: 84, + 0x20E2: 84, + 0x20E3: 84, + 0x20E4: 84, + 0x20E5: 84, + 0x20E6: 84, + 0x20E7: 84, + 0x20E8: 84, + 0x20E9: 84, + 0x20EA: 84, + 0x20EB: 84, + 0x20EC: 84, + 0x20ED: 84, + 0x20EE: 84, + 0x20EF: 84, + 0x20F0: 84, + 0x2CEF: 84, + 0x2CF0: 84, + 0x2CF1: 84, + 0x2D7F: 84, + 0x2DE0: 84, + 0x2DE1: 84, + 0x2DE2: 84, + 0x2DE3: 84, + 0x2DE4: 84, + 0x2DE5: 84, + 0x2DE6: 84, + 0x2DE7: 84, + 0x2DE8: 84, + 0x2DE9: 84, + 0x2DEA: 84, + 0x2DEB: 84, + 0x2DEC: 84, + 0x2DED: 84, + 0x2DEE: 84, + 0x2DEF: 84, + 0x2DF0: 84, + 0x2DF1: 84, + 0x2DF2: 84, + 0x2DF3: 84, + 0x2DF4: 84, + 0x2DF5: 84, + 0x2DF6: 84, + 0x2DF7: 84, + 0x2DF8: 84, + 0x2DF9: 84, + 0x2DFA: 84, + 0x2DFB: 84, + 0x2DFC: 84, + 0x2DFD: 84, + 0x2DFE: 84, + 0x2DFF: 84, + 0x302A: 84, + 0x302B: 84, + 0x302C: 84, + 0x302D: 84, + 0x3099: 84, + 0x309A: 84, + 0xA66F: 84, + 0xA670: 84, + 0xA671: 84, + 0xA672: 84, + 0xA674: 84, + 0xA675: 84, + 0xA676: 84, + 0xA677: 84, + 0xA678: 84, + 0xA679: 84, + 0xA67A: 84, + 0xA67B: 84, + 0xA67C: 84, + 0xA67D: 84, + 0xA69E: 84, + 0xA69F: 84, + 0xA6F0: 84, + 0xA6F1: 84, + 0xA802: 84, + 0xA806: 84, + 0xA80B: 84, + 0xA825: 84, + 0xA826: 84, + 0xA82C: 84, + 0xA840: 68, + 0xA841: 68, + 0xA842: 68, + 0xA843: 68, + 0xA844: 68, + 0xA845: 68, + 0xA846: 68, + 0xA847: 68, + 0xA848: 68, + 0xA849: 68, + 0xA84A: 68, + 0xA84B: 68, + 0xA84C: 68, + 0xA84D: 68, + 0xA84E: 68, + 0xA84F: 68, + 0xA850: 68, + 0xA851: 68, + 0xA852: 68, + 0xA853: 68, + 0xA854: 68, + 0xA855: 68, + 0xA856: 68, + 0xA857: 68, + 0xA858: 68, + 0xA859: 68, + 0xA85A: 68, + 0xA85B: 68, + 0xA85C: 68, + 0xA85D: 68, + 0xA85E: 68, + 0xA85F: 68, + 0xA860: 68, + 0xA861: 68, + 0xA862: 68, + 0xA863: 68, + 0xA864: 68, + 0xA865: 68, + 0xA866: 68, + 0xA867: 68, + 0xA868: 68, + 0xA869: 68, + 0xA86A: 68, + 0xA86B: 68, + 0xA86C: 68, + 0xA86D: 68, + 0xA86E: 68, + 0xA86F: 68, + 0xA870: 68, + 0xA871: 68, + 0xA872: 76, + 0xA8C4: 84, + 0xA8C5: 84, + 0xA8E0: 84, + 0xA8E1: 84, + 0xA8E2: 84, + 0xA8E3: 84, + 0xA8E4: 84, + 0xA8E5: 84, + 0xA8E6: 84, + 0xA8E7: 84, + 0xA8E8: 84, + 0xA8E9: 84, + 0xA8EA: 84, + 0xA8EB: 84, + 0xA8EC: 84, + 0xA8ED: 84, + 0xA8EE: 84, + 0xA8EF: 84, + 0xA8F0: 84, + 0xA8F1: 84, + 0xA8FF: 84, + 0xA926: 84, + 0xA927: 84, + 0xA928: 84, + 0xA929: 84, + 0xA92A: 84, + 0xA92B: 84, + 0xA92C: 84, + 0xA92D: 84, + 0xA947: 84, + 0xA948: 84, + 0xA949: 84, + 0xA94A: 84, + 0xA94B: 84, + 0xA94C: 84, + 0xA94D: 84, + 0xA94E: 84, + 0xA94F: 84, + 0xA950: 84, + 0xA951: 84, + 0xA980: 84, + 0xA981: 84, + 0xA982: 84, + 0xA9B3: 84, + 0xA9B6: 84, + 0xA9B7: 84, + 0xA9B8: 84, + 0xA9B9: 84, + 0xA9BC: 84, + 0xA9BD: 84, + 0xA9E5: 84, + 0xAA29: 84, + 0xAA2A: 84, + 0xAA2B: 84, + 0xAA2C: 84, + 0xAA2D: 84, + 0xAA2E: 84, + 0xAA31: 84, + 0xAA32: 84, + 0xAA35: 84, + 0xAA36: 84, + 0xAA43: 84, + 0xAA4C: 84, + 0xAA7C: 84, + 0xAAB0: 84, + 0xAAB2: 84, + 0xAAB3: 84, + 0xAAB4: 84, + 0xAAB7: 84, + 0xAAB8: 84, + 0xAABE: 84, + 0xAABF: 84, + 0xAAC1: 84, + 0xAAEC: 84, + 0xAAED: 84, + 0xAAF6: 84, + 0xABE5: 84, + 0xABE8: 84, + 0xABED: 84, + 0xFB1E: 84, + 0xFE00: 84, + 0xFE01: 84, + 0xFE02: 84, + 0xFE03: 84, + 0xFE04: 84, + 0xFE05: 84, + 0xFE06: 84, + 0xFE07: 84, + 0xFE08: 84, + 0xFE09: 84, + 0xFE0A: 84, + 0xFE0B: 84, + 0xFE0C: 84, + 0xFE0D: 84, + 0xFE0E: 84, + 0xFE0F: 84, + 0xFE20: 84, + 0xFE21: 84, + 0xFE22: 84, + 0xFE23: 84, + 0xFE24: 84, + 0xFE25: 84, + 0xFE26: 84, + 0xFE27: 84, + 0xFE28: 84, + 0xFE29: 84, + 0xFE2A: 84, + 0xFE2B: 84, + 0xFE2C: 84, + 0xFE2D: 84, + 0xFE2E: 84, + 0xFE2F: 84, + 0xFEFF: 84, + 0xFFF9: 84, + 0xFFFA: 84, + 0xFFFB: 84, + 0x101FD: 84, + 0x102E0: 84, + 0x10376: 84, + 0x10377: 84, + 0x10378: 84, + 0x10379: 84, + 0x1037A: 84, + 0x10A01: 84, + 0x10A02: 84, + 0x10A03: 84, + 0x10A05: 84, + 0x10A06: 84, + 0x10A0C: 84, + 0x10A0D: 84, + 0x10A0E: 84, + 0x10A0F: 84, + 0x10A38: 84, + 0x10A39: 84, + 0x10A3A: 84, + 0x10A3F: 84, + 0x10AC0: 68, + 0x10AC1: 68, + 0x10AC2: 68, + 0x10AC3: 68, + 0x10AC4: 68, + 0x10AC5: 82, + 0x10AC7: 82, + 0x10AC9: 82, + 0x10ACA: 82, + 0x10ACD: 76, + 0x10ACE: 82, + 0x10ACF: 82, + 0x10AD0: 82, + 0x10AD1: 82, + 0x10AD2: 82, + 0x10AD3: 68, + 0x10AD4: 68, + 0x10AD5: 68, + 0x10AD6: 68, + 0x10AD7: 76, + 0x10AD8: 68, + 0x10AD9: 68, + 0x10ADA: 68, + 0x10ADB: 68, + 0x10ADC: 68, + 0x10ADD: 82, + 0x10ADE: 68, + 0x10ADF: 68, + 0x10AE0: 68, + 0x10AE1: 82, + 0x10AE4: 82, + 0x10AE5: 84, + 0x10AE6: 84, + 0x10AEB: 68, + 0x10AEC: 68, + 0x10AED: 68, + 0x10AEE: 68, + 0x10AEF: 82, + 0x10B80: 68, + 0x10B81: 82, + 0x10B82: 68, + 0x10B83: 82, + 0x10B84: 82, + 0x10B85: 82, + 0x10B86: 68, + 0x10B87: 68, + 0x10B88: 68, + 0x10B89: 82, + 0x10B8A: 68, + 0x10B8B: 68, + 0x10B8C: 82, + 0x10B8D: 68, + 0x10B8E: 82, + 0x10B8F: 82, + 0x10B90: 68, + 0x10B91: 82, + 0x10BA9: 82, + 0x10BAA: 82, + 0x10BAB: 82, + 0x10BAC: 82, + 0x10BAD: 68, + 0x10BAE: 68, + 0x10D00: 76, + 0x10D01: 68, + 0x10D02: 68, + 0x10D03: 68, + 0x10D04: 68, + 0x10D05: 68, + 0x10D06: 68, + 0x10D07: 68, + 0x10D08: 68, + 0x10D09: 68, + 0x10D0A: 68, + 0x10D0B: 68, + 0x10D0C: 68, + 0x10D0D: 68, + 0x10D0E: 68, + 0x10D0F: 68, + 0x10D10: 68, + 0x10D11: 68, + 0x10D12: 68, + 0x10D13: 68, + 0x10D14: 68, + 0x10D15: 68, + 0x10D16: 68, + 0x10D17: 68, + 0x10D18: 68, + 0x10D19: 68, + 0x10D1A: 68, + 0x10D1B: 68, + 0x10D1C: 68, + 0x10D1D: 68, + 0x10D1E: 68, + 0x10D1F: 68, + 0x10D20: 68, + 0x10D21: 68, + 0x10D22: 82, + 0x10D23: 68, + 0x10D24: 84, + 0x10D25: 84, + 0x10D26: 84, + 0x10D27: 84, + 0x10D69: 84, + 0x10D6A: 84, + 0x10D6B: 84, + 0x10D6C: 84, + 0x10D6D: 84, + 0x10EAB: 84, + 0x10EAC: 84, + 0x10EC2: 82, + 0x10EC3: 68, + 0x10EC4: 68, + 0x10EFC: 84, + 0x10EFD: 84, + 0x10EFE: 84, + 0x10EFF: 84, + 0x10F30: 68, + 0x10F31: 68, + 0x10F32: 68, + 0x10F33: 82, + 0x10F34: 68, + 0x10F35: 68, + 0x10F36: 68, + 0x10F37: 68, + 0x10F38: 68, + 0x10F39: 68, + 0x10F3A: 68, + 0x10F3B: 68, + 0x10F3C: 68, + 0x10F3D: 68, + 0x10F3E: 68, + 0x10F3F: 68, + 0x10F40: 68, + 0x10F41: 68, + 0x10F42: 68, + 0x10F43: 68, + 0x10F44: 68, + 0x10F46: 84, + 0x10F47: 84, + 0x10F48: 84, + 0x10F49: 84, + 0x10F4A: 84, + 0x10F4B: 84, + 0x10F4C: 84, + 0x10F4D: 84, + 0x10F4E: 84, + 0x10F4F: 84, + 0x10F50: 84, + 0x10F51: 68, + 0x10F52: 68, + 0x10F53: 68, + 0x10F54: 82, + 0x10F70: 68, + 0x10F71: 68, + 0x10F72: 68, + 0x10F73: 68, + 0x10F74: 82, + 0x10F75: 82, + 0x10F76: 68, + 0x10F77: 68, + 0x10F78: 68, + 0x10F79: 68, + 0x10F7A: 68, + 0x10F7B: 68, + 0x10F7C: 68, + 0x10F7D: 68, + 0x10F7E: 68, + 0x10F7F: 68, + 0x10F80: 68, + 0x10F81: 68, + 0x10F82: 84, + 0x10F83: 84, + 0x10F84: 84, + 0x10F85: 84, + 0x10FB0: 68, + 0x10FB2: 68, + 0x10FB3: 68, + 0x10FB4: 82, + 0x10FB5: 82, + 0x10FB6: 82, + 0x10FB8: 68, + 0x10FB9: 82, + 0x10FBA: 82, + 0x10FBB: 68, + 0x10FBC: 68, + 0x10FBD: 82, + 0x10FBE: 68, + 0x10FBF: 68, + 0x10FC1: 68, + 0x10FC2: 82, + 0x10FC3: 82, + 0x10FC4: 68, + 0x10FC9: 82, + 0x10FCA: 68, + 0x10FCB: 76, + 0x11001: 84, + 0x11038: 84, + 0x11039: 84, + 0x1103A: 84, + 0x1103B: 84, + 0x1103C: 84, + 0x1103D: 84, + 0x1103E: 84, + 0x1103F: 84, + 0x11040: 84, + 0x11041: 84, + 0x11042: 84, + 0x11043: 84, + 0x11044: 84, + 0x11045: 84, + 0x11046: 84, + 0x11070: 84, + 0x11073: 84, + 0x11074: 84, + 0x1107F: 84, + 0x11080: 84, + 0x11081: 84, + 0x110B3: 84, + 0x110B4: 84, + 0x110B5: 84, + 0x110B6: 84, + 0x110B9: 84, + 0x110BA: 84, + 0x110C2: 84, + 0x11100: 84, + 0x11101: 84, + 0x11102: 84, + 0x11127: 84, + 0x11128: 84, + 0x11129: 84, + 0x1112A: 84, + 0x1112B: 84, + 0x1112D: 84, + 0x1112E: 84, + 0x1112F: 84, + 0x11130: 84, + 0x11131: 84, + 0x11132: 84, + 0x11133: 84, + 0x11134: 84, + 0x11173: 84, + 0x11180: 84, + 0x11181: 84, + 0x111B6: 84, + 0x111B7: 84, + 0x111B8: 84, + 0x111B9: 84, + 0x111BA: 84, + 0x111BB: 84, + 0x111BC: 84, + 0x111BD: 84, + 0x111BE: 84, + 0x111C9: 84, + 0x111CA: 84, + 0x111CB: 84, + 0x111CC: 84, + 0x111CF: 84, + 0x1122F: 84, + 0x11230: 84, + 0x11231: 84, + 0x11234: 84, + 0x11236: 84, + 0x11237: 84, + 0x1123E: 84, + 0x11241: 84, + 0x112DF: 84, + 0x112E3: 84, + 0x112E4: 84, + 0x112E5: 84, + 0x112E6: 84, + 0x112E7: 84, + 0x112E8: 84, + 0x112E9: 84, + 0x112EA: 84, + 0x11300: 84, + 0x11301: 84, + 0x1133B: 84, + 0x1133C: 84, + 0x11340: 84, + 0x11366: 84, + 0x11367: 84, + 0x11368: 84, + 0x11369: 84, + 0x1136A: 84, + 0x1136B: 84, + 0x1136C: 84, + 0x11370: 84, + 0x11371: 84, + 0x11372: 84, + 0x11373: 84, + 0x11374: 84, + 0x113BB: 84, + 0x113BC: 84, + 0x113BD: 84, + 0x113BE: 84, + 0x113BF: 84, + 0x113C0: 84, + 0x113CE: 84, + 0x113D0: 84, + 0x113D2: 84, + 0x113E1: 84, + 0x113E2: 84, + 0x11438: 84, + 0x11439: 84, + 0x1143A: 84, + 0x1143B: 84, + 0x1143C: 84, + 0x1143D: 84, + 0x1143E: 84, + 0x1143F: 84, + 0x11442: 84, + 0x11443: 84, + 0x11444: 84, + 0x11446: 84, + 0x1145E: 84, + 0x114B3: 84, + 0x114B4: 84, + 0x114B5: 84, + 0x114B6: 84, + 0x114B7: 84, + 0x114B8: 84, + 0x114BA: 84, + 0x114BF: 84, + 0x114C0: 84, + 0x114C2: 84, + 0x114C3: 84, + 0x115B2: 84, + 0x115B3: 84, + 0x115B4: 84, + 0x115B5: 84, + 0x115BC: 84, + 0x115BD: 84, + 0x115BF: 84, + 0x115C0: 84, + 0x115DC: 84, + 0x115DD: 84, + 0x11633: 84, + 0x11634: 84, + 0x11635: 84, + 0x11636: 84, + 0x11637: 84, + 0x11638: 84, + 0x11639: 84, + 0x1163A: 84, + 0x1163D: 84, + 0x1163F: 84, + 0x11640: 84, + 0x116AB: 84, + 0x116AD: 84, + 0x116B0: 84, + 0x116B1: 84, + 0x116B2: 84, + 0x116B3: 84, + 0x116B4: 84, + 0x116B5: 84, + 0x116B7: 84, + 0x1171D: 84, + 0x1171F: 84, + 0x11722: 84, + 0x11723: 84, + 0x11724: 84, + 0x11725: 84, + 0x11727: 84, + 0x11728: 84, + 0x11729: 84, + 0x1172A: 84, + 0x1172B: 84, + 0x1182F: 84, + 0x11830: 84, + 0x11831: 84, + 0x11832: 84, + 0x11833: 84, + 0x11834: 84, + 0x11835: 84, + 0x11836: 84, + 0x11837: 84, + 0x11839: 84, + 0x1183A: 84, + 0x1193B: 84, + 0x1193C: 84, + 0x1193E: 84, + 0x11943: 84, + 0x119D4: 84, + 0x119D5: 84, + 0x119D6: 84, + 0x119D7: 84, + 0x119DA: 84, + 0x119DB: 84, + 0x119E0: 84, + 0x11A01: 84, + 0x11A02: 84, + 0x11A03: 84, + 0x11A04: 84, + 0x11A05: 84, + 0x11A06: 84, + 0x11A07: 84, + 0x11A08: 84, + 0x11A09: 84, + 0x11A0A: 84, + 0x11A33: 84, + 0x11A34: 84, + 0x11A35: 84, + 0x11A36: 84, + 0x11A37: 84, + 0x11A38: 84, + 0x11A3B: 84, + 0x11A3C: 84, + 0x11A3D: 84, + 0x11A3E: 84, + 0x11A47: 84, + 0x11A51: 84, + 0x11A52: 84, + 0x11A53: 84, + 0x11A54: 84, + 0x11A55: 84, + 0x11A56: 84, + 0x11A59: 84, + 0x11A5A: 84, + 0x11A5B: 84, + 0x11A8A: 84, + 0x11A8B: 84, + 0x11A8C: 84, + 0x11A8D: 84, + 0x11A8E: 84, + 0x11A8F: 84, + 0x11A90: 84, + 0x11A91: 84, + 0x11A92: 84, + 0x11A93: 84, + 0x11A94: 84, + 0x11A95: 84, + 0x11A96: 84, + 0x11A98: 84, + 0x11A99: 84, + 0x11C30: 84, + 0x11C31: 84, + 0x11C32: 84, + 0x11C33: 84, + 0x11C34: 84, + 0x11C35: 84, + 0x11C36: 84, + 0x11C38: 84, + 0x11C39: 84, + 0x11C3A: 84, + 0x11C3B: 84, + 0x11C3C: 84, + 0x11C3D: 84, + 0x11C3F: 84, + 0x11C92: 84, + 0x11C93: 84, + 0x11C94: 84, + 0x11C95: 84, + 0x11C96: 84, + 0x11C97: 84, + 0x11C98: 84, + 0x11C99: 84, + 0x11C9A: 84, + 0x11C9B: 84, + 0x11C9C: 84, + 0x11C9D: 84, + 0x11C9E: 84, + 0x11C9F: 84, + 0x11CA0: 84, + 0x11CA1: 84, + 0x11CA2: 84, + 0x11CA3: 84, + 0x11CA4: 84, + 0x11CA5: 84, + 0x11CA6: 84, + 0x11CA7: 84, + 0x11CAA: 84, + 0x11CAB: 84, + 0x11CAC: 84, + 0x11CAD: 84, + 0x11CAE: 84, + 0x11CAF: 84, + 0x11CB0: 84, + 0x11CB2: 84, + 0x11CB3: 84, + 0x11CB5: 84, + 0x11CB6: 84, + 0x11D31: 84, + 0x11D32: 84, + 0x11D33: 84, + 0x11D34: 84, + 0x11D35: 84, + 0x11D36: 84, + 0x11D3A: 84, + 0x11D3C: 84, + 0x11D3D: 84, + 0x11D3F: 84, + 0x11D40: 84, + 0x11D41: 84, + 0x11D42: 84, + 0x11D43: 84, + 0x11D44: 84, + 0x11D45: 84, + 0x11D47: 84, + 0x11D90: 84, + 0x11D91: 84, + 0x11D95: 84, + 0x11D97: 84, + 0x11EF3: 84, + 0x11EF4: 84, + 0x11F00: 84, + 0x11F01: 84, + 0x11F36: 84, + 0x11F37: 84, + 0x11F38: 84, + 0x11F39: 84, + 0x11F3A: 84, + 0x11F40: 84, + 0x11F42: 84, + 0x11F5A: 84, + 0x13430: 84, + 0x13431: 84, + 0x13432: 84, + 0x13433: 84, + 0x13434: 84, + 0x13435: 84, + 0x13436: 84, + 0x13437: 84, + 0x13438: 84, + 0x13439: 84, + 0x1343A: 84, + 0x1343B: 84, + 0x1343C: 84, + 0x1343D: 84, + 0x1343E: 84, + 0x1343F: 84, + 0x13440: 84, + 0x13447: 84, + 0x13448: 84, + 0x13449: 84, + 0x1344A: 84, + 0x1344B: 84, + 0x1344C: 84, + 0x1344D: 84, + 0x1344E: 84, + 0x1344F: 84, + 0x13450: 84, + 0x13451: 84, + 0x13452: 84, + 0x13453: 84, + 0x13454: 84, + 0x13455: 84, + 0x1611E: 84, + 0x1611F: 84, + 0x16120: 84, + 0x16121: 84, + 0x16122: 84, + 0x16123: 84, + 0x16124: 84, + 0x16125: 84, + 0x16126: 84, + 0x16127: 84, + 0x16128: 84, + 0x16129: 84, + 0x1612D: 84, + 0x1612E: 84, + 0x1612F: 84, + 0x16AF0: 84, + 0x16AF1: 84, + 0x16AF2: 84, + 0x16AF3: 84, + 0x16AF4: 84, + 0x16B30: 84, + 0x16B31: 84, + 0x16B32: 84, + 0x16B33: 84, + 0x16B34: 84, + 0x16B35: 84, + 0x16B36: 84, + 0x16F4F: 84, + 0x16F8F: 84, + 0x16F90: 84, + 0x16F91: 84, + 0x16F92: 84, + 0x16FE4: 84, + 0x1BC9D: 84, + 0x1BC9E: 84, + 0x1BCA0: 84, + 0x1BCA1: 84, + 0x1BCA2: 84, + 0x1BCA3: 84, + 0x1CF00: 84, + 0x1CF01: 84, + 0x1CF02: 84, + 0x1CF03: 84, + 0x1CF04: 84, + 0x1CF05: 84, + 0x1CF06: 84, + 0x1CF07: 84, + 0x1CF08: 84, + 0x1CF09: 84, + 0x1CF0A: 84, + 0x1CF0B: 84, + 0x1CF0C: 84, + 0x1CF0D: 84, + 0x1CF0E: 84, + 0x1CF0F: 84, + 0x1CF10: 84, + 0x1CF11: 84, + 0x1CF12: 84, + 0x1CF13: 84, + 0x1CF14: 84, + 0x1CF15: 84, + 0x1CF16: 84, + 0x1CF17: 84, + 0x1CF18: 84, + 0x1CF19: 84, + 0x1CF1A: 84, + 0x1CF1B: 84, + 0x1CF1C: 84, + 0x1CF1D: 84, + 0x1CF1E: 84, + 0x1CF1F: 84, + 0x1CF20: 84, + 0x1CF21: 84, + 0x1CF22: 84, + 0x1CF23: 84, + 0x1CF24: 84, + 0x1CF25: 84, + 0x1CF26: 84, + 0x1CF27: 84, + 0x1CF28: 84, + 0x1CF29: 84, + 0x1CF2A: 84, + 0x1CF2B: 84, + 0x1CF2C: 84, + 0x1CF2D: 84, + 0x1CF30: 84, + 0x1CF31: 84, + 0x1CF32: 84, + 0x1CF33: 84, + 0x1CF34: 84, + 0x1CF35: 84, + 0x1CF36: 84, + 0x1CF37: 84, + 0x1CF38: 84, + 0x1CF39: 84, + 0x1CF3A: 84, + 0x1CF3B: 84, + 0x1CF3C: 84, + 0x1CF3D: 84, + 0x1CF3E: 84, + 0x1CF3F: 84, + 0x1CF40: 84, + 0x1CF41: 84, + 0x1CF42: 84, + 0x1CF43: 84, + 0x1CF44: 84, + 0x1CF45: 84, + 0x1CF46: 84, + 0x1D167: 84, + 0x1D168: 84, + 0x1D169: 84, + 0x1D173: 84, + 0x1D174: 84, + 0x1D175: 84, + 0x1D176: 84, + 0x1D177: 84, + 0x1D178: 84, + 0x1D179: 84, + 0x1D17A: 84, + 0x1D17B: 84, + 0x1D17C: 84, + 0x1D17D: 84, + 0x1D17E: 84, + 0x1D17F: 84, + 0x1D180: 84, + 0x1D181: 84, + 0x1D182: 84, + 0x1D185: 84, + 0x1D186: 84, + 0x1D187: 84, + 0x1D188: 84, + 0x1D189: 84, + 0x1D18A: 84, + 0x1D18B: 84, + 0x1D1AA: 84, + 0x1D1AB: 84, + 0x1D1AC: 84, + 0x1D1AD: 84, + 0x1D242: 84, + 0x1D243: 84, + 0x1D244: 84, + 0x1DA00: 84, + 0x1DA01: 84, + 0x1DA02: 84, + 0x1DA03: 84, + 0x1DA04: 84, + 0x1DA05: 84, + 0x1DA06: 84, + 0x1DA07: 84, + 0x1DA08: 84, + 0x1DA09: 84, + 0x1DA0A: 84, + 0x1DA0B: 84, + 0x1DA0C: 84, + 0x1DA0D: 84, + 0x1DA0E: 84, + 0x1DA0F: 84, + 0x1DA10: 84, + 0x1DA11: 84, + 0x1DA12: 84, + 0x1DA13: 84, + 0x1DA14: 84, + 0x1DA15: 84, + 0x1DA16: 84, + 0x1DA17: 84, + 0x1DA18: 84, + 0x1DA19: 84, + 0x1DA1A: 84, + 0x1DA1B: 84, + 0x1DA1C: 84, + 0x1DA1D: 84, + 0x1DA1E: 84, + 0x1DA1F: 84, + 0x1DA20: 84, + 0x1DA21: 84, + 0x1DA22: 84, + 0x1DA23: 84, + 0x1DA24: 84, + 0x1DA25: 84, + 0x1DA26: 84, + 0x1DA27: 84, + 0x1DA28: 84, + 0x1DA29: 84, + 0x1DA2A: 84, + 0x1DA2B: 84, + 0x1DA2C: 84, + 0x1DA2D: 84, + 0x1DA2E: 84, + 0x1DA2F: 84, + 0x1DA30: 84, + 0x1DA31: 84, + 0x1DA32: 84, + 0x1DA33: 84, + 0x1DA34: 84, + 0x1DA35: 84, + 0x1DA36: 84, + 0x1DA3B: 84, + 0x1DA3C: 84, + 0x1DA3D: 84, + 0x1DA3E: 84, + 0x1DA3F: 84, + 0x1DA40: 84, + 0x1DA41: 84, + 0x1DA42: 84, + 0x1DA43: 84, + 0x1DA44: 84, + 0x1DA45: 84, + 0x1DA46: 84, + 0x1DA47: 84, + 0x1DA48: 84, + 0x1DA49: 84, + 0x1DA4A: 84, + 0x1DA4B: 84, + 0x1DA4C: 84, + 0x1DA4D: 84, + 0x1DA4E: 84, + 0x1DA4F: 84, + 0x1DA50: 84, + 0x1DA51: 84, + 0x1DA52: 84, + 0x1DA53: 84, + 0x1DA54: 84, + 0x1DA55: 84, + 0x1DA56: 84, + 0x1DA57: 84, + 0x1DA58: 84, + 0x1DA59: 84, + 0x1DA5A: 84, + 0x1DA5B: 84, + 0x1DA5C: 84, + 0x1DA5D: 84, + 0x1DA5E: 84, + 0x1DA5F: 84, + 0x1DA60: 84, + 0x1DA61: 84, + 0x1DA62: 84, + 0x1DA63: 84, + 0x1DA64: 84, + 0x1DA65: 84, + 0x1DA66: 84, + 0x1DA67: 84, + 0x1DA68: 84, + 0x1DA69: 84, + 0x1DA6A: 84, + 0x1DA6B: 84, + 0x1DA6C: 84, + 0x1DA75: 84, + 0x1DA84: 84, + 0x1DA9B: 84, + 0x1DA9C: 84, + 0x1DA9D: 84, + 0x1DA9E: 84, + 0x1DA9F: 84, + 0x1DAA1: 84, + 0x1DAA2: 84, + 0x1DAA3: 84, + 0x1DAA4: 84, + 0x1DAA5: 84, + 0x1DAA6: 84, + 0x1DAA7: 84, + 0x1DAA8: 84, + 0x1DAA9: 84, + 0x1DAAA: 84, + 0x1DAAB: 84, + 0x1DAAC: 84, + 0x1DAAD: 84, + 0x1DAAE: 84, + 0x1DAAF: 84, + 0x1E000: 84, + 0x1E001: 84, + 0x1E002: 84, + 0x1E003: 84, + 0x1E004: 84, + 0x1E005: 84, + 0x1E006: 84, + 0x1E008: 84, + 0x1E009: 84, + 0x1E00A: 84, + 0x1E00B: 84, + 0x1E00C: 84, + 0x1E00D: 84, + 0x1E00E: 84, + 0x1E00F: 84, + 0x1E010: 84, + 0x1E011: 84, + 0x1E012: 84, + 0x1E013: 84, + 0x1E014: 84, + 0x1E015: 84, + 0x1E016: 84, + 0x1E017: 84, + 0x1E018: 84, + 0x1E01B: 84, + 0x1E01C: 84, + 0x1E01D: 84, + 0x1E01E: 84, + 0x1E01F: 84, + 0x1E020: 84, + 0x1E021: 84, + 0x1E023: 84, + 0x1E024: 84, + 0x1E026: 84, + 0x1E027: 84, + 0x1E028: 84, + 0x1E029: 84, + 0x1E02A: 84, + 0x1E08F: 84, + 0x1E130: 84, + 0x1E131: 84, + 0x1E132: 84, + 0x1E133: 84, + 0x1E134: 84, + 0x1E135: 84, + 0x1E136: 84, + 0x1E2AE: 84, + 0x1E2EC: 84, + 0x1E2ED: 84, + 0x1E2EE: 84, + 0x1E2EF: 84, + 0x1E4EC: 84, + 0x1E4ED: 84, + 0x1E4EE: 84, + 0x1E4EF: 84, + 0x1E5EE: 84, + 0x1E5EF: 84, + 0x1E8D0: 84, + 0x1E8D1: 84, + 0x1E8D2: 84, + 0x1E8D3: 84, + 0x1E8D4: 84, + 0x1E8D5: 84, + 0x1E8D6: 84, + 0x1E900: 68, + 0x1E901: 68, + 0x1E902: 68, + 0x1E903: 68, + 0x1E904: 68, + 0x1E905: 68, + 0x1E906: 68, + 0x1E907: 68, + 0x1E908: 68, + 0x1E909: 68, + 0x1E90A: 68, + 0x1E90B: 68, + 0x1E90C: 68, + 0x1E90D: 68, + 0x1E90E: 68, + 0x1E90F: 68, + 0x1E910: 68, + 0x1E911: 68, + 0x1E912: 68, + 0x1E913: 68, + 0x1E914: 68, + 0x1E915: 68, + 0x1E916: 68, + 0x1E917: 68, + 0x1E918: 68, + 0x1E919: 68, + 0x1E91A: 68, + 0x1E91B: 68, + 0x1E91C: 68, + 0x1E91D: 68, + 0x1E91E: 68, + 0x1E91F: 68, + 0x1E920: 68, + 0x1E921: 68, + 0x1E922: 68, + 0x1E923: 68, + 0x1E924: 68, + 0x1E925: 68, + 0x1E926: 68, + 0x1E927: 68, + 0x1E928: 68, + 0x1E929: 68, + 0x1E92A: 68, + 0x1E92B: 68, + 0x1E92C: 68, + 0x1E92D: 68, + 0x1E92E: 68, + 0x1E92F: 68, + 0x1E930: 68, + 0x1E931: 68, + 0x1E932: 68, + 0x1E933: 68, + 0x1E934: 68, + 0x1E935: 68, + 0x1E936: 68, + 0x1E937: 68, + 0x1E938: 68, + 0x1E939: 68, + 0x1E93A: 68, + 0x1E93B: 68, + 0x1E93C: 68, + 0x1E93D: 68, + 0x1E93E: 68, + 0x1E93F: 68, + 0x1E940: 68, + 0x1E941: 68, + 0x1E942: 68, + 0x1E943: 68, + 0x1E944: 84, + 0x1E945: 84, + 0x1E946: 84, + 0x1E947: 84, + 0x1E948: 84, + 0x1E949: 84, + 0x1E94A: 84, + 0x1E94B: 84, + 0xE0001: 84, + 0xE0020: 84, + 0xE0021: 84, + 0xE0022: 84, + 0xE0023: 84, + 0xE0024: 84, + 0xE0025: 84, + 0xE0026: 84, + 0xE0027: 84, + 0xE0028: 84, + 0xE0029: 84, + 0xE002A: 84, + 0xE002B: 84, + 0xE002C: 84, + 0xE002D: 84, + 0xE002E: 84, + 0xE002F: 84, + 0xE0030: 84, + 0xE0031: 84, + 0xE0032: 84, + 0xE0033: 84, + 0xE0034: 84, + 0xE0035: 84, + 0xE0036: 84, + 0xE0037: 84, + 0xE0038: 84, + 0xE0039: 84, + 0xE003A: 84, + 0xE003B: 84, + 0xE003C: 84, + 0xE003D: 84, + 0xE003E: 84, + 0xE003F: 84, + 0xE0040: 84, + 0xE0041: 84, + 0xE0042: 84, + 0xE0043: 84, + 0xE0044: 84, + 0xE0045: 84, + 0xE0046: 84, + 0xE0047: 84, + 0xE0048: 84, + 0xE0049: 84, + 0xE004A: 84, + 0xE004B: 84, + 0xE004C: 84, + 0xE004D: 84, + 0xE004E: 84, + 0xE004F: 84, + 0xE0050: 84, + 0xE0051: 84, + 0xE0052: 84, + 0xE0053: 84, + 0xE0054: 84, + 0xE0055: 84, + 0xE0056: 84, + 0xE0057: 84, + 0xE0058: 84, + 0xE0059: 84, + 0xE005A: 84, + 0xE005B: 84, + 0xE005C: 84, + 0xE005D: 84, + 0xE005E: 84, + 0xE005F: 84, + 0xE0060: 84, + 0xE0061: 84, + 0xE0062: 84, + 0xE0063: 84, + 0xE0064: 84, + 0xE0065: 84, + 0xE0066: 84, + 0xE0067: 84, + 0xE0068: 84, + 0xE0069: 84, + 0xE006A: 84, + 0xE006B: 84, + 0xE006C: 84, + 0xE006D: 84, + 0xE006E: 84, + 0xE006F: 84, + 0xE0070: 84, + 0xE0071: 84, + 0xE0072: 84, + 0xE0073: 84, + 0xE0074: 84, + 0xE0075: 84, + 0xE0076: 84, + 0xE0077: 84, + 0xE0078: 84, + 0xE0079: 84, + 0xE007A: 84, + 0xE007B: 84, + 0xE007C: 84, + 0xE007D: 84, + 0xE007E: 84, + 0xE007F: 84, + 0xE0100: 84, + 0xE0101: 84, + 0xE0102: 84, + 0xE0103: 84, + 0xE0104: 84, + 0xE0105: 84, + 0xE0106: 84, + 0xE0107: 84, + 0xE0108: 84, + 0xE0109: 84, + 0xE010A: 84, + 0xE010B: 84, + 0xE010C: 84, + 0xE010D: 84, + 0xE010E: 84, + 0xE010F: 84, + 0xE0110: 84, + 0xE0111: 84, + 0xE0112: 84, + 0xE0113: 84, + 0xE0114: 84, + 0xE0115: 84, + 0xE0116: 84, + 0xE0117: 84, + 0xE0118: 84, + 0xE0119: 84, + 0xE011A: 84, + 0xE011B: 84, + 0xE011C: 84, + 0xE011D: 84, + 0xE011E: 84, + 0xE011F: 84, + 0xE0120: 84, + 0xE0121: 84, + 0xE0122: 84, + 0xE0123: 84, + 0xE0124: 84, + 0xE0125: 84, + 0xE0126: 84, + 0xE0127: 84, + 0xE0128: 84, + 0xE0129: 84, + 0xE012A: 84, + 0xE012B: 84, + 0xE012C: 84, + 0xE012D: 84, + 0xE012E: 84, + 0xE012F: 84, + 0xE0130: 84, + 0xE0131: 84, + 0xE0132: 84, + 0xE0133: 84, + 0xE0134: 84, + 0xE0135: 84, + 0xE0136: 84, + 0xE0137: 84, + 0xE0138: 84, + 0xE0139: 84, + 0xE013A: 84, + 0xE013B: 84, + 0xE013C: 84, + 0xE013D: 84, + 0xE013E: 84, + 0xE013F: 84, + 0xE0140: 84, + 0xE0141: 84, + 0xE0142: 84, + 0xE0143: 84, + 0xE0144: 84, + 0xE0145: 84, + 0xE0146: 84, + 0xE0147: 84, + 0xE0148: 84, + 0xE0149: 84, + 0xE014A: 84, + 0xE014B: 84, + 0xE014C: 84, + 0xE014D: 84, + 0xE014E: 84, + 0xE014F: 84, + 0xE0150: 84, + 0xE0151: 84, + 0xE0152: 84, + 0xE0153: 84, + 0xE0154: 84, + 0xE0155: 84, + 0xE0156: 84, + 0xE0157: 84, + 0xE0158: 84, + 0xE0159: 84, + 0xE015A: 84, + 0xE015B: 84, + 0xE015C: 84, + 0xE015D: 84, + 0xE015E: 84, + 0xE015F: 84, + 0xE0160: 84, + 0xE0161: 84, + 0xE0162: 84, + 0xE0163: 84, + 0xE0164: 84, + 0xE0165: 84, + 0xE0166: 84, + 0xE0167: 84, + 0xE0168: 84, + 0xE0169: 84, + 0xE016A: 84, + 0xE016B: 84, + 0xE016C: 84, + 0xE016D: 84, + 0xE016E: 84, + 0xE016F: 84, + 0xE0170: 84, + 0xE0171: 84, + 0xE0172: 84, + 0xE0173: 84, + 0xE0174: 84, + 0xE0175: 84, + 0xE0176: 84, + 0xE0177: 84, + 0xE0178: 84, + 0xE0179: 84, + 0xE017A: 84, + 0xE017B: 84, + 0xE017C: 84, + 0xE017D: 84, + 0xE017E: 84, + 0xE017F: 84, + 0xE0180: 84, + 0xE0181: 84, + 0xE0182: 84, + 0xE0183: 84, + 0xE0184: 84, + 0xE0185: 84, + 0xE0186: 84, + 0xE0187: 84, + 0xE0188: 84, + 0xE0189: 84, + 0xE018A: 84, + 0xE018B: 84, + 0xE018C: 84, + 0xE018D: 84, + 0xE018E: 84, + 0xE018F: 84, + 0xE0190: 84, + 0xE0191: 84, + 0xE0192: 84, + 0xE0193: 84, + 0xE0194: 84, + 0xE0195: 84, + 0xE0196: 84, + 0xE0197: 84, + 0xE0198: 84, + 0xE0199: 84, + 0xE019A: 84, + 0xE019B: 84, + 0xE019C: 84, + 0xE019D: 84, + 0xE019E: 84, + 0xE019F: 84, + 0xE01A0: 84, + 0xE01A1: 84, + 0xE01A2: 84, + 0xE01A3: 84, + 0xE01A4: 84, + 0xE01A5: 84, + 0xE01A6: 84, + 0xE01A7: 84, + 0xE01A8: 84, + 0xE01A9: 84, + 0xE01AA: 84, + 0xE01AB: 84, + 0xE01AC: 84, + 0xE01AD: 84, + 0xE01AE: 84, + 0xE01AF: 84, + 0xE01B0: 84, + 0xE01B1: 84, + 0xE01B2: 84, + 0xE01B3: 84, + 0xE01B4: 84, + 0xE01B5: 84, + 0xE01B6: 84, + 0xE01B7: 84, + 0xE01B8: 84, + 0xE01B9: 84, + 0xE01BA: 84, + 0xE01BB: 84, + 0xE01BC: 84, + 0xE01BD: 84, + 0xE01BE: 84, + 0xE01BF: 84, + 0xE01C0: 84, + 0xE01C1: 84, + 0xE01C2: 84, + 0xE01C3: 84, + 0xE01C4: 84, + 0xE01C5: 84, + 0xE01C6: 84, + 0xE01C7: 84, + 0xE01C8: 84, + 0xE01C9: 84, + 0xE01CA: 84, + 0xE01CB: 84, + 0xE01CC: 84, + 0xE01CD: 84, + 0xE01CE: 84, + 0xE01CF: 84, + 0xE01D0: 84, + 0xE01D1: 84, + 0xE01D2: 84, + 0xE01D3: 84, + 0xE01D4: 84, + 0xE01D5: 84, + 0xE01D6: 84, + 0xE01D7: 84, + 0xE01D8: 84, + 0xE01D9: 84, + 0xE01DA: 84, + 0xE01DB: 84, + 0xE01DC: 84, + 0xE01DD: 84, + 0xE01DE: 84, + 0xE01DF: 84, + 0xE01E0: 84, + 0xE01E1: 84, + 0xE01E2: 84, + 0xE01E3: 84, + 0xE01E4: 84, + 0xE01E5: 84, + 0xE01E6: 84, + 0xE01E7: 84, + 0xE01E8: 84, + 0xE01E9: 84, + 0xE01EA: 84, + 0xE01EB: 84, + 0xE01EC: 84, + 0xE01ED: 84, + 0xE01EE: 84, + 0xE01EF: 84, +} +codepoint_classes = { + "PVALID": ( + 0x2D0000002E, + 0x300000003A, + 0x610000007B, + 0xDF000000F7, + 0xF800000100, + 0x10100000102, + 0x10300000104, + 0x10500000106, + 0x10700000108, + 0x1090000010A, + 0x10B0000010C, + 0x10D0000010E, + 0x10F00000110, + 0x11100000112, + 0x11300000114, + 0x11500000116, + 0x11700000118, + 0x1190000011A, + 0x11B0000011C, + 0x11D0000011E, + 0x11F00000120, + 0x12100000122, + 0x12300000124, + 0x12500000126, + 0x12700000128, + 0x1290000012A, + 0x12B0000012C, + 0x12D0000012E, + 0x12F00000130, + 0x13100000132, + 0x13500000136, + 0x13700000139, + 0x13A0000013B, + 0x13C0000013D, + 0x13E0000013F, + 0x14200000143, + 0x14400000145, + 0x14600000147, + 0x14800000149, + 0x14B0000014C, + 0x14D0000014E, + 0x14F00000150, + 0x15100000152, + 0x15300000154, + 0x15500000156, + 0x15700000158, + 0x1590000015A, + 0x15B0000015C, + 0x15D0000015E, + 0x15F00000160, + 0x16100000162, + 0x16300000164, + 0x16500000166, + 0x16700000168, + 0x1690000016A, + 0x16B0000016C, + 0x16D0000016E, + 0x16F00000170, + 0x17100000172, + 0x17300000174, + 0x17500000176, + 0x17700000178, + 0x17A0000017B, + 0x17C0000017D, + 0x17E0000017F, + 0x18000000181, + 0x18300000184, + 0x18500000186, + 0x18800000189, + 0x18C0000018E, + 0x19200000193, + 0x19500000196, + 0x1990000019C, + 0x19E0000019F, + 0x1A1000001A2, + 0x1A3000001A4, + 0x1A5000001A6, + 0x1A8000001A9, + 0x1AA000001AC, + 0x1AD000001AE, + 0x1B0000001B1, + 0x1B4000001B5, + 0x1B6000001B7, + 0x1B9000001BC, + 0x1BD000001C4, + 0x1CE000001CF, + 0x1D0000001D1, + 0x1D2000001D3, + 0x1D4000001D5, + 0x1D6000001D7, + 0x1D8000001D9, + 0x1DA000001DB, + 0x1DC000001DE, + 0x1DF000001E0, + 0x1E1000001E2, + 0x1E3000001E4, + 0x1E5000001E6, + 0x1E7000001E8, + 0x1E9000001EA, + 0x1EB000001EC, + 0x1ED000001EE, + 0x1EF000001F1, + 0x1F5000001F6, + 0x1F9000001FA, + 0x1FB000001FC, + 0x1FD000001FE, + 0x1FF00000200, + 0x20100000202, + 0x20300000204, + 0x20500000206, + 0x20700000208, + 0x2090000020A, + 0x20B0000020C, + 0x20D0000020E, + 0x20F00000210, + 0x21100000212, + 0x21300000214, + 0x21500000216, + 0x21700000218, + 0x2190000021A, + 0x21B0000021C, + 0x21D0000021E, + 0x21F00000220, + 0x22100000222, + 0x22300000224, + 0x22500000226, + 0x22700000228, + 0x2290000022A, + 0x22B0000022C, + 0x22D0000022E, + 0x22F00000230, + 0x23100000232, + 0x2330000023A, + 0x23C0000023D, + 0x23F00000241, + 0x24200000243, + 0x24700000248, + 0x2490000024A, + 0x24B0000024C, + 0x24D0000024E, + 0x24F000002B0, + 0x2B9000002C2, + 0x2C6000002D2, + 0x2EC000002ED, + 0x2EE000002EF, + 0x30000000340, + 0x34200000343, + 0x3460000034F, + 0x35000000370, + 0x37100000372, + 0x37300000374, + 0x37700000378, + 0x37B0000037E, + 0x39000000391, + 0x3AC000003CF, + 0x3D7000003D8, + 0x3D9000003DA, + 0x3DB000003DC, + 0x3DD000003DE, + 0x3DF000003E0, + 0x3E1000003E2, + 0x3E3000003E4, + 0x3E5000003E6, + 0x3E7000003E8, + 0x3E9000003EA, + 0x3EB000003EC, + 0x3ED000003EE, + 0x3EF000003F0, + 0x3F3000003F4, + 0x3F8000003F9, + 0x3FB000003FD, + 0x43000000460, + 0x46100000462, + 0x46300000464, + 0x46500000466, + 0x46700000468, + 0x4690000046A, + 0x46B0000046C, + 0x46D0000046E, + 0x46F00000470, + 0x47100000472, + 0x47300000474, + 0x47500000476, + 0x47700000478, + 0x4790000047A, + 0x47B0000047C, + 0x47D0000047E, + 0x47F00000480, + 0x48100000482, + 0x48300000488, + 0x48B0000048C, + 0x48D0000048E, + 0x48F00000490, + 0x49100000492, + 0x49300000494, + 0x49500000496, + 0x49700000498, + 0x4990000049A, + 0x49B0000049C, + 0x49D0000049E, + 0x49F000004A0, + 0x4A1000004A2, + 0x4A3000004A4, + 0x4A5000004A6, + 0x4A7000004A8, + 0x4A9000004AA, + 0x4AB000004AC, + 0x4AD000004AE, + 0x4AF000004B0, + 0x4B1000004B2, + 0x4B3000004B4, + 0x4B5000004B6, + 0x4B7000004B8, + 0x4B9000004BA, + 0x4BB000004BC, + 0x4BD000004BE, + 0x4BF000004C0, + 0x4C2000004C3, + 0x4C4000004C5, + 0x4C6000004C7, + 0x4C8000004C9, + 0x4CA000004CB, + 0x4CC000004CD, + 0x4CE000004D0, + 0x4D1000004D2, + 0x4D3000004D4, + 0x4D5000004D6, + 0x4D7000004D8, + 0x4D9000004DA, + 0x4DB000004DC, + 0x4DD000004DE, + 0x4DF000004E0, + 0x4E1000004E2, + 0x4E3000004E4, + 0x4E5000004E6, + 0x4E7000004E8, + 0x4E9000004EA, + 0x4EB000004EC, + 0x4ED000004EE, + 0x4EF000004F0, + 0x4F1000004F2, + 0x4F3000004F4, + 0x4F5000004F6, + 0x4F7000004F8, + 0x4F9000004FA, + 0x4FB000004FC, + 0x4FD000004FE, + 0x4FF00000500, + 0x50100000502, + 0x50300000504, + 0x50500000506, + 0x50700000508, + 0x5090000050A, + 0x50B0000050C, + 0x50D0000050E, + 0x50F00000510, + 0x51100000512, + 0x51300000514, + 0x51500000516, + 0x51700000518, + 0x5190000051A, + 0x51B0000051C, + 0x51D0000051E, + 0x51F00000520, + 0x52100000522, + 0x52300000524, + 0x52500000526, + 0x52700000528, + 0x5290000052A, + 0x52B0000052C, + 0x52D0000052E, + 0x52F00000530, + 0x5590000055A, + 0x56000000587, + 0x58800000589, + 0x591000005BE, + 0x5BF000005C0, + 0x5C1000005C3, + 0x5C4000005C6, + 0x5C7000005C8, + 0x5D0000005EB, + 0x5EF000005F3, + 0x6100000061B, + 0x62000000640, + 0x64100000660, + 0x66E00000675, + 0x679000006D4, + 0x6D5000006DD, + 0x6DF000006E9, + 0x6EA000006F0, + 0x6FA00000700, + 0x7100000074B, + 0x74D000007B2, + 0x7C0000007F6, + 0x7FD000007FE, + 0x8000000082E, + 0x8400000085C, + 0x8600000086B, + 0x87000000888, + 0x8890000088F, + 0x897000008E2, + 0x8E300000958, + 0x96000000964, + 0x96600000970, + 0x97100000984, + 0x9850000098D, + 0x98F00000991, + 0x993000009A9, + 0x9AA000009B1, + 0x9B2000009B3, + 0x9B6000009BA, + 0x9BC000009C5, + 0x9C7000009C9, + 0x9CB000009CF, + 0x9D7000009D8, + 0x9E0000009E4, + 0x9E6000009F2, + 0x9FC000009FD, + 0x9FE000009FF, + 0xA0100000A04, + 0xA0500000A0B, + 0xA0F00000A11, + 0xA1300000A29, + 0xA2A00000A31, + 0xA3200000A33, + 0xA3500000A36, + 0xA3800000A3A, + 0xA3C00000A3D, + 0xA3E00000A43, + 0xA4700000A49, + 0xA4B00000A4E, + 0xA5100000A52, + 0xA5C00000A5D, + 0xA6600000A76, + 0xA8100000A84, + 0xA8500000A8E, + 0xA8F00000A92, + 0xA9300000AA9, + 0xAAA00000AB1, + 0xAB200000AB4, + 0xAB500000ABA, + 0xABC00000AC6, + 0xAC700000ACA, + 0xACB00000ACE, + 0xAD000000AD1, + 0xAE000000AE4, + 0xAE600000AF0, + 0xAF900000B00, + 0xB0100000B04, + 0xB0500000B0D, + 0xB0F00000B11, + 0xB1300000B29, + 0xB2A00000B31, + 0xB3200000B34, + 0xB3500000B3A, + 0xB3C00000B45, + 0xB4700000B49, + 0xB4B00000B4E, + 0xB5500000B58, + 0xB5F00000B64, + 0xB6600000B70, + 0xB7100000B72, + 0xB8200000B84, + 0xB8500000B8B, + 0xB8E00000B91, + 0xB9200000B96, + 0xB9900000B9B, + 0xB9C00000B9D, + 0xB9E00000BA0, + 0xBA300000BA5, + 0xBA800000BAB, + 0xBAE00000BBA, + 0xBBE00000BC3, + 0xBC600000BC9, + 0xBCA00000BCE, + 0xBD000000BD1, + 0xBD700000BD8, + 0xBE600000BF0, + 0xC0000000C0D, + 0xC0E00000C11, + 0xC1200000C29, + 0xC2A00000C3A, + 0xC3C00000C45, + 0xC4600000C49, + 0xC4A00000C4E, + 0xC5500000C57, + 0xC5800000C5B, + 0xC5D00000C5E, + 0xC6000000C64, + 0xC6600000C70, + 0xC8000000C84, + 0xC8500000C8D, + 0xC8E00000C91, + 0xC9200000CA9, + 0xCAA00000CB4, + 0xCB500000CBA, + 0xCBC00000CC5, + 0xCC600000CC9, + 0xCCA00000CCE, + 0xCD500000CD7, + 0xCDD00000CDF, + 0xCE000000CE4, + 0xCE600000CF0, + 0xCF100000CF4, + 0xD0000000D0D, + 0xD0E00000D11, + 0xD1200000D45, + 0xD4600000D49, + 0xD4A00000D4F, + 0xD5400000D58, + 0xD5F00000D64, + 0xD6600000D70, + 0xD7A00000D80, + 0xD8100000D84, + 0xD8500000D97, + 0xD9A00000DB2, + 0xDB300000DBC, + 0xDBD00000DBE, + 0xDC000000DC7, + 0xDCA00000DCB, + 0xDCF00000DD5, + 0xDD600000DD7, + 0xDD800000DE0, + 0xDE600000DF0, + 0xDF200000DF4, + 0xE0100000E33, + 0xE3400000E3B, + 0xE4000000E4F, + 0xE5000000E5A, + 0xE8100000E83, + 0xE8400000E85, + 0xE8600000E8B, + 0xE8C00000EA4, + 0xEA500000EA6, + 0xEA700000EB3, + 0xEB400000EBE, + 0xEC000000EC5, + 0xEC600000EC7, + 0xEC800000ECF, + 0xED000000EDA, + 0xEDE00000EE0, + 0xF0000000F01, + 0xF0B00000F0C, + 0xF1800000F1A, + 0xF2000000F2A, + 0xF3500000F36, + 0xF3700000F38, + 0xF3900000F3A, + 0xF3E00000F43, + 0xF4400000F48, + 0xF4900000F4D, + 0xF4E00000F52, + 0xF5300000F57, + 0xF5800000F5C, + 0xF5D00000F69, + 0xF6A00000F6D, + 0xF7100000F73, + 0xF7400000F75, + 0xF7A00000F81, + 0xF8200000F85, + 0xF8600000F93, + 0xF9400000F98, + 0xF9900000F9D, + 0xF9E00000FA2, + 0xFA300000FA7, + 0xFA800000FAC, + 0xFAD00000FB9, + 0xFBA00000FBD, + 0xFC600000FC7, + 0x10000000104A, + 0x10500000109E, + 0x10D0000010FB, + 0x10FD00001100, + 0x120000001249, + 0x124A0000124E, + 0x125000001257, + 0x125800001259, + 0x125A0000125E, + 0x126000001289, + 0x128A0000128E, + 0x1290000012B1, + 0x12B2000012B6, + 0x12B8000012BF, + 0x12C0000012C1, + 0x12C2000012C6, + 0x12C8000012D7, + 0x12D800001311, + 0x131200001316, + 0x13180000135B, + 0x135D00001360, + 0x138000001390, + 0x13A0000013F6, + 0x14010000166D, + 0x166F00001680, + 0x16810000169B, + 0x16A0000016EB, + 0x16F1000016F9, + 0x170000001716, + 0x171F00001735, + 0x174000001754, + 0x17600000176D, + 0x176E00001771, + 0x177200001774, + 0x1780000017B4, + 0x17B6000017D4, + 0x17D7000017D8, + 0x17DC000017DE, + 0x17E0000017EA, + 0x18100000181A, + 0x182000001879, + 0x1880000018AB, + 0x18B0000018F6, + 0x19000000191F, + 0x19200000192C, + 0x19300000193C, + 0x19460000196E, + 0x197000001975, + 0x1980000019AC, + 0x19B0000019CA, + 0x19D0000019DA, + 0x1A0000001A1C, + 0x1A2000001A5F, + 0x1A6000001A7D, + 0x1A7F00001A8A, + 0x1A9000001A9A, + 0x1AA700001AA8, + 0x1AB000001ABE, + 0x1ABF00001ACF, + 0x1B0000001B4D, + 0x1B5000001B5A, + 0x1B6B00001B74, + 0x1B8000001BF4, + 0x1C0000001C38, + 0x1C4000001C4A, + 0x1C4D00001C7E, + 0x1C8A00001C8B, + 0x1CD000001CD3, + 0x1CD400001CFB, + 0x1D0000001D2C, + 0x1D2F00001D30, + 0x1D3B00001D3C, + 0x1D4E00001D4F, + 0x1D6B00001D78, + 0x1D7900001D9B, + 0x1DC000001E00, + 0x1E0100001E02, + 0x1E0300001E04, + 0x1E0500001E06, + 0x1E0700001E08, + 0x1E0900001E0A, + 0x1E0B00001E0C, + 0x1E0D00001E0E, + 0x1E0F00001E10, + 0x1E1100001E12, + 0x1E1300001E14, + 0x1E1500001E16, + 0x1E1700001E18, + 0x1E1900001E1A, + 0x1E1B00001E1C, + 0x1E1D00001E1E, + 0x1E1F00001E20, + 0x1E2100001E22, + 0x1E2300001E24, + 0x1E2500001E26, + 0x1E2700001E28, + 0x1E2900001E2A, + 0x1E2B00001E2C, + 0x1E2D00001E2E, + 0x1E2F00001E30, + 0x1E3100001E32, + 0x1E3300001E34, + 0x1E3500001E36, + 0x1E3700001E38, + 0x1E3900001E3A, + 0x1E3B00001E3C, + 0x1E3D00001E3E, + 0x1E3F00001E40, + 0x1E4100001E42, + 0x1E4300001E44, + 0x1E4500001E46, + 0x1E4700001E48, + 0x1E4900001E4A, + 0x1E4B00001E4C, + 0x1E4D00001E4E, + 0x1E4F00001E50, + 0x1E5100001E52, + 0x1E5300001E54, + 0x1E5500001E56, + 0x1E5700001E58, + 0x1E5900001E5A, + 0x1E5B00001E5C, + 0x1E5D00001E5E, + 0x1E5F00001E60, + 0x1E6100001E62, + 0x1E6300001E64, + 0x1E6500001E66, + 0x1E6700001E68, + 0x1E6900001E6A, + 0x1E6B00001E6C, + 0x1E6D00001E6E, + 0x1E6F00001E70, + 0x1E7100001E72, + 0x1E7300001E74, + 0x1E7500001E76, + 0x1E7700001E78, + 0x1E7900001E7A, + 0x1E7B00001E7C, + 0x1E7D00001E7E, + 0x1E7F00001E80, + 0x1E8100001E82, + 0x1E8300001E84, + 0x1E8500001E86, + 0x1E8700001E88, + 0x1E8900001E8A, + 0x1E8B00001E8C, + 0x1E8D00001E8E, + 0x1E8F00001E90, + 0x1E9100001E92, + 0x1E9300001E94, + 0x1E9500001E9A, + 0x1E9C00001E9E, + 0x1E9F00001EA0, + 0x1EA100001EA2, + 0x1EA300001EA4, + 0x1EA500001EA6, + 0x1EA700001EA8, + 0x1EA900001EAA, + 0x1EAB00001EAC, + 0x1EAD00001EAE, + 0x1EAF00001EB0, + 0x1EB100001EB2, + 0x1EB300001EB4, + 0x1EB500001EB6, + 0x1EB700001EB8, + 0x1EB900001EBA, + 0x1EBB00001EBC, + 0x1EBD00001EBE, + 0x1EBF00001EC0, + 0x1EC100001EC2, + 0x1EC300001EC4, + 0x1EC500001EC6, + 0x1EC700001EC8, + 0x1EC900001ECA, + 0x1ECB00001ECC, + 0x1ECD00001ECE, + 0x1ECF00001ED0, + 0x1ED100001ED2, + 0x1ED300001ED4, + 0x1ED500001ED6, + 0x1ED700001ED8, + 0x1ED900001EDA, + 0x1EDB00001EDC, + 0x1EDD00001EDE, + 0x1EDF00001EE0, + 0x1EE100001EE2, + 0x1EE300001EE4, + 0x1EE500001EE6, + 0x1EE700001EE8, + 0x1EE900001EEA, + 0x1EEB00001EEC, + 0x1EED00001EEE, + 0x1EEF00001EF0, + 0x1EF100001EF2, + 0x1EF300001EF4, + 0x1EF500001EF6, + 0x1EF700001EF8, + 0x1EF900001EFA, + 0x1EFB00001EFC, + 0x1EFD00001EFE, + 0x1EFF00001F08, + 0x1F1000001F16, + 0x1F2000001F28, + 0x1F3000001F38, + 0x1F4000001F46, + 0x1F5000001F58, + 0x1F6000001F68, + 0x1F7000001F71, + 0x1F7200001F73, + 0x1F7400001F75, + 0x1F7600001F77, + 0x1F7800001F79, + 0x1F7A00001F7B, + 0x1F7C00001F7D, + 0x1FB000001FB2, + 0x1FB600001FB7, + 0x1FC600001FC7, + 0x1FD000001FD3, + 0x1FD600001FD8, + 0x1FE000001FE3, + 0x1FE400001FE8, + 0x1FF600001FF7, + 0x214E0000214F, + 0x218400002185, + 0x2C3000002C60, + 0x2C6100002C62, + 0x2C6500002C67, + 0x2C6800002C69, + 0x2C6A00002C6B, + 0x2C6C00002C6D, + 0x2C7100002C72, + 0x2C7300002C75, + 0x2C7600002C7C, + 0x2C8100002C82, + 0x2C8300002C84, + 0x2C8500002C86, + 0x2C8700002C88, + 0x2C8900002C8A, + 0x2C8B00002C8C, + 0x2C8D00002C8E, + 0x2C8F00002C90, + 0x2C9100002C92, + 0x2C9300002C94, + 0x2C9500002C96, + 0x2C9700002C98, + 0x2C9900002C9A, + 0x2C9B00002C9C, + 0x2C9D00002C9E, + 0x2C9F00002CA0, + 0x2CA100002CA2, + 0x2CA300002CA4, + 0x2CA500002CA6, + 0x2CA700002CA8, + 0x2CA900002CAA, + 0x2CAB00002CAC, + 0x2CAD00002CAE, + 0x2CAF00002CB0, + 0x2CB100002CB2, + 0x2CB300002CB4, + 0x2CB500002CB6, + 0x2CB700002CB8, + 0x2CB900002CBA, + 0x2CBB00002CBC, + 0x2CBD00002CBE, + 0x2CBF00002CC0, + 0x2CC100002CC2, + 0x2CC300002CC4, + 0x2CC500002CC6, + 0x2CC700002CC8, + 0x2CC900002CCA, + 0x2CCB00002CCC, + 0x2CCD00002CCE, + 0x2CCF00002CD0, + 0x2CD100002CD2, + 0x2CD300002CD4, + 0x2CD500002CD6, + 0x2CD700002CD8, + 0x2CD900002CDA, + 0x2CDB00002CDC, + 0x2CDD00002CDE, + 0x2CDF00002CE0, + 0x2CE100002CE2, + 0x2CE300002CE5, + 0x2CEC00002CED, + 0x2CEE00002CF2, + 0x2CF300002CF4, + 0x2D0000002D26, + 0x2D2700002D28, + 0x2D2D00002D2E, + 0x2D3000002D68, + 0x2D7F00002D97, + 0x2DA000002DA7, + 0x2DA800002DAF, + 0x2DB000002DB7, + 0x2DB800002DBF, + 0x2DC000002DC7, + 0x2DC800002DCF, + 0x2DD000002DD7, + 0x2DD800002DDF, + 0x2DE000002E00, + 0x2E2F00002E30, + 0x300500003008, + 0x302A0000302E, + 0x303C0000303D, + 0x304100003097, + 0x30990000309B, + 0x309D0000309F, + 0x30A1000030FB, + 0x30FC000030FF, + 0x310500003130, + 0x31A0000031C0, + 0x31F000003200, + 0x340000004DC0, + 0x4E000000A48D, + 0xA4D00000A4FE, + 0xA5000000A60D, + 0xA6100000A62C, + 0xA6410000A642, + 0xA6430000A644, + 0xA6450000A646, + 0xA6470000A648, + 0xA6490000A64A, + 0xA64B0000A64C, + 0xA64D0000A64E, + 0xA64F0000A650, + 0xA6510000A652, + 0xA6530000A654, + 0xA6550000A656, + 0xA6570000A658, + 0xA6590000A65A, + 0xA65B0000A65C, + 0xA65D0000A65E, + 0xA65F0000A660, + 0xA6610000A662, + 0xA6630000A664, + 0xA6650000A666, + 0xA6670000A668, + 0xA6690000A66A, + 0xA66B0000A66C, + 0xA66D0000A670, + 0xA6740000A67E, + 0xA67F0000A680, + 0xA6810000A682, + 0xA6830000A684, + 0xA6850000A686, + 0xA6870000A688, + 0xA6890000A68A, + 0xA68B0000A68C, + 0xA68D0000A68E, + 0xA68F0000A690, + 0xA6910000A692, + 0xA6930000A694, + 0xA6950000A696, + 0xA6970000A698, + 0xA6990000A69A, + 0xA69B0000A69C, + 0xA69E0000A6E6, + 0xA6F00000A6F2, + 0xA7170000A720, + 0xA7230000A724, + 0xA7250000A726, + 0xA7270000A728, + 0xA7290000A72A, + 0xA72B0000A72C, + 0xA72D0000A72E, + 0xA72F0000A732, + 0xA7330000A734, + 0xA7350000A736, + 0xA7370000A738, + 0xA7390000A73A, + 0xA73B0000A73C, + 0xA73D0000A73E, + 0xA73F0000A740, + 0xA7410000A742, + 0xA7430000A744, + 0xA7450000A746, + 0xA7470000A748, + 0xA7490000A74A, + 0xA74B0000A74C, + 0xA74D0000A74E, + 0xA74F0000A750, + 0xA7510000A752, + 0xA7530000A754, + 0xA7550000A756, + 0xA7570000A758, + 0xA7590000A75A, + 0xA75B0000A75C, + 0xA75D0000A75E, + 0xA75F0000A760, + 0xA7610000A762, + 0xA7630000A764, + 0xA7650000A766, + 0xA7670000A768, + 0xA7690000A76A, + 0xA76B0000A76C, + 0xA76D0000A76E, + 0xA76F0000A770, + 0xA7710000A779, + 0xA77A0000A77B, + 0xA77C0000A77D, + 0xA77F0000A780, + 0xA7810000A782, + 0xA7830000A784, + 0xA7850000A786, + 0xA7870000A789, + 0xA78C0000A78D, + 0xA78E0000A790, + 0xA7910000A792, + 0xA7930000A796, + 0xA7970000A798, + 0xA7990000A79A, + 0xA79B0000A79C, + 0xA79D0000A79E, + 0xA79F0000A7A0, + 0xA7A10000A7A2, + 0xA7A30000A7A4, + 0xA7A50000A7A6, + 0xA7A70000A7A8, + 0xA7A90000A7AA, + 0xA7AF0000A7B0, + 0xA7B50000A7B6, + 0xA7B70000A7B8, + 0xA7B90000A7BA, + 0xA7BB0000A7BC, + 0xA7BD0000A7BE, + 0xA7BF0000A7C0, + 0xA7C10000A7C2, + 0xA7C30000A7C4, + 0xA7C80000A7C9, + 0xA7CA0000A7CB, + 0xA7CD0000A7CE, + 0xA7D10000A7D2, + 0xA7D30000A7D4, + 0xA7D50000A7D6, + 0xA7D70000A7D8, + 0xA7D90000A7DA, + 0xA7DB0000A7DC, + 0xA7F60000A7F8, + 0xA7FA0000A828, + 0xA82C0000A82D, + 0xA8400000A874, + 0xA8800000A8C6, + 0xA8D00000A8DA, + 0xA8E00000A8F8, + 0xA8FB0000A8FC, + 0xA8FD0000A92E, + 0xA9300000A954, + 0xA9800000A9C1, + 0xA9CF0000A9DA, + 0xA9E00000A9FF, + 0xAA000000AA37, + 0xAA400000AA4E, + 0xAA500000AA5A, + 0xAA600000AA77, + 0xAA7A0000AAC3, + 0xAADB0000AADE, + 0xAAE00000AAF0, + 0xAAF20000AAF7, + 0xAB010000AB07, + 0xAB090000AB0F, + 0xAB110000AB17, + 0xAB200000AB27, + 0xAB280000AB2F, + 0xAB300000AB5B, + 0xAB600000AB69, + 0xABC00000ABEB, + 0xABEC0000ABEE, + 0xABF00000ABFA, + 0xAC000000D7A4, + 0xFA0E0000FA10, + 0xFA110000FA12, + 0xFA130000FA15, + 0xFA1F0000FA20, + 0xFA210000FA22, + 0xFA230000FA25, + 0xFA270000FA2A, + 0xFB1E0000FB1F, + 0xFE200000FE30, + 0xFE730000FE74, + 0x100000001000C, + 0x1000D00010027, + 0x100280001003B, + 0x1003C0001003E, + 0x1003F0001004E, + 0x100500001005E, + 0x10080000100FB, + 0x101FD000101FE, + 0x102800001029D, + 0x102A0000102D1, + 0x102E0000102E1, + 0x1030000010320, + 0x1032D00010341, + 0x103420001034A, + 0x103500001037B, + 0x103800001039E, + 0x103A0000103C4, + 0x103C8000103D0, + 0x104280001049E, + 0x104A0000104AA, + 0x104D8000104FC, + 0x1050000010528, + 0x1053000010564, + 0x10597000105A2, + 0x105A3000105B2, + 0x105B3000105BA, + 0x105BB000105BD, + 0x105C0000105F4, + 0x1060000010737, + 0x1074000010756, + 0x1076000010768, + 0x1078000010781, + 0x1080000010806, + 0x1080800010809, + 0x1080A00010836, + 0x1083700010839, + 0x1083C0001083D, + 0x1083F00010856, + 0x1086000010877, + 0x108800001089F, + 0x108E0000108F3, + 0x108F4000108F6, + 0x1090000010916, + 0x109200001093A, + 0x10980000109B8, + 0x109BE000109C0, + 0x10A0000010A04, + 0x10A0500010A07, + 0x10A0C00010A14, + 0x10A1500010A18, + 0x10A1900010A36, + 0x10A3800010A3B, + 0x10A3F00010A40, + 0x10A6000010A7D, + 0x10A8000010A9D, + 0x10AC000010AC8, + 0x10AC900010AE7, + 0x10B0000010B36, + 0x10B4000010B56, + 0x10B6000010B73, + 0x10B8000010B92, + 0x10C0000010C49, + 0x10CC000010CF3, + 0x10D0000010D28, + 0x10D3000010D3A, + 0x10D4000010D50, + 0x10D6900010D6E, + 0x10D6F00010D86, + 0x10E8000010EAA, + 0x10EAB00010EAD, + 0x10EB000010EB2, + 0x10EC200010EC5, + 0x10EFC00010F1D, + 0x10F2700010F28, + 0x10F3000010F51, + 0x10F7000010F86, + 0x10FB000010FC5, + 0x10FE000010FF7, + 0x1100000011047, + 0x1106600011076, + 0x1107F000110BB, + 0x110C2000110C3, + 0x110D0000110E9, + 0x110F0000110FA, + 0x1110000011135, + 0x1113600011140, + 0x1114400011148, + 0x1115000011174, + 0x1117600011177, + 0x11180000111C5, + 0x111C9000111CD, + 0x111CE000111DB, + 0x111DC000111DD, + 0x1120000011212, + 0x1121300011238, + 0x1123E00011242, + 0x1128000011287, + 0x1128800011289, + 0x1128A0001128E, + 0x1128F0001129E, + 0x1129F000112A9, + 0x112B0000112EB, + 0x112F0000112FA, + 0x1130000011304, + 0x113050001130D, + 0x1130F00011311, + 0x1131300011329, + 0x1132A00011331, + 0x1133200011334, + 0x113350001133A, + 0x1133B00011345, + 0x1134700011349, + 0x1134B0001134E, + 0x1135000011351, + 0x1135700011358, + 0x1135D00011364, + 0x113660001136D, + 0x1137000011375, + 0x113800001138A, + 0x1138B0001138C, + 0x1138E0001138F, + 0x11390000113B6, + 0x113B7000113C1, + 0x113C2000113C3, + 0x113C5000113C6, + 0x113C7000113CB, + 0x113CC000113D4, + 0x113E1000113E3, + 0x114000001144B, + 0x114500001145A, + 0x1145E00011462, + 0x11480000114C6, + 0x114C7000114C8, + 0x114D0000114DA, + 0x11580000115B6, + 0x115B8000115C1, + 0x115D8000115DE, + 0x1160000011641, + 0x1164400011645, + 0x116500001165A, + 0x11680000116B9, + 0x116C0000116CA, + 0x116D0000116E4, + 0x117000001171B, + 0x1171D0001172C, + 0x117300001173A, + 0x1174000011747, + 0x118000001183B, + 0x118C0000118EA, + 0x118FF00011907, + 0x119090001190A, + 0x1190C00011914, + 0x1191500011917, + 0x1191800011936, + 0x1193700011939, + 0x1193B00011944, + 0x119500001195A, + 0x119A0000119A8, + 0x119AA000119D8, + 0x119DA000119E2, + 0x119E3000119E5, + 0x11A0000011A3F, + 0x11A4700011A48, + 0x11A5000011A9A, + 0x11A9D00011A9E, + 0x11AB000011AF9, + 0x11BC000011BE1, + 0x11BF000011BFA, + 0x11C0000011C09, + 0x11C0A00011C37, + 0x11C3800011C41, + 0x11C5000011C5A, + 0x11C7200011C90, + 0x11C9200011CA8, + 0x11CA900011CB7, + 0x11D0000011D07, + 0x11D0800011D0A, + 0x11D0B00011D37, + 0x11D3A00011D3B, + 0x11D3C00011D3E, + 0x11D3F00011D48, + 0x11D5000011D5A, + 0x11D6000011D66, + 0x11D6700011D69, + 0x11D6A00011D8F, + 0x11D9000011D92, + 0x11D9300011D99, + 0x11DA000011DAA, + 0x11EE000011EF7, + 0x11F0000011F11, + 0x11F1200011F3B, + 0x11F3E00011F43, + 0x11F5000011F5B, + 0x11FB000011FB1, + 0x120000001239A, + 0x1248000012544, + 0x12F9000012FF1, + 0x1300000013430, + 0x1344000013456, + 0x13460000143FB, + 0x1440000014647, + 0x161000001613A, + 0x1680000016A39, + 0x16A4000016A5F, + 0x16A6000016A6A, + 0x16A7000016ABF, + 0x16AC000016ACA, + 0x16AD000016AEE, + 0x16AF000016AF5, + 0x16B0000016B37, + 0x16B4000016B44, + 0x16B5000016B5A, + 0x16B6300016B78, + 0x16B7D00016B90, + 0x16D4000016D6D, + 0x16D7000016D7A, + 0x16E6000016E80, + 0x16F0000016F4B, + 0x16F4F00016F88, + 0x16F8F00016FA0, + 0x16FE000016FE2, + 0x16FE300016FE5, + 0x16FF000016FF2, + 0x17000000187F8, + 0x1880000018CD6, + 0x18CFF00018D09, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B123, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1B1550001B156, + 0x1B1640001B168, + 0x1B1700001B2FC, + 0x1BC000001BC6B, + 0x1BC700001BC7D, + 0x1BC800001BC89, + 0x1BC900001BC9A, + 0x1BC9D0001BC9F, + 0x1CCF00001CCFA, + 0x1CF000001CF2E, + 0x1CF300001CF47, + 0x1DA000001DA37, + 0x1DA3B0001DA6D, + 0x1DA750001DA76, + 0x1DA840001DA85, + 0x1DA9B0001DAA0, + 0x1DAA10001DAB0, + 0x1DF000001DF1F, + 0x1DF250001DF2B, + 0x1E0000001E007, + 0x1E0080001E019, + 0x1E01B0001E022, + 0x1E0230001E025, + 0x1E0260001E02B, + 0x1E08F0001E090, + 0x1E1000001E12D, + 0x1E1300001E13E, + 0x1E1400001E14A, + 0x1E14E0001E14F, + 0x1E2900001E2AF, + 0x1E2C00001E2FA, + 0x1E4D00001E4FA, + 0x1E5D00001E5FB, + 0x1E7E00001E7E7, + 0x1E7E80001E7EC, + 0x1E7ED0001E7EF, + 0x1E7F00001E7FF, + 0x1E8000001E8C5, + 0x1E8D00001E8D7, + 0x1E9220001E94C, + 0x1E9500001E95A, + 0x200000002A6E0, + 0x2A7000002B73A, + 0x2B7400002B81E, + 0x2B8200002CEA2, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x300000003134B, + 0x31350000323B0, + ), + "CONTEXTJ": (0x200C0000200E,), + "CONTEXTO": ( + 0xB7000000B8, + 0x37500000376, + 0x5F3000005F5, + 0x6600000066A, + 0x6F0000006FA, + 0x30FB000030FC, + ), +} diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/intranges.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/intranges.py new file mode 100644 index 0000000000000000000000000000000000000000..7bfaa8d80d7dc471d572db0f949460901126e8bd --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/intranges.py @@ -0,0 +1,57 @@ +""" +Given a list of integers, made up of (hopefully) a small number of long runs +of consecutive integers, compute a representation of the form +((start1, end1), (start2, end2) ...). Then answer the question "was x present +in the original list?" in time O(log(# runs)). +""" + +import bisect +from typing import List, Tuple + + +def intranges_from_list(list_: List[int]) -> Tuple[int, ...]: + """Represent a list of integers as a sequence of ranges: + ((start_0, end_0), (start_1, end_1), ...), such that the original + integers are exactly those x such that start_i <= x < end_i for some i. + + Ranges are encoded as single integers (start << 32 | end), not as tuples. + """ + + sorted_list = sorted(list_) + ranges = [] + last_write = -1 + for i in range(len(sorted_list)): + if i + 1 < len(sorted_list): + if sorted_list[i] == sorted_list[i + 1] - 1: + continue + current_range = sorted_list[last_write + 1 : i + 1] + ranges.append(_encode_range(current_range[0], current_range[-1] + 1)) + last_write = i + + return tuple(ranges) + + +def _encode_range(start: int, end: int) -> int: + return (start << 32) | end + + +def _decode_range(r: int) -> Tuple[int, int]: + return (r >> 32), (r & ((1 << 32) - 1)) + + +def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool: + """Determine if `int_` falls into one of the ranges in `ranges`.""" + tuple_ = _encode_range(int_, 0) + pos = bisect.bisect_left(ranges, tuple_) + # we could be immediately ahead of a tuple (start, end) + # with start < int_ <= end + if pos > 0: + left, right = _decode_range(ranges[pos - 1]) + if left <= int_ < right: + return True + # or we could be immediately behind a tuple (int_, end) + if pos < len(ranges): + left, _ = _decode_range(ranges[pos]) + if left == int_: + return True + return False diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/package_data.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/package_data.py new file mode 100644 index 0000000000000000000000000000000000000000..7272c8d92364886c51fefd22837ed5ceab145606 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/package_data.py @@ -0,0 +1 @@ +__version__ = "3.11" diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/py.typed b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/uts46data.py b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/uts46data.py new file mode 100644 index 0000000000000000000000000000000000000000..4610b71dad9196838d4e1e04e76d5e7c9baf8cd9 --- /dev/null +++ b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/idna/uts46data.py @@ -0,0 +1,8841 @@ +# This file is automatically generated by tools/idna-data +# vim: set fileencoding=utf-8 : + +from typing import List, Tuple, Union + +"""IDNA Mapping Table from UTS46.""" + + +__version__ = "16.0.0" + + +def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x0, "V"), + (0x1, "V"), + (0x2, "V"), + (0x3, "V"), + (0x4, "V"), + (0x5, "V"), + (0x6, "V"), + (0x7, "V"), + (0x8, "V"), + (0x9, "V"), + (0xA, "V"), + (0xB, "V"), + (0xC, "V"), + (0xD, "V"), + (0xE, "V"), + (0xF, "V"), + (0x10, "V"), + (0x11, "V"), + (0x12, "V"), + (0x13, "V"), + (0x14, "V"), + (0x15, "V"), + (0x16, "V"), + (0x17, "V"), + (0x18, "V"), + (0x19, "V"), + (0x1A, "V"), + (0x1B, "V"), + (0x1C, "V"), + (0x1D, "V"), + (0x1E, "V"), + (0x1F, "V"), + (0x20, "V"), + (0x21, "V"), + (0x22, "V"), + (0x23, "V"), + (0x24, "V"), + (0x25, "V"), + (0x26, "V"), + (0x27, "V"), + (0x28, "V"), + (0x29, "V"), + (0x2A, "V"), + (0x2B, "V"), + (0x2C, "V"), + (0x2D, "V"), + (0x2E, "V"), + (0x2F, "V"), + (0x30, "V"), + (0x31, "V"), + (0x32, "V"), + (0x33, "V"), + (0x34, "V"), + (0x35, "V"), + (0x36, "V"), + (0x37, "V"), + (0x38, "V"), + (0x39, "V"), + (0x3A, "V"), + (0x3B, "V"), + (0x3C, "V"), + (0x3D, "V"), + (0x3E, "V"), + (0x3F, "V"), + (0x40, "V"), + (0x41, "M", "a"), + (0x42, "M", "b"), + (0x43, "M", "c"), + (0x44, "M", "d"), + (0x45, "M", "e"), + (0x46, "M", "f"), + (0x47, "M", "g"), + (0x48, "M", "h"), + (0x49, "M", "i"), + (0x4A, "M", "j"), + (0x4B, "M", "k"), + (0x4C, "M", "l"), + (0x4D, "M", "m"), + (0x4E, "M", "n"), + (0x4F, "M", "o"), + (0x50, "M", "p"), + (0x51, "M", "q"), + (0x52, "M", "r"), + (0x53, "M", "s"), + (0x54, "M", "t"), + (0x55, "M", "u"), + (0x56, "M", "v"), + (0x57, "M", "w"), + (0x58, "M", "x"), + (0x59, "M", "y"), + (0x5A, "M", "z"), + (0x5B, "V"), + (0x5C, "V"), + (0x5D, "V"), + (0x5E, "V"), + (0x5F, "V"), + (0x60, "V"), + (0x61, "V"), + (0x62, "V"), + (0x63, "V"), + ] + + +def _seg_1() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x64, "V"), + (0x65, "V"), + (0x66, "V"), + (0x67, "V"), + (0x68, "V"), + (0x69, "V"), + (0x6A, "V"), + (0x6B, "V"), + (0x6C, "V"), + (0x6D, "V"), + (0x6E, "V"), + (0x6F, "V"), + (0x70, "V"), + (0x71, "V"), + (0x72, "V"), + (0x73, "V"), + (0x74, "V"), + (0x75, "V"), + (0x76, "V"), + (0x77, "V"), + (0x78, "V"), + (0x79, "V"), + (0x7A, "V"), + (0x7B, "V"), + (0x7C, "V"), + (0x7D, "V"), + (0x7E, "V"), + (0x7F, "V"), + (0x80, "X"), + (0x81, "X"), + (0x82, "X"), + (0x83, "X"), + (0x84, "X"), + (0x85, "X"), + (0x86, "X"), + (0x87, "X"), + (0x88, "X"), + (0x89, "X"), + (0x8A, "X"), + (0x8B, "X"), + (0x8C, "X"), + (0x8D, "X"), + (0x8E, "X"), + (0x8F, "X"), + (0x90, "X"), + (0x91, "X"), + (0x92, "X"), + (0x93, "X"), + (0x94, "X"), + (0x95, "X"), + (0x96, "X"), + (0x97, "X"), + (0x98, "X"), + (0x99, "X"), + (0x9A, "X"), + (0x9B, "X"), + (0x9C, "X"), + (0x9D, "X"), + (0x9E, "X"), + (0x9F, "X"), + (0xA0, "M", " "), + (0xA1, "V"), + (0xA2, "V"), + (0xA3, "V"), + (0xA4, "V"), + (0xA5, "V"), + (0xA6, "V"), + (0xA7, "V"), + (0xA8, "M", " ̈"), + (0xA9, "V"), + (0xAA, "M", "a"), + (0xAB, "V"), + (0xAC, "V"), + (0xAD, "I"), + (0xAE, "V"), + (0xAF, "M", " ̄"), + (0xB0, "V"), + (0xB1, "V"), + (0xB2, "M", "2"), + (0xB3, "M", "3"), + (0xB4, "M", " ́"), + (0xB5, "M", "μ"), + (0xB6, "V"), + (0xB7, "V"), + (0xB8, "M", " ̧"), + (0xB9, "M", "1"), + (0xBA, "M", "o"), + (0xBB, "V"), + (0xBC, "M", "1⁄4"), + (0xBD, "M", "1⁄2"), + (0xBE, "M", "3⁄4"), + (0xBF, "V"), + (0xC0, "M", "à"), + (0xC1, "M", "á"), + (0xC2, "M", "â"), + (0xC3, "M", "ã"), + (0xC4, "M", "ä"), + (0xC5, "M", "å"), + (0xC6, "M", "æ"), + (0xC7, "M", "ç"), + ] + + +def _seg_2() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC8, "M", "è"), + (0xC9, "M", "é"), + (0xCA, "M", "ê"), + (0xCB, "M", "ë"), + (0xCC, "M", "ì"), + (0xCD, "M", "í"), + (0xCE, "M", "î"), + (0xCF, "M", "ï"), + (0xD0, "M", "ð"), + (0xD1, "M", "ñ"), + (0xD2, "M", "ò"), + (0xD3, "M", "ó"), + (0xD4, "M", "ô"), + (0xD5, "M", "õ"), + (0xD6, "M", "ö"), + (0xD7, "V"), + (0xD8, "M", "ø"), + (0xD9, "M", "ù"), + (0xDA, "M", "ú"), + (0xDB, "M", "û"), + (0xDC, "M", "ü"), + (0xDD, "M", "ý"), + (0xDE, "M", "þ"), + (0xDF, "D", "ss"), + (0xE0, "V"), + (0xE1, "V"), + (0xE2, "V"), + (0xE3, "V"), + (0xE4, "V"), + (0xE5, "V"), + (0xE6, "V"), + (0xE7, "V"), + (0xE8, "V"), + (0xE9, "V"), + (0xEA, "V"), + (0xEB, "V"), + (0xEC, "V"), + (0xED, "V"), + (0xEE, "V"), + (0xEF, "V"), + (0xF0, "V"), + (0xF1, "V"), + (0xF2, "V"), + (0xF3, "V"), + (0xF4, "V"), + (0xF5, "V"), + (0xF6, "V"), + (0xF7, "V"), + (0xF8, "V"), + (0xF9, "V"), + (0xFA, "V"), + (0xFB, "V"), + (0xFC, "V"), + (0xFD, "V"), + (0xFE, "V"), + (0xFF, "V"), + (0x100, "M", "ā"), + (0x101, "V"), + (0x102, "M", "ă"), + (0x103, "V"), + (0x104, "M", "ą"), + (0x105, "V"), + (0x106, "M", "ć"), + (0x107, "V"), + (0x108, "M", "ĉ"), + (0x109, "V"), + (0x10A, "M", "ċ"), + (0x10B, "V"), + (0x10C, "M", "č"), + (0x10D, "V"), + (0x10E, "M", "ď"), + (0x10F, "V"), + (0x110, "M", "đ"), + (0x111, "V"), + (0x112, "M", "ē"), + (0x113, "V"), + (0x114, "M", "ĕ"), + (0x115, "V"), + (0x116, "M", "ė"), + (0x117, "V"), + (0x118, "M", "ę"), + (0x119, "V"), + (0x11A, "M", "ě"), + (0x11B, "V"), + (0x11C, "M", "ĝ"), + (0x11D, "V"), + (0x11E, "M", "ğ"), + (0x11F, "V"), + (0x120, "M", "ġ"), + (0x121, "V"), + (0x122, "M", "ģ"), + (0x123, "V"), + (0x124, "M", "ĥ"), + (0x125, "V"), + (0x126, "M", "ħ"), + (0x127, "V"), + (0x128, "M", "ĩ"), + (0x129, "V"), + (0x12A, "M", "ī"), + (0x12B, "V"), + ] + + +def _seg_3() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x12C, "M", "ĭ"), + (0x12D, "V"), + (0x12E, "M", "į"), + (0x12F, "V"), + (0x130, "M", "i̇"), + (0x131, "V"), + (0x132, "M", "ij"), + (0x134, "M", "ĵ"), + (0x135, "V"), + (0x136, "M", "ķ"), + (0x137, "V"), + (0x139, "M", "ĺ"), + (0x13A, "V"), + (0x13B, "M", "ļ"), + (0x13C, "V"), + (0x13D, "M", "ľ"), + (0x13E, "V"), + (0x13F, "M", "l·"), + (0x141, "M", "ł"), + (0x142, "V"), + (0x143, "M", "ń"), + (0x144, "V"), + (0x145, "M", "ņ"), + (0x146, "V"), + (0x147, "M", "ň"), + (0x148, "V"), + (0x149, "M", "ʼn"), + (0x14A, "M", "ŋ"), + (0x14B, "V"), + (0x14C, "M", "ō"), + (0x14D, "V"), + (0x14E, "M", "ŏ"), + (0x14F, "V"), + (0x150, "M", "ő"), + (0x151, "V"), + (0x152, "M", "œ"), + (0x153, "V"), + (0x154, "M", "ŕ"), + (0x155, "V"), + (0x156, "M", "ŗ"), + (0x157, "V"), + (0x158, "M", "ř"), + (0x159, "V"), + (0x15A, "M", "ś"), + (0x15B, "V"), + (0x15C, "M", "ŝ"), + (0x15D, "V"), + (0x15E, "M", "ş"), + (0x15F, "V"), + (0x160, "M", "š"), + (0x161, "V"), + (0x162, "M", "ţ"), + (0x163, "V"), + (0x164, "M", "ť"), + (0x165, "V"), + (0x166, "M", "ŧ"), + (0x167, "V"), + (0x168, "M", "ũ"), + (0x169, "V"), + (0x16A, "M", "ū"), + (0x16B, "V"), + (0x16C, "M", "ŭ"), + (0x16D, "V"), + (0x16E, "M", "ů"), + (0x16F, "V"), + (0x170, "M", "ű"), + (0x171, "V"), + (0x172, "M", "ų"), + (0x173, "V"), + (0x174, "M", "ŵ"), + (0x175, "V"), + (0x176, "M", "ŷ"), + (0x177, "V"), + (0x178, "M", "ÿ"), + (0x179, "M", "ź"), + (0x17A, "V"), + (0x17B, "M", "ż"), + (0x17C, "V"), + (0x17D, "M", "ž"), + (0x17E, "V"), + (0x17F, "M", "s"), + (0x180, "V"), + (0x181, "M", "ɓ"), + (0x182, "M", "ƃ"), + (0x183, "V"), + (0x184, "M", "ƅ"), + (0x185, "V"), + (0x186, "M", "ɔ"), + (0x187, "M", "ƈ"), + (0x188, "V"), + (0x189, "M", "ɖ"), + (0x18A, "M", "ɗ"), + (0x18B, "M", "ƌ"), + (0x18C, "V"), + (0x18E, "M", "ǝ"), + (0x18F, "M", "ə"), + (0x190, "M", "ɛ"), + (0x191, "M", "ƒ"), + (0x192, "V"), + (0x193, "M", "ɠ"), + ] + + +def _seg_4() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x194, "M", "ɣ"), + (0x195, "V"), + (0x196, "M", "ɩ"), + (0x197, "M", "ɨ"), + (0x198, "M", "ƙ"), + (0x199, "V"), + (0x19C, "M", "ɯ"), + (0x19D, "M", "ɲ"), + (0x19E, "V"), + (0x19F, "M", "ɵ"), + (0x1A0, "M", "ơ"), + (0x1A1, "V"), + (0x1A2, "M", "ƣ"), + (0x1A3, "V"), + (0x1A4, "M", "ƥ"), + (0x1A5, "V"), + (0x1A6, "M", "ʀ"), + (0x1A7, "M", "ƨ"), + (0x1A8, "V"), + (0x1A9, "M", "ʃ"), + (0x1AA, "V"), + (0x1AC, "M", "ƭ"), + (0x1AD, "V"), + (0x1AE, "M", "ʈ"), + (0x1AF, "M", "ư"), + (0x1B0, "V"), + (0x1B1, "M", "ʊ"), + (0x1B2, "M", "ʋ"), + (0x1B3, "M", "ƴ"), + (0x1B4, "V"), + (0x1B5, "M", "ƶ"), + (0x1B6, "V"), + (0x1B7, "M", "ʒ"), + (0x1B8, "M", "ƹ"), + (0x1B9, "V"), + (0x1BC, "M", "ƽ"), + (0x1BD, "V"), + (0x1C4, "M", "dž"), + (0x1C7, "M", "lj"), + (0x1CA, "M", "nj"), + (0x1CD, "M", "ǎ"), + (0x1CE, "V"), + (0x1CF, "M", "ǐ"), + (0x1D0, "V"), + (0x1D1, "M", "ǒ"), + (0x1D2, "V"), + (0x1D3, "M", "ǔ"), + (0x1D4, "V"), + (0x1D5, "M", "ǖ"), + (0x1D6, "V"), + (0x1D7, "M", "ǘ"), + (0x1D8, "V"), + (0x1D9, "M", "ǚ"), + (0x1DA, "V"), + (0x1DB, "M", "ǜ"), + (0x1DC, "V"), + (0x1DE, "M", "ǟ"), + (0x1DF, "V"), + (0x1E0, "M", "ǡ"), + (0x1E1, "V"), + (0x1E2, "M", "ǣ"), + (0x1E3, "V"), + (0x1E4, "M", "ǥ"), + (0x1E5, "V"), + (0x1E6, "M", "ǧ"), + (0x1E7, "V"), + (0x1E8, "M", "ǩ"), + (0x1E9, "V"), + (0x1EA, "M", "ǫ"), + (0x1EB, "V"), + (0x1EC, "M", "ǭ"), + (0x1ED, "V"), + (0x1EE, "M", "ǯ"), + (0x1EF, "V"), + (0x1F1, "M", "dz"), + (0x1F4, "M", "ǵ"), + (0x1F5, "V"), + (0x1F6, "M", "ƕ"), + (0x1F7, "M", "ƿ"), + (0x1F8, "M", "ǹ"), + (0x1F9, "V"), + (0x1FA, "M", "ǻ"), + (0x1FB, "V"), + (0x1FC, "M", "ǽ"), + (0x1FD, "V"), + (0x1FE, "M", "ǿ"), + (0x1FF, "V"), + (0x200, "M", "ȁ"), + (0x201, "V"), + (0x202, "M", "ȃ"), + (0x203, "V"), + (0x204, "M", "ȅ"), + (0x205, "V"), + (0x206, "M", "ȇ"), + (0x207, "V"), + (0x208, "M", "ȉ"), + (0x209, "V"), + (0x20A, "M", "ȋ"), + (0x20B, "V"), + (0x20C, "M", "ȍ"), + ] + + +def _seg_5() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x20D, "V"), + (0x20E, "M", "ȏ"), + (0x20F, "V"), + (0x210, "M", "ȑ"), + (0x211, "V"), + (0x212, "M", "ȓ"), + (0x213, "V"), + (0x214, "M", "ȕ"), + (0x215, "V"), + (0x216, "M", "ȗ"), + (0x217, "V"), + (0x218, "M", "ș"), + (0x219, "V"), + (0x21A, "M", "ț"), + (0x21B, "V"), + (0x21C, "M", "ȝ"), + (0x21D, "V"), + (0x21E, "M", "ȟ"), + (0x21F, "V"), + (0x220, "M", "ƞ"), + (0x221, "V"), + (0x222, "M", "ȣ"), + (0x223, "V"), + (0x224, "M", "ȥ"), + (0x225, "V"), + (0x226, "M", "ȧ"), + (0x227, "V"), + (0x228, "M", "ȩ"), + (0x229, "V"), + (0x22A, "M", "ȫ"), + (0x22B, "V"), + (0x22C, "M", "ȭ"), + (0x22D, "V"), + (0x22E, "M", "ȯ"), + (0x22F, "V"), + (0x230, "M", "ȱ"), + (0x231, "V"), + (0x232, "M", "ȳ"), + (0x233, "V"), + (0x23A, "M", "ⱥ"), + (0x23B, "M", "ȼ"), + (0x23C, "V"), + (0x23D, "M", "ƚ"), + (0x23E, "M", "ⱦ"), + (0x23F, "V"), + (0x241, "M", "ɂ"), + (0x242, "V"), + (0x243, "M", "ƀ"), + (0x244, "M", "ʉ"), + (0x245, "M", "ʌ"), + (0x246, "M", "ɇ"), + (0x247, "V"), + (0x248, "M", "ɉ"), + (0x249, "V"), + (0x24A, "M", "ɋ"), + (0x24B, "V"), + (0x24C, "M", "ɍ"), + (0x24D, "V"), + (0x24E, "M", "ɏ"), + (0x24F, "V"), + (0x2B0, "M", "h"), + (0x2B1, "M", "ɦ"), + (0x2B2, "M", "j"), + (0x2B3, "M", "r"), + (0x2B4, "M", "ɹ"), + (0x2B5, "M", "ɻ"), + (0x2B6, "M", "ʁ"), + (0x2B7, "M", "w"), + (0x2B8, "M", "y"), + (0x2B9, "V"), + (0x2D8, "M", " ̆"), + (0x2D9, "M", " ̇"), + (0x2DA, "M", " ̊"), + (0x2DB, "M", " ̨"), + (0x2DC, "M", " ̃"), + (0x2DD, "M", " ̋"), + (0x2DE, "V"), + (0x2E0, "M", "ɣ"), + (0x2E1, "M", "l"), + (0x2E2, "M", "s"), + (0x2E3, "M", "x"), + (0x2E4, "M", "ʕ"), + (0x2E5, "V"), + (0x340, "M", "̀"), + (0x341, "M", "́"), + (0x342, "V"), + (0x343, "M", "̓"), + (0x344, "M", "̈́"), + (0x345, "M", "ι"), + (0x346, "V"), + (0x34F, "I"), + (0x350, "V"), + (0x370, "M", "ͱ"), + (0x371, "V"), + (0x372, "M", "ͳ"), + (0x373, "V"), + (0x374, "M", "ʹ"), + (0x375, "V"), + (0x376, "M", "ͷ"), + (0x377, "V"), + ] + + +def _seg_6() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x378, "X"), + (0x37A, "M", " ι"), + (0x37B, "V"), + (0x37E, "M", ";"), + (0x37F, "M", "ϳ"), + (0x380, "X"), + (0x384, "M", " ́"), + (0x385, "M", " ̈́"), + (0x386, "M", "ά"), + (0x387, "M", "·"), + (0x388, "M", "έ"), + (0x389, "M", "ή"), + (0x38A, "M", "ί"), + (0x38B, "X"), + (0x38C, "M", "ό"), + (0x38D, "X"), + (0x38E, "M", "ύ"), + (0x38F, "M", "ώ"), + (0x390, "V"), + (0x391, "M", "α"), + (0x392, "M", "β"), + (0x393, "M", "γ"), + (0x394, "M", "δ"), + (0x395, "M", "ε"), + (0x396, "M", "ζ"), + (0x397, "M", "η"), + (0x398, "M", "θ"), + (0x399, "M", "ι"), + (0x39A, "M", "κ"), + (0x39B, "M", "λ"), + (0x39C, "M", "μ"), + (0x39D, "M", "ν"), + (0x39E, "M", "ξ"), + (0x39F, "M", "ο"), + (0x3A0, "M", "π"), + (0x3A1, "M", "ρ"), + (0x3A2, "X"), + (0x3A3, "M", "σ"), + (0x3A4, "M", "τ"), + (0x3A5, "M", "υ"), + (0x3A6, "M", "φ"), + (0x3A7, "M", "χ"), + (0x3A8, "M", "ψ"), + (0x3A9, "M", "ω"), + (0x3AA, "M", "ϊ"), + (0x3AB, "M", "ϋ"), + (0x3AC, "V"), + (0x3C2, "D", "σ"), + (0x3C3, "V"), + (0x3CF, "M", "ϗ"), + (0x3D0, "M", "β"), + (0x3D1, "M", "θ"), + (0x3D2, "M", "υ"), + (0x3D3, "M", "ύ"), + (0x3D4, "M", "ϋ"), + (0x3D5, "M", "φ"), + (0x3D6, "M", "π"), + (0x3D7, "V"), + (0x3D8, "M", "ϙ"), + (0x3D9, "V"), + (0x3DA, "M", "ϛ"), + (0x3DB, "V"), + (0x3DC, "M", "ϝ"), + (0x3DD, "V"), + (0x3DE, "M", "ϟ"), + (0x3DF, "V"), + (0x3E0, "M", "ϡ"), + (0x3E1, "V"), + (0x3E2, "M", "ϣ"), + (0x3E3, "V"), + (0x3E4, "M", "ϥ"), + (0x3E5, "V"), + (0x3E6, "M", "ϧ"), + (0x3E7, "V"), + (0x3E8, "M", "ϩ"), + (0x3E9, "V"), + (0x3EA, "M", "ϫ"), + (0x3EB, "V"), + (0x3EC, "M", "ϭ"), + (0x3ED, "V"), + (0x3EE, "M", "ϯ"), + (0x3EF, "V"), + (0x3F0, "M", "κ"), + (0x3F1, "M", "ρ"), + (0x3F2, "M", "σ"), + (0x3F3, "V"), + (0x3F4, "M", "θ"), + (0x3F5, "M", "ε"), + (0x3F6, "V"), + (0x3F7, "M", "ϸ"), + (0x3F8, "V"), + (0x3F9, "M", "σ"), + (0x3FA, "M", "ϻ"), + (0x3FB, "V"), + (0x3FD, "M", "ͻ"), + (0x3FE, "M", "ͼ"), + (0x3FF, "M", "ͽ"), + (0x400, "M", "ѐ"), + (0x401, "M", "ё"), + (0x402, "M", "ђ"), + ] + + +def _seg_7() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x403, "M", "ѓ"), + (0x404, "M", "є"), + (0x405, "M", "ѕ"), + (0x406, "M", "і"), + (0x407, "M", "ї"), + (0x408, "M", "ј"), + (0x409, "M", "љ"), + (0x40A, "M", "њ"), + (0x40B, "M", "ћ"), + (0x40C, "M", "ќ"), + (0x40D, "M", "ѝ"), + (0x40E, "M", "ў"), + (0x40F, "M", "џ"), + (0x410, "M", "а"), + (0x411, "M", "б"), + (0x412, "M", "в"), + (0x413, "M", "г"), + (0x414, "M", "д"), + (0x415, "M", "е"), + (0x416, "M", "ж"), + (0x417, "M", "з"), + (0x418, "M", "и"), + (0x419, "M", "й"), + (0x41A, "M", "к"), + (0x41B, "M", "л"), + (0x41C, "M", "м"), + (0x41D, "M", "н"), + (0x41E, "M", "о"), + (0x41F, "M", "п"), + (0x420, "M", "р"), + (0x421, "M", "с"), + (0x422, "M", "т"), + (0x423, "M", "у"), + (0x424, "M", "ф"), + (0x425, "M", "х"), + (0x426, "M", "ц"), + (0x427, "M", "ч"), + (0x428, "M", "ш"), + (0x429, "M", "щ"), + (0x42A, "M", "ъ"), + (0x42B, "M", "ы"), + (0x42C, "M", "ь"), + (0x42D, "M", "э"), + (0x42E, "M", "ю"), + (0x42F, "M", "я"), + (0x430, "V"), + (0x460, "M", "ѡ"), + (0x461, "V"), + (0x462, "M", "ѣ"), + (0x463, "V"), + (0x464, "M", "ѥ"), + (0x465, "V"), + (0x466, "M", "ѧ"), + (0x467, "V"), + (0x468, "M", "ѩ"), + (0x469, "V"), + (0x46A, "M", "ѫ"), + (0x46B, "V"), + (0x46C, "M", "ѭ"), + (0x46D, "V"), + (0x46E, "M", "ѯ"), + (0x46F, "V"), + (0x470, "M", "ѱ"), + (0x471, "V"), + (0x472, "M", "ѳ"), + (0x473, "V"), + (0x474, "M", "ѵ"), + (0x475, "V"), + (0x476, "M", "ѷ"), + (0x477, "V"), + (0x478, "M", "ѹ"), + (0x479, "V"), + (0x47A, "M", "ѻ"), + (0x47B, "V"), + (0x47C, "M", "ѽ"), + (0x47D, "V"), + (0x47E, "M", "ѿ"), + (0x47F, "V"), + (0x480, "M", "ҁ"), + (0x481, "V"), + (0x48A, "M", "ҋ"), + (0x48B, "V"), + (0x48C, "M", "ҍ"), + (0x48D, "V"), + (0x48E, "M", "ҏ"), + (0x48F, "V"), + (0x490, "M", "ґ"), + (0x491, "V"), + (0x492, "M", "ғ"), + (0x493, "V"), + (0x494, "M", "ҕ"), + (0x495, "V"), + (0x496, "M", "җ"), + (0x497, "V"), + (0x498, "M", "ҙ"), + (0x499, "V"), + (0x49A, "M", "қ"), + (0x49B, "V"), + (0x49C, "M", "ҝ"), + (0x49D, "V"), + ] + + +def _seg_8() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x49E, "M", "ҟ"), + (0x49F, "V"), + (0x4A0, "M", "ҡ"), + (0x4A1, "V"), + (0x4A2, "M", "ң"), + (0x4A3, "V"), + (0x4A4, "M", "ҥ"), + (0x4A5, "V"), + (0x4A6, "M", "ҧ"), + (0x4A7, "V"), + (0x4A8, "M", "ҩ"), + (0x4A9, "V"), + (0x4AA, "M", "ҫ"), + (0x4AB, "V"), + (0x4AC, "M", "ҭ"), + (0x4AD, "V"), + (0x4AE, "M", "ү"), + (0x4AF, "V"), + (0x4B0, "M", "ұ"), + (0x4B1, "V"), + (0x4B2, "M", "ҳ"), + (0x4B3, "V"), + (0x4B4, "M", "ҵ"), + (0x4B5, "V"), + (0x4B6, "M", "ҷ"), + (0x4B7, "V"), + (0x4B8, "M", "ҹ"), + (0x4B9, "V"), + (0x4BA, "M", "һ"), + (0x4BB, "V"), + (0x4BC, "M", "ҽ"), + (0x4BD, "V"), + (0x4BE, "M", "ҿ"), + (0x4BF, "V"), + (0x4C0, "M", "ӏ"), + (0x4C1, "M", "ӂ"), + (0x4C2, "V"), + (0x4C3, "M", "ӄ"), + (0x4C4, "V"), + (0x4C5, "M", "ӆ"), + (0x4C6, "V"), + (0x4C7, "M", "ӈ"), + (0x4C8, "V"), + (0x4C9, "M", "ӊ"), + (0x4CA, "V"), + (0x4CB, "M", "ӌ"), + (0x4CC, "V"), + (0x4CD, "M", "ӎ"), + (0x4CE, "V"), + (0x4D0, "M", "ӑ"), + (0x4D1, "V"), + (0x4D2, "M", "ӓ"), + (0x4D3, "V"), + (0x4D4, "M", "ӕ"), + (0x4D5, "V"), + (0x4D6, "M", "ӗ"), + (0x4D7, "V"), + (0x4D8, "M", "ә"), + (0x4D9, "V"), + (0x4DA, "M", "ӛ"), + (0x4DB, "V"), + (0x4DC, "M", "ӝ"), + (0x4DD, "V"), + (0x4DE, "M", "ӟ"), + (0x4DF, "V"), + (0x4E0, "M", "ӡ"), + (0x4E1, "V"), + (0x4E2, "M", "ӣ"), + (0x4E3, "V"), + (0x4E4, "M", "ӥ"), + (0x4E5, "V"), + (0x4E6, "M", "ӧ"), + (0x4E7, "V"), + (0x4E8, "M", "ө"), + (0x4E9, "V"), + (0x4EA, "M", "ӫ"), + (0x4EB, "V"), + (0x4EC, "M", "ӭ"), + (0x4ED, "V"), + (0x4EE, "M", "ӯ"), + (0x4EF, "V"), + (0x4F0, "M", "ӱ"), + (0x4F1, "V"), + (0x4F2, "M", "ӳ"), + (0x4F3, "V"), + (0x4F4, "M", "ӵ"), + (0x4F5, "V"), + (0x4F6, "M", "ӷ"), + (0x4F7, "V"), + (0x4F8, "M", "ӹ"), + (0x4F9, "V"), + (0x4FA, "M", "ӻ"), + (0x4FB, "V"), + (0x4FC, "M", "ӽ"), + (0x4FD, "V"), + (0x4FE, "M", "ӿ"), + (0x4FF, "V"), + (0x500, "M", "ԁ"), + (0x501, "V"), + (0x502, "M", "ԃ"), + ] + + +def _seg_9() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x503, "V"), + (0x504, "M", "ԅ"), + (0x505, "V"), + (0x506, "M", "ԇ"), + (0x507, "V"), + (0x508, "M", "ԉ"), + (0x509, "V"), + (0x50A, "M", "ԋ"), + (0x50B, "V"), + (0x50C, "M", "ԍ"), + (0x50D, "V"), + (0x50E, "M", "ԏ"), + (0x50F, "V"), + (0x510, "M", "ԑ"), + (0x511, "V"), + (0x512, "M", "ԓ"), + (0x513, "V"), + (0x514, "M", "ԕ"), + (0x515, "V"), + (0x516, "M", "ԗ"), + (0x517, "V"), + (0x518, "M", "ԙ"), + (0x519, "V"), + (0x51A, "M", "ԛ"), + (0x51B, "V"), + (0x51C, "M", "ԝ"), + (0x51D, "V"), + (0x51E, "M", "ԟ"), + (0x51F, "V"), + (0x520, "M", "ԡ"), + (0x521, "V"), + (0x522, "M", "ԣ"), + (0x523, "V"), + (0x524, "M", "ԥ"), + (0x525, "V"), + (0x526, "M", "ԧ"), + (0x527, "V"), + (0x528, "M", "ԩ"), + (0x529, "V"), + (0x52A, "M", "ԫ"), + (0x52B, "V"), + (0x52C, "M", "ԭ"), + (0x52D, "V"), + (0x52E, "M", "ԯ"), + (0x52F, "V"), + (0x530, "X"), + (0x531, "M", "ա"), + (0x532, "M", "բ"), + (0x533, "M", "գ"), + (0x534, "M", "դ"), + (0x535, "M", "ե"), + (0x536, "M", "զ"), + (0x537, "M", "է"), + (0x538, "M", "ը"), + (0x539, "M", "թ"), + (0x53A, "M", "ժ"), + (0x53B, "M", "ի"), + (0x53C, "M", "լ"), + (0x53D, "M", "խ"), + (0x53E, "M", "ծ"), + (0x53F, "M", "կ"), + (0x540, "M", "հ"), + (0x541, "M", "ձ"), + (0x542, "M", "ղ"), + (0x543, "M", "ճ"), + (0x544, "M", "մ"), + (0x545, "M", "յ"), + (0x546, "M", "ն"), + (0x547, "M", "շ"), + (0x548, "M", "ո"), + (0x549, "M", "չ"), + (0x54A, "M", "պ"), + (0x54B, "M", "ջ"), + (0x54C, "M", "ռ"), + (0x54D, "M", "ս"), + (0x54E, "M", "վ"), + (0x54F, "M", "տ"), + (0x550, "M", "ր"), + (0x551, "M", "ց"), + (0x552, "M", "ւ"), + (0x553, "M", "փ"), + (0x554, "M", "ք"), + (0x555, "M", "օ"), + (0x556, "M", "ֆ"), + (0x557, "X"), + (0x559, "V"), + (0x587, "M", "եւ"), + (0x588, "V"), + (0x58B, "X"), + (0x58D, "V"), + (0x590, "X"), + (0x591, "V"), + (0x5C8, "X"), + (0x5D0, "V"), + (0x5EB, "X"), + (0x5EF, "V"), + (0x5F5, "X"), + (0x606, "V"), + (0x61C, "X"), + (0x61D, "V"), + ] + + +def _seg_10() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x675, "M", "اٴ"), + (0x676, "M", "وٴ"), + (0x677, "M", "ۇٴ"), + (0x678, "M", "يٴ"), + (0x679, "V"), + (0x6DD, "X"), + (0x6DE, "V"), + (0x70E, "X"), + (0x710, "V"), + (0x74B, "X"), + (0x74D, "V"), + (0x7B2, "X"), + (0x7C0, "V"), + (0x7FB, "X"), + (0x7FD, "V"), + (0x82E, "X"), + (0x830, "V"), + (0x83F, "X"), + (0x840, "V"), + (0x85C, "X"), + (0x85E, "V"), + (0x85F, "X"), + (0x860, "V"), + (0x86B, "X"), + (0x870, "V"), + (0x88F, "X"), + (0x897, "V"), + (0x8E2, "X"), + (0x8E3, "V"), + (0x958, "M", "क़"), + (0x959, "M", "ख़"), + (0x95A, "M", "ग़"), + (0x95B, "M", "ज़"), + (0x95C, "M", "ड़"), + (0x95D, "M", "ढ़"), + (0x95E, "M", "फ़"), + (0x95F, "M", "य़"), + (0x960, "V"), + (0x984, "X"), + (0x985, "V"), + (0x98D, "X"), + (0x98F, "V"), + (0x991, "X"), + (0x993, "V"), + (0x9A9, "X"), + (0x9AA, "V"), + (0x9B1, "X"), + (0x9B2, "V"), + (0x9B3, "X"), + (0x9B6, "V"), + (0x9BA, "X"), + (0x9BC, "V"), + (0x9C5, "X"), + (0x9C7, "V"), + (0x9C9, "X"), + (0x9CB, "V"), + (0x9CF, "X"), + (0x9D7, "V"), + (0x9D8, "X"), + (0x9DC, "M", "ড়"), + (0x9DD, "M", "ঢ়"), + (0x9DE, "X"), + (0x9DF, "M", "য়"), + (0x9E0, "V"), + (0x9E4, "X"), + (0x9E6, "V"), + (0x9FF, "X"), + (0xA01, "V"), + (0xA04, "X"), + (0xA05, "V"), + (0xA0B, "X"), + (0xA0F, "V"), + (0xA11, "X"), + (0xA13, "V"), + (0xA29, "X"), + (0xA2A, "V"), + (0xA31, "X"), + (0xA32, "V"), + (0xA33, "M", "ਲ਼"), + (0xA34, "X"), + (0xA35, "V"), + (0xA36, "M", "ਸ਼"), + (0xA37, "X"), + (0xA38, "V"), + (0xA3A, "X"), + (0xA3C, "V"), + (0xA3D, "X"), + (0xA3E, "V"), + (0xA43, "X"), + (0xA47, "V"), + (0xA49, "X"), + (0xA4B, "V"), + (0xA4E, "X"), + (0xA51, "V"), + (0xA52, "X"), + (0xA59, "M", "ਖ਼"), + (0xA5A, "M", "ਗ਼"), + (0xA5B, "M", "ਜ਼"), + (0xA5C, "V"), + (0xA5D, "X"), + ] + + +def _seg_11() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA5E, "M", "ਫ਼"), + (0xA5F, "X"), + (0xA66, "V"), + (0xA77, "X"), + (0xA81, "V"), + (0xA84, "X"), + (0xA85, "V"), + (0xA8E, "X"), + (0xA8F, "V"), + (0xA92, "X"), + (0xA93, "V"), + (0xAA9, "X"), + (0xAAA, "V"), + (0xAB1, "X"), + (0xAB2, "V"), + (0xAB4, "X"), + (0xAB5, "V"), + (0xABA, "X"), + (0xABC, "V"), + (0xAC6, "X"), + (0xAC7, "V"), + (0xACA, "X"), + (0xACB, "V"), + (0xACE, "X"), + (0xAD0, "V"), + (0xAD1, "X"), + (0xAE0, "V"), + (0xAE4, "X"), + (0xAE6, "V"), + (0xAF2, "X"), + (0xAF9, "V"), + (0xB00, "X"), + (0xB01, "V"), + (0xB04, "X"), + (0xB05, "V"), + (0xB0D, "X"), + (0xB0F, "V"), + (0xB11, "X"), + (0xB13, "V"), + (0xB29, "X"), + (0xB2A, "V"), + (0xB31, "X"), + (0xB32, "V"), + (0xB34, "X"), + (0xB35, "V"), + (0xB3A, "X"), + (0xB3C, "V"), + (0xB45, "X"), + (0xB47, "V"), + (0xB49, "X"), + (0xB4B, "V"), + (0xB4E, "X"), + (0xB55, "V"), + (0xB58, "X"), + (0xB5C, "M", "ଡ଼"), + (0xB5D, "M", "ଢ଼"), + (0xB5E, "X"), + (0xB5F, "V"), + (0xB64, "X"), + (0xB66, "V"), + (0xB78, "X"), + (0xB82, "V"), + (0xB84, "X"), + (0xB85, "V"), + (0xB8B, "X"), + (0xB8E, "V"), + (0xB91, "X"), + (0xB92, "V"), + (0xB96, "X"), + (0xB99, "V"), + (0xB9B, "X"), + (0xB9C, "V"), + (0xB9D, "X"), + (0xB9E, "V"), + (0xBA0, "X"), + (0xBA3, "V"), + (0xBA5, "X"), + (0xBA8, "V"), + (0xBAB, "X"), + (0xBAE, "V"), + (0xBBA, "X"), + (0xBBE, "V"), + (0xBC3, "X"), + (0xBC6, "V"), + (0xBC9, "X"), + (0xBCA, "V"), + (0xBCE, "X"), + (0xBD0, "V"), + (0xBD1, "X"), + (0xBD7, "V"), + (0xBD8, "X"), + (0xBE6, "V"), + (0xBFB, "X"), + (0xC00, "V"), + (0xC0D, "X"), + (0xC0E, "V"), + (0xC11, "X"), + (0xC12, "V"), + (0xC29, "X"), + (0xC2A, "V"), + ] + + +def _seg_12() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC3A, "X"), + (0xC3C, "V"), + (0xC45, "X"), + (0xC46, "V"), + (0xC49, "X"), + (0xC4A, "V"), + (0xC4E, "X"), + (0xC55, "V"), + (0xC57, "X"), + (0xC58, "V"), + (0xC5B, "X"), + (0xC5D, "V"), + (0xC5E, "X"), + (0xC60, "V"), + (0xC64, "X"), + (0xC66, "V"), + (0xC70, "X"), + (0xC77, "V"), + (0xC8D, "X"), + (0xC8E, "V"), + (0xC91, "X"), + (0xC92, "V"), + (0xCA9, "X"), + (0xCAA, "V"), + (0xCB4, "X"), + (0xCB5, "V"), + (0xCBA, "X"), + (0xCBC, "V"), + (0xCC5, "X"), + (0xCC6, "V"), + (0xCC9, "X"), + (0xCCA, "V"), + (0xCCE, "X"), + (0xCD5, "V"), + (0xCD7, "X"), + (0xCDD, "V"), + (0xCDF, "X"), + (0xCE0, "V"), + (0xCE4, "X"), + (0xCE6, "V"), + (0xCF0, "X"), + (0xCF1, "V"), + (0xCF4, "X"), + (0xD00, "V"), + (0xD0D, "X"), + (0xD0E, "V"), + (0xD11, "X"), + (0xD12, "V"), + (0xD45, "X"), + (0xD46, "V"), + (0xD49, "X"), + (0xD4A, "V"), + (0xD50, "X"), + (0xD54, "V"), + (0xD64, "X"), + (0xD66, "V"), + (0xD80, "X"), + (0xD81, "V"), + (0xD84, "X"), + (0xD85, "V"), + (0xD97, "X"), + (0xD9A, "V"), + (0xDB2, "X"), + (0xDB3, "V"), + (0xDBC, "X"), + (0xDBD, "V"), + (0xDBE, "X"), + (0xDC0, "V"), + (0xDC7, "X"), + (0xDCA, "V"), + (0xDCB, "X"), + (0xDCF, "V"), + (0xDD5, "X"), + (0xDD6, "V"), + (0xDD7, "X"), + (0xDD8, "V"), + (0xDE0, "X"), + (0xDE6, "V"), + (0xDF0, "X"), + (0xDF2, "V"), + (0xDF5, "X"), + (0xE01, "V"), + (0xE33, "M", "ํา"), + (0xE34, "V"), + (0xE3B, "X"), + (0xE3F, "V"), + (0xE5C, "X"), + (0xE81, "V"), + (0xE83, "X"), + (0xE84, "V"), + (0xE85, "X"), + (0xE86, "V"), + (0xE8B, "X"), + (0xE8C, "V"), + (0xEA4, "X"), + (0xEA5, "V"), + (0xEA6, "X"), + (0xEA7, "V"), + (0xEB3, "M", "ໍາ"), + (0xEB4, "V"), + ] + + +def _seg_13() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xEBE, "X"), + (0xEC0, "V"), + (0xEC5, "X"), + (0xEC6, "V"), + (0xEC7, "X"), + (0xEC8, "V"), + (0xECF, "X"), + (0xED0, "V"), + (0xEDA, "X"), + (0xEDC, "M", "ຫນ"), + (0xEDD, "M", "ຫມ"), + (0xEDE, "V"), + (0xEE0, "X"), + (0xF00, "V"), + (0xF0C, "M", "་"), + (0xF0D, "V"), + (0xF43, "M", "གྷ"), + (0xF44, "V"), + (0xF48, "X"), + (0xF49, "V"), + (0xF4D, "M", "ཌྷ"), + (0xF4E, "V"), + (0xF52, "M", "དྷ"), + (0xF53, "V"), + (0xF57, "M", "བྷ"), + (0xF58, "V"), + (0xF5C, "M", "ཛྷ"), + (0xF5D, "V"), + (0xF69, "M", "ཀྵ"), + (0xF6A, "V"), + (0xF6D, "X"), + (0xF71, "V"), + (0xF73, "M", "ཱི"), + (0xF74, "V"), + (0xF75, "M", "ཱུ"), + (0xF76, "M", "ྲྀ"), + (0xF77, "M", "ྲཱྀ"), + (0xF78, "M", "ླྀ"), + (0xF79, "M", "ླཱྀ"), + (0xF7A, "V"), + (0xF81, "M", "ཱྀ"), + (0xF82, "V"), + (0xF93, "M", "ྒྷ"), + (0xF94, "V"), + (0xF98, "X"), + (0xF99, "V"), + (0xF9D, "M", "ྜྷ"), + (0xF9E, "V"), + (0xFA2, "M", "ྡྷ"), + (0xFA3, "V"), + (0xFA7, "M", "ྦྷ"), + (0xFA8, "V"), + (0xFAC, "M", "ྫྷ"), + (0xFAD, "V"), + (0xFB9, "M", "ྐྵ"), + (0xFBA, "V"), + (0xFBD, "X"), + (0xFBE, "V"), + (0xFCD, "X"), + (0xFCE, "V"), + (0xFDB, "X"), + (0x1000, "V"), + (0x10A0, "M", "ⴀ"), + (0x10A1, "M", "ⴁ"), + (0x10A2, "M", "ⴂ"), + (0x10A3, "M", "ⴃ"), + (0x10A4, "M", "ⴄ"), + (0x10A5, "M", "ⴅ"), + (0x10A6, "M", "ⴆ"), + (0x10A7, "M", "ⴇ"), + (0x10A8, "M", "ⴈ"), + (0x10A9, "M", "ⴉ"), + (0x10AA, "M", "ⴊ"), + (0x10AB, "M", "ⴋ"), + (0x10AC, "M", "ⴌ"), + (0x10AD, "M", "ⴍ"), + (0x10AE, "M", "ⴎ"), + (0x10AF, "M", "ⴏ"), + (0x10B0, "M", "ⴐ"), + (0x10B1, "M", "ⴑ"), + (0x10B2, "M", "ⴒ"), + (0x10B3, "M", "ⴓ"), + (0x10B4, "M", "ⴔ"), + (0x10B5, "M", "ⴕ"), + (0x10B6, "M", "ⴖ"), + (0x10B7, "M", "ⴗ"), + (0x10B8, "M", "ⴘ"), + (0x10B9, "M", "ⴙ"), + (0x10BA, "M", "ⴚ"), + (0x10BB, "M", "ⴛ"), + (0x10BC, "M", "ⴜ"), + (0x10BD, "M", "ⴝ"), + (0x10BE, "M", "ⴞ"), + (0x10BF, "M", "ⴟ"), + (0x10C0, "M", "ⴠ"), + (0x10C1, "M", "ⴡ"), + (0x10C2, "M", "ⴢ"), + (0x10C3, "M", "ⴣ"), + (0x10C4, "M", "ⴤ"), + (0x10C5, "M", "ⴥ"), + ] + + +def _seg_14() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10C6, "X"), + (0x10C7, "M", "ⴧ"), + (0x10C8, "X"), + (0x10CD, "M", "ⴭ"), + (0x10CE, "X"), + (0x10D0, "V"), + (0x10FC, "M", "ნ"), + (0x10FD, "V"), + (0x115F, "I"), + (0x1161, "V"), + (0x1249, "X"), + (0x124A, "V"), + (0x124E, "X"), + (0x1250, "V"), + (0x1257, "X"), + (0x1258, "V"), + (0x1259, "X"), + (0x125A, "V"), + (0x125E, "X"), + (0x1260, "V"), + (0x1289, "X"), + (0x128A, "V"), + (0x128E, "X"), + (0x1290, "V"), + (0x12B1, "X"), + (0x12B2, "V"), + (0x12B6, "X"), + (0x12B8, "V"), + (0x12BF, "X"), + (0x12C0, "V"), + (0x12C1, "X"), + (0x12C2, "V"), + (0x12C6, "X"), + (0x12C8, "V"), + (0x12D7, "X"), + (0x12D8, "V"), + (0x1311, "X"), + (0x1312, "V"), + (0x1316, "X"), + (0x1318, "V"), + (0x135B, "X"), + (0x135D, "V"), + (0x137D, "X"), + (0x1380, "V"), + (0x139A, "X"), + (0x13A0, "V"), + (0x13F6, "X"), + (0x13F8, "M", "Ᏸ"), + (0x13F9, "M", "Ᏹ"), + (0x13FA, "M", "Ᏺ"), + (0x13FB, "M", "Ᏻ"), + (0x13FC, "M", "Ᏼ"), + (0x13FD, "M", "Ᏽ"), + (0x13FE, "X"), + (0x1400, "V"), + (0x1680, "X"), + (0x1681, "V"), + (0x169D, "X"), + (0x16A0, "V"), + (0x16F9, "X"), + (0x1700, "V"), + (0x1716, "X"), + (0x171F, "V"), + (0x1737, "X"), + (0x1740, "V"), + (0x1754, "X"), + (0x1760, "V"), + (0x176D, "X"), + (0x176E, "V"), + (0x1771, "X"), + (0x1772, "V"), + (0x1774, "X"), + (0x1780, "V"), + (0x17B4, "I"), + (0x17B6, "V"), + (0x17DE, "X"), + (0x17E0, "V"), + (0x17EA, "X"), + (0x17F0, "V"), + (0x17FA, "X"), + (0x1800, "V"), + (0x180B, "I"), + (0x1810, "V"), + (0x181A, "X"), + (0x1820, "V"), + (0x1879, "X"), + (0x1880, "V"), + (0x18AB, "X"), + (0x18B0, "V"), + (0x18F6, "X"), + (0x1900, "V"), + (0x191F, "X"), + (0x1920, "V"), + (0x192C, "X"), + (0x1930, "V"), + (0x193C, "X"), + (0x1940, "V"), + (0x1941, "X"), + (0x1944, "V"), + (0x196E, "X"), + ] + + +def _seg_15() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1970, "V"), + (0x1975, "X"), + (0x1980, "V"), + (0x19AC, "X"), + (0x19B0, "V"), + (0x19CA, "X"), + (0x19D0, "V"), + (0x19DB, "X"), + (0x19DE, "V"), + (0x1A1C, "X"), + (0x1A1E, "V"), + (0x1A5F, "X"), + (0x1A60, "V"), + (0x1A7D, "X"), + (0x1A7F, "V"), + (0x1A8A, "X"), + (0x1A90, "V"), + (0x1A9A, "X"), + (0x1AA0, "V"), + (0x1AAE, "X"), + (0x1AB0, "V"), + (0x1ACF, "X"), + (0x1B00, "V"), + (0x1B4D, "X"), + (0x1B4E, "V"), + (0x1BF4, "X"), + (0x1BFC, "V"), + (0x1C38, "X"), + (0x1C3B, "V"), + (0x1C4A, "X"), + (0x1C4D, "V"), + (0x1C80, "M", "в"), + (0x1C81, "M", "д"), + (0x1C82, "M", "о"), + (0x1C83, "M", "с"), + (0x1C84, "M", "т"), + (0x1C86, "M", "ъ"), + (0x1C87, "M", "ѣ"), + (0x1C88, "M", "ꙋ"), + (0x1C89, "M", "ᲊ"), + (0x1C8A, "V"), + (0x1C8B, "X"), + (0x1C90, "M", "ა"), + (0x1C91, "M", "ბ"), + (0x1C92, "M", "გ"), + (0x1C93, "M", "დ"), + (0x1C94, "M", "ე"), + (0x1C95, "M", "ვ"), + (0x1C96, "M", "ზ"), + (0x1C97, "M", "თ"), + (0x1C98, "M", "ი"), + (0x1C99, "M", "კ"), + (0x1C9A, "M", "ლ"), + (0x1C9B, "M", "მ"), + (0x1C9C, "M", "ნ"), + (0x1C9D, "M", "ო"), + (0x1C9E, "M", "პ"), + (0x1C9F, "M", "ჟ"), + (0x1CA0, "M", "რ"), + (0x1CA1, "M", "ს"), + (0x1CA2, "M", "ტ"), + (0x1CA3, "M", "უ"), + (0x1CA4, "M", "ფ"), + (0x1CA5, "M", "ქ"), + (0x1CA6, "M", "ღ"), + (0x1CA7, "M", "ყ"), + (0x1CA8, "M", "შ"), + (0x1CA9, "M", "ჩ"), + (0x1CAA, "M", "ც"), + (0x1CAB, "M", "ძ"), + (0x1CAC, "M", "წ"), + (0x1CAD, "M", "ჭ"), + (0x1CAE, "M", "ხ"), + (0x1CAF, "M", "ჯ"), + (0x1CB0, "M", "ჰ"), + (0x1CB1, "M", "ჱ"), + (0x1CB2, "M", "ჲ"), + (0x1CB3, "M", "ჳ"), + (0x1CB4, "M", "ჴ"), + (0x1CB5, "M", "ჵ"), + (0x1CB6, "M", "ჶ"), + (0x1CB7, "M", "ჷ"), + (0x1CB8, "M", "ჸ"), + (0x1CB9, "M", "ჹ"), + (0x1CBA, "M", "ჺ"), + (0x1CBB, "X"), + (0x1CBD, "M", "ჽ"), + (0x1CBE, "M", "ჾ"), + (0x1CBF, "M", "ჿ"), + (0x1CC0, "V"), + (0x1CC8, "X"), + (0x1CD0, "V"), + (0x1CFB, "X"), + (0x1D00, "V"), + (0x1D2C, "M", "a"), + (0x1D2D, "M", "æ"), + (0x1D2E, "M", "b"), + (0x1D2F, "V"), + (0x1D30, "M", "d"), + (0x1D31, "M", "e"), + ] + + +def _seg_16() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D32, "M", "ǝ"), + (0x1D33, "M", "g"), + (0x1D34, "M", "h"), + (0x1D35, "M", "i"), + (0x1D36, "M", "j"), + (0x1D37, "M", "k"), + (0x1D38, "M", "l"), + (0x1D39, "M", "m"), + (0x1D3A, "M", "n"), + (0x1D3B, "V"), + (0x1D3C, "M", "o"), + (0x1D3D, "M", "ȣ"), + (0x1D3E, "M", "p"), + (0x1D3F, "M", "r"), + (0x1D40, "M", "t"), + (0x1D41, "M", "u"), + (0x1D42, "M", "w"), + (0x1D43, "M", "a"), + (0x1D44, "M", "ɐ"), + (0x1D45, "M", "ɑ"), + (0x1D46, "M", "ᴂ"), + (0x1D47, "M", "b"), + (0x1D48, "M", "d"), + (0x1D49, "M", "e"), + (0x1D4A, "M", "ə"), + (0x1D4B, "M", "ɛ"), + (0x1D4C, "M", "ɜ"), + (0x1D4D, "M", "g"), + (0x1D4E, "V"), + (0x1D4F, "M", "k"), + (0x1D50, "M", "m"), + (0x1D51, "M", "ŋ"), + (0x1D52, "M", "o"), + (0x1D53, "M", "ɔ"), + (0x1D54, "M", "ᴖ"), + (0x1D55, "M", "ᴗ"), + (0x1D56, "M", "p"), + (0x1D57, "M", "t"), + (0x1D58, "M", "u"), + (0x1D59, "M", "ᴝ"), + (0x1D5A, "M", "ɯ"), + (0x1D5B, "M", "v"), + (0x1D5C, "M", "ᴥ"), + (0x1D5D, "M", "β"), + (0x1D5E, "M", "γ"), + (0x1D5F, "M", "δ"), + (0x1D60, "M", "φ"), + (0x1D61, "M", "χ"), + (0x1D62, "M", "i"), + (0x1D63, "M", "r"), + (0x1D64, "M", "u"), + (0x1D65, "M", "v"), + (0x1D66, "M", "β"), + (0x1D67, "M", "γ"), + (0x1D68, "M", "ρ"), + (0x1D69, "M", "φ"), + (0x1D6A, "M", "χ"), + (0x1D6B, "V"), + (0x1D78, "M", "н"), + (0x1D79, "V"), + (0x1D9B, "M", "ɒ"), + (0x1D9C, "M", "c"), + (0x1D9D, "M", "ɕ"), + (0x1D9E, "M", "ð"), + (0x1D9F, "M", "ɜ"), + (0x1DA0, "M", "f"), + (0x1DA1, "M", "ɟ"), + (0x1DA2, "M", "ɡ"), + (0x1DA3, "M", "ɥ"), + (0x1DA4, "M", "ɨ"), + (0x1DA5, "M", "ɩ"), + (0x1DA6, "M", "ɪ"), + (0x1DA7, "M", "ᵻ"), + (0x1DA8, "M", "ʝ"), + (0x1DA9, "M", "ɭ"), + (0x1DAA, "M", "ᶅ"), + (0x1DAB, "M", "ʟ"), + (0x1DAC, "M", "ɱ"), + (0x1DAD, "M", "ɰ"), + (0x1DAE, "M", "ɲ"), + (0x1DAF, "M", "ɳ"), + (0x1DB0, "M", "ɴ"), + (0x1DB1, "M", "ɵ"), + (0x1DB2, "M", "ɸ"), + (0x1DB3, "M", "ʂ"), + (0x1DB4, "M", "ʃ"), + (0x1DB5, "M", "ƫ"), + (0x1DB6, "M", "ʉ"), + (0x1DB7, "M", "ʊ"), + (0x1DB8, "M", "ᴜ"), + (0x1DB9, "M", "ʋ"), + (0x1DBA, "M", "ʌ"), + (0x1DBB, "M", "z"), + (0x1DBC, "M", "ʐ"), + (0x1DBD, "M", "ʑ"), + (0x1DBE, "M", "ʒ"), + (0x1DBF, "M", "θ"), + (0x1DC0, "V"), + (0x1E00, "M", "ḁ"), + (0x1E01, "V"), + ] + + +def _seg_17() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E02, "M", "ḃ"), + (0x1E03, "V"), + (0x1E04, "M", "ḅ"), + (0x1E05, "V"), + (0x1E06, "M", "ḇ"), + (0x1E07, "V"), + (0x1E08, "M", "ḉ"), + (0x1E09, "V"), + (0x1E0A, "M", "ḋ"), + (0x1E0B, "V"), + (0x1E0C, "M", "ḍ"), + (0x1E0D, "V"), + (0x1E0E, "M", "ḏ"), + (0x1E0F, "V"), + (0x1E10, "M", "ḑ"), + (0x1E11, "V"), + (0x1E12, "M", "ḓ"), + (0x1E13, "V"), + (0x1E14, "M", "ḕ"), + (0x1E15, "V"), + (0x1E16, "M", "ḗ"), + (0x1E17, "V"), + (0x1E18, "M", "ḙ"), + (0x1E19, "V"), + (0x1E1A, "M", "ḛ"), + (0x1E1B, "V"), + (0x1E1C, "M", "ḝ"), + (0x1E1D, "V"), + (0x1E1E, "M", "ḟ"), + (0x1E1F, "V"), + (0x1E20, "M", "ḡ"), + (0x1E21, "V"), + (0x1E22, "M", "ḣ"), + (0x1E23, "V"), + (0x1E24, "M", "ḥ"), + (0x1E25, "V"), + (0x1E26, "M", "ḧ"), + (0x1E27, "V"), + (0x1E28, "M", "ḩ"), + (0x1E29, "V"), + (0x1E2A, "M", "ḫ"), + (0x1E2B, "V"), + (0x1E2C, "M", "ḭ"), + (0x1E2D, "V"), + (0x1E2E, "M", "ḯ"), + (0x1E2F, "V"), + (0x1E30, "M", "ḱ"), + (0x1E31, "V"), + (0x1E32, "M", "ḳ"), + (0x1E33, "V"), + (0x1E34, "M", "ḵ"), + (0x1E35, "V"), + (0x1E36, "M", "ḷ"), + (0x1E37, "V"), + (0x1E38, "M", "ḹ"), + (0x1E39, "V"), + (0x1E3A, "M", "ḻ"), + (0x1E3B, "V"), + (0x1E3C, "M", "ḽ"), + (0x1E3D, "V"), + (0x1E3E, "M", "ḿ"), + (0x1E3F, "V"), + (0x1E40, "M", "ṁ"), + (0x1E41, "V"), + (0x1E42, "M", "ṃ"), + (0x1E43, "V"), + (0x1E44, "M", "ṅ"), + (0x1E45, "V"), + (0x1E46, "M", "ṇ"), + (0x1E47, "V"), + (0x1E48, "M", "ṉ"), + (0x1E49, "V"), + (0x1E4A, "M", "ṋ"), + (0x1E4B, "V"), + (0x1E4C, "M", "ṍ"), + (0x1E4D, "V"), + (0x1E4E, "M", "ṏ"), + (0x1E4F, "V"), + (0x1E50, "M", "ṑ"), + (0x1E51, "V"), + (0x1E52, "M", "ṓ"), + (0x1E53, "V"), + (0x1E54, "M", "ṕ"), + (0x1E55, "V"), + (0x1E56, "M", "ṗ"), + (0x1E57, "V"), + (0x1E58, "M", "ṙ"), + (0x1E59, "V"), + (0x1E5A, "M", "ṛ"), + (0x1E5B, "V"), + (0x1E5C, "M", "ṝ"), + (0x1E5D, "V"), + (0x1E5E, "M", "ṟ"), + (0x1E5F, "V"), + (0x1E60, "M", "ṡ"), + (0x1E61, "V"), + (0x1E62, "M", "ṣ"), + (0x1E63, "V"), + (0x1E64, "M", "ṥ"), + (0x1E65, "V"), + ] + + +def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E66, "M", "ṧ"), + (0x1E67, "V"), + (0x1E68, "M", "ṩ"), + (0x1E69, "V"), + (0x1E6A, "M", "ṫ"), + (0x1E6B, "V"), + (0x1E6C, "M", "ṭ"), + (0x1E6D, "V"), + (0x1E6E, "M", "ṯ"), + (0x1E6F, "V"), + (0x1E70, "M", "ṱ"), + (0x1E71, "V"), + (0x1E72, "M", "ṳ"), + (0x1E73, "V"), + (0x1E74, "M", "ṵ"), + (0x1E75, "V"), + (0x1E76, "M", "ṷ"), + (0x1E77, "V"), + (0x1E78, "M", "ṹ"), + (0x1E79, "V"), + (0x1E7A, "M", "ṻ"), + (0x1E7B, "V"), + (0x1E7C, "M", "ṽ"), + (0x1E7D, "V"), + (0x1E7E, "M", "ṿ"), + (0x1E7F, "V"), + (0x1E80, "M", "ẁ"), + (0x1E81, "V"), + (0x1E82, "M", "ẃ"), + (0x1E83, "V"), + (0x1E84, "M", "ẅ"), + (0x1E85, "V"), + (0x1E86, "M", "ẇ"), + (0x1E87, "V"), + (0x1E88, "M", "ẉ"), + (0x1E89, "V"), + (0x1E8A, "M", "ẋ"), + (0x1E8B, "V"), + (0x1E8C, "M", "ẍ"), + (0x1E8D, "V"), + (0x1E8E, "M", "ẏ"), + (0x1E8F, "V"), + (0x1E90, "M", "ẑ"), + (0x1E91, "V"), + (0x1E92, "M", "ẓ"), + (0x1E93, "V"), + (0x1E94, "M", "ẕ"), + (0x1E95, "V"), + (0x1E9A, "M", "aʾ"), + (0x1E9B, "M", "ṡ"), + (0x1E9C, "V"), + (0x1E9E, "M", "ß"), + (0x1E9F, "V"), + (0x1EA0, "M", "ạ"), + (0x1EA1, "V"), + (0x1EA2, "M", "ả"), + (0x1EA3, "V"), + (0x1EA4, "M", "ấ"), + (0x1EA5, "V"), + (0x1EA6, "M", "ầ"), + (0x1EA7, "V"), + (0x1EA8, "M", "ẩ"), + (0x1EA9, "V"), + (0x1EAA, "M", "ẫ"), + (0x1EAB, "V"), + (0x1EAC, "M", "ậ"), + (0x1EAD, "V"), + (0x1EAE, "M", "ắ"), + (0x1EAF, "V"), + (0x1EB0, "M", "ằ"), + (0x1EB1, "V"), + (0x1EB2, "M", "ẳ"), + (0x1EB3, "V"), + (0x1EB4, "M", "ẵ"), + (0x1EB5, "V"), + (0x1EB6, "M", "ặ"), + (0x1EB7, "V"), + (0x1EB8, "M", "ẹ"), + (0x1EB9, "V"), + (0x1EBA, "M", "ẻ"), + (0x1EBB, "V"), + (0x1EBC, "M", "ẽ"), + (0x1EBD, "V"), + (0x1EBE, "M", "ế"), + (0x1EBF, "V"), + (0x1EC0, "M", "ề"), + (0x1EC1, "V"), + (0x1EC2, "M", "ể"), + (0x1EC3, "V"), + (0x1EC4, "M", "ễ"), + (0x1EC5, "V"), + (0x1EC6, "M", "ệ"), + (0x1EC7, "V"), + (0x1EC8, "M", "ỉ"), + (0x1EC9, "V"), + (0x1ECA, "M", "ị"), + (0x1ECB, "V"), + (0x1ECC, "M", "ọ"), + (0x1ECD, "V"), + (0x1ECE, "M", "ỏ"), + ] + + +def _seg_19() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1ECF, "V"), + (0x1ED0, "M", "ố"), + (0x1ED1, "V"), + (0x1ED2, "M", "ồ"), + (0x1ED3, "V"), + (0x1ED4, "M", "ổ"), + (0x1ED5, "V"), + (0x1ED6, "M", "ỗ"), + (0x1ED7, "V"), + (0x1ED8, "M", "ộ"), + (0x1ED9, "V"), + (0x1EDA, "M", "ớ"), + (0x1EDB, "V"), + (0x1EDC, "M", "ờ"), + (0x1EDD, "V"), + (0x1EDE, "M", "ở"), + (0x1EDF, "V"), + (0x1EE0, "M", "ỡ"), + (0x1EE1, "V"), + (0x1EE2, "M", "ợ"), + (0x1EE3, "V"), + (0x1EE4, "M", "ụ"), + (0x1EE5, "V"), + (0x1EE6, "M", "ủ"), + (0x1EE7, "V"), + (0x1EE8, "M", "ứ"), + (0x1EE9, "V"), + (0x1EEA, "M", "ừ"), + (0x1EEB, "V"), + (0x1EEC, "M", "ử"), + (0x1EED, "V"), + (0x1EEE, "M", "ữ"), + (0x1EEF, "V"), + (0x1EF0, "M", "ự"), + (0x1EF1, "V"), + (0x1EF2, "M", "ỳ"), + (0x1EF3, "V"), + (0x1EF4, "M", "ỵ"), + (0x1EF5, "V"), + (0x1EF6, "M", "ỷ"), + (0x1EF7, "V"), + (0x1EF8, "M", "ỹ"), + (0x1EF9, "V"), + (0x1EFA, "M", "ỻ"), + (0x1EFB, "V"), + (0x1EFC, "M", "ỽ"), + (0x1EFD, "V"), + (0x1EFE, "M", "ỿ"), + (0x1EFF, "V"), + (0x1F08, "M", "ἀ"), + (0x1F09, "M", "ἁ"), + (0x1F0A, "M", "ἂ"), + (0x1F0B, "M", "ἃ"), + (0x1F0C, "M", "ἄ"), + (0x1F0D, "M", "ἅ"), + (0x1F0E, "M", "ἆ"), + (0x1F0F, "M", "ἇ"), + (0x1F10, "V"), + (0x1F16, "X"), + (0x1F18, "M", "ἐ"), + (0x1F19, "M", "ἑ"), + (0x1F1A, "M", "ἒ"), + (0x1F1B, "M", "ἓ"), + (0x1F1C, "M", "ἔ"), + (0x1F1D, "M", "ἕ"), + (0x1F1E, "X"), + (0x1F20, "V"), + (0x1F28, "M", "ἠ"), + (0x1F29, "M", "ἡ"), + (0x1F2A, "M", "ἢ"), + (0x1F2B, "M", "ἣ"), + (0x1F2C, "M", "ἤ"), + (0x1F2D, "M", "ἥ"), + (0x1F2E, "M", "ἦ"), + (0x1F2F, "M", "ἧ"), + (0x1F30, "V"), + (0x1F38, "M", "ἰ"), + (0x1F39, "M", "ἱ"), + (0x1F3A, "M", "ἲ"), + (0x1F3B, "M", "ἳ"), + (0x1F3C, "M", "ἴ"), + (0x1F3D, "M", "ἵ"), + (0x1F3E, "M", "ἶ"), + (0x1F3F, "M", "ἷ"), + (0x1F40, "V"), + (0x1F46, "X"), + (0x1F48, "M", "ὀ"), + (0x1F49, "M", "ὁ"), + (0x1F4A, "M", "ὂ"), + (0x1F4B, "M", "ὃ"), + (0x1F4C, "M", "ὄ"), + (0x1F4D, "M", "ὅ"), + (0x1F4E, "X"), + (0x1F50, "V"), + (0x1F58, "X"), + (0x1F59, "M", "ὑ"), + (0x1F5A, "X"), + (0x1F5B, "M", "ὓ"), + (0x1F5C, "X"), + (0x1F5D, "M", "ὕ"), + ] + + +def _seg_20() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F5E, "X"), + (0x1F5F, "M", "ὗ"), + (0x1F60, "V"), + (0x1F68, "M", "ὠ"), + (0x1F69, "M", "ὡ"), + (0x1F6A, "M", "ὢ"), + (0x1F6B, "M", "ὣ"), + (0x1F6C, "M", "ὤ"), + (0x1F6D, "M", "ὥ"), + (0x1F6E, "M", "ὦ"), + (0x1F6F, "M", "ὧ"), + (0x1F70, "V"), + (0x1F71, "M", "ά"), + (0x1F72, "V"), + (0x1F73, "M", "έ"), + (0x1F74, "V"), + (0x1F75, "M", "ή"), + (0x1F76, "V"), + (0x1F77, "M", "ί"), + (0x1F78, "V"), + (0x1F79, "M", "ό"), + (0x1F7A, "V"), + (0x1F7B, "M", "ύ"), + (0x1F7C, "V"), + (0x1F7D, "M", "ώ"), + (0x1F7E, "X"), + (0x1F80, "M", "ἀι"), + (0x1F81, "M", "ἁι"), + (0x1F82, "M", "ἂι"), + (0x1F83, "M", "ἃι"), + (0x1F84, "M", "ἄι"), + (0x1F85, "M", "ἅι"), + (0x1F86, "M", "ἆι"), + (0x1F87, "M", "ἇι"), + (0x1F88, "M", "ἀι"), + (0x1F89, "M", "ἁι"), + (0x1F8A, "M", "ἂι"), + (0x1F8B, "M", "ἃι"), + (0x1F8C, "M", "ἄι"), + (0x1F8D, "M", "ἅι"), + (0x1F8E, "M", "ἆι"), + (0x1F8F, "M", "ἇι"), + (0x1F90, "M", "ἠι"), + (0x1F91, "M", "ἡι"), + (0x1F92, "M", "ἢι"), + (0x1F93, "M", "ἣι"), + (0x1F94, "M", "ἤι"), + (0x1F95, "M", "ἥι"), + (0x1F96, "M", "ἦι"), + (0x1F97, "M", "ἧι"), + (0x1F98, "M", "ἠι"), + (0x1F99, "M", "ἡι"), + (0x1F9A, "M", "ἢι"), + (0x1F9B, "M", "ἣι"), + (0x1F9C, "M", "ἤι"), + (0x1F9D, "M", "ἥι"), + (0x1F9E, "M", "ἦι"), + (0x1F9F, "M", "ἧι"), + (0x1FA0, "M", "ὠι"), + (0x1FA1, "M", "ὡι"), + (0x1FA2, "M", "ὢι"), + (0x1FA3, "M", "ὣι"), + (0x1FA4, "M", "ὤι"), + (0x1FA5, "M", "ὥι"), + (0x1FA6, "M", "ὦι"), + (0x1FA7, "M", "ὧι"), + (0x1FA8, "M", "ὠι"), + (0x1FA9, "M", "ὡι"), + (0x1FAA, "M", "ὢι"), + (0x1FAB, "M", "ὣι"), + (0x1FAC, "M", "ὤι"), + (0x1FAD, "M", "ὥι"), + (0x1FAE, "M", "ὦι"), + (0x1FAF, "M", "ὧι"), + (0x1FB0, "V"), + (0x1FB2, "M", "ὰι"), + (0x1FB3, "M", "αι"), + (0x1FB4, "M", "άι"), + (0x1FB5, "X"), + (0x1FB6, "V"), + (0x1FB7, "M", "ᾶι"), + (0x1FB8, "M", "ᾰ"), + (0x1FB9, "M", "ᾱ"), + (0x1FBA, "M", "ὰ"), + (0x1FBB, "M", "ά"), + (0x1FBC, "M", "αι"), + (0x1FBD, "M", " ̓"), + (0x1FBE, "M", "ι"), + (0x1FBF, "M", " ̓"), + (0x1FC0, "M", " ͂"), + (0x1FC1, "M", " ̈͂"), + (0x1FC2, "M", "ὴι"), + (0x1FC3, "M", "ηι"), + (0x1FC4, "M", "ήι"), + (0x1FC5, "X"), + (0x1FC6, "V"), + (0x1FC7, "M", "ῆι"), + (0x1FC8, "M", "ὲ"), + (0x1FC9, "M", "έ"), + (0x1FCA, "M", "ὴ"), + ] + + +def _seg_21() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1FCB, "M", "ή"), + (0x1FCC, "M", "ηι"), + (0x1FCD, "M", " ̓̀"), + (0x1FCE, "M", " ̓́"), + (0x1FCF, "M", " ̓͂"), + (0x1FD0, "V"), + (0x1FD3, "M", "ΐ"), + (0x1FD4, "X"), + (0x1FD6, "V"), + (0x1FD8, "M", "ῐ"), + (0x1FD9, "M", "ῑ"), + (0x1FDA, "M", "ὶ"), + (0x1FDB, "M", "ί"), + (0x1FDC, "X"), + (0x1FDD, "M", " ̔̀"), + (0x1FDE, "M", " ̔́"), + (0x1FDF, "M", " ̔͂"), + (0x1FE0, "V"), + (0x1FE3, "M", "ΰ"), + (0x1FE4, "V"), + (0x1FE8, "M", "ῠ"), + (0x1FE9, "M", "ῡ"), + (0x1FEA, "M", "ὺ"), + (0x1FEB, "M", "ύ"), + (0x1FEC, "M", "ῥ"), + (0x1FED, "M", " ̈̀"), + (0x1FEE, "M", " ̈́"), + (0x1FEF, "M", "`"), + (0x1FF0, "X"), + (0x1FF2, "M", "ὼι"), + (0x1FF3, "M", "ωι"), + (0x1FF4, "M", "ώι"), + (0x1FF5, "X"), + (0x1FF6, "V"), + (0x1FF7, "M", "ῶι"), + (0x1FF8, "M", "ὸ"), + (0x1FF9, "M", "ό"), + (0x1FFA, "M", "ὼ"), + (0x1FFB, "M", "ώ"), + (0x1FFC, "M", "ωι"), + (0x1FFD, "M", " ́"), + (0x1FFE, "M", " ̔"), + (0x1FFF, "X"), + (0x2000, "M", " "), + (0x200B, "I"), + (0x200C, "D", ""), + (0x200E, "X"), + (0x2010, "V"), + (0x2011, "M", "‐"), + (0x2012, "V"), + (0x2017, "M", " ̳"), + (0x2018, "V"), + (0x2024, "X"), + (0x2027, "V"), + (0x2028, "X"), + (0x202F, "M", " "), + (0x2030, "V"), + (0x2033, "M", "′′"), + (0x2034, "M", "′′′"), + (0x2035, "V"), + (0x2036, "M", "‵‵"), + (0x2037, "M", "‵‵‵"), + (0x2038, "V"), + (0x203C, "M", "!!"), + (0x203D, "V"), + (0x203E, "M", " ̅"), + (0x203F, "V"), + (0x2047, "M", "??"), + (0x2048, "M", "?!"), + (0x2049, "M", "!?"), + (0x204A, "V"), + (0x2057, "M", "′′′′"), + (0x2058, "V"), + (0x205F, "M", " "), + (0x2060, "I"), + (0x2065, "X"), + (0x206A, "I"), + (0x2070, "M", "0"), + (0x2071, "M", "i"), + (0x2072, "X"), + (0x2074, "M", "4"), + (0x2075, "M", "5"), + (0x2076, "M", "6"), + (0x2077, "M", "7"), + (0x2078, "M", "8"), + (0x2079, "M", "9"), + (0x207A, "M", "+"), + (0x207B, "M", "−"), + (0x207C, "M", "="), + (0x207D, "M", "("), + (0x207E, "M", ")"), + (0x207F, "M", "n"), + (0x2080, "M", "0"), + (0x2081, "M", "1"), + (0x2082, "M", "2"), + (0x2083, "M", "3"), + (0x2084, "M", "4"), + (0x2085, "M", "5"), + (0x2086, "M", "6"), + (0x2087, "M", "7"), + ] + + +def _seg_22() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2088, "M", "8"), + (0x2089, "M", "9"), + (0x208A, "M", "+"), + (0x208B, "M", "−"), + (0x208C, "M", "="), + (0x208D, "M", "("), + (0x208E, "M", ")"), + (0x208F, "X"), + (0x2090, "M", "a"), + (0x2091, "M", "e"), + (0x2092, "M", "o"), + (0x2093, "M", "x"), + (0x2094, "M", "ə"), + (0x2095, "M", "h"), + (0x2096, "M", "k"), + (0x2097, "M", "l"), + (0x2098, "M", "m"), + (0x2099, "M", "n"), + (0x209A, "M", "p"), + (0x209B, "M", "s"), + (0x209C, "M", "t"), + (0x209D, "X"), + (0x20A0, "V"), + (0x20A8, "M", "rs"), + (0x20A9, "V"), + (0x20C1, "X"), + (0x20D0, "V"), + (0x20F1, "X"), + (0x2100, "M", "a/c"), + (0x2101, "M", "a/s"), + (0x2102, "M", "c"), + (0x2103, "M", "°c"), + (0x2104, "V"), + (0x2105, "M", "c/o"), + (0x2106, "M", "c/u"), + (0x2107, "M", "ɛ"), + (0x2108, "V"), + (0x2109, "M", "°f"), + (0x210A, "M", "g"), + (0x210B, "M", "h"), + (0x210F, "M", "ħ"), + (0x2110, "M", "i"), + (0x2112, "M", "l"), + (0x2114, "V"), + (0x2115, "M", "n"), + (0x2116, "M", "no"), + (0x2117, "V"), + (0x2119, "M", "p"), + (0x211A, "M", "q"), + (0x211B, "M", "r"), + (0x211E, "V"), + (0x2120, "M", "sm"), + (0x2121, "M", "tel"), + (0x2122, "M", "tm"), + (0x2123, "V"), + (0x2124, "M", "z"), + (0x2125, "V"), + (0x2126, "M", "ω"), + (0x2127, "V"), + (0x2128, "M", "z"), + (0x2129, "V"), + (0x212A, "M", "k"), + (0x212B, "M", "å"), + (0x212C, "M", "b"), + (0x212D, "M", "c"), + (0x212E, "V"), + (0x212F, "M", "e"), + (0x2131, "M", "f"), + (0x2132, "M", "ⅎ"), + (0x2133, "M", "m"), + (0x2134, "M", "o"), + (0x2135, "M", "א"), + (0x2136, "M", "ב"), + (0x2137, "M", "ג"), + (0x2138, "M", "ד"), + (0x2139, "M", "i"), + (0x213A, "V"), + (0x213B, "M", "fax"), + (0x213C, "M", "π"), + (0x213D, "M", "γ"), + (0x213F, "M", "π"), + (0x2140, "M", "∑"), + (0x2141, "V"), + (0x2145, "M", "d"), + (0x2147, "M", "e"), + (0x2148, "M", "i"), + (0x2149, "M", "j"), + (0x214A, "V"), + (0x2150, "M", "1⁄7"), + (0x2151, "M", "1⁄9"), + (0x2152, "M", "1⁄10"), + (0x2153, "M", "1⁄3"), + (0x2154, "M", "2⁄3"), + (0x2155, "M", "1⁄5"), + (0x2156, "M", "2⁄5"), + (0x2157, "M", "3⁄5"), + (0x2158, "M", "4⁄5"), + (0x2159, "M", "1⁄6"), + (0x215A, "M", "5⁄6"), + (0x215B, "M", "1⁄8"), + ] + + +def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x215C, "M", "3⁄8"), + (0x215D, "M", "5⁄8"), + (0x215E, "M", "7⁄8"), + (0x215F, "M", "1⁄"), + (0x2160, "M", "i"), + (0x2161, "M", "ii"), + (0x2162, "M", "iii"), + (0x2163, "M", "iv"), + (0x2164, "M", "v"), + (0x2165, "M", "vi"), + (0x2166, "M", "vii"), + (0x2167, "M", "viii"), + (0x2168, "M", "ix"), + (0x2169, "M", "x"), + (0x216A, "M", "xi"), + (0x216B, "M", "xii"), + (0x216C, "M", "l"), + (0x216D, "M", "c"), + (0x216E, "M", "d"), + (0x216F, "M", "m"), + (0x2170, "M", "i"), + (0x2171, "M", "ii"), + (0x2172, "M", "iii"), + (0x2173, "M", "iv"), + (0x2174, "M", "v"), + (0x2175, "M", "vi"), + (0x2176, "M", "vii"), + (0x2177, "M", "viii"), + (0x2178, "M", "ix"), + (0x2179, "M", "x"), + (0x217A, "M", "xi"), + (0x217B, "M", "xii"), + (0x217C, "M", "l"), + (0x217D, "M", "c"), + (0x217E, "M", "d"), + (0x217F, "M", "m"), + (0x2180, "V"), + (0x2183, "M", "ↄ"), + (0x2184, "V"), + (0x2189, "M", "0⁄3"), + (0x218A, "V"), + (0x218C, "X"), + (0x2190, "V"), + (0x222C, "M", "∫∫"), + (0x222D, "M", "∫∫∫"), + (0x222E, "V"), + (0x222F, "M", "∮∮"), + (0x2230, "M", "∮∮∮"), + (0x2231, "V"), + (0x2329, "M", "〈"), + (0x232A, "M", "〉"), + (0x232B, "V"), + (0x242A, "X"), + (0x2440, "V"), + (0x244B, "X"), + (0x2460, "M", "1"), + (0x2461, "M", "2"), + (0x2462, "M", "3"), + (0x2463, "M", "4"), + (0x2464, "M", "5"), + (0x2465, "M", "6"), + (0x2466, "M", "7"), + (0x2467, "M", "8"), + (0x2468, "M", "9"), + (0x2469, "M", "10"), + (0x246A, "M", "11"), + (0x246B, "M", "12"), + (0x246C, "M", "13"), + (0x246D, "M", "14"), + (0x246E, "M", "15"), + (0x246F, "M", "16"), + (0x2470, "M", "17"), + (0x2471, "M", "18"), + (0x2472, "M", "19"), + (0x2473, "M", "20"), + (0x2474, "M", "(1)"), + (0x2475, "M", "(2)"), + (0x2476, "M", "(3)"), + (0x2477, "M", "(4)"), + (0x2478, "M", "(5)"), + (0x2479, "M", "(6)"), + (0x247A, "M", "(7)"), + (0x247B, "M", "(8)"), + (0x247C, "M", "(9)"), + (0x247D, "M", "(10)"), + (0x247E, "M", "(11)"), + (0x247F, "M", "(12)"), + (0x2480, "M", "(13)"), + (0x2481, "M", "(14)"), + (0x2482, "M", "(15)"), + (0x2483, "M", "(16)"), + (0x2484, "M", "(17)"), + (0x2485, "M", "(18)"), + (0x2486, "M", "(19)"), + (0x2487, "M", "(20)"), + (0x2488, "X"), + (0x249C, "M", "(a)"), + (0x249D, "M", "(b)"), + (0x249E, "M", "(c)"), + (0x249F, "M", "(d)"), + ] + + +def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x24A0, "M", "(e)"), + (0x24A1, "M", "(f)"), + (0x24A2, "M", "(g)"), + (0x24A3, "M", "(h)"), + (0x24A4, "M", "(i)"), + (0x24A5, "M", "(j)"), + (0x24A6, "M", "(k)"), + (0x24A7, "M", "(l)"), + (0x24A8, "M", "(m)"), + (0x24A9, "M", "(n)"), + (0x24AA, "M", "(o)"), + (0x24AB, "M", "(p)"), + (0x24AC, "M", "(q)"), + (0x24AD, "M", "(r)"), + (0x24AE, "M", "(s)"), + (0x24AF, "M", "(t)"), + (0x24B0, "M", "(u)"), + (0x24B1, "M", "(v)"), + (0x24B2, "M", "(w)"), + (0x24B3, "M", "(x)"), + (0x24B4, "M", "(y)"), + (0x24B5, "M", "(z)"), + (0x24B6, "M", "a"), + (0x24B7, "M", "b"), + (0x24B8, "M", "c"), + (0x24B9, "M", "d"), + (0x24BA, "M", "e"), + (0x24BB, "M", "f"), + (0x24BC, "M", "g"), + (0x24BD, "M", "h"), + (0x24BE, "M", "i"), + (0x24BF, "M", "j"), + (0x24C0, "M", "k"), + (0x24C1, "M", "l"), + (0x24C2, "M", "m"), + (0x24C3, "M", "n"), + (0x24C4, "M", "o"), + (0x24C5, "M", "p"), + (0x24C6, "M", "q"), + (0x24C7, "M", "r"), + (0x24C8, "M", "s"), + (0x24C9, "M", "t"), + (0x24CA, "M", "u"), + (0x24CB, "M", "v"), + (0x24CC, "M", "w"), + (0x24CD, "M", "x"), + (0x24CE, "M", "y"), + (0x24CF, "M", "z"), + (0x24D0, "M", "a"), + (0x24D1, "M", "b"), + (0x24D2, "M", "c"), + (0x24D3, "M", "d"), + (0x24D4, "M", "e"), + (0x24D5, "M", "f"), + (0x24D6, "M", "g"), + (0x24D7, "M", "h"), + (0x24D8, "M", "i"), + (0x24D9, "M", "j"), + (0x24DA, "M", "k"), + (0x24DB, "M", "l"), + (0x24DC, "M", "m"), + (0x24DD, "M", "n"), + (0x24DE, "M", "o"), + (0x24DF, "M", "p"), + (0x24E0, "M", "q"), + (0x24E1, "M", "r"), + (0x24E2, "M", "s"), + (0x24E3, "M", "t"), + (0x24E4, "M", "u"), + (0x24E5, "M", "v"), + (0x24E6, "M", "w"), + (0x24E7, "M", "x"), + (0x24E8, "M", "y"), + (0x24E9, "M", "z"), + (0x24EA, "M", "0"), + (0x24EB, "V"), + (0x2A0C, "M", "∫∫∫∫"), + (0x2A0D, "V"), + (0x2A74, "M", "::="), + (0x2A75, "M", "=="), + (0x2A76, "M", "==="), + (0x2A77, "V"), + (0x2ADC, "M", "⫝̸"), + (0x2ADD, "V"), + (0x2B74, "X"), + (0x2B76, "V"), + (0x2B96, "X"), + (0x2B97, "V"), + (0x2C00, "M", "ⰰ"), + (0x2C01, "M", "ⰱ"), + (0x2C02, "M", "ⰲ"), + (0x2C03, "M", "ⰳ"), + (0x2C04, "M", "ⰴ"), + (0x2C05, "M", "ⰵ"), + (0x2C06, "M", "ⰶ"), + (0x2C07, "M", "ⰷ"), + (0x2C08, "M", "ⰸ"), + (0x2C09, "M", "ⰹ"), + (0x2C0A, "M", "ⰺ"), + (0x2C0B, "M", "ⰻ"), + ] + + +def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2C0C, "M", "ⰼ"), + (0x2C0D, "M", "ⰽ"), + (0x2C0E, "M", "ⰾ"), + (0x2C0F, "M", "ⰿ"), + (0x2C10, "M", "ⱀ"), + (0x2C11, "M", "ⱁ"), + (0x2C12, "M", "ⱂ"), + (0x2C13, "M", "ⱃ"), + (0x2C14, "M", "ⱄ"), + (0x2C15, "M", "ⱅ"), + (0x2C16, "M", "ⱆ"), + (0x2C17, "M", "ⱇ"), + (0x2C18, "M", "ⱈ"), + (0x2C19, "M", "ⱉ"), + (0x2C1A, "M", "ⱊ"), + (0x2C1B, "M", "ⱋ"), + (0x2C1C, "M", "ⱌ"), + (0x2C1D, "M", "ⱍ"), + (0x2C1E, "M", "ⱎ"), + (0x2C1F, "M", "ⱏ"), + (0x2C20, "M", "ⱐ"), + (0x2C21, "M", "ⱑ"), + (0x2C22, "M", "ⱒ"), + (0x2C23, "M", "ⱓ"), + (0x2C24, "M", "ⱔ"), + (0x2C25, "M", "ⱕ"), + (0x2C26, "M", "ⱖ"), + (0x2C27, "M", "ⱗ"), + (0x2C28, "M", "ⱘ"), + (0x2C29, "M", "ⱙ"), + (0x2C2A, "M", "ⱚ"), + (0x2C2B, "M", "ⱛ"), + (0x2C2C, "M", "ⱜ"), + (0x2C2D, "M", "ⱝ"), + (0x2C2E, "M", "ⱞ"), + (0x2C2F, "M", "ⱟ"), + (0x2C30, "V"), + (0x2C60, "M", "ⱡ"), + (0x2C61, "V"), + (0x2C62, "M", "ɫ"), + (0x2C63, "M", "ᵽ"), + (0x2C64, "M", "ɽ"), + (0x2C65, "V"), + (0x2C67, "M", "ⱨ"), + (0x2C68, "V"), + (0x2C69, "M", "ⱪ"), + (0x2C6A, "V"), + (0x2C6B, "M", "ⱬ"), + (0x2C6C, "V"), + (0x2C6D, "M", "ɑ"), + (0x2C6E, "M", "ɱ"), + (0x2C6F, "M", "ɐ"), + (0x2C70, "M", "ɒ"), + (0x2C71, "V"), + (0x2C72, "M", "ⱳ"), + (0x2C73, "V"), + (0x2C75, "M", "ⱶ"), + (0x2C76, "V"), + (0x2C7C, "M", "j"), + (0x2C7D, "M", "v"), + (0x2C7E, "M", "ȿ"), + (0x2C7F, "M", "ɀ"), + (0x2C80, "M", "ⲁ"), + (0x2C81, "V"), + (0x2C82, "M", "ⲃ"), + (0x2C83, "V"), + (0x2C84, "M", "ⲅ"), + (0x2C85, "V"), + (0x2C86, "M", "ⲇ"), + (0x2C87, "V"), + (0x2C88, "M", "ⲉ"), + (0x2C89, "V"), + (0x2C8A, "M", "ⲋ"), + (0x2C8B, "V"), + (0x2C8C, "M", "ⲍ"), + (0x2C8D, "V"), + (0x2C8E, "M", "ⲏ"), + (0x2C8F, "V"), + (0x2C90, "M", "ⲑ"), + (0x2C91, "V"), + (0x2C92, "M", "ⲓ"), + (0x2C93, "V"), + (0x2C94, "M", "ⲕ"), + (0x2C95, "V"), + (0x2C96, "M", "ⲗ"), + (0x2C97, "V"), + (0x2C98, "M", "ⲙ"), + (0x2C99, "V"), + (0x2C9A, "M", "ⲛ"), + (0x2C9B, "V"), + (0x2C9C, "M", "ⲝ"), + (0x2C9D, "V"), + (0x2C9E, "M", "ⲟ"), + (0x2C9F, "V"), + (0x2CA0, "M", "ⲡ"), + (0x2CA1, "V"), + (0x2CA2, "M", "ⲣ"), + (0x2CA3, "V"), + (0x2CA4, "M", "ⲥ"), + (0x2CA5, "V"), + ] + + +def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2CA6, "M", "ⲧ"), + (0x2CA7, "V"), + (0x2CA8, "M", "ⲩ"), + (0x2CA9, "V"), + (0x2CAA, "M", "ⲫ"), + (0x2CAB, "V"), + (0x2CAC, "M", "ⲭ"), + (0x2CAD, "V"), + (0x2CAE, "M", "ⲯ"), + (0x2CAF, "V"), + (0x2CB0, "M", "ⲱ"), + (0x2CB1, "V"), + (0x2CB2, "M", "ⲳ"), + (0x2CB3, "V"), + (0x2CB4, "M", "ⲵ"), + (0x2CB5, "V"), + (0x2CB6, "M", "ⲷ"), + (0x2CB7, "V"), + (0x2CB8, "M", "ⲹ"), + (0x2CB9, "V"), + (0x2CBA, "M", "ⲻ"), + (0x2CBB, "V"), + (0x2CBC, "M", "ⲽ"), + (0x2CBD, "V"), + (0x2CBE, "M", "ⲿ"), + (0x2CBF, "V"), + (0x2CC0, "M", "ⳁ"), + (0x2CC1, "V"), + (0x2CC2, "M", "ⳃ"), + (0x2CC3, "V"), + (0x2CC4, "M", "ⳅ"), + (0x2CC5, "V"), + (0x2CC6, "M", "ⳇ"), + (0x2CC7, "V"), + (0x2CC8, "M", "ⳉ"), + (0x2CC9, "V"), + (0x2CCA, "M", "ⳋ"), + (0x2CCB, "V"), + (0x2CCC, "M", "ⳍ"), + (0x2CCD, "V"), + (0x2CCE, "M", "ⳏ"), + (0x2CCF, "V"), + (0x2CD0, "M", "ⳑ"), + (0x2CD1, "V"), + (0x2CD2, "M", "ⳓ"), + (0x2CD3, "V"), + (0x2CD4, "M", "ⳕ"), + (0x2CD5, "V"), + (0x2CD6, "M", "ⳗ"), + (0x2CD7, "V"), + (0x2CD8, "M", "ⳙ"), + (0x2CD9, "V"), + (0x2CDA, "M", "ⳛ"), + (0x2CDB, "V"), + (0x2CDC, "M", "ⳝ"), + (0x2CDD, "V"), + (0x2CDE, "M", "ⳟ"), + (0x2CDF, "V"), + (0x2CE0, "M", "ⳡ"), + (0x2CE1, "V"), + (0x2CE2, "M", "ⳣ"), + (0x2CE3, "V"), + (0x2CEB, "M", "ⳬ"), + (0x2CEC, "V"), + (0x2CED, "M", "ⳮ"), + (0x2CEE, "V"), + (0x2CF2, "M", "ⳳ"), + (0x2CF3, "V"), + (0x2CF4, "X"), + (0x2CF9, "V"), + (0x2D26, "X"), + (0x2D27, "V"), + (0x2D28, "X"), + (0x2D2D, "V"), + (0x2D2E, "X"), + (0x2D30, "V"), + (0x2D68, "X"), + (0x2D6F, "M", "ⵡ"), + (0x2D70, "V"), + (0x2D71, "X"), + (0x2D7F, "V"), + (0x2D97, "X"), + (0x2DA0, "V"), + (0x2DA7, "X"), + (0x2DA8, "V"), + (0x2DAF, "X"), + (0x2DB0, "V"), + (0x2DB7, "X"), + (0x2DB8, "V"), + (0x2DBF, "X"), + (0x2DC0, "V"), + (0x2DC7, "X"), + (0x2DC8, "V"), + (0x2DCF, "X"), + (0x2DD0, "V"), + (0x2DD7, "X"), + (0x2DD8, "V"), + (0x2DDF, "X"), + (0x2DE0, "V"), + (0x2E5E, "X"), + ] + + +def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2E80, "V"), + (0x2E9A, "X"), + (0x2E9B, "V"), + (0x2E9F, "M", "母"), + (0x2EA0, "V"), + (0x2EF3, "M", "龟"), + (0x2EF4, "X"), + (0x2F00, "M", "一"), + (0x2F01, "M", "丨"), + (0x2F02, "M", "丶"), + (0x2F03, "M", "丿"), + (0x2F04, "M", "乙"), + (0x2F05, "M", "亅"), + (0x2F06, "M", "二"), + (0x2F07, "M", "亠"), + (0x2F08, "M", "人"), + (0x2F09, "M", "儿"), + (0x2F0A, "M", "入"), + (0x2F0B, "M", "八"), + (0x2F0C, "M", "冂"), + (0x2F0D, "M", "冖"), + (0x2F0E, "M", "冫"), + (0x2F0F, "M", "几"), + (0x2F10, "M", "凵"), + (0x2F11, "M", "刀"), + (0x2F12, "M", "力"), + (0x2F13, "M", "勹"), + (0x2F14, "M", "匕"), + (0x2F15, "M", "匚"), + (0x2F16, "M", "匸"), + (0x2F17, "M", "十"), + (0x2F18, "M", "卜"), + (0x2F19, "M", "卩"), + (0x2F1A, "M", "厂"), + (0x2F1B, "M", "厶"), + (0x2F1C, "M", "又"), + (0x2F1D, "M", "口"), + (0x2F1E, "M", "囗"), + (0x2F1F, "M", "土"), + (0x2F20, "M", "士"), + (0x2F21, "M", "夂"), + (0x2F22, "M", "夊"), + (0x2F23, "M", "夕"), + (0x2F24, "M", "大"), + (0x2F25, "M", "女"), + (0x2F26, "M", "子"), + (0x2F27, "M", "宀"), + (0x2F28, "M", "寸"), + (0x2F29, "M", "小"), + (0x2F2A, "M", "尢"), + (0x2F2B, "M", "尸"), + (0x2F2C, "M", "屮"), + (0x2F2D, "M", "山"), + (0x2F2E, "M", "巛"), + (0x2F2F, "M", "工"), + (0x2F30, "M", "己"), + (0x2F31, "M", "巾"), + (0x2F32, "M", "干"), + (0x2F33, "M", "幺"), + (0x2F34, "M", "广"), + (0x2F35, "M", "廴"), + (0x2F36, "M", "廾"), + (0x2F37, "M", "弋"), + (0x2F38, "M", "弓"), + (0x2F39, "M", "彐"), + (0x2F3A, "M", "彡"), + (0x2F3B, "M", "彳"), + (0x2F3C, "M", "心"), + (0x2F3D, "M", "戈"), + (0x2F3E, "M", "戶"), + (0x2F3F, "M", "手"), + (0x2F40, "M", "支"), + (0x2F41, "M", "攴"), + (0x2F42, "M", "文"), + (0x2F43, "M", "斗"), + (0x2F44, "M", "斤"), + (0x2F45, "M", "方"), + (0x2F46, "M", "无"), + (0x2F47, "M", "日"), + (0x2F48, "M", "曰"), + (0x2F49, "M", "月"), + (0x2F4A, "M", "木"), + (0x2F4B, "M", "欠"), + (0x2F4C, "M", "止"), + (0x2F4D, "M", "歹"), + (0x2F4E, "M", "殳"), + (0x2F4F, "M", "毋"), + (0x2F50, "M", "比"), + (0x2F51, "M", "毛"), + (0x2F52, "M", "氏"), + (0x2F53, "M", "气"), + (0x2F54, "M", "水"), + (0x2F55, "M", "火"), + (0x2F56, "M", "爪"), + (0x2F57, "M", "父"), + (0x2F58, "M", "爻"), + (0x2F59, "M", "爿"), + (0x2F5A, "M", "片"), + (0x2F5B, "M", "牙"), + (0x2F5C, "M", "牛"), + ] + + +def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F5D, "M", "犬"), + (0x2F5E, "M", "玄"), + (0x2F5F, "M", "玉"), + (0x2F60, "M", "瓜"), + (0x2F61, "M", "瓦"), + (0x2F62, "M", "甘"), + (0x2F63, "M", "生"), + (0x2F64, "M", "用"), + (0x2F65, "M", "田"), + (0x2F66, "M", "疋"), + (0x2F67, "M", "疒"), + (0x2F68, "M", "癶"), + (0x2F69, "M", "白"), + (0x2F6A, "M", "皮"), + (0x2F6B, "M", "皿"), + (0x2F6C, "M", "目"), + (0x2F6D, "M", "矛"), + (0x2F6E, "M", "矢"), + (0x2F6F, "M", "石"), + (0x2F70, "M", "示"), + (0x2F71, "M", "禸"), + (0x2F72, "M", "禾"), + (0x2F73, "M", "穴"), + (0x2F74, "M", "立"), + (0x2F75, "M", "竹"), + (0x2F76, "M", "米"), + (0x2F77, "M", "糸"), + (0x2F78, "M", "缶"), + (0x2F79, "M", "网"), + (0x2F7A, "M", "羊"), + (0x2F7B, "M", "羽"), + (0x2F7C, "M", "老"), + (0x2F7D, "M", "而"), + (0x2F7E, "M", "耒"), + (0x2F7F, "M", "耳"), + (0x2F80, "M", "聿"), + (0x2F81, "M", "肉"), + (0x2F82, "M", "臣"), + (0x2F83, "M", "自"), + (0x2F84, "M", "至"), + (0x2F85, "M", "臼"), + (0x2F86, "M", "舌"), + (0x2F87, "M", "舛"), + (0x2F88, "M", "舟"), + (0x2F89, "M", "艮"), + (0x2F8A, "M", "色"), + (0x2F8B, "M", "艸"), + (0x2F8C, "M", "虍"), + (0x2F8D, "M", "虫"), + (0x2F8E, "M", "血"), + (0x2F8F, "M", "行"), + (0x2F90, "M", "衣"), + (0x2F91, "M", "襾"), + (0x2F92, "M", "見"), + (0x2F93, "M", "角"), + (0x2F94, "M", "言"), + (0x2F95, "M", "谷"), + (0x2F96, "M", "豆"), + (0x2F97, "M", "豕"), + (0x2F98, "M", "豸"), + (0x2F99, "M", "貝"), + (0x2F9A, "M", "赤"), + (0x2F9B, "M", "走"), + (0x2F9C, "M", "足"), + (0x2F9D, "M", "身"), + (0x2F9E, "M", "車"), + (0x2F9F, "M", "辛"), + (0x2FA0, "M", "辰"), + (0x2FA1, "M", "辵"), + (0x2FA2, "M", "邑"), + (0x2FA3, "M", "酉"), + (0x2FA4, "M", "釆"), + (0x2FA5, "M", "里"), + (0x2FA6, "M", "金"), + (0x2FA7, "M", "長"), + (0x2FA8, "M", "門"), + (0x2FA9, "M", "阜"), + (0x2FAA, "M", "隶"), + (0x2FAB, "M", "隹"), + (0x2FAC, "M", "雨"), + (0x2FAD, "M", "靑"), + (0x2FAE, "M", "非"), + (0x2FAF, "M", "面"), + (0x2FB0, "M", "革"), + (0x2FB1, "M", "韋"), + (0x2FB2, "M", "韭"), + (0x2FB3, "M", "音"), + (0x2FB4, "M", "頁"), + (0x2FB5, "M", "風"), + (0x2FB6, "M", "飛"), + (0x2FB7, "M", "食"), + (0x2FB8, "M", "首"), + (0x2FB9, "M", "香"), + (0x2FBA, "M", "馬"), + (0x2FBB, "M", "骨"), + (0x2FBC, "M", "高"), + (0x2FBD, "M", "髟"), + (0x2FBE, "M", "鬥"), + (0x2FBF, "M", "鬯"), + (0x2FC0, "M", "鬲"), + ] + + +def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2FC1, "M", "鬼"), + (0x2FC2, "M", "魚"), + (0x2FC3, "M", "鳥"), + (0x2FC4, "M", "鹵"), + (0x2FC5, "M", "鹿"), + (0x2FC6, "M", "麥"), + (0x2FC7, "M", "麻"), + (0x2FC8, "M", "黃"), + (0x2FC9, "M", "黍"), + (0x2FCA, "M", "黑"), + (0x2FCB, "M", "黹"), + (0x2FCC, "M", "黽"), + (0x2FCD, "M", "鼎"), + (0x2FCE, "M", "鼓"), + (0x2FCF, "M", "鼠"), + (0x2FD0, "M", "鼻"), + (0x2FD1, "M", "齊"), + (0x2FD2, "M", "齒"), + (0x2FD3, "M", "龍"), + (0x2FD4, "M", "龜"), + (0x2FD5, "M", "龠"), + (0x2FD6, "X"), + (0x3000, "M", " "), + (0x3001, "V"), + (0x3002, "M", "."), + (0x3003, "V"), + (0x3036, "M", "〒"), + (0x3037, "V"), + (0x3038, "M", "十"), + (0x3039, "M", "卄"), + (0x303A, "M", "卅"), + (0x303B, "V"), + (0x3040, "X"), + (0x3041, "V"), + (0x3097, "X"), + (0x3099, "V"), + (0x309B, "M", " ゙"), + (0x309C, "M", " ゚"), + (0x309D, "V"), + (0x309F, "M", "より"), + (0x30A0, "V"), + (0x30FF, "M", "コト"), + (0x3100, "X"), + (0x3105, "V"), + (0x3130, "X"), + (0x3131, "M", "ᄀ"), + (0x3132, "M", "ᄁ"), + (0x3133, "M", "ᆪ"), + (0x3134, "M", "ᄂ"), + (0x3135, "M", "ᆬ"), + (0x3136, "M", "ᆭ"), + (0x3137, "M", "ᄃ"), + (0x3138, "M", "ᄄ"), + (0x3139, "M", "ᄅ"), + (0x313A, "M", "ᆰ"), + (0x313B, "M", "ᆱ"), + (0x313C, "M", "ᆲ"), + (0x313D, "M", "ᆳ"), + (0x313E, "M", "ᆴ"), + (0x313F, "M", "ᆵ"), + (0x3140, "M", "ᄚ"), + (0x3141, "M", "ᄆ"), + (0x3142, "M", "ᄇ"), + (0x3143, "M", "ᄈ"), + (0x3144, "M", "ᄡ"), + (0x3145, "M", "ᄉ"), + (0x3146, "M", "ᄊ"), + (0x3147, "M", "ᄋ"), + (0x3148, "M", "ᄌ"), + (0x3149, "M", "ᄍ"), + (0x314A, "M", "ᄎ"), + (0x314B, "M", "ᄏ"), + (0x314C, "M", "ᄐ"), + (0x314D, "M", "ᄑ"), + (0x314E, "M", "ᄒ"), + (0x314F, "M", "ᅡ"), + (0x3150, "M", "ᅢ"), + (0x3151, "M", "ᅣ"), + (0x3152, "M", "ᅤ"), + (0x3153, "M", "ᅥ"), + (0x3154, "M", "ᅦ"), + (0x3155, "M", "ᅧ"), + (0x3156, "M", "ᅨ"), + (0x3157, "M", "ᅩ"), + (0x3158, "M", "ᅪ"), + (0x3159, "M", "ᅫ"), + (0x315A, "M", "ᅬ"), + (0x315B, "M", "ᅭ"), + (0x315C, "M", "ᅮ"), + (0x315D, "M", "ᅯ"), + (0x315E, "M", "ᅰ"), + (0x315F, "M", "ᅱ"), + (0x3160, "M", "ᅲ"), + (0x3161, "M", "ᅳ"), + (0x3162, "M", "ᅴ"), + (0x3163, "M", "ᅵ"), + (0x3164, "I"), + (0x3165, "M", "ᄔ"), + (0x3166, "M", "ᄕ"), + (0x3167, "M", "ᇇ"), + ] + + +def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3168, "M", "ᇈ"), + (0x3169, "M", "ᇌ"), + (0x316A, "M", "ᇎ"), + (0x316B, "M", "ᇓ"), + (0x316C, "M", "ᇗ"), + (0x316D, "M", "ᇙ"), + (0x316E, "M", "ᄜ"), + (0x316F, "M", "ᇝ"), + (0x3170, "M", "ᇟ"), + (0x3171, "M", "ᄝ"), + (0x3172, "M", "ᄞ"), + (0x3173, "M", "ᄠ"), + (0x3174, "M", "ᄢ"), + (0x3175, "M", "ᄣ"), + (0x3176, "M", "ᄧ"), + (0x3177, "M", "ᄩ"), + (0x3178, "M", "ᄫ"), + (0x3179, "M", "ᄬ"), + (0x317A, "M", "ᄭ"), + (0x317B, "M", "ᄮ"), + (0x317C, "M", "ᄯ"), + (0x317D, "M", "ᄲ"), + (0x317E, "M", "ᄶ"), + (0x317F, "M", "ᅀ"), + (0x3180, "M", "ᅇ"), + (0x3181, "M", "ᅌ"), + (0x3182, "M", "ᇱ"), + (0x3183, "M", "ᇲ"), + (0x3184, "M", "ᅗ"), + (0x3185, "M", "ᅘ"), + (0x3186, "M", "ᅙ"), + (0x3187, "M", "ᆄ"), + (0x3188, "M", "ᆅ"), + (0x3189, "M", "ᆈ"), + (0x318A, "M", "ᆑ"), + (0x318B, "M", "ᆒ"), + (0x318C, "M", "ᆔ"), + (0x318D, "M", "ᆞ"), + (0x318E, "M", "ᆡ"), + (0x318F, "X"), + (0x3190, "V"), + (0x3192, "M", "一"), + (0x3193, "M", "二"), + (0x3194, "M", "三"), + (0x3195, "M", "四"), + (0x3196, "M", "上"), + (0x3197, "M", "中"), + (0x3198, "M", "下"), + (0x3199, "M", "甲"), + (0x319A, "M", "乙"), + (0x319B, "M", "丙"), + (0x319C, "M", "丁"), + (0x319D, "M", "天"), + (0x319E, "M", "地"), + (0x319F, "M", "人"), + (0x31A0, "V"), + (0x31E6, "X"), + (0x31F0, "V"), + (0x3200, "M", "(ᄀ)"), + (0x3201, "M", "(ᄂ)"), + (0x3202, "M", "(ᄃ)"), + (0x3203, "M", "(ᄅ)"), + (0x3204, "M", "(ᄆ)"), + (0x3205, "M", "(ᄇ)"), + (0x3206, "M", "(ᄉ)"), + (0x3207, "M", "(ᄋ)"), + (0x3208, "M", "(ᄌ)"), + (0x3209, "M", "(ᄎ)"), + (0x320A, "M", "(ᄏ)"), + (0x320B, "M", "(ᄐ)"), + (0x320C, "M", "(ᄑ)"), + (0x320D, "M", "(ᄒ)"), + (0x320E, "M", "(가)"), + (0x320F, "M", "(나)"), + (0x3210, "M", "(다)"), + (0x3211, "M", "(라)"), + (0x3212, "M", "(마)"), + (0x3213, "M", "(바)"), + (0x3214, "M", "(사)"), + (0x3215, "M", "(아)"), + (0x3216, "M", "(자)"), + (0x3217, "M", "(차)"), + (0x3218, "M", "(카)"), + (0x3219, "M", "(타)"), + (0x321A, "M", "(파)"), + (0x321B, "M", "(하)"), + (0x321C, "M", "(주)"), + (0x321D, "M", "(오전)"), + (0x321E, "M", "(오후)"), + (0x321F, "X"), + (0x3220, "M", "(一)"), + (0x3221, "M", "(二)"), + (0x3222, "M", "(三)"), + (0x3223, "M", "(四)"), + (0x3224, "M", "(五)"), + (0x3225, "M", "(六)"), + (0x3226, "M", "(七)"), + (0x3227, "M", "(八)"), + (0x3228, "M", "(九)"), + (0x3229, "M", "(十)"), + ] + + +def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x322A, "M", "(月)"), + (0x322B, "M", "(火)"), + (0x322C, "M", "(水)"), + (0x322D, "M", "(木)"), + (0x322E, "M", "(金)"), + (0x322F, "M", "(土)"), + (0x3230, "M", "(日)"), + (0x3231, "M", "(株)"), + (0x3232, "M", "(有)"), + (0x3233, "M", "(社)"), + (0x3234, "M", "(名)"), + (0x3235, "M", "(特)"), + (0x3236, "M", "(財)"), + (0x3237, "M", "(祝)"), + (0x3238, "M", "(労)"), + (0x3239, "M", "(代)"), + (0x323A, "M", "(呼)"), + (0x323B, "M", "(学)"), + (0x323C, "M", "(監)"), + (0x323D, "M", "(企)"), + (0x323E, "M", "(資)"), + (0x323F, "M", "(協)"), + (0x3240, "M", "(祭)"), + (0x3241, "M", "(休)"), + (0x3242, "M", "(自)"), + (0x3243, "M", "(至)"), + (0x3244, "M", "問"), + (0x3245, "M", "幼"), + (0x3246, "M", "文"), + (0x3247, "M", "箏"), + (0x3248, "V"), + (0x3250, "M", "pte"), + (0x3251, "M", "21"), + (0x3252, "M", "22"), + (0x3253, "M", "23"), + (0x3254, "M", "24"), + (0x3255, "M", "25"), + (0x3256, "M", "26"), + (0x3257, "M", "27"), + (0x3258, "M", "28"), + (0x3259, "M", "29"), + (0x325A, "M", "30"), + (0x325B, "M", "31"), + (0x325C, "M", "32"), + (0x325D, "M", "33"), + (0x325E, "M", "34"), + (0x325F, "M", "35"), + (0x3260, "M", "ᄀ"), + (0x3261, "M", "ᄂ"), + (0x3262, "M", "ᄃ"), + (0x3263, "M", "ᄅ"), + (0x3264, "M", "ᄆ"), + (0x3265, "M", "ᄇ"), + (0x3266, "M", "ᄉ"), + (0x3267, "M", "ᄋ"), + (0x3268, "M", "ᄌ"), + (0x3269, "M", "ᄎ"), + (0x326A, "M", "ᄏ"), + (0x326B, "M", "ᄐ"), + (0x326C, "M", "ᄑ"), + (0x326D, "M", "ᄒ"), + (0x326E, "M", "가"), + (0x326F, "M", "나"), + (0x3270, "M", "다"), + (0x3271, "M", "라"), + (0x3272, "M", "마"), + (0x3273, "M", "바"), + (0x3274, "M", "사"), + (0x3275, "M", "아"), + (0x3276, "M", "자"), + (0x3277, "M", "차"), + (0x3278, "M", "카"), + (0x3279, "M", "타"), + (0x327A, "M", "파"), + (0x327B, "M", "하"), + (0x327C, "M", "참고"), + (0x327D, "M", "주의"), + (0x327E, "M", "우"), + (0x327F, "V"), + (0x3280, "M", "一"), + (0x3281, "M", "二"), + (0x3282, "M", "三"), + (0x3283, "M", "四"), + (0x3284, "M", "五"), + (0x3285, "M", "六"), + (0x3286, "M", "七"), + (0x3287, "M", "八"), + (0x3288, "M", "九"), + (0x3289, "M", "十"), + (0x328A, "M", "月"), + (0x328B, "M", "火"), + (0x328C, "M", "水"), + (0x328D, "M", "木"), + (0x328E, "M", "金"), + (0x328F, "M", "土"), + (0x3290, "M", "日"), + (0x3291, "M", "株"), + (0x3292, "M", "有"), + (0x3293, "M", "社"), + (0x3294, "M", "名"), + ] + + +def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3295, "M", "特"), + (0x3296, "M", "財"), + (0x3297, "M", "祝"), + (0x3298, "M", "労"), + (0x3299, "M", "秘"), + (0x329A, "M", "男"), + (0x329B, "M", "女"), + (0x329C, "M", "適"), + (0x329D, "M", "優"), + (0x329E, "M", "印"), + (0x329F, "M", "注"), + (0x32A0, "M", "項"), + (0x32A1, "M", "休"), + (0x32A2, "M", "写"), + (0x32A3, "M", "正"), + (0x32A4, "M", "上"), + (0x32A5, "M", "中"), + (0x32A6, "M", "下"), + (0x32A7, "M", "左"), + (0x32A8, "M", "右"), + (0x32A9, "M", "医"), + (0x32AA, "M", "宗"), + (0x32AB, "M", "学"), + (0x32AC, "M", "監"), + (0x32AD, "M", "企"), + (0x32AE, "M", "資"), + (0x32AF, "M", "協"), + (0x32B0, "M", "夜"), + (0x32B1, "M", "36"), + (0x32B2, "M", "37"), + (0x32B3, "M", "38"), + (0x32B4, "M", "39"), + (0x32B5, "M", "40"), + (0x32B6, "M", "41"), + (0x32B7, "M", "42"), + (0x32B8, "M", "43"), + (0x32B9, "M", "44"), + (0x32BA, "M", "45"), + (0x32BB, "M", "46"), + (0x32BC, "M", "47"), + (0x32BD, "M", "48"), + (0x32BE, "M", "49"), + (0x32BF, "M", "50"), + (0x32C0, "M", "1月"), + (0x32C1, "M", "2月"), + (0x32C2, "M", "3月"), + (0x32C3, "M", "4月"), + (0x32C4, "M", "5月"), + (0x32C5, "M", "6月"), + (0x32C6, "M", "7月"), + (0x32C7, "M", "8月"), + (0x32C8, "M", "9月"), + (0x32C9, "M", "10月"), + (0x32CA, "M", "11月"), + (0x32CB, "M", "12月"), + (0x32CC, "M", "hg"), + (0x32CD, "M", "erg"), + (0x32CE, "M", "ev"), + (0x32CF, "M", "ltd"), + (0x32D0, "M", "ア"), + (0x32D1, "M", "イ"), + (0x32D2, "M", "ウ"), + (0x32D3, "M", "エ"), + (0x32D4, "M", "オ"), + (0x32D5, "M", "カ"), + (0x32D6, "M", "キ"), + (0x32D7, "M", "ク"), + (0x32D8, "M", "ケ"), + (0x32D9, "M", "コ"), + (0x32DA, "M", "サ"), + (0x32DB, "M", "シ"), + (0x32DC, "M", "ス"), + (0x32DD, "M", "セ"), + (0x32DE, "M", "ソ"), + (0x32DF, "M", "タ"), + (0x32E0, "M", "チ"), + (0x32E1, "M", "ツ"), + (0x32E2, "M", "テ"), + (0x32E3, "M", "ト"), + (0x32E4, "M", "ナ"), + (0x32E5, "M", "ニ"), + (0x32E6, "M", "ヌ"), + (0x32E7, "M", "ネ"), + (0x32E8, "M", "ノ"), + (0x32E9, "M", "ハ"), + (0x32EA, "M", "ヒ"), + (0x32EB, "M", "フ"), + (0x32EC, "M", "ヘ"), + (0x32ED, "M", "ホ"), + (0x32EE, "M", "マ"), + (0x32EF, "M", "ミ"), + (0x32F0, "M", "ム"), + (0x32F1, "M", "メ"), + (0x32F2, "M", "モ"), + (0x32F3, "M", "ヤ"), + (0x32F4, "M", "ユ"), + (0x32F5, "M", "ヨ"), + (0x32F6, "M", "ラ"), + (0x32F7, "M", "リ"), + (0x32F8, "M", "ル"), + ] + + +def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x32F9, "M", "レ"), + (0x32FA, "M", "ロ"), + (0x32FB, "M", "ワ"), + (0x32FC, "M", "ヰ"), + (0x32FD, "M", "ヱ"), + (0x32FE, "M", "ヲ"), + (0x32FF, "M", "令和"), + (0x3300, "M", "アパート"), + (0x3301, "M", "アルファ"), + (0x3302, "M", "アンペア"), + (0x3303, "M", "アール"), + (0x3304, "M", "イニング"), + (0x3305, "M", "インチ"), + (0x3306, "M", "ウォン"), + (0x3307, "M", "エスクード"), + (0x3308, "M", "エーカー"), + (0x3309, "M", "オンス"), + (0x330A, "M", "オーム"), + (0x330B, "M", "カイリ"), + (0x330C, "M", "カラット"), + (0x330D, "M", "カロリー"), + (0x330E, "M", "ガロン"), + (0x330F, "M", "ガンマ"), + (0x3310, "M", "ギガ"), + (0x3311, "M", "ギニー"), + (0x3312, "M", "キュリー"), + (0x3313, "M", "ギルダー"), + (0x3314, "M", "キロ"), + (0x3315, "M", "キログラム"), + (0x3316, "M", "キロメートル"), + (0x3317, "M", "キロワット"), + (0x3318, "M", "グラム"), + (0x3319, "M", "グラムトン"), + (0x331A, "M", "クルゼイロ"), + (0x331B, "M", "クローネ"), + (0x331C, "M", "ケース"), + (0x331D, "M", "コルナ"), + (0x331E, "M", "コーポ"), + (0x331F, "M", "サイクル"), + (0x3320, "M", "サンチーム"), + (0x3321, "M", "シリング"), + (0x3322, "M", "センチ"), + (0x3323, "M", "セント"), + (0x3324, "M", "ダース"), + (0x3325, "M", "デシ"), + (0x3326, "M", "ドル"), + (0x3327, "M", "トン"), + (0x3328, "M", "ナノ"), + (0x3329, "M", "ノット"), + (0x332A, "M", "ハイツ"), + (0x332B, "M", "パーセント"), + (0x332C, "M", "パーツ"), + (0x332D, "M", "バーレル"), + (0x332E, "M", "ピアストル"), + (0x332F, "M", "ピクル"), + (0x3330, "M", "ピコ"), + (0x3331, "M", "ビル"), + (0x3332, "M", "ファラッド"), + (0x3333, "M", "フィート"), + (0x3334, "M", "ブッシェル"), + (0x3335, "M", "フラン"), + (0x3336, "M", "ヘクタール"), + (0x3337, "M", "ペソ"), + (0x3338, "M", "ペニヒ"), + (0x3339, "M", "ヘルツ"), + (0x333A, "M", "ペンス"), + (0x333B, "M", "ページ"), + (0x333C, "M", "ベータ"), + (0x333D, "M", "ポイント"), + (0x333E, "M", "ボルト"), + (0x333F, "M", "ホン"), + (0x3340, "M", "ポンド"), + (0x3341, "M", "ホール"), + (0x3342, "M", "ホーン"), + (0x3343, "M", "マイクロ"), + (0x3344, "M", "マイル"), + (0x3345, "M", "マッハ"), + (0x3346, "M", "マルク"), + (0x3347, "M", "マンション"), + (0x3348, "M", "ミクロン"), + (0x3349, "M", "ミリ"), + (0x334A, "M", "ミリバール"), + (0x334B, "M", "メガ"), + (0x334C, "M", "メガトン"), + (0x334D, "M", "メートル"), + (0x334E, "M", "ヤード"), + (0x334F, "M", "ヤール"), + (0x3350, "M", "ユアン"), + (0x3351, "M", "リットル"), + (0x3352, "M", "リラ"), + (0x3353, "M", "ルピー"), + (0x3354, "M", "ルーブル"), + (0x3355, "M", "レム"), + (0x3356, "M", "レントゲン"), + (0x3357, "M", "ワット"), + (0x3358, "M", "0点"), + (0x3359, "M", "1点"), + (0x335A, "M", "2点"), + (0x335B, "M", "3点"), + (0x335C, "M", "4点"), + ] + + +def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x335D, "M", "5点"), + (0x335E, "M", "6点"), + (0x335F, "M", "7点"), + (0x3360, "M", "8点"), + (0x3361, "M", "9点"), + (0x3362, "M", "10点"), + (0x3363, "M", "11点"), + (0x3364, "M", "12点"), + (0x3365, "M", "13点"), + (0x3366, "M", "14点"), + (0x3367, "M", "15点"), + (0x3368, "M", "16点"), + (0x3369, "M", "17点"), + (0x336A, "M", "18点"), + (0x336B, "M", "19点"), + (0x336C, "M", "20点"), + (0x336D, "M", "21点"), + (0x336E, "M", "22点"), + (0x336F, "M", "23点"), + (0x3370, "M", "24点"), + (0x3371, "M", "hpa"), + (0x3372, "M", "da"), + (0x3373, "M", "au"), + (0x3374, "M", "bar"), + (0x3375, "M", "ov"), + (0x3376, "M", "pc"), + (0x3377, "M", "dm"), + (0x3378, "M", "dm2"), + (0x3379, "M", "dm3"), + (0x337A, "M", "iu"), + (0x337B, "M", "平成"), + (0x337C, "M", "昭和"), + (0x337D, "M", "大正"), + (0x337E, "M", "明治"), + (0x337F, "M", "株式会社"), + (0x3380, "M", "pa"), + (0x3381, "M", "na"), + (0x3382, "M", "μa"), + (0x3383, "M", "ma"), + (0x3384, "M", "ka"), + (0x3385, "M", "kb"), + (0x3386, "M", "mb"), + (0x3387, "M", "gb"), + (0x3388, "M", "cal"), + (0x3389, "M", "kcal"), + (0x338A, "M", "pf"), + (0x338B, "M", "nf"), + (0x338C, "M", "μf"), + (0x338D, "M", "μg"), + (0x338E, "M", "mg"), + (0x338F, "M", "kg"), + (0x3390, "M", "hz"), + (0x3391, "M", "khz"), + (0x3392, "M", "mhz"), + (0x3393, "M", "ghz"), + (0x3394, "M", "thz"), + (0x3395, "M", "μl"), + (0x3396, "M", "ml"), + (0x3397, "M", "dl"), + (0x3398, "M", "kl"), + (0x3399, "M", "fm"), + (0x339A, "M", "nm"), + (0x339B, "M", "μm"), + (0x339C, "M", "mm"), + (0x339D, "M", "cm"), + (0x339E, "M", "km"), + (0x339F, "M", "mm2"), + (0x33A0, "M", "cm2"), + (0x33A1, "M", "m2"), + (0x33A2, "M", "km2"), + (0x33A3, "M", "mm3"), + (0x33A4, "M", "cm3"), + (0x33A5, "M", "m3"), + (0x33A6, "M", "km3"), + (0x33A7, "M", "m∕s"), + (0x33A8, "M", "m∕s2"), + (0x33A9, "M", "pa"), + (0x33AA, "M", "kpa"), + (0x33AB, "M", "mpa"), + (0x33AC, "M", "gpa"), + (0x33AD, "M", "rad"), + (0x33AE, "M", "rad∕s"), + (0x33AF, "M", "rad∕s2"), + (0x33B0, "M", "ps"), + (0x33B1, "M", "ns"), + (0x33B2, "M", "μs"), + (0x33B3, "M", "ms"), + (0x33B4, "M", "pv"), + (0x33B5, "M", "nv"), + (0x33B6, "M", "μv"), + (0x33B7, "M", "mv"), + (0x33B8, "M", "kv"), + (0x33B9, "M", "mv"), + (0x33BA, "M", "pw"), + (0x33BB, "M", "nw"), + (0x33BC, "M", "μw"), + (0x33BD, "M", "mw"), + (0x33BE, "M", "kw"), + (0x33BF, "M", "mw"), + (0x33C0, "M", "kω"), + ] + + +def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x33C1, "M", "mω"), + (0x33C2, "X"), + (0x33C3, "M", "bq"), + (0x33C4, "M", "cc"), + (0x33C5, "M", "cd"), + (0x33C6, "M", "c∕kg"), + (0x33C7, "X"), + (0x33C8, "M", "db"), + (0x33C9, "M", "gy"), + (0x33CA, "M", "ha"), + (0x33CB, "M", "hp"), + (0x33CC, "M", "in"), + (0x33CD, "M", "kk"), + (0x33CE, "M", "km"), + (0x33CF, "M", "kt"), + (0x33D0, "M", "lm"), + (0x33D1, "M", "ln"), + (0x33D2, "M", "log"), + (0x33D3, "M", "lx"), + (0x33D4, "M", "mb"), + (0x33D5, "M", "mil"), + (0x33D6, "M", "mol"), + (0x33D7, "M", "ph"), + (0x33D8, "X"), + (0x33D9, "M", "ppm"), + (0x33DA, "M", "pr"), + (0x33DB, "M", "sr"), + (0x33DC, "M", "sv"), + (0x33DD, "M", "wb"), + (0x33DE, "M", "v∕m"), + (0x33DF, "M", "a∕m"), + (0x33E0, "M", "1日"), + (0x33E1, "M", "2日"), + (0x33E2, "M", "3日"), + (0x33E3, "M", "4日"), + (0x33E4, "M", "5日"), + (0x33E5, "M", "6日"), + (0x33E6, "M", "7日"), + (0x33E7, "M", "8日"), + (0x33E8, "M", "9日"), + (0x33E9, "M", "10日"), + (0x33EA, "M", "11日"), + (0x33EB, "M", "12日"), + (0x33EC, "M", "13日"), + (0x33ED, "M", "14日"), + (0x33EE, "M", "15日"), + (0x33EF, "M", "16日"), + (0x33F0, "M", "17日"), + (0x33F1, "M", "18日"), + (0x33F2, "M", "19日"), + (0x33F3, "M", "20日"), + (0x33F4, "M", "21日"), + (0x33F5, "M", "22日"), + (0x33F6, "M", "23日"), + (0x33F7, "M", "24日"), + (0x33F8, "M", "25日"), + (0x33F9, "M", "26日"), + (0x33FA, "M", "27日"), + (0x33FB, "M", "28日"), + (0x33FC, "M", "29日"), + (0x33FD, "M", "30日"), + (0x33FE, "M", "31日"), + (0x33FF, "M", "gal"), + (0x3400, "V"), + (0xA48D, "X"), + (0xA490, "V"), + (0xA4C7, "X"), + (0xA4D0, "V"), + (0xA62C, "X"), + (0xA640, "M", "ꙁ"), + (0xA641, "V"), + (0xA642, "M", "ꙃ"), + (0xA643, "V"), + (0xA644, "M", "ꙅ"), + (0xA645, "V"), + (0xA646, "M", "ꙇ"), + (0xA647, "V"), + (0xA648, "M", "ꙉ"), + (0xA649, "V"), + (0xA64A, "M", "ꙋ"), + (0xA64B, "V"), + (0xA64C, "M", "ꙍ"), + (0xA64D, "V"), + (0xA64E, "M", "ꙏ"), + (0xA64F, "V"), + (0xA650, "M", "ꙑ"), + (0xA651, "V"), + (0xA652, "M", "ꙓ"), + (0xA653, "V"), + (0xA654, "M", "ꙕ"), + (0xA655, "V"), + (0xA656, "M", "ꙗ"), + (0xA657, "V"), + (0xA658, "M", "ꙙ"), + (0xA659, "V"), + (0xA65A, "M", "ꙛ"), + (0xA65B, "V"), + (0xA65C, "M", "ꙝ"), + (0xA65D, "V"), + (0xA65E, "M", "ꙟ"), + ] + + +def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA65F, "V"), + (0xA660, "M", "ꙡ"), + (0xA661, "V"), + (0xA662, "M", "ꙣ"), + (0xA663, "V"), + (0xA664, "M", "ꙥ"), + (0xA665, "V"), + (0xA666, "M", "ꙧ"), + (0xA667, "V"), + (0xA668, "M", "ꙩ"), + (0xA669, "V"), + (0xA66A, "M", "ꙫ"), + (0xA66B, "V"), + (0xA66C, "M", "ꙭ"), + (0xA66D, "V"), + (0xA680, "M", "ꚁ"), + (0xA681, "V"), + (0xA682, "M", "ꚃ"), + (0xA683, "V"), + (0xA684, "M", "ꚅ"), + (0xA685, "V"), + (0xA686, "M", "ꚇ"), + (0xA687, "V"), + (0xA688, "M", "ꚉ"), + (0xA689, "V"), + (0xA68A, "M", "ꚋ"), + (0xA68B, "V"), + (0xA68C, "M", "ꚍ"), + (0xA68D, "V"), + (0xA68E, "M", "ꚏ"), + (0xA68F, "V"), + (0xA690, "M", "ꚑ"), + (0xA691, "V"), + (0xA692, "M", "ꚓ"), + (0xA693, "V"), + (0xA694, "M", "ꚕ"), + (0xA695, "V"), + (0xA696, "M", "ꚗ"), + (0xA697, "V"), + (0xA698, "M", "ꚙ"), + (0xA699, "V"), + (0xA69A, "M", "ꚛ"), + (0xA69B, "V"), + (0xA69C, "M", "ъ"), + (0xA69D, "M", "ь"), + (0xA69E, "V"), + (0xA6F8, "X"), + (0xA700, "V"), + (0xA722, "M", "ꜣ"), + (0xA723, "V"), + (0xA724, "M", "ꜥ"), + (0xA725, "V"), + (0xA726, "M", "ꜧ"), + (0xA727, "V"), + (0xA728, "M", "ꜩ"), + (0xA729, "V"), + (0xA72A, "M", "ꜫ"), + (0xA72B, "V"), + (0xA72C, "M", "ꜭ"), + (0xA72D, "V"), + (0xA72E, "M", "ꜯ"), + (0xA72F, "V"), + (0xA732, "M", "ꜳ"), + (0xA733, "V"), + (0xA734, "M", "ꜵ"), + (0xA735, "V"), + (0xA736, "M", "ꜷ"), + (0xA737, "V"), + (0xA738, "M", "ꜹ"), + (0xA739, "V"), + (0xA73A, "M", "ꜻ"), + (0xA73B, "V"), + (0xA73C, "M", "ꜽ"), + (0xA73D, "V"), + (0xA73E, "M", "ꜿ"), + (0xA73F, "V"), + (0xA740, "M", "ꝁ"), + (0xA741, "V"), + (0xA742, "M", "ꝃ"), + (0xA743, "V"), + (0xA744, "M", "ꝅ"), + (0xA745, "V"), + (0xA746, "M", "ꝇ"), + (0xA747, "V"), + (0xA748, "M", "ꝉ"), + (0xA749, "V"), + (0xA74A, "M", "ꝋ"), + (0xA74B, "V"), + (0xA74C, "M", "ꝍ"), + (0xA74D, "V"), + (0xA74E, "M", "ꝏ"), + (0xA74F, "V"), + (0xA750, "M", "ꝑ"), + (0xA751, "V"), + (0xA752, "M", "ꝓ"), + (0xA753, "V"), + (0xA754, "M", "ꝕ"), + (0xA755, "V"), + (0xA756, "M", "ꝗ"), + (0xA757, "V"), + ] + + +def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA758, "M", "ꝙ"), + (0xA759, "V"), + (0xA75A, "M", "ꝛ"), + (0xA75B, "V"), + (0xA75C, "M", "ꝝ"), + (0xA75D, "V"), + (0xA75E, "M", "ꝟ"), + (0xA75F, "V"), + (0xA760, "M", "ꝡ"), + (0xA761, "V"), + (0xA762, "M", "ꝣ"), + (0xA763, "V"), + (0xA764, "M", "ꝥ"), + (0xA765, "V"), + (0xA766, "M", "ꝧ"), + (0xA767, "V"), + (0xA768, "M", "ꝩ"), + (0xA769, "V"), + (0xA76A, "M", "ꝫ"), + (0xA76B, "V"), + (0xA76C, "M", "ꝭ"), + (0xA76D, "V"), + (0xA76E, "M", "ꝯ"), + (0xA76F, "V"), + (0xA770, "M", "ꝯ"), + (0xA771, "V"), + (0xA779, "M", "ꝺ"), + (0xA77A, "V"), + (0xA77B, "M", "ꝼ"), + (0xA77C, "V"), + (0xA77D, "M", "ᵹ"), + (0xA77E, "M", "ꝿ"), + (0xA77F, "V"), + (0xA780, "M", "ꞁ"), + (0xA781, "V"), + (0xA782, "M", "ꞃ"), + (0xA783, "V"), + (0xA784, "M", "ꞅ"), + (0xA785, "V"), + (0xA786, "M", "ꞇ"), + (0xA787, "V"), + (0xA78B, "M", "ꞌ"), + (0xA78C, "V"), + (0xA78D, "M", "ɥ"), + (0xA78E, "V"), + (0xA790, "M", "ꞑ"), + (0xA791, "V"), + (0xA792, "M", "ꞓ"), + (0xA793, "V"), + (0xA796, "M", "ꞗ"), + (0xA797, "V"), + (0xA798, "M", "ꞙ"), + (0xA799, "V"), + (0xA79A, "M", "ꞛ"), + (0xA79B, "V"), + (0xA79C, "M", "ꞝ"), + (0xA79D, "V"), + (0xA79E, "M", "ꞟ"), + (0xA79F, "V"), + (0xA7A0, "M", "ꞡ"), + (0xA7A1, "V"), + (0xA7A2, "M", "ꞣ"), + (0xA7A3, "V"), + (0xA7A4, "M", "ꞥ"), + (0xA7A5, "V"), + (0xA7A6, "M", "ꞧ"), + (0xA7A7, "V"), + (0xA7A8, "M", "ꞩ"), + (0xA7A9, "V"), + (0xA7AA, "M", "ɦ"), + (0xA7AB, "M", "ɜ"), + (0xA7AC, "M", "ɡ"), + (0xA7AD, "M", "ɬ"), + (0xA7AE, "M", "ɪ"), + (0xA7AF, "V"), + (0xA7B0, "M", "ʞ"), + (0xA7B1, "M", "ʇ"), + (0xA7B2, "M", "ʝ"), + (0xA7B3, "M", "ꭓ"), + (0xA7B4, "M", "ꞵ"), + (0xA7B5, "V"), + (0xA7B6, "M", "ꞷ"), + (0xA7B7, "V"), + (0xA7B8, "M", "ꞹ"), + (0xA7B9, "V"), + (0xA7BA, "M", "ꞻ"), + (0xA7BB, "V"), + (0xA7BC, "M", "ꞽ"), + (0xA7BD, "V"), + (0xA7BE, "M", "ꞿ"), + (0xA7BF, "V"), + (0xA7C0, "M", "ꟁ"), + (0xA7C1, "V"), + (0xA7C2, "M", "ꟃ"), + (0xA7C3, "V"), + (0xA7C4, "M", "ꞔ"), + (0xA7C5, "M", "ʂ"), + (0xA7C6, "M", "ᶎ"), + (0xA7C7, "M", "ꟈ"), + (0xA7C8, "V"), + ] + + +def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA7C9, "M", "ꟊ"), + (0xA7CA, "V"), + (0xA7CB, "M", "ɤ"), + (0xA7CC, "M", "ꟍ"), + (0xA7CD, "V"), + (0xA7CE, "X"), + (0xA7D0, "M", "ꟑ"), + (0xA7D1, "V"), + (0xA7D2, "X"), + (0xA7D3, "V"), + (0xA7D4, "X"), + (0xA7D5, "V"), + (0xA7D6, "M", "ꟗ"), + (0xA7D7, "V"), + (0xA7D8, "M", "ꟙ"), + (0xA7D9, "V"), + (0xA7DA, "M", "ꟛ"), + (0xA7DB, "V"), + (0xA7DC, "M", "ƛ"), + (0xA7DD, "X"), + (0xA7F2, "M", "c"), + (0xA7F3, "M", "f"), + (0xA7F4, "M", "q"), + (0xA7F5, "M", "ꟶ"), + (0xA7F6, "V"), + (0xA7F8, "M", "ħ"), + (0xA7F9, "M", "œ"), + (0xA7FA, "V"), + (0xA82D, "X"), + (0xA830, "V"), + (0xA83A, "X"), + (0xA840, "V"), + (0xA878, "X"), + (0xA880, "V"), + (0xA8C6, "X"), + (0xA8CE, "V"), + (0xA8DA, "X"), + (0xA8E0, "V"), + (0xA954, "X"), + (0xA95F, "V"), + (0xA97D, "X"), + (0xA980, "V"), + (0xA9CE, "X"), + (0xA9CF, "V"), + (0xA9DA, "X"), + (0xA9DE, "V"), + (0xA9FF, "X"), + (0xAA00, "V"), + (0xAA37, "X"), + (0xAA40, "V"), + (0xAA4E, "X"), + (0xAA50, "V"), + (0xAA5A, "X"), + (0xAA5C, "V"), + (0xAAC3, "X"), + (0xAADB, "V"), + (0xAAF7, "X"), + (0xAB01, "V"), + (0xAB07, "X"), + (0xAB09, "V"), + (0xAB0F, "X"), + (0xAB11, "V"), + (0xAB17, "X"), + (0xAB20, "V"), + (0xAB27, "X"), + (0xAB28, "V"), + (0xAB2F, "X"), + (0xAB30, "V"), + (0xAB5C, "M", "ꜧ"), + (0xAB5D, "M", "ꬷ"), + (0xAB5E, "M", "ɫ"), + (0xAB5F, "M", "ꭒ"), + (0xAB60, "V"), + (0xAB69, "M", "ʍ"), + (0xAB6A, "V"), + (0xAB6C, "X"), + (0xAB70, "M", "Ꭰ"), + (0xAB71, "M", "Ꭱ"), + (0xAB72, "M", "Ꭲ"), + (0xAB73, "M", "Ꭳ"), + (0xAB74, "M", "Ꭴ"), + (0xAB75, "M", "Ꭵ"), + (0xAB76, "M", "Ꭶ"), + (0xAB77, "M", "Ꭷ"), + (0xAB78, "M", "Ꭸ"), + (0xAB79, "M", "Ꭹ"), + (0xAB7A, "M", "Ꭺ"), + (0xAB7B, "M", "Ꭻ"), + (0xAB7C, "M", "Ꭼ"), + (0xAB7D, "M", "Ꭽ"), + (0xAB7E, "M", "Ꭾ"), + (0xAB7F, "M", "Ꭿ"), + (0xAB80, "M", "Ꮀ"), + (0xAB81, "M", "Ꮁ"), + (0xAB82, "M", "Ꮂ"), + (0xAB83, "M", "Ꮃ"), + (0xAB84, "M", "Ꮄ"), + (0xAB85, "M", "Ꮅ"), + (0xAB86, "M", "Ꮆ"), + (0xAB87, "M", "Ꮇ"), + ] + + +def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xAB88, "M", "Ꮈ"), + (0xAB89, "M", "Ꮉ"), + (0xAB8A, "M", "Ꮊ"), + (0xAB8B, "M", "Ꮋ"), + (0xAB8C, "M", "Ꮌ"), + (0xAB8D, "M", "Ꮍ"), + (0xAB8E, "M", "Ꮎ"), + (0xAB8F, "M", "Ꮏ"), + (0xAB90, "M", "Ꮐ"), + (0xAB91, "M", "Ꮑ"), + (0xAB92, "M", "Ꮒ"), + (0xAB93, "M", "Ꮓ"), + (0xAB94, "M", "Ꮔ"), + (0xAB95, "M", "Ꮕ"), + (0xAB96, "M", "Ꮖ"), + (0xAB97, "M", "Ꮗ"), + (0xAB98, "M", "Ꮘ"), + (0xAB99, "M", "Ꮙ"), + (0xAB9A, "M", "Ꮚ"), + (0xAB9B, "M", "Ꮛ"), + (0xAB9C, "M", "Ꮜ"), + (0xAB9D, "M", "Ꮝ"), + (0xAB9E, "M", "Ꮞ"), + (0xAB9F, "M", "Ꮟ"), + (0xABA0, "M", "Ꮠ"), + (0xABA1, "M", "Ꮡ"), + (0xABA2, "M", "Ꮢ"), + (0xABA3, "M", "Ꮣ"), + (0xABA4, "M", "Ꮤ"), + (0xABA5, "M", "Ꮥ"), + (0xABA6, "M", "Ꮦ"), + (0xABA7, "M", "Ꮧ"), + (0xABA8, "M", "Ꮨ"), + (0xABA9, "M", "Ꮩ"), + (0xABAA, "M", "Ꮪ"), + (0xABAB, "M", "Ꮫ"), + (0xABAC, "M", "Ꮬ"), + (0xABAD, "M", "Ꮭ"), + (0xABAE, "M", "Ꮮ"), + (0xABAF, "M", "Ꮯ"), + (0xABB0, "M", "Ꮰ"), + (0xABB1, "M", "Ꮱ"), + (0xABB2, "M", "Ꮲ"), + (0xABB3, "M", "Ꮳ"), + (0xABB4, "M", "Ꮴ"), + (0xABB5, "M", "Ꮵ"), + (0xABB6, "M", "Ꮶ"), + (0xABB7, "M", "Ꮷ"), + (0xABB8, "M", "Ꮸ"), + (0xABB9, "M", "Ꮹ"), + (0xABBA, "M", "Ꮺ"), + (0xABBB, "M", "Ꮻ"), + (0xABBC, "M", "Ꮼ"), + (0xABBD, "M", "Ꮽ"), + (0xABBE, "M", "Ꮾ"), + (0xABBF, "M", "Ꮿ"), + (0xABC0, "V"), + (0xABEE, "X"), + (0xABF0, "V"), + (0xABFA, "X"), + (0xAC00, "V"), + (0xD7A4, "X"), + (0xD7B0, "V"), + (0xD7C7, "X"), + (0xD7CB, "V"), + (0xD7FC, "X"), + (0xF900, "M", "豈"), + (0xF901, "M", "更"), + (0xF902, "M", "車"), + (0xF903, "M", "賈"), + (0xF904, "M", "滑"), + (0xF905, "M", "串"), + (0xF906, "M", "句"), + (0xF907, "M", "龜"), + (0xF909, "M", "契"), + (0xF90A, "M", "金"), + (0xF90B, "M", "喇"), + (0xF90C, "M", "奈"), + (0xF90D, "M", "懶"), + (0xF90E, "M", "癩"), + (0xF90F, "M", "羅"), + (0xF910, "M", "蘿"), + (0xF911, "M", "螺"), + (0xF912, "M", "裸"), + (0xF913, "M", "邏"), + (0xF914, "M", "樂"), + (0xF915, "M", "洛"), + (0xF916, "M", "烙"), + (0xF917, "M", "珞"), + (0xF918, "M", "落"), + (0xF919, "M", "酪"), + (0xF91A, "M", "駱"), + (0xF91B, "M", "亂"), + (0xF91C, "M", "卵"), + (0xF91D, "M", "欄"), + (0xF91E, "M", "爛"), + (0xF91F, "M", "蘭"), + (0xF920, "M", "鸞"), + (0xF921, "M", "嵐"), + (0xF922, "M", "濫"), + ] + + +def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF923, "M", "藍"), + (0xF924, "M", "襤"), + (0xF925, "M", "拉"), + (0xF926, "M", "臘"), + (0xF927, "M", "蠟"), + (0xF928, "M", "廊"), + (0xF929, "M", "朗"), + (0xF92A, "M", "浪"), + (0xF92B, "M", "狼"), + (0xF92C, "M", "郎"), + (0xF92D, "M", "來"), + (0xF92E, "M", "冷"), + (0xF92F, "M", "勞"), + (0xF930, "M", "擄"), + (0xF931, "M", "櫓"), + (0xF932, "M", "爐"), + (0xF933, "M", "盧"), + (0xF934, "M", "老"), + (0xF935, "M", "蘆"), + (0xF936, "M", "虜"), + (0xF937, "M", "路"), + (0xF938, "M", "露"), + (0xF939, "M", "魯"), + (0xF93A, "M", "鷺"), + (0xF93B, "M", "碌"), + (0xF93C, "M", "祿"), + (0xF93D, "M", "綠"), + (0xF93E, "M", "菉"), + (0xF93F, "M", "錄"), + (0xF940, "M", "鹿"), + (0xF941, "M", "論"), + (0xF942, "M", "壟"), + (0xF943, "M", "弄"), + (0xF944, "M", "籠"), + (0xF945, "M", "聾"), + (0xF946, "M", "牢"), + (0xF947, "M", "磊"), + (0xF948, "M", "賂"), + (0xF949, "M", "雷"), + (0xF94A, "M", "壘"), + (0xF94B, "M", "屢"), + (0xF94C, "M", "樓"), + (0xF94D, "M", "淚"), + (0xF94E, "M", "漏"), + (0xF94F, "M", "累"), + (0xF950, "M", "縷"), + (0xF951, "M", "陋"), + (0xF952, "M", "勒"), + (0xF953, "M", "肋"), + (0xF954, "M", "凜"), + (0xF955, "M", "凌"), + (0xF956, "M", "稜"), + (0xF957, "M", "綾"), + (0xF958, "M", "菱"), + (0xF959, "M", "陵"), + (0xF95A, "M", "讀"), + (0xF95B, "M", "拏"), + (0xF95C, "M", "樂"), + (0xF95D, "M", "諾"), + (0xF95E, "M", "丹"), + (0xF95F, "M", "寧"), + (0xF960, "M", "怒"), + (0xF961, "M", "率"), + (0xF962, "M", "異"), + (0xF963, "M", "北"), + (0xF964, "M", "磻"), + (0xF965, "M", "便"), + (0xF966, "M", "復"), + (0xF967, "M", "不"), + (0xF968, "M", "泌"), + (0xF969, "M", "數"), + (0xF96A, "M", "索"), + (0xF96B, "M", "參"), + (0xF96C, "M", "塞"), + (0xF96D, "M", "省"), + (0xF96E, "M", "葉"), + (0xF96F, "M", "說"), + (0xF970, "M", "殺"), + (0xF971, "M", "辰"), + (0xF972, "M", "沈"), + (0xF973, "M", "拾"), + (0xF974, "M", "若"), + (0xF975, "M", "掠"), + (0xF976, "M", "略"), + (0xF977, "M", "亮"), + (0xF978, "M", "兩"), + (0xF979, "M", "凉"), + (0xF97A, "M", "梁"), + (0xF97B, "M", "糧"), + (0xF97C, "M", "良"), + (0xF97D, "M", "諒"), + (0xF97E, "M", "量"), + (0xF97F, "M", "勵"), + (0xF980, "M", "呂"), + (0xF981, "M", "女"), + (0xF982, "M", "廬"), + (0xF983, "M", "旅"), + (0xF984, "M", "濾"), + (0xF985, "M", "礪"), + (0xF986, "M", "閭"), + ] + + +def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF987, "M", "驪"), + (0xF988, "M", "麗"), + (0xF989, "M", "黎"), + (0xF98A, "M", "力"), + (0xF98B, "M", "曆"), + (0xF98C, "M", "歷"), + (0xF98D, "M", "轢"), + (0xF98E, "M", "年"), + (0xF98F, "M", "憐"), + (0xF990, "M", "戀"), + (0xF991, "M", "撚"), + (0xF992, "M", "漣"), + (0xF993, "M", "煉"), + (0xF994, "M", "璉"), + (0xF995, "M", "秊"), + (0xF996, "M", "練"), + (0xF997, "M", "聯"), + (0xF998, "M", "輦"), + (0xF999, "M", "蓮"), + (0xF99A, "M", "連"), + (0xF99B, "M", "鍊"), + (0xF99C, "M", "列"), + (0xF99D, "M", "劣"), + (0xF99E, "M", "咽"), + (0xF99F, "M", "烈"), + (0xF9A0, "M", "裂"), + (0xF9A1, "M", "說"), + (0xF9A2, "M", "廉"), + (0xF9A3, "M", "念"), + (0xF9A4, "M", "捻"), + (0xF9A5, "M", "殮"), + (0xF9A6, "M", "簾"), + (0xF9A7, "M", "獵"), + (0xF9A8, "M", "令"), + (0xF9A9, "M", "囹"), + (0xF9AA, "M", "寧"), + (0xF9AB, "M", "嶺"), + (0xF9AC, "M", "怜"), + (0xF9AD, "M", "玲"), + (0xF9AE, "M", "瑩"), + (0xF9AF, "M", "羚"), + (0xF9B0, "M", "聆"), + (0xF9B1, "M", "鈴"), + (0xF9B2, "M", "零"), + (0xF9B3, "M", "靈"), + (0xF9B4, "M", "領"), + (0xF9B5, "M", "例"), + (0xF9B6, "M", "禮"), + (0xF9B7, "M", "醴"), + (0xF9B8, "M", "隸"), + (0xF9B9, "M", "惡"), + (0xF9BA, "M", "了"), + (0xF9BB, "M", "僚"), + (0xF9BC, "M", "寮"), + (0xF9BD, "M", "尿"), + (0xF9BE, "M", "料"), + (0xF9BF, "M", "樂"), + (0xF9C0, "M", "燎"), + (0xF9C1, "M", "療"), + (0xF9C2, "M", "蓼"), + (0xF9C3, "M", "遼"), + (0xF9C4, "M", "龍"), + (0xF9C5, "M", "暈"), + (0xF9C6, "M", "阮"), + (0xF9C7, "M", "劉"), + (0xF9C8, "M", "杻"), + (0xF9C9, "M", "柳"), + (0xF9CA, "M", "流"), + (0xF9CB, "M", "溜"), + (0xF9CC, "M", "琉"), + (0xF9CD, "M", "留"), + (0xF9CE, "M", "硫"), + (0xF9CF, "M", "紐"), + (0xF9D0, "M", "類"), + (0xF9D1, "M", "六"), + (0xF9D2, "M", "戮"), + (0xF9D3, "M", "陸"), + (0xF9D4, "M", "倫"), + (0xF9D5, "M", "崙"), + (0xF9D6, "M", "淪"), + (0xF9D7, "M", "輪"), + (0xF9D8, "M", "律"), + (0xF9D9, "M", "慄"), + (0xF9DA, "M", "栗"), + (0xF9DB, "M", "率"), + (0xF9DC, "M", "隆"), + (0xF9DD, "M", "利"), + (0xF9DE, "M", "吏"), + (0xF9DF, "M", "履"), + (0xF9E0, "M", "易"), + (0xF9E1, "M", "李"), + (0xF9E2, "M", "梨"), + (0xF9E3, "M", "泥"), + (0xF9E4, "M", "理"), + (0xF9E5, "M", "痢"), + (0xF9E6, "M", "罹"), + (0xF9E7, "M", "裏"), + (0xF9E8, "M", "裡"), + (0xF9E9, "M", "里"), + (0xF9EA, "M", "離"), + ] + + +def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF9EB, "M", "匿"), + (0xF9EC, "M", "溺"), + (0xF9ED, "M", "吝"), + (0xF9EE, "M", "燐"), + (0xF9EF, "M", "璘"), + (0xF9F0, "M", "藺"), + (0xF9F1, "M", "隣"), + (0xF9F2, "M", "鱗"), + (0xF9F3, "M", "麟"), + (0xF9F4, "M", "林"), + (0xF9F5, "M", "淋"), + (0xF9F6, "M", "臨"), + (0xF9F7, "M", "立"), + (0xF9F8, "M", "笠"), + (0xF9F9, "M", "粒"), + (0xF9FA, "M", "狀"), + (0xF9FB, "M", "炙"), + (0xF9FC, "M", "識"), + (0xF9FD, "M", "什"), + (0xF9FE, "M", "茶"), + (0xF9FF, "M", "刺"), + (0xFA00, "M", "切"), + (0xFA01, "M", "度"), + (0xFA02, "M", "拓"), + (0xFA03, "M", "糖"), + (0xFA04, "M", "宅"), + (0xFA05, "M", "洞"), + (0xFA06, "M", "暴"), + (0xFA07, "M", "輻"), + (0xFA08, "M", "行"), + (0xFA09, "M", "降"), + (0xFA0A, "M", "見"), + (0xFA0B, "M", "廓"), + (0xFA0C, "M", "兀"), + (0xFA0D, "M", "嗀"), + (0xFA0E, "V"), + (0xFA10, "M", "塚"), + (0xFA11, "V"), + (0xFA12, "M", "晴"), + (0xFA13, "V"), + (0xFA15, "M", "凞"), + (0xFA16, "M", "猪"), + (0xFA17, "M", "益"), + (0xFA18, "M", "礼"), + (0xFA19, "M", "神"), + (0xFA1A, "M", "祥"), + (0xFA1B, "M", "福"), + (0xFA1C, "M", "靖"), + (0xFA1D, "M", "精"), + (0xFA1E, "M", "羽"), + (0xFA1F, "V"), + (0xFA20, "M", "蘒"), + (0xFA21, "V"), + (0xFA22, "M", "諸"), + (0xFA23, "V"), + (0xFA25, "M", "逸"), + (0xFA26, "M", "都"), + (0xFA27, "V"), + (0xFA2A, "M", "飯"), + (0xFA2B, "M", "飼"), + (0xFA2C, "M", "館"), + (0xFA2D, "M", "鶴"), + (0xFA2E, "M", "郞"), + (0xFA2F, "M", "隷"), + (0xFA30, "M", "侮"), + (0xFA31, "M", "僧"), + (0xFA32, "M", "免"), + (0xFA33, "M", "勉"), + (0xFA34, "M", "勤"), + (0xFA35, "M", "卑"), + (0xFA36, "M", "喝"), + (0xFA37, "M", "嘆"), + (0xFA38, "M", "器"), + (0xFA39, "M", "塀"), + (0xFA3A, "M", "墨"), + (0xFA3B, "M", "層"), + (0xFA3C, "M", "屮"), + (0xFA3D, "M", "悔"), + (0xFA3E, "M", "慨"), + (0xFA3F, "M", "憎"), + (0xFA40, "M", "懲"), + (0xFA41, "M", "敏"), + (0xFA42, "M", "既"), + (0xFA43, "M", "暑"), + (0xFA44, "M", "梅"), + (0xFA45, "M", "海"), + (0xFA46, "M", "渚"), + (0xFA47, "M", "漢"), + (0xFA48, "M", "煮"), + (0xFA49, "M", "爫"), + (0xFA4A, "M", "琢"), + (0xFA4B, "M", "碑"), + (0xFA4C, "M", "社"), + (0xFA4D, "M", "祉"), + (0xFA4E, "M", "祈"), + (0xFA4F, "M", "祐"), + (0xFA50, "M", "祖"), + (0xFA51, "M", "祝"), + (0xFA52, "M", "禍"), + (0xFA53, "M", "禎"), + ] + + +def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFA54, "M", "穀"), + (0xFA55, "M", "突"), + (0xFA56, "M", "節"), + (0xFA57, "M", "練"), + (0xFA58, "M", "縉"), + (0xFA59, "M", "繁"), + (0xFA5A, "M", "署"), + (0xFA5B, "M", "者"), + (0xFA5C, "M", "臭"), + (0xFA5D, "M", "艹"), + (0xFA5F, "M", "著"), + (0xFA60, "M", "褐"), + (0xFA61, "M", "視"), + (0xFA62, "M", "謁"), + (0xFA63, "M", "謹"), + (0xFA64, "M", "賓"), + (0xFA65, "M", "贈"), + (0xFA66, "M", "辶"), + (0xFA67, "M", "逸"), + (0xFA68, "M", "難"), + (0xFA69, "M", "響"), + (0xFA6A, "M", "頻"), + (0xFA6B, "M", "恵"), + (0xFA6C, "M", "𤋮"), + (0xFA6D, "M", "舘"), + (0xFA6E, "X"), + (0xFA70, "M", "並"), + (0xFA71, "M", "况"), + (0xFA72, "M", "全"), + (0xFA73, "M", "侀"), + (0xFA74, "M", "充"), + (0xFA75, "M", "冀"), + (0xFA76, "M", "勇"), + (0xFA77, "M", "勺"), + (0xFA78, "M", "喝"), + (0xFA79, "M", "啕"), + (0xFA7A, "M", "喙"), + (0xFA7B, "M", "嗢"), + (0xFA7C, "M", "塚"), + (0xFA7D, "M", "墳"), + (0xFA7E, "M", "奄"), + (0xFA7F, "M", "奔"), + (0xFA80, "M", "婢"), + (0xFA81, "M", "嬨"), + (0xFA82, "M", "廒"), + (0xFA83, "M", "廙"), + (0xFA84, "M", "彩"), + (0xFA85, "M", "徭"), + (0xFA86, "M", "惘"), + (0xFA87, "M", "慎"), + (0xFA88, "M", "愈"), + (0xFA89, "M", "憎"), + (0xFA8A, "M", "慠"), + (0xFA8B, "M", "懲"), + (0xFA8C, "M", "戴"), + (0xFA8D, "M", "揄"), + (0xFA8E, "M", "搜"), + (0xFA8F, "M", "摒"), + (0xFA90, "M", "敖"), + (0xFA91, "M", "晴"), + (0xFA92, "M", "朗"), + (0xFA93, "M", "望"), + (0xFA94, "M", "杖"), + (0xFA95, "M", "歹"), + (0xFA96, "M", "殺"), + (0xFA97, "M", "流"), + (0xFA98, "M", "滛"), + (0xFA99, "M", "滋"), + (0xFA9A, "M", "漢"), + (0xFA9B, "M", "瀞"), + (0xFA9C, "M", "煮"), + (0xFA9D, "M", "瞧"), + (0xFA9E, "M", "爵"), + (0xFA9F, "M", "犯"), + (0xFAA0, "M", "猪"), + (0xFAA1, "M", "瑱"), + (0xFAA2, "M", "甆"), + (0xFAA3, "M", "画"), + (0xFAA4, "M", "瘝"), + (0xFAA5, "M", "瘟"), + (0xFAA6, "M", "益"), + (0xFAA7, "M", "盛"), + (0xFAA8, "M", "直"), + (0xFAA9, "M", "睊"), + (0xFAAA, "M", "着"), + (0xFAAB, "M", "磌"), + (0xFAAC, "M", "窱"), + (0xFAAD, "M", "節"), + (0xFAAE, "M", "类"), + (0xFAAF, "M", "絛"), + (0xFAB0, "M", "練"), + (0xFAB1, "M", "缾"), + (0xFAB2, "M", "者"), + (0xFAB3, "M", "荒"), + (0xFAB4, "M", "華"), + (0xFAB5, "M", "蝹"), + (0xFAB6, "M", "襁"), + (0xFAB7, "M", "覆"), + (0xFAB8, "M", "視"), + (0xFAB9, "M", "調"), + ] + + +def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFABA, "M", "諸"), + (0xFABB, "M", "請"), + (0xFABC, "M", "謁"), + (0xFABD, "M", "諾"), + (0xFABE, "M", "諭"), + (0xFABF, "M", "謹"), + (0xFAC0, "M", "變"), + (0xFAC1, "M", "贈"), + (0xFAC2, "M", "輸"), + (0xFAC3, "M", "遲"), + (0xFAC4, "M", "醙"), + (0xFAC5, "M", "鉶"), + (0xFAC6, "M", "陼"), + (0xFAC7, "M", "難"), + (0xFAC8, "M", "靖"), + (0xFAC9, "M", "韛"), + (0xFACA, "M", "響"), + (0xFACB, "M", "頋"), + (0xFACC, "M", "頻"), + (0xFACD, "M", "鬒"), + (0xFACE, "M", "龜"), + (0xFACF, "M", "𢡊"), + (0xFAD0, "M", "𢡄"), + (0xFAD1, "M", "𣏕"), + (0xFAD2, "M", "㮝"), + (0xFAD3, "M", "䀘"), + (0xFAD4, "M", "䀹"), + (0xFAD5, "M", "𥉉"), + (0xFAD6, "M", "𥳐"), + (0xFAD7, "M", "𧻓"), + (0xFAD8, "M", "齃"), + (0xFAD9, "M", "龎"), + (0xFADA, "X"), + (0xFB00, "M", "ff"), + (0xFB01, "M", "fi"), + (0xFB02, "M", "fl"), + (0xFB03, "M", "ffi"), + (0xFB04, "M", "ffl"), + (0xFB05, "M", "st"), + (0xFB07, "X"), + (0xFB13, "M", "մն"), + (0xFB14, "M", "մե"), + (0xFB15, "M", "մի"), + (0xFB16, "M", "վն"), + (0xFB17, "M", "մխ"), + (0xFB18, "X"), + (0xFB1D, "M", "יִ"), + (0xFB1E, "V"), + (0xFB1F, "M", "ײַ"), + (0xFB20, "M", "ע"), + (0xFB21, "M", "א"), + (0xFB22, "M", "ד"), + (0xFB23, "M", "ה"), + (0xFB24, "M", "כ"), + (0xFB25, "M", "ל"), + (0xFB26, "M", "ם"), + (0xFB27, "M", "ר"), + (0xFB28, "M", "ת"), + (0xFB29, "M", "+"), + (0xFB2A, "M", "שׁ"), + (0xFB2B, "M", "שׂ"), + (0xFB2C, "M", "שּׁ"), + (0xFB2D, "M", "שּׂ"), + (0xFB2E, "M", "אַ"), + (0xFB2F, "M", "אָ"), + (0xFB30, "M", "אּ"), + (0xFB31, "M", "בּ"), + (0xFB32, "M", "גּ"), + (0xFB33, "M", "דּ"), + (0xFB34, "M", "הּ"), + (0xFB35, "M", "וּ"), + (0xFB36, "M", "זּ"), + (0xFB37, "X"), + (0xFB38, "M", "טּ"), + (0xFB39, "M", "יּ"), + (0xFB3A, "M", "ךּ"), + (0xFB3B, "M", "כּ"), + (0xFB3C, "M", "לּ"), + (0xFB3D, "X"), + (0xFB3E, "M", "מּ"), + (0xFB3F, "X"), + (0xFB40, "M", "נּ"), + (0xFB41, "M", "סּ"), + (0xFB42, "X"), + (0xFB43, "M", "ףּ"), + (0xFB44, "M", "פּ"), + (0xFB45, "X"), + (0xFB46, "M", "צּ"), + (0xFB47, "M", "קּ"), + (0xFB48, "M", "רּ"), + (0xFB49, "M", "שּ"), + (0xFB4A, "M", "תּ"), + (0xFB4B, "M", "וֹ"), + (0xFB4C, "M", "בֿ"), + (0xFB4D, "M", "כֿ"), + (0xFB4E, "M", "פֿ"), + (0xFB4F, "M", "אל"), + (0xFB50, "M", "ٱ"), + (0xFB52, "M", "ٻ"), + (0xFB56, "M", "پ"), + ] + + +def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFB5A, "M", "ڀ"), + (0xFB5E, "M", "ٺ"), + (0xFB62, "M", "ٿ"), + (0xFB66, "M", "ٹ"), + (0xFB6A, "M", "ڤ"), + (0xFB6E, "M", "ڦ"), + (0xFB72, "M", "ڄ"), + (0xFB76, "M", "ڃ"), + (0xFB7A, "M", "چ"), + (0xFB7E, "M", "ڇ"), + (0xFB82, "M", "ڍ"), + (0xFB84, "M", "ڌ"), + (0xFB86, "M", "ڎ"), + (0xFB88, "M", "ڈ"), + (0xFB8A, "M", "ژ"), + (0xFB8C, "M", "ڑ"), + (0xFB8E, "M", "ک"), + (0xFB92, "M", "گ"), + (0xFB96, "M", "ڳ"), + (0xFB9A, "M", "ڱ"), + (0xFB9E, "M", "ں"), + (0xFBA0, "M", "ڻ"), + (0xFBA4, "M", "ۀ"), + (0xFBA6, "M", "ہ"), + (0xFBAA, "M", "ھ"), + (0xFBAE, "M", "ے"), + (0xFBB0, "M", "ۓ"), + (0xFBB2, "V"), + (0xFBC3, "X"), + (0xFBD3, "M", "ڭ"), + (0xFBD7, "M", "ۇ"), + (0xFBD9, "M", "ۆ"), + (0xFBDB, "M", "ۈ"), + (0xFBDD, "M", "ۇٴ"), + (0xFBDE, "M", "ۋ"), + (0xFBE0, "M", "ۅ"), + (0xFBE2, "M", "ۉ"), + (0xFBE4, "M", "ې"), + (0xFBE8, "M", "ى"), + (0xFBEA, "M", "ئا"), + (0xFBEC, "M", "ئە"), + (0xFBEE, "M", "ئو"), + (0xFBF0, "M", "ئۇ"), + (0xFBF2, "M", "ئۆ"), + (0xFBF4, "M", "ئۈ"), + (0xFBF6, "M", "ئې"), + (0xFBF9, "M", "ئى"), + (0xFBFC, "M", "ی"), + (0xFC00, "M", "ئج"), + (0xFC01, "M", "ئح"), + (0xFC02, "M", "ئم"), + (0xFC03, "M", "ئى"), + (0xFC04, "M", "ئي"), + (0xFC05, "M", "بج"), + (0xFC06, "M", "بح"), + (0xFC07, "M", "بخ"), + (0xFC08, "M", "بم"), + (0xFC09, "M", "بى"), + (0xFC0A, "M", "بي"), + (0xFC0B, "M", "تج"), + (0xFC0C, "M", "تح"), + (0xFC0D, "M", "تخ"), + (0xFC0E, "M", "تم"), + (0xFC0F, "M", "تى"), + (0xFC10, "M", "تي"), + (0xFC11, "M", "ثج"), + (0xFC12, "M", "ثم"), + (0xFC13, "M", "ثى"), + (0xFC14, "M", "ثي"), + (0xFC15, "M", "جح"), + (0xFC16, "M", "جم"), + (0xFC17, "M", "حج"), + (0xFC18, "M", "حم"), + (0xFC19, "M", "خج"), + (0xFC1A, "M", "خح"), + (0xFC1B, "M", "خم"), + (0xFC1C, "M", "سج"), + (0xFC1D, "M", "سح"), + (0xFC1E, "M", "سخ"), + (0xFC1F, "M", "سم"), + (0xFC20, "M", "صح"), + (0xFC21, "M", "صم"), + (0xFC22, "M", "ضج"), + (0xFC23, "M", "ضح"), + (0xFC24, "M", "ضخ"), + (0xFC25, "M", "ضم"), + (0xFC26, "M", "طح"), + (0xFC27, "M", "طم"), + (0xFC28, "M", "ظم"), + (0xFC29, "M", "عج"), + (0xFC2A, "M", "عم"), + (0xFC2B, "M", "غج"), + (0xFC2C, "M", "غم"), + (0xFC2D, "M", "فج"), + (0xFC2E, "M", "فح"), + (0xFC2F, "M", "فخ"), + (0xFC30, "M", "فم"), + (0xFC31, "M", "فى"), + (0xFC32, "M", "في"), + (0xFC33, "M", "قح"), + ] + + +def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFC34, "M", "قم"), + (0xFC35, "M", "قى"), + (0xFC36, "M", "قي"), + (0xFC37, "M", "كا"), + (0xFC38, "M", "كج"), + (0xFC39, "M", "كح"), + (0xFC3A, "M", "كخ"), + (0xFC3B, "M", "كل"), + (0xFC3C, "M", "كم"), + (0xFC3D, "M", "كى"), + (0xFC3E, "M", "كي"), + (0xFC3F, "M", "لج"), + (0xFC40, "M", "لح"), + (0xFC41, "M", "لخ"), + (0xFC42, "M", "لم"), + (0xFC43, "M", "لى"), + (0xFC44, "M", "لي"), + (0xFC45, "M", "مج"), + (0xFC46, "M", "مح"), + (0xFC47, "M", "مخ"), + (0xFC48, "M", "مم"), + (0xFC49, "M", "مى"), + (0xFC4A, "M", "مي"), + (0xFC4B, "M", "نج"), + (0xFC4C, "M", "نح"), + (0xFC4D, "M", "نخ"), + (0xFC4E, "M", "نم"), + (0xFC4F, "M", "نى"), + (0xFC50, "M", "ني"), + (0xFC51, "M", "هج"), + (0xFC52, "M", "هم"), + (0xFC53, "M", "هى"), + (0xFC54, "M", "هي"), + (0xFC55, "M", "يج"), + (0xFC56, "M", "يح"), + (0xFC57, "M", "يخ"), + (0xFC58, "M", "يم"), + (0xFC59, "M", "يى"), + (0xFC5A, "M", "يي"), + (0xFC5B, "M", "ذٰ"), + (0xFC5C, "M", "رٰ"), + (0xFC5D, "M", "ىٰ"), + (0xFC5E, "M", " ٌّ"), + (0xFC5F, "M", " ٍّ"), + (0xFC60, "M", " َّ"), + (0xFC61, "M", " ُّ"), + (0xFC62, "M", " ِّ"), + (0xFC63, "M", " ّٰ"), + (0xFC64, "M", "ئر"), + (0xFC65, "M", "ئز"), + (0xFC66, "M", "ئم"), + (0xFC67, "M", "ئن"), + (0xFC68, "M", "ئى"), + (0xFC69, "M", "ئي"), + (0xFC6A, "M", "بر"), + (0xFC6B, "M", "بز"), + (0xFC6C, "M", "بم"), + (0xFC6D, "M", "بن"), + (0xFC6E, "M", "بى"), + (0xFC6F, "M", "بي"), + (0xFC70, "M", "تر"), + (0xFC71, "M", "تز"), + (0xFC72, "M", "تم"), + (0xFC73, "M", "تن"), + (0xFC74, "M", "تى"), + (0xFC75, "M", "تي"), + (0xFC76, "M", "ثر"), + (0xFC77, "M", "ثز"), + (0xFC78, "M", "ثم"), + (0xFC79, "M", "ثن"), + (0xFC7A, "M", "ثى"), + (0xFC7B, "M", "ثي"), + (0xFC7C, "M", "فى"), + (0xFC7D, "M", "في"), + (0xFC7E, "M", "قى"), + (0xFC7F, "M", "قي"), + (0xFC80, "M", "كا"), + (0xFC81, "M", "كل"), + (0xFC82, "M", "كم"), + (0xFC83, "M", "كى"), + (0xFC84, "M", "كي"), + (0xFC85, "M", "لم"), + (0xFC86, "M", "لى"), + (0xFC87, "M", "لي"), + (0xFC88, "M", "ما"), + (0xFC89, "M", "مم"), + (0xFC8A, "M", "نر"), + (0xFC8B, "M", "نز"), + (0xFC8C, "M", "نم"), + (0xFC8D, "M", "نن"), + (0xFC8E, "M", "نى"), + (0xFC8F, "M", "ني"), + (0xFC90, "M", "ىٰ"), + (0xFC91, "M", "ير"), + (0xFC92, "M", "يز"), + (0xFC93, "M", "يم"), + (0xFC94, "M", "ين"), + (0xFC95, "M", "يى"), + (0xFC96, "M", "يي"), + (0xFC97, "M", "ئج"), + ] + + +def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFC98, "M", "ئح"), + (0xFC99, "M", "ئخ"), + (0xFC9A, "M", "ئم"), + (0xFC9B, "M", "ئه"), + (0xFC9C, "M", "بج"), + (0xFC9D, "M", "بح"), + (0xFC9E, "M", "بخ"), + (0xFC9F, "M", "بم"), + (0xFCA0, "M", "به"), + (0xFCA1, "M", "تج"), + (0xFCA2, "M", "تح"), + (0xFCA3, "M", "تخ"), + (0xFCA4, "M", "تم"), + (0xFCA5, "M", "ته"), + (0xFCA6, "M", "ثم"), + (0xFCA7, "M", "جح"), + (0xFCA8, "M", "جم"), + (0xFCA9, "M", "حج"), + (0xFCAA, "M", "حم"), + (0xFCAB, "M", "خج"), + (0xFCAC, "M", "خم"), + (0xFCAD, "M", "سج"), + (0xFCAE, "M", "سح"), + (0xFCAF, "M", "سخ"), + (0xFCB0, "M", "سم"), + (0xFCB1, "M", "صح"), + (0xFCB2, "M", "صخ"), + (0xFCB3, "M", "صم"), + (0xFCB4, "M", "ضج"), + (0xFCB5, "M", "ضح"), + (0xFCB6, "M", "ضخ"), + (0xFCB7, "M", "ضم"), + (0xFCB8, "M", "طح"), + (0xFCB9, "M", "ظم"), + (0xFCBA, "M", "عج"), + (0xFCBB, "M", "عم"), + (0xFCBC, "M", "غج"), + (0xFCBD, "M", "غم"), + (0xFCBE, "M", "فج"), + (0xFCBF, "M", "فح"), + (0xFCC0, "M", "فخ"), + (0xFCC1, "M", "فم"), + (0xFCC2, "M", "قح"), + (0xFCC3, "M", "قم"), + (0xFCC4, "M", "كج"), + (0xFCC5, "M", "كح"), + (0xFCC6, "M", "كخ"), + (0xFCC7, "M", "كل"), + (0xFCC8, "M", "كم"), + (0xFCC9, "M", "لج"), + (0xFCCA, "M", "لح"), + (0xFCCB, "M", "لخ"), + (0xFCCC, "M", "لم"), + (0xFCCD, "M", "له"), + (0xFCCE, "M", "مج"), + (0xFCCF, "M", "مح"), + (0xFCD0, "M", "مخ"), + (0xFCD1, "M", "مم"), + (0xFCD2, "M", "نج"), + (0xFCD3, "M", "نح"), + (0xFCD4, "M", "نخ"), + (0xFCD5, "M", "نم"), + (0xFCD6, "M", "نه"), + (0xFCD7, "M", "هج"), + (0xFCD8, "M", "هم"), + (0xFCD9, "M", "هٰ"), + (0xFCDA, "M", "يج"), + (0xFCDB, "M", "يح"), + (0xFCDC, "M", "يخ"), + (0xFCDD, "M", "يم"), + (0xFCDE, "M", "يه"), + (0xFCDF, "M", "ئم"), + (0xFCE0, "M", "ئه"), + (0xFCE1, "M", "بم"), + (0xFCE2, "M", "به"), + (0xFCE3, "M", "تم"), + (0xFCE4, "M", "ته"), + (0xFCE5, "M", "ثم"), + (0xFCE6, "M", "ثه"), + (0xFCE7, "M", "سم"), + (0xFCE8, "M", "سه"), + (0xFCE9, "M", "شم"), + (0xFCEA, "M", "شه"), + (0xFCEB, "M", "كل"), + (0xFCEC, "M", "كم"), + (0xFCED, "M", "لم"), + (0xFCEE, "M", "نم"), + (0xFCEF, "M", "نه"), + (0xFCF0, "M", "يم"), + (0xFCF1, "M", "يه"), + (0xFCF2, "M", "ـَّ"), + (0xFCF3, "M", "ـُّ"), + (0xFCF4, "M", "ـِّ"), + (0xFCF5, "M", "طى"), + (0xFCF6, "M", "طي"), + (0xFCF7, "M", "عى"), + (0xFCF8, "M", "عي"), + (0xFCF9, "M", "غى"), + (0xFCFA, "M", "غي"), + (0xFCFB, "M", "سى"), + ] + + +def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFCFC, "M", "سي"), + (0xFCFD, "M", "شى"), + (0xFCFE, "M", "شي"), + (0xFCFF, "M", "حى"), + (0xFD00, "M", "حي"), + (0xFD01, "M", "جى"), + (0xFD02, "M", "جي"), + (0xFD03, "M", "خى"), + (0xFD04, "M", "خي"), + (0xFD05, "M", "صى"), + (0xFD06, "M", "صي"), + (0xFD07, "M", "ضى"), + (0xFD08, "M", "ضي"), + (0xFD09, "M", "شج"), + (0xFD0A, "M", "شح"), + (0xFD0B, "M", "شخ"), + (0xFD0C, "M", "شم"), + (0xFD0D, "M", "شر"), + (0xFD0E, "M", "سر"), + (0xFD0F, "M", "صر"), + (0xFD10, "M", "ضر"), + (0xFD11, "M", "طى"), + (0xFD12, "M", "طي"), + (0xFD13, "M", "عى"), + (0xFD14, "M", "عي"), + (0xFD15, "M", "غى"), + (0xFD16, "M", "غي"), + (0xFD17, "M", "سى"), + (0xFD18, "M", "سي"), + (0xFD19, "M", "شى"), + (0xFD1A, "M", "شي"), + (0xFD1B, "M", "حى"), + (0xFD1C, "M", "حي"), + (0xFD1D, "M", "جى"), + (0xFD1E, "M", "جي"), + (0xFD1F, "M", "خى"), + (0xFD20, "M", "خي"), + (0xFD21, "M", "صى"), + (0xFD22, "M", "صي"), + (0xFD23, "M", "ضى"), + (0xFD24, "M", "ضي"), + (0xFD25, "M", "شج"), + (0xFD26, "M", "شح"), + (0xFD27, "M", "شخ"), + (0xFD28, "M", "شم"), + (0xFD29, "M", "شر"), + (0xFD2A, "M", "سر"), + (0xFD2B, "M", "صر"), + (0xFD2C, "M", "ضر"), + (0xFD2D, "M", "شج"), + (0xFD2E, "M", "شح"), + (0xFD2F, "M", "شخ"), + (0xFD30, "M", "شم"), + (0xFD31, "M", "سه"), + (0xFD32, "M", "شه"), + (0xFD33, "M", "طم"), + (0xFD34, "M", "سج"), + (0xFD35, "M", "سح"), + (0xFD36, "M", "سخ"), + (0xFD37, "M", "شج"), + (0xFD38, "M", "شح"), + (0xFD39, "M", "شخ"), + (0xFD3A, "M", "طم"), + (0xFD3B, "M", "ظم"), + (0xFD3C, "M", "اً"), + (0xFD3E, "V"), + (0xFD50, "M", "تجم"), + (0xFD51, "M", "تحج"), + (0xFD53, "M", "تحم"), + (0xFD54, "M", "تخم"), + (0xFD55, "M", "تمج"), + (0xFD56, "M", "تمح"), + (0xFD57, "M", "تمخ"), + (0xFD58, "M", "جمح"), + (0xFD5A, "M", "حمي"), + (0xFD5B, "M", "حمى"), + (0xFD5C, "M", "سحج"), + (0xFD5D, "M", "سجح"), + (0xFD5E, "M", "سجى"), + (0xFD5F, "M", "سمح"), + (0xFD61, "M", "سمج"), + (0xFD62, "M", "سمم"), + (0xFD64, "M", "صحح"), + (0xFD66, "M", "صمم"), + (0xFD67, "M", "شحم"), + (0xFD69, "M", "شجي"), + (0xFD6A, "M", "شمخ"), + (0xFD6C, "M", "شمم"), + (0xFD6E, "M", "ضحى"), + (0xFD6F, "M", "ضخم"), + (0xFD71, "M", "طمح"), + (0xFD73, "M", "طمم"), + (0xFD74, "M", "طمي"), + (0xFD75, "M", "عجم"), + (0xFD76, "M", "عمم"), + (0xFD78, "M", "عمى"), + (0xFD79, "M", "غمم"), + (0xFD7A, "M", "غمي"), + (0xFD7B, "M", "غمى"), + (0xFD7C, "M", "فخم"), + ] + + +def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFD7E, "M", "قمح"), + (0xFD7F, "M", "قمم"), + (0xFD80, "M", "لحم"), + (0xFD81, "M", "لحي"), + (0xFD82, "M", "لحى"), + (0xFD83, "M", "لجج"), + (0xFD85, "M", "لخم"), + (0xFD87, "M", "لمح"), + (0xFD89, "M", "محج"), + (0xFD8A, "M", "محم"), + (0xFD8B, "M", "محي"), + (0xFD8C, "M", "مجح"), + (0xFD8D, "M", "مجم"), + (0xFD8E, "M", "مخج"), + (0xFD8F, "M", "مخم"), + (0xFD90, "X"), + (0xFD92, "M", "مجخ"), + (0xFD93, "M", "همج"), + (0xFD94, "M", "همم"), + (0xFD95, "M", "نحم"), + (0xFD96, "M", "نحى"), + (0xFD97, "M", "نجم"), + (0xFD99, "M", "نجى"), + (0xFD9A, "M", "نمي"), + (0xFD9B, "M", "نمى"), + (0xFD9C, "M", "يمم"), + (0xFD9E, "M", "بخي"), + (0xFD9F, "M", "تجي"), + (0xFDA0, "M", "تجى"), + (0xFDA1, "M", "تخي"), + (0xFDA2, "M", "تخى"), + (0xFDA3, "M", "تمي"), + (0xFDA4, "M", "تمى"), + (0xFDA5, "M", "جمي"), + (0xFDA6, "M", "جحى"), + (0xFDA7, "M", "جمى"), + (0xFDA8, "M", "سخى"), + (0xFDA9, "M", "صحي"), + (0xFDAA, "M", "شحي"), + (0xFDAB, "M", "ضحي"), + (0xFDAC, "M", "لجي"), + (0xFDAD, "M", "لمي"), + (0xFDAE, "M", "يحي"), + (0xFDAF, "M", "يجي"), + (0xFDB0, "M", "يمي"), + (0xFDB1, "M", "ممي"), + (0xFDB2, "M", "قمي"), + (0xFDB3, "M", "نحي"), + (0xFDB4, "M", "قمح"), + (0xFDB5, "M", "لحم"), + (0xFDB6, "M", "عمي"), + (0xFDB7, "M", "كمي"), + (0xFDB8, "M", "نجح"), + (0xFDB9, "M", "مخي"), + (0xFDBA, "M", "لجم"), + (0xFDBB, "M", "كمم"), + (0xFDBC, "M", "لجم"), + (0xFDBD, "M", "نجح"), + (0xFDBE, "M", "جحي"), + (0xFDBF, "M", "حجي"), + (0xFDC0, "M", "مجي"), + (0xFDC1, "M", "فمي"), + (0xFDC2, "M", "بحي"), + (0xFDC3, "M", "كمم"), + (0xFDC4, "M", "عجم"), + (0xFDC5, "M", "صمم"), + (0xFDC6, "M", "سخي"), + (0xFDC7, "M", "نجي"), + (0xFDC8, "X"), + (0xFDCF, "V"), + (0xFDD0, "X"), + (0xFDF0, "M", "صلے"), + (0xFDF1, "M", "قلے"), + (0xFDF2, "M", "الله"), + (0xFDF3, "M", "اكبر"), + (0xFDF4, "M", "محمد"), + (0xFDF5, "M", "صلعم"), + (0xFDF6, "M", "رسول"), + (0xFDF7, "M", "عليه"), + (0xFDF8, "M", "وسلم"), + (0xFDF9, "M", "صلى"), + (0xFDFA, "M", "صلى الله عليه وسلم"), + (0xFDFB, "M", "جل جلاله"), + (0xFDFC, "M", "ریال"), + (0xFDFD, "V"), + (0xFE00, "I"), + (0xFE10, "M", ","), + (0xFE11, "M", "、"), + (0xFE12, "X"), + (0xFE13, "M", ":"), + (0xFE14, "M", ";"), + (0xFE15, "M", "!"), + (0xFE16, "M", "?"), + (0xFE17, "M", "〖"), + (0xFE18, "M", "〗"), + (0xFE19, "X"), + (0xFE20, "V"), + (0xFE30, "X"), + (0xFE31, "M", "—"), + (0xFE32, "M", "–"), + ] + + +def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFE33, "M", "_"), + (0xFE35, "M", "("), + (0xFE36, "M", ")"), + (0xFE37, "M", "{"), + (0xFE38, "M", "}"), + (0xFE39, "M", "〔"), + (0xFE3A, "M", "〕"), + (0xFE3B, "M", "【"), + (0xFE3C, "M", "】"), + (0xFE3D, "M", "《"), + (0xFE3E, "M", "》"), + (0xFE3F, "M", "〈"), + (0xFE40, "M", "〉"), + (0xFE41, "M", "「"), + (0xFE42, "M", "」"), + (0xFE43, "M", "『"), + (0xFE44, "M", "』"), + (0xFE45, "V"), + (0xFE47, "M", "["), + (0xFE48, "M", "]"), + (0xFE49, "M", " ̅"), + (0xFE4D, "M", "_"), + (0xFE50, "M", ","), + (0xFE51, "M", "、"), + (0xFE52, "X"), + (0xFE54, "M", ";"), + (0xFE55, "M", ":"), + (0xFE56, "M", "?"), + (0xFE57, "M", "!"), + (0xFE58, "M", "—"), + (0xFE59, "M", "("), + (0xFE5A, "M", ")"), + (0xFE5B, "M", "{"), + (0xFE5C, "M", "}"), + (0xFE5D, "M", "〔"), + (0xFE5E, "M", "〕"), + (0xFE5F, "M", "#"), + (0xFE60, "M", "&"), + (0xFE61, "M", "*"), + (0xFE62, "M", "+"), + (0xFE63, "M", "-"), + (0xFE64, "M", "<"), + (0xFE65, "M", ">"), + (0xFE66, "M", "="), + (0xFE67, "X"), + (0xFE68, "M", "\\"), + (0xFE69, "M", "$"), + (0xFE6A, "M", "%"), + (0xFE6B, "M", "@"), + (0xFE6C, "X"), + (0xFE70, "M", " ً"), + (0xFE71, "M", "ـً"), + (0xFE72, "M", " ٌ"), + (0xFE73, "V"), + (0xFE74, "M", " ٍ"), + (0xFE75, "X"), + (0xFE76, "M", " َ"), + (0xFE77, "M", "ـَ"), + (0xFE78, "M", " ُ"), + (0xFE79, "M", "ـُ"), + (0xFE7A, "M", " ِ"), + (0xFE7B, "M", "ـِ"), + (0xFE7C, "M", " ّ"), + (0xFE7D, "M", "ـّ"), + (0xFE7E, "M", " ْ"), + (0xFE7F, "M", "ـْ"), + (0xFE80, "M", "ء"), + (0xFE81, "M", "آ"), + (0xFE83, "M", "أ"), + (0xFE85, "M", "ؤ"), + (0xFE87, "M", "إ"), + (0xFE89, "M", "ئ"), + (0xFE8D, "M", "ا"), + (0xFE8F, "M", "ب"), + (0xFE93, "M", "ة"), + (0xFE95, "M", "ت"), + (0xFE99, "M", "ث"), + (0xFE9D, "M", "ج"), + (0xFEA1, "M", "ح"), + (0xFEA5, "M", "خ"), + (0xFEA9, "M", "د"), + (0xFEAB, "M", "ذ"), + (0xFEAD, "M", "ر"), + (0xFEAF, "M", "ز"), + (0xFEB1, "M", "س"), + (0xFEB5, "M", "ش"), + (0xFEB9, "M", "ص"), + (0xFEBD, "M", "ض"), + (0xFEC1, "M", "ط"), + (0xFEC5, "M", "ظ"), + (0xFEC9, "M", "ع"), + (0xFECD, "M", "غ"), + (0xFED1, "M", "ف"), + (0xFED5, "M", "ق"), + (0xFED9, "M", "ك"), + (0xFEDD, "M", "ل"), + (0xFEE1, "M", "م"), + (0xFEE5, "M", "ن"), + (0xFEE9, "M", "ه"), + (0xFEED, "M", "و"), + ] + + +def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFEEF, "M", "ى"), + (0xFEF1, "M", "ي"), + (0xFEF5, "M", "لآ"), + (0xFEF7, "M", "لأ"), + (0xFEF9, "M", "لإ"), + (0xFEFB, "M", "لا"), + (0xFEFD, "X"), + (0xFEFF, "I"), + (0xFF00, "X"), + (0xFF01, "M", "!"), + (0xFF02, "M", '"'), + (0xFF03, "M", "#"), + (0xFF04, "M", "$"), + (0xFF05, "M", "%"), + (0xFF06, "M", "&"), + (0xFF07, "M", "'"), + (0xFF08, "M", "("), + (0xFF09, "M", ")"), + (0xFF0A, "M", "*"), + (0xFF0B, "M", "+"), + (0xFF0C, "M", ","), + (0xFF0D, "M", "-"), + (0xFF0E, "M", "."), + (0xFF0F, "M", "/"), + (0xFF10, "M", "0"), + (0xFF11, "M", "1"), + (0xFF12, "M", "2"), + (0xFF13, "M", "3"), + (0xFF14, "M", "4"), + (0xFF15, "M", "5"), + (0xFF16, "M", "6"), + (0xFF17, "M", "7"), + (0xFF18, "M", "8"), + (0xFF19, "M", "9"), + (0xFF1A, "M", ":"), + (0xFF1B, "M", ";"), + (0xFF1C, "M", "<"), + (0xFF1D, "M", "="), + (0xFF1E, "M", ">"), + (0xFF1F, "M", "?"), + (0xFF20, "M", "@"), + (0xFF21, "M", "a"), + (0xFF22, "M", "b"), + (0xFF23, "M", "c"), + (0xFF24, "M", "d"), + (0xFF25, "M", "e"), + (0xFF26, "M", "f"), + (0xFF27, "M", "g"), + (0xFF28, "M", "h"), + (0xFF29, "M", "i"), + (0xFF2A, "M", "j"), + (0xFF2B, "M", "k"), + (0xFF2C, "M", "l"), + (0xFF2D, "M", "m"), + (0xFF2E, "M", "n"), + (0xFF2F, "M", "o"), + (0xFF30, "M", "p"), + (0xFF31, "M", "q"), + (0xFF32, "M", "r"), + (0xFF33, "M", "s"), + (0xFF34, "M", "t"), + (0xFF35, "M", "u"), + (0xFF36, "M", "v"), + (0xFF37, "M", "w"), + (0xFF38, "M", "x"), + (0xFF39, "M", "y"), + (0xFF3A, "M", "z"), + (0xFF3B, "M", "["), + (0xFF3C, "M", "\\"), + (0xFF3D, "M", "]"), + (0xFF3E, "M", "^"), + (0xFF3F, "M", "_"), + (0xFF40, "M", "`"), + (0xFF41, "M", "a"), + (0xFF42, "M", "b"), + (0xFF43, "M", "c"), + (0xFF44, "M", "d"), + (0xFF45, "M", "e"), + (0xFF46, "M", "f"), + (0xFF47, "M", "g"), + (0xFF48, "M", "h"), + (0xFF49, "M", "i"), + (0xFF4A, "M", "j"), + (0xFF4B, "M", "k"), + (0xFF4C, "M", "l"), + (0xFF4D, "M", "m"), + (0xFF4E, "M", "n"), + (0xFF4F, "M", "o"), + (0xFF50, "M", "p"), + (0xFF51, "M", "q"), + (0xFF52, "M", "r"), + (0xFF53, "M", "s"), + (0xFF54, "M", "t"), + (0xFF55, "M", "u"), + (0xFF56, "M", "v"), + (0xFF57, "M", "w"), + (0xFF58, "M", "x"), + (0xFF59, "M", "y"), + (0xFF5A, "M", "z"), + (0xFF5B, "M", "{"), + ] + + +def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFF5C, "M", "|"), + (0xFF5D, "M", "}"), + (0xFF5E, "M", "~"), + (0xFF5F, "M", "⦅"), + (0xFF60, "M", "⦆"), + (0xFF61, "M", "."), + (0xFF62, "M", "「"), + (0xFF63, "M", "」"), + (0xFF64, "M", "、"), + (0xFF65, "M", "・"), + (0xFF66, "M", "ヲ"), + (0xFF67, "M", "ァ"), + (0xFF68, "M", "ィ"), + (0xFF69, "M", "ゥ"), + (0xFF6A, "M", "ェ"), + (0xFF6B, "M", "ォ"), + (0xFF6C, "M", "ャ"), + (0xFF6D, "M", "ュ"), + (0xFF6E, "M", "ョ"), + (0xFF6F, "M", "ッ"), + (0xFF70, "M", "ー"), + (0xFF71, "M", "ア"), + (0xFF72, "M", "イ"), + (0xFF73, "M", "ウ"), + (0xFF74, "M", "エ"), + (0xFF75, "M", "オ"), + (0xFF76, "M", "カ"), + (0xFF77, "M", "キ"), + (0xFF78, "M", "ク"), + (0xFF79, "M", "ケ"), + (0xFF7A, "M", "コ"), + (0xFF7B, "M", "サ"), + (0xFF7C, "M", "シ"), + (0xFF7D, "M", "ス"), + (0xFF7E, "M", "セ"), + (0xFF7F, "M", "ソ"), + (0xFF80, "M", "タ"), + (0xFF81, "M", "チ"), + (0xFF82, "M", "ツ"), + (0xFF83, "M", "テ"), + (0xFF84, "M", "ト"), + (0xFF85, "M", "ナ"), + (0xFF86, "M", "ニ"), + (0xFF87, "M", "ヌ"), + (0xFF88, "M", "ネ"), + (0xFF89, "M", "ノ"), + (0xFF8A, "M", "ハ"), + (0xFF8B, "M", "ヒ"), + (0xFF8C, "M", "フ"), + (0xFF8D, "M", "ヘ"), + (0xFF8E, "M", "ホ"), + (0xFF8F, "M", "マ"), + (0xFF90, "M", "ミ"), + (0xFF91, "M", "ム"), + (0xFF92, "M", "メ"), + (0xFF93, "M", "モ"), + (0xFF94, "M", "ヤ"), + (0xFF95, "M", "ユ"), + (0xFF96, "M", "ヨ"), + (0xFF97, "M", "ラ"), + (0xFF98, "M", "リ"), + (0xFF99, "M", "ル"), + (0xFF9A, "M", "レ"), + (0xFF9B, "M", "ロ"), + (0xFF9C, "M", "ワ"), + (0xFF9D, "M", "ン"), + (0xFF9E, "M", "゙"), + (0xFF9F, "M", "゚"), + (0xFFA0, "I"), + (0xFFA1, "M", "ᄀ"), + (0xFFA2, "M", "ᄁ"), + (0xFFA3, "M", "ᆪ"), + (0xFFA4, "M", "ᄂ"), + (0xFFA5, "M", "ᆬ"), + (0xFFA6, "M", "ᆭ"), + (0xFFA7, "M", "ᄃ"), + (0xFFA8, "M", "ᄄ"), + (0xFFA9, "M", "ᄅ"), + (0xFFAA, "M", "ᆰ"), + (0xFFAB, "M", "ᆱ"), + (0xFFAC, "M", "ᆲ"), + (0xFFAD, "M", "ᆳ"), + (0xFFAE, "M", "ᆴ"), + (0xFFAF, "M", "ᆵ"), + (0xFFB0, "M", "ᄚ"), + (0xFFB1, "M", "ᄆ"), + (0xFFB2, "M", "ᄇ"), + (0xFFB3, "M", "ᄈ"), + (0xFFB4, "M", "ᄡ"), + (0xFFB5, "M", "ᄉ"), + (0xFFB6, "M", "ᄊ"), + (0xFFB7, "M", "ᄋ"), + (0xFFB8, "M", "ᄌ"), + (0xFFB9, "M", "ᄍ"), + (0xFFBA, "M", "ᄎ"), + (0xFFBB, "M", "ᄏ"), + (0xFFBC, "M", "ᄐ"), + (0xFFBD, "M", "ᄑ"), + (0xFFBE, "M", "ᄒ"), + (0xFFBF, "X"), + ] + + +def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFFC2, "M", "ᅡ"), + (0xFFC3, "M", "ᅢ"), + (0xFFC4, "M", "ᅣ"), + (0xFFC5, "M", "ᅤ"), + (0xFFC6, "M", "ᅥ"), + (0xFFC7, "M", "ᅦ"), + (0xFFC8, "X"), + (0xFFCA, "M", "ᅧ"), + (0xFFCB, "M", "ᅨ"), + (0xFFCC, "M", "ᅩ"), + (0xFFCD, "M", "ᅪ"), + (0xFFCE, "M", "ᅫ"), + (0xFFCF, "M", "ᅬ"), + (0xFFD0, "X"), + (0xFFD2, "M", "ᅭ"), + (0xFFD3, "M", "ᅮ"), + (0xFFD4, "M", "ᅯ"), + (0xFFD5, "M", "ᅰ"), + (0xFFD6, "M", "ᅱ"), + (0xFFD7, "M", "ᅲ"), + (0xFFD8, "X"), + (0xFFDA, "M", "ᅳ"), + (0xFFDB, "M", "ᅴ"), + (0xFFDC, "M", "ᅵ"), + (0xFFDD, "X"), + (0xFFE0, "M", "¢"), + (0xFFE1, "M", "£"), + (0xFFE2, "M", "¬"), + (0xFFE3, "M", " ̄"), + (0xFFE4, "M", "¦"), + (0xFFE5, "M", "¥"), + (0xFFE6, "M", "₩"), + (0xFFE7, "X"), + (0xFFE8, "M", "│"), + (0xFFE9, "M", "←"), + (0xFFEA, "M", "↑"), + (0xFFEB, "M", "→"), + (0xFFEC, "M", "↓"), + (0xFFED, "M", "■"), + (0xFFEE, "M", "○"), + (0xFFEF, "X"), + (0x10000, "V"), + (0x1000C, "X"), + (0x1000D, "V"), + (0x10027, "X"), + (0x10028, "V"), + (0x1003B, "X"), + (0x1003C, "V"), + (0x1003E, "X"), + (0x1003F, "V"), + (0x1004E, "X"), + (0x10050, "V"), + (0x1005E, "X"), + (0x10080, "V"), + (0x100FB, "X"), + (0x10100, "V"), + (0x10103, "X"), + (0x10107, "V"), + (0x10134, "X"), + (0x10137, "V"), + (0x1018F, "X"), + (0x10190, "V"), + (0x1019D, "X"), + (0x101A0, "V"), + (0x101A1, "X"), + (0x101D0, "V"), + (0x101FE, "X"), + (0x10280, "V"), + (0x1029D, "X"), + (0x102A0, "V"), + (0x102D1, "X"), + (0x102E0, "V"), + (0x102FC, "X"), + (0x10300, "V"), + (0x10324, "X"), + (0x1032D, "V"), + (0x1034B, "X"), + (0x10350, "V"), + (0x1037B, "X"), + (0x10380, "V"), + (0x1039E, "X"), + (0x1039F, "V"), + (0x103C4, "X"), + (0x103C8, "V"), + (0x103D6, "X"), + (0x10400, "M", "𐐨"), + (0x10401, "M", "𐐩"), + (0x10402, "M", "𐐪"), + (0x10403, "M", "𐐫"), + (0x10404, "M", "𐐬"), + (0x10405, "M", "𐐭"), + (0x10406, "M", "𐐮"), + (0x10407, "M", "𐐯"), + (0x10408, "M", "𐐰"), + (0x10409, "M", "𐐱"), + (0x1040A, "M", "𐐲"), + (0x1040B, "M", "𐐳"), + (0x1040C, "M", "𐐴"), + (0x1040D, "M", "𐐵"), + (0x1040E, "M", "𐐶"), + ] + + +def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1040F, "M", "𐐷"), + (0x10410, "M", "𐐸"), + (0x10411, "M", "𐐹"), + (0x10412, "M", "𐐺"), + (0x10413, "M", "𐐻"), + (0x10414, "M", "𐐼"), + (0x10415, "M", "𐐽"), + (0x10416, "M", "𐐾"), + (0x10417, "M", "𐐿"), + (0x10418, "M", "𐑀"), + (0x10419, "M", "𐑁"), + (0x1041A, "M", "𐑂"), + (0x1041B, "M", "𐑃"), + (0x1041C, "M", "𐑄"), + (0x1041D, "M", "𐑅"), + (0x1041E, "M", "𐑆"), + (0x1041F, "M", "𐑇"), + (0x10420, "M", "𐑈"), + (0x10421, "M", "𐑉"), + (0x10422, "M", "𐑊"), + (0x10423, "M", "𐑋"), + (0x10424, "M", "𐑌"), + (0x10425, "M", "𐑍"), + (0x10426, "M", "𐑎"), + (0x10427, "M", "𐑏"), + (0x10428, "V"), + (0x1049E, "X"), + (0x104A0, "V"), + (0x104AA, "X"), + (0x104B0, "M", "𐓘"), + (0x104B1, "M", "𐓙"), + (0x104B2, "M", "𐓚"), + (0x104B3, "M", "𐓛"), + (0x104B4, "M", "𐓜"), + (0x104B5, "M", "𐓝"), + (0x104B6, "M", "𐓞"), + (0x104B7, "M", "𐓟"), + (0x104B8, "M", "𐓠"), + (0x104B9, "M", "𐓡"), + (0x104BA, "M", "𐓢"), + (0x104BB, "M", "𐓣"), + (0x104BC, "M", "𐓤"), + (0x104BD, "M", "𐓥"), + (0x104BE, "M", "𐓦"), + (0x104BF, "M", "𐓧"), + (0x104C0, "M", "𐓨"), + (0x104C1, "M", "𐓩"), + (0x104C2, "M", "𐓪"), + (0x104C3, "M", "𐓫"), + (0x104C4, "M", "𐓬"), + (0x104C5, "M", "𐓭"), + (0x104C6, "M", "𐓮"), + (0x104C7, "M", "𐓯"), + (0x104C8, "M", "𐓰"), + (0x104C9, "M", "𐓱"), + (0x104CA, "M", "𐓲"), + (0x104CB, "M", "𐓳"), + (0x104CC, "M", "𐓴"), + (0x104CD, "M", "𐓵"), + (0x104CE, "M", "𐓶"), + (0x104CF, "M", "𐓷"), + (0x104D0, "M", "𐓸"), + (0x104D1, "M", "𐓹"), + (0x104D2, "M", "𐓺"), + (0x104D3, "M", "𐓻"), + (0x104D4, "X"), + (0x104D8, "V"), + (0x104FC, "X"), + (0x10500, "V"), + (0x10528, "X"), + (0x10530, "V"), + (0x10564, "X"), + (0x1056F, "V"), + (0x10570, "M", "𐖗"), + (0x10571, "M", "𐖘"), + (0x10572, "M", "𐖙"), + (0x10573, "M", "𐖚"), + (0x10574, "M", "𐖛"), + (0x10575, "M", "𐖜"), + (0x10576, "M", "𐖝"), + (0x10577, "M", "𐖞"), + (0x10578, "M", "𐖟"), + (0x10579, "M", "𐖠"), + (0x1057A, "M", "𐖡"), + (0x1057B, "X"), + (0x1057C, "M", "𐖣"), + (0x1057D, "M", "𐖤"), + (0x1057E, "M", "𐖥"), + (0x1057F, "M", "𐖦"), + (0x10580, "M", "𐖧"), + (0x10581, "M", "𐖨"), + (0x10582, "M", "𐖩"), + (0x10583, "M", "𐖪"), + (0x10584, "M", "𐖫"), + (0x10585, "M", "𐖬"), + (0x10586, "M", "𐖭"), + (0x10587, "M", "𐖮"), + (0x10588, "M", "𐖯"), + (0x10589, "M", "𐖰"), + (0x1058A, "M", "𐖱"), + ] + + +def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1058B, "X"), + (0x1058C, "M", "𐖳"), + (0x1058D, "M", "𐖴"), + (0x1058E, "M", "𐖵"), + (0x1058F, "M", "𐖶"), + (0x10590, "M", "𐖷"), + (0x10591, "M", "𐖸"), + (0x10592, "M", "𐖹"), + (0x10593, "X"), + (0x10594, "M", "𐖻"), + (0x10595, "M", "𐖼"), + (0x10596, "X"), + (0x10597, "V"), + (0x105A2, "X"), + (0x105A3, "V"), + (0x105B2, "X"), + (0x105B3, "V"), + (0x105BA, "X"), + (0x105BB, "V"), + (0x105BD, "X"), + (0x105C0, "V"), + (0x105F4, "X"), + (0x10600, "V"), + (0x10737, "X"), + (0x10740, "V"), + (0x10756, "X"), + (0x10760, "V"), + (0x10768, "X"), + (0x10780, "V"), + (0x10781, "M", "ː"), + (0x10782, "M", "ˑ"), + (0x10783, "M", "æ"), + (0x10784, "M", "ʙ"), + (0x10785, "M", "ɓ"), + (0x10786, "X"), + (0x10787, "M", "ʣ"), + (0x10788, "M", "ꭦ"), + (0x10789, "M", "ʥ"), + (0x1078A, "M", "ʤ"), + (0x1078B, "M", "ɖ"), + (0x1078C, "M", "ɗ"), + (0x1078D, "M", "ᶑ"), + (0x1078E, "M", "ɘ"), + (0x1078F, "M", "ɞ"), + (0x10790, "M", "ʩ"), + (0x10791, "M", "ɤ"), + (0x10792, "M", "ɢ"), + (0x10793, "M", "ɠ"), + (0x10794, "M", "ʛ"), + (0x10795, "M", "ħ"), + (0x10796, "M", "ʜ"), + (0x10797, "M", "ɧ"), + (0x10798, "M", "ʄ"), + (0x10799, "M", "ʪ"), + (0x1079A, "M", "ʫ"), + (0x1079B, "M", "ɬ"), + (0x1079C, "M", "𝼄"), + (0x1079D, "M", "ꞎ"), + (0x1079E, "M", "ɮ"), + (0x1079F, "M", "𝼅"), + (0x107A0, "M", "ʎ"), + (0x107A1, "M", "𝼆"), + (0x107A2, "M", "ø"), + (0x107A3, "M", "ɶ"), + (0x107A4, "M", "ɷ"), + (0x107A5, "M", "q"), + (0x107A6, "M", "ɺ"), + (0x107A7, "M", "𝼈"), + (0x107A8, "M", "ɽ"), + (0x107A9, "M", "ɾ"), + (0x107AA, "M", "ʀ"), + (0x107AB, "M", "ʨ"), + (0x107AC, "M", "ʦ"), + (0x107AD, "M", "ꭧ"), + (0x107AE, "M", "ʧ"), + (0x107AF, "M", "ʈ"), + (0x107B0, "M", "ⱱ"), + (0x107B1, "X"), + (0x107B2, "M", "ʏ"), + (0x107B3, "M", "ʡ"), + (0x107B4, "M", "ʢ"), + (0x107B5, "M", "ʘ"), + (0x107B6, "M", "ǀ"), + (0x107B7, "M", "ǁ"), + (0x107B8, "M", "ǂ"), + (0x107B9, "M", "𝼊"), + (0x107BA, "M", "𝼞"), + (0x107BB, "X"), + (0x10800, "V"), + (0x10806, "X"), + (0x10808, "V"), + (0x10809, "X"), + (0x1080A, "V"), + (0x10836, "X"), + (0x10837, "V"), + (0x10839, "X"), + (0x1083C, "V"), + (0x1083D, "X"), + (0x1083F, "V"), + (0x10856, "X"), + ] + + +def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10857, "V"), + (0x1089F, "X"), + (0x108A7, "V"), + (0x108B0, "X"), + (0x108E0, "V"), + (0x108F3, "X"), + (0x108F4, "V"), + (0x108F6, "X"), + (0x108FB, "V"), + (0x1091C, "X"), + (0x1091F, "V"), + (0x1093A, "X"), + (0x1093F, "V"), + (0x10940, "X"), + (0x10980, "V"), + (0x109B8, "X"), + (0x109BC, "V"), + (0x109D0, "X"), + (0x109D2, "V"), + (0x10A04, "X"), + (0x10A05, "V"), + (0x10A07, "X"), + (0x10A0C, "V"), + (0x10A14, "X"), + (0x10A15, "V"), + (0x10A18, "X"), + (0x10A19, "V"), + (0x10A36, "X"), + (0x10A38, "V"), + (0x10A3B, "X"), + (0x10A3F, "V"), + (0x10A49, "X"), + (0x10A50, "V"), + (0x10A59, "X"), + (0x10A60, "V"), + (0x10AA0, "X"), + (0x10AC0, "V"), + (0x10AE7, "X"), + (0x10AEB, "V"), + (0x10AF7, "X"), + (0x10B00, "V"), + (0x10B36, "X"), + (0x10B39, "V"), + (0x10B56, "X"), + (0x10B58, "V"), + (0x10B73, "X"), + (0x10B78, "V"), + (0x10B92, "X"), + (0x10B99, "V"), + (0x10B9D, "X"), + (0x10BA9, "V"), + (0x10BB0, "X"), + (0x10C00, "V"), + (0x10C49, "X"), + (0x10C80, "M", "𐳀"), + (0x10C81, "M", "𐳁"), + (0x10C82, "M", "𐳂"), + (0x10C83, "M", "𐳃"), + (0x10C84, "M", "𐳄"), + (0x10C85, "M", "𐳅"), + (0x10C86, "M", "𐳆"), + (0x10C87, "M", "𐳇"), + (0x10C88, "M", "𐳈"), + (0x10C89, "M", "𐳉"), + (0x10C8A, "M", "𐳊"), + (0x10C8B, "M", "𐳋"), + (0x10C8C, "M", "𐳌"), + (0x10C8D, "M", "𐳍"), + (0x10C8E, "M", "𐳎"), + (0x10C8F, "M", "𐳏"), + (0x10C90, "M", "𐳐"), + (0x10C91, "M", "𐳑"), + (0x10C92, "M", "𐳒"), + (0x10C93, "M", "𐳓"), + (0x10C94, "M", "𐳔"), + (0x10C95, "M", "𐳕"), + (0x10C96, "M", "𐳖"), + (0x10C97, "M", "𐳗"), + (0x10C98, "M", "𐳘"), + (0x10C99, "M", "𐳙"), + (0x10C9A, "M", "𐳚"), + (0x10C9B, "M", "𐳛"), + (0x10C9C, "M", "𐳜"), + (0x10C9D, "M", "𐳝"), + (0x10C9E, "M", "𐳞"), + (0x10C9F, "M", "𐳟"), + (0x10CA0, "M", "𐳠"), + (0x10CA1, "M", "𐳡"), + (0x10CA2, "M", "𐳢"), + (0x10CA3, "M", "𐳣"), + (0x10CA4, "M", "𐳤"), + (0x10CA5, "M", "𐳥"), + (0x10CA6, "M", "𐳦"), + (0x10CA7, "M", "𐳧"), + (0x10CA8, "M", "𐳨"), + (0x10CA9, "M", "𐳩"), + (0x10CAA, "M", "𐳪"), + (0x10CAB, "M", "𐳫"), + (0x10CAC, "M", "𐳬"), + (0x10CAD, "M", "𐳭"), + ] + + +def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10CAE, "M", "𐳮"), + (0x10CAF, "M", "𐳯"), + (0x10CB0, "M", "𐳰"), + (0x10CB1, "M", "𐳱"), + (0x10CB2, "M", "𐳲"), + (0x10CB3, "X"), + (0x10CC0, "V"), + (0x10CF3, "X"), + (0x10CFA, "V"), + (0x10D28, "X"), + (0x10D30, "V"), + (0x10D3A, "X"), + (0x10D40, "V"), + (0x10D50, "M", "𐵰"), + (0x10D51, "M", "𐵱"), + (0x10D52, "M", "𐵲"), + (0x10D53, "M", "𐵳"), + (0x10D54, "M", "𐵴"), + (0x10D55, "M", "𐵵"), + (0x10D56, "M", "𐵶"), + (0x10D57, "M", "𐵷"), + (0x10D58, "M", "𐵸"), + (0x10D59, "M", "𐵹"), + (0x10D5A, "M", "𐵺"), + (0x10D5B, "M", "𐵻"), + (0x10D5C, "M", "𐵼"), + (0x10D5D, "M", "𐵽"), + (0x10D5E, "M", "𐵾"), + (0x10D5F, "M", "𐵿"), + (0x10D60, "M", "𐶀"), + (0x10D61, "M", "𐶁"), + (0x10D62, "M", "𐶂"), + (0x10D63, "M", "𐶃"), + (0x10D64, "M", "𐶄"), + (0x10D65, "M", "𐶅"), + (0x10D66, "X"), + (0x10D69, "V"), + (0x10D86, "X"), + (0x10D8E, "V"), + (0x10D90, "X"), + (0x10E60, "V"), + (0x10E7F, "X"), + (0x10E80, "V"), + (0x10EAA, "X"), + (0x10EAB, "V"), + (0x10EAE, "X"), + (0x10EB0, "V"), + (0x10EB2, "X"), + (0x10EC2, "V"), + (0x10EC5, "X"), + (0x10EFC, "V"), + (0x10F28, "X"), + (0x10F30, "V"), + (0x10F5A, "X"), + (0x10F70, "V"), + (0x10F8A, "X"), + (0x10FB0, "V"), + (0x10FCC, "X"), + (0x10FE0, "V"), + (0x10FF7, "X"), + (0x11000, "V"), + (0x1104E, "X"), + (0x11052, "V"), + (0x11076, "X"), + (0x1107F, "V"), + (0x110BD, "X"), + (0x110BE, "V"), + (0x110C3, "X"), + (0x110D0, "V"), + (0x110E9, "X"), + (0x110F0, "V"), + (0x110FA, "X"), + (0x11100, "V"), + (0x11135, "X"), + (0x11136, "V"), + (0x11148, "X"), + (0x11150, "V"), + (0x11177, "X"), + (0x11180, "V"), + (0x111E0, "X"), + (0x111E1, "V"), + (0x111F5, "X"), + (0x11200, "V"), + (0x11212, "X"), + (0x11213, "V"), + (0x11242, "X"), + (0x11280, "V"), + (0x11287, "X"), + (0x11288, "V"), + (0x11289, "X"), + (0x1128A, "V"), + (0x1128E, "X"), + (0x1128F, "V"), + (0x1129E, "X"), + (0x1129F, "V"), + (0x112AA, "X"), + (0x112B0, "V"), + (0x112EB, "X"), + (0x112F0, "V"), + (0x112FA, "X"), + ] + + +def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x11300, "V"), + (0x11304, "X"), + (0x11305, "V"), + (0x1130D, "X"), + (0x1130F, "V"), + (0x11311, "X"), + (0x11313, "V"), + (0x11329, "X"), + (0x1132A, "V"), + (0x11331, "X"), + (0x11332, "V"), + (0x11334, "X"), + (0x11335, "V"), + (0x1133A, "X"), + (0x1133B, "V"), + (0x11345, "X"), + (0x11347, "V"), + (0x11349, "X"), + (0x1134B, "V"), + (0x1134E, "X"), + (0x11350, "V"), + (0x11351, "X"), + (0x11357, "V"), + (0x11358, "X"), + (0x1135D, "V"), + (0x11364, "X"), + (0x11366, "V"), + (0x1136D, "X"), + (0x11370, "V"), + (0x11375, "X"), + (0x11380, "V"), + (0x1138A, "X"), + (0x1138B, "V"), + (0x1138C, "X"), + (0x1138E, "V"), + (0x1138F, "X"), + (0x11390, "V"), + (0x113B6, "X"), + (0x113B7, "V"), + (0x113C1, "X"), + (0x113C2, "V"), + (0x113C3, "X"), + (0x113C5, "V"), + (0x113C6, "X"), + (0x113C7, "V"), + (0x113CB, "X"), + (0x113CC, "V"), + (0x113D6, "X"), + (0x113D7, "V"), + (0x113D9, "X"), + (0x113E1, "V"), + (0x113E3, "X"), + (0x11400, "V"), + (0x1145C, "X"), + (0x1145D, "V"), + (0x11462, "X"), + (0x11480, "V"), + (0x114C8, "X"), + (0x114D0, "V"), + (0x114DA, "X"), + (0x11580, "V"), + (0x115B6, "X"), + (0x115B8, "V"), + (0x115DE, "X"), + (0x11600, "V"), + (0x11645, "X"), + (0x11650, "V"), + (0x1165A, "X"), + (0x11660, "V"), + (0x1166D, "X"), + (0x11680, "V"), + (0x116BA, "X"), + (0x116C0, "V"), + (0x116CA, "X"), + (0x116D0, "V"), + (0x116E4, "X"), + (0x11700, "V"), + (0x1171B, "X"), + (0x1171D, "V"), + (0x1172C, "X"), + (0x11730, "V"), + (0x11747, "X"), + (0x11800, "V"), + (0x1183C, "X"), + (0x118A0, "M", "𑣀"), + (0x118A1, "M", "𑣁"), + (0x118A2, "M", "𑣂"), + (0x118A3, "M", "𑣃"), + (0x118A4, "M", "𑣄"), + (0x118A5, "M", "𑣅"), + (0x118A6, "M", "𑣆"), + (0x118A7, "M", "𑣇"), + (0x118A8, "M", "𑣈"), + (0x118A9, "M", "𑣉"), + (0x118AA, "M", "𑣊"), + (0x118AB, "M", "𑣋"), + (0x118AC, "M", "𑣌"), + (0x118AD, "M", "𑣍"), + (0x118AE, "M", "𑣎"), + (0x118AF, "M", "𑣏"), + ] + + +def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x118B0, "M", "𑣐"), + (0x118B1, "M", "𑣑"), + (0x118B2, "M", "𑣒"), + (0x118B3, "M", "𑣓"), + (0x118B4, "M", "𑣔"), + (0x118B5, "M", "𑣕"), + (0x118B6, "M", "𑣖"), + (0x118B7, "M", "𑣗"), + (0x118B8, "M", "𑣘"), + (0x118B9, "M", "𑣙"), + (0x118BA, "M", "𑣚"), + (0x118BB, "M", "𑣛"), + (0x118BC, "M", "𑣜"), + (0x118BD, "M", "𑣝"), + (0x118BE, "M", "𑣞"), + (0x118BF, "M", "𑣟"), + (0x118C0, "V"), + (0x118F3, "X"), + (0x118FF, "V"), + (0x11907, "X"), + (0x11909, "V"), + (0x1190A, "X"), + (0x1190C, "V"), + (0x11914, "X"), + (0x11915, "V"), + (0x11917, "X"), + (0x11918, "V"), + (0x11936, "X"), + (0x11937, "V"), + (0x11939, "X"), + (0x1193B, "V"), + (0x11947, "X"), + (0x11950, "V"), + (0x1195A, "X"), + (0x119A0, "V"), + (0x119A8, "X"), + (0x119AA, "V"), + (0x119D8, "X"), + (0x119DA, "V"), + (0x119E5, "X"), + (0x11A00, "V"), + (0x11A48, "X"), + (0x11A50, "V"), + (0x11AA3, "X"), + (0x11AB0, "V"), + (0x11AF9, "X"), + (0x11B00, "V"), + (0x11B0A, "X"), + (0x11BC0, "V"), + (0x11BE2, "X"), + (0x11BF0, "V"), + (0x11BFA, "X"), + (0x11C00, "V"), + (0x11C09, "X"), + (0x11C0A, "V"), + (0x11C37, "X"), + (0x11C38, "V"), + (0x11C46, "X"), + (0x11C50, "V"), + (0x11C6D, "X"), + (0x11C70, "V"), + (0x11C90, "X"), + (0x11C92, "V"), + (0x11CA8, "X"), + (0x11CA9, "V"), + (0x11CB7, "X"), + (0x11D00, "V"), + (0x11D07, "X"), + (0x11D08, "V"), + (0x11D0A, "X"), + (0x11D0B, "V"), + (0x11D37, "X"), + (0x11D3A, "V"), + (0x11D3B, "X"), + (0x11D3C, "V"), + (0x11D3E, "X"), + (0x11D3F, "V"), + (0x11D48, "X"), + (0x11D50, "V"), + (0x11D5A, "X"), + (0x11D60, "V"), + (0x11D66, "X"), + (0x11D67, "V"), + (0x11D69, "X"), + (0x11D6A, "V"), + (0x11D8F, "X"), + (0x11D90, "V"), + (0x11D92, "X"), + (0x11D93, "V"), + (0x11D99, "X"), + (0x11DA0, "V"), + (0x11DAA, "X"), + (0x11EE0, "V"), + (0x11EF9, "X"), + (0x11F00, "V"), + (0x11F11, "X"), + (0x11F12, "V"), + (0x11F3B, "X"), + (0x11F3E, "V"), + (0x11F5B, "X"), + ] + + +def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x11FB0, "V"), + (0x11FB1, "X"), + (0x11FC0, "V"), + (0x11FF2, "X"), + (0x11FFF, "V"), + (0x1239A, "X"), + (0x12400, "V"), + (0x1246F, "X"), + (0x12470, "V"), + (0x12475, "X"), + (0x12480, "V"), + (0x12544, "X"), + (0x12F90, "V"), + (0x12FF3, "X"), + (0x13000, "V"), + (0x13430, "X"), + (0x13440, "V"), + (0x13456, "X"), + (0x13460, "V"), + (0x143FB, "X"), + (0x14400, "V"), + (0x14647, "X"), + (0x16100, "V"), + (0x1613A, "X"), + (0x16800, "V"), + (0x16A39, "X"), + (0x16A40, "V"), + (0x16A5F, "X"), + (0x16A60, "V"), + (0x16A6A, "X"), + (0x16A6E, "V"), + (0x16ABF, "X"), + (0x16AC0, "V"), + (0x16ACA, "X"), + (0x16AD0, "V"), + (0x16AEE, "X"), + (0x16AF0, "V"), + (0x16AF6, "X"), + (0x16B00, "V"), + (0x16B46, "X"), + (0x16B50, "V"), + (0x16B5A, "X"), + (0x16B5B, "V"), + (0x16B62, "X"), + (0x16B63, "V"), + (0x16B78, "X"), + (0x16B7D, "V"), + (0x16B90, "X"), + (0x16D40, "V"), + (0x16D7A, "X"), + (0x16E40, "M", "𖹠"), + (0x16E41, "M", "𖹡"), + (0x16E42, "M", "𖹢"), + (0x16E43, "M", "𖹣"), + (0x16E44, "M", "𖹤"), + (0x16E45, "M", "𖹥"), + (0x16E46, "M", "𖹦"), + (0x16E47, "M", "𖹧"), + (0x16E48, "M", "𖹨"), + (0x16E49, "M", "𖹩"), + (0x16E4A, "M", "𖹪"), + (0x16E4B, "M", "𖹫"), + (0x16E4C, "M", "𖹬"), + (0x16E4D, "M", "𖹭"), + (0x16E4E, "M", "𖹮"), + (0x16E4F, "M", "𖹯"), + (0x16E50, "M", "𖹰"), + (0x16E51, "M", "𖹱"), + (0x16E52, "M", "𖹲"), + (0x16E53, "M", "𖹳"), + (0x16E54, "M", "𖹴"), + (0x16E55, "M", "𖹵"), + (0x16E56, "M", "𖹶"), + (0x16E57, "M", "𖹷"), + (0x16E58, "M", "𖹸"), + (0x16E59, "M", "𖹹"), + (0x16E5A, "M", "𖹺"), + (0x16E5B, "M", "𖹻"), + (0x16E5C, "M", "𖹼"), + (0x16E5D, "M", "𖹽"), + (0x16E5E, "M", "𖹾"), + (0x16E5F, "M", "𖹿"), + (0x16E60, "V"), + (0x16E9B, "X"), + (0x16F00, "V"), + (0x16F4B, "X"), + (0x16F4F, "V"), + (0x16F88, "X"), + (0x16F8F, "V"), + (0x16FA0, "X"), + (0x16FE0, "V"), + (0x16FE5, "X"), + (0x16FF0, "V"), + (0x16FF2, "X"), + (0x17000, "V"), + (0x187F8, "X"), + (0x18800, "V"), + (0x18CD6, "X"), + (0x18CFF, "V"), + (0x18D09, "X"), + ] + + +def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1AFF0, "V"), + (0x1AFF4, "X"), + (0x1AFF5, "V"), + (0x1AFFC, "X"), + (0x1AFFD, "V"), + (0x1AFFF, "X"), + (0x1B000, "V"), + (0x1B123, "X"), + (0x1B132, "V"), + (0x1B133, "X"), + (0x1B150, "V"), + (0x1B153, "X"), + (0x1B155, "V"), + (0x1B156, "X"), + (0x1B164, "V"), + (0x1B168, "X"), + (0x1B170, "V"), + (0x1B2FC, "X"), + (0x1BC00, "V"), + (0x1BC6B, "X"), + (0x1BC70, "V"), + (0x1BC7D, "X"), + (0x1BC80, "V"), + (0x1BC89, "X"), + (0x1BC90, "V"), + (0x1BC9A, "X"), + (0x1BC9C, "V"), + (0x1BCA0, "I"), + (0x1BCA4, "X"), + (0x1CC00, "V"), + (0x1CCD6, "M", "a"), + (0x1CCD7, "M", "b"), + (0x1CCD8, "M", "c"), + (0x1CCD9, "M", "d"), + (0x1CCDA, "M", "e"), + (0x1CCDB, "M", "f"), + (0x1CCDC, "M", "g"), + (0x1CCDD, "M", "h"), + (0x1CCDE, "M", "i"), + (0x1CCDF, "M", "j"), + (0x1CCE0, "M", "k"), + (0x1CCE1, "M", "l"), + (0x1CCE2, "M", "m"), + (0x1CCE3, "M", "n"), + (0x1CCE4, "M", "o"), + (0x1CCE5, "M", "p"), + (0x1CCE6, "M", "q"), + (0x1CCE7, "M", "r"), + (0x1CCE8, "M", "s"), + (0x1CCE9, "M", "t"), + (0x1CCEA, "M", "u"), + (0x1CCEB, "M", "v"), + (0x1CCEC, "M", "w"), + (0x1CCED, "M", "x"), + (0x1CCEE, "M", "y"), + (0x1CCEF, "M", "z"), + (0x1CCF0, "M", "0"), + (0x1CCF1, "M", "1"), + (0x1CCF2, "M", "2"), + (0x1CCF3, "M", "3"), + (0x1CCF4, "M", "4"), + (0x1CCF5, "M", "5"), + (0x1CCF6, "M", "6"), + (0x1CCF7, "M", "7"), + (0x1CCF8, "M", "8"), + (0x1CCF9, "M", "9"), + (0x1CCFA, "X"), + (0x1CD00, "V"), + (0x1CEB4, "X"), + (0x1CF00, "V"), + (0x1CF2E, "X"), + (0x1CF30, "V"), + (0x1CF47, "X"), + (0x1CF50, "V"), + (0x1CFC4, "X"), + (0x1D000, "V"), + (0x1D0F6, "X"), + (0x1D100, "V"), + (0x1D127, "X"), + (0x1D129, "V"), + (0x1D15E, "M", "𝅗𝅥"), + (0x1D15F, "M", "𝅘𝅥"), + (0x1D160, "M", "𝅘𝅥𝅮"), + (0x1D161, "M", "𝅘𝅥𝅯"), + (0x1D162, "M", "𝅘𝅥𝅰"), + (0x1D163, "M", "𝅘𝅥𝅱"), + (0x1D164, "M", "𝅘𝅥𝅲"), + (0x1D165, "V"), + (0x1D173, "I"), + (0x1D17B, "V"), + (0x1D1BB, "M", "𝆹𝅥"), + (0x1D1BC, "M", "𝆺𝅥"), + (0x1D1BD, "M", "𝆹𝅥𝅮"), + (0x1D1BE, "M", "𝆺𝅥𝅮"), + (0x1D1BF, "M", "𝆹𝅥𝅯"), + (0x1D1C0, "M", "𝆺𝅥𝅯"), + (0x1D1C1, "V"), + (0x1D1EB, "X"), + (0x1D200, "V"), + (0x1D246, "X"), + ] + + +def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D2C0, "V"), + (0x1D2D4, "X"), + (0x1D2E0, "V"), + (0x1D2F4, "X"), + (0x1D300, "V"), + (0x1D357, "X"), + (0x1D360, "V"), + (0x1D379, "X"), + (0x1D400, "M", "a"), + (0x1D401, "M", "b"), + (0x1D402, "M", "c"), + (0x1D403, "M", "d"), + (0x1D404, "M", "e"), + (0x1D405, "M", "f"), + (0x1D406, "M", "g"), + (0x1D407, "M", "h"), + (0x1D408, "M", "i"), + (0x1D409, "M", "j"), + (0x1D40A, "M", "k"), + (0x1D40B, "M", "l"), + (0x1D40C, "M", "m"), + (0x1D40D, "M", "n"), + (0x1D40E, "M", "o"), + (0x1D40F, "M", "p"), + (0x1D410, "M", "q"), + (0x1D411, "M", "r"), + (0x1D412, "M", "s"), + (0x1D413, "M", "t"), + (0x1D414, "M", "u"), + (0x1D415, "M", "v"), + (0x1D416, "M", "w"), + (0x1D417, "M", "x"), + (0x1D418, "M", "y"), + (0x1D419, "M", "z"), + (0x1D41A, "M", "a"), + (0x1D41B, "M", "b"), + (0x1D41C, "M", "c"), + (0x1D41D, "M", "d"), + (0x1D41E, "M", "e"), + (0x1D41F, "M", "f"), + (0x1D420, "M", "g"), + (0x1D421, "M", "h"), + (0x1D422, "M", "i"), + (0x1D423, "M", "j"), + (0x1D424, "M", "k"), + (0x1D425, "M", "l"), + (0x1D426, "M", "m"), + (0x1D427, "M", "n"), + (0x1D428, "M", "o"), + (0x1D429, "M", "p"), + (0x1D42A, "M", "q"), + (0x1D42B, "M", "r"), + (0x1D42C, "M", "s"), + (0x1D42D, "M", "t"), + (0x1D42E, "M", "u"), + (0x1D42F, "M", "v"), + (0x1D430, "M", "w"), + (0x1D431, "M", "x"), + (0x1D432, "M", "y"), + (0x1D433, "M", "z"), + (0x1D434, "M", "a"), + (0x1D435, "M", "b"), + (0x1D436, "M", "c"), + (0x1D437, "M", "d"), + (0x1D438, "M", "e"), + (0x1D439, "M", "f"), + (0x1D43A, "M", "g"), + (0x1D43B, "M", "h"), + (0x1D43C, "M", "i"), + (0x1D43D, "M", "j"), + (0x1D43E, "M", "k"), + (0x1D43F, "M", "l"), + (0x1D440, "M", "m"), + (0x1D441, "M", "n"), + (0x1D442, "M", "o"), + (0x1D443, "M", "p"), + (0x1D444, "M", "q"), + (0x1D445, "M", "r"), + (0x1D446, "M", "s"), + (0x1D447, "M", "t"), + (0x1D448, "M", "u"), + (0x1D449, "M", "v"), + (0x1D44A, "M", "w"), + (0x1D44B, "M", "x"), + (0x1D44C, "M", "y"), + (0x1D44D, "M", "z"), + (0x1D44E, "M", "a"), + (0x1D44F, "M", "b"), + (0x1D450, "M", "c"), + (0x1D451, "M", "d"), + (0x1D452, "M", "e"), + (0x1D453, "M", "f"), + (0x1D454, "M", "g"), + (0x1D455, "X"), + (0x1D456, "M", "i"), + (0x1D457, "M", "j"), + (0x1D458, "M", "k"), + (0x1D459, "M", "l"), + (0x1D45A, "M", "m"), + (0x1D45B, "M", "n"), + ] + + +def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D45C, "M", "o"), + (0x1D45D, "M", "p"), + (0x1D45E, "M", "q"), + (0x1D45F, "M", "r"), + (0x1D460, "M", "s"), + (0x1D461, "M", "t"), + (0x1D462, "M", "u"), + (0x1D463, "M", "v"), + (0x1D464, "M", "w"), + (0x1D465, "M", "x"), + (0x1D466, "M", "y"), + (0x1D467, "M", "z"), + (0x1D468, "M", "a"), + (0x1D469, "M", "b"), + (0x1D46A, "M", "c"), + (0x1D46B, "M", "d"), + (0x1D46C, "M", "e"), + (0x1D46D, "M", "f"), + (0x1D46E, "M", "g"), + (0x1D46F, "M", "h"), + (0x1D470, "M", "i"), + (0x1D471, "M", "j"), + (0x1D472, "M", "k"), + (0x1D473, "M", "l"), + (0x1D474, "M", "m"), + (0x1D475, "M", "n"), + (0x1D476, "M", "o"), + (0x1D477, "M", "p"), + (0x1D478, "M", "q"), + (0x1D479, "M", "r"), + (0x1D47A, "M", "s"), + (0x1D47B, "M", "t"), + (0x1D47C, "M", "u"), + (0x1D47D, "M", "v"), + (0x1D47E, "M", "w"), + (0x1D47F, "M", "x"), + (0x1D480, "M", "y"), + (0x1D481, "M", "z"), + (0x1D482, "M", "a"), + (0x1D483, "M", "b"), + (0x1D484, "M", "c"), + (0x1D485, "M", "d"), + (0x1D486, "M", "e"), + (0x1D487, "M", "f"), + (0x1D488, "M", "g"), + (0x1D489, "M", "h"), + (0x1D48A, "M", "i"), + (0x1D48B, "M", "j"), + (0x1D48C, "M", "k"), + (0x1D48D, "M", "l"), + (0x1D48E, "M", "m"), + (0x1D48F, "M", "n"), + (0x1D490, "M", "o"), + (0x1D491, "M", "p"), + (0x1D492, "M", "q"), + (0x1D493, "M", "r"), + (0x1D494, "M", "s"), + (0x1D495, "M", "t"), + (0x1D496, "M", "u"), + (0x1D497, "M", "v"), + (0x1D498, "M", "w"), + (0x1D499, "M", "x"), + (0x1D49A, "M", "y"), + (0x1D49B, "M", "z"), + (0x1D49C, "M", "a"), + (0x1D49D, "X"), + (0x1D49E, "M", "c"), + (0x1D49F, "M", "d"), + (0x1D4A0, "X"), + (0x1D4A2, "M", "g"), + (0x1D4A3, "X"), + (0x1D4A5, "M", "j"), + (0x1D4A6, "M", "k"), + (0x1D4A7, "X"), + (0x1D4A9, "M", "n"), + (0x1D4AA, "M", "o"), + (0x1D4AB, "M", "p"), + (0x1D4AC, "M", "q"), + (0x1D4AD, "X"), + (0x1D4AE, "M", "s"), + (0x1D4AF, "M", "t"), + (0x1D4B0, "M", "u"), + (0x1D4B1, "M", "v"), + (0x1D4B2, "M", "w"), + (0x1D4B3, "M", "x"), + (0x1D4B4, "M", "y"), + (0x1D4B5, "M", "z"), + (0x1D4B6, "M", "a"), + (0x1D4B7, "M", "b"), + (0x1D4B8, "M", "c"), + (0x1D4B9, "M", "d"), + (0x1D4BA, "X"), + (0x1D4BB, "M", "f"), + (0x1D4BC, "X"), + (0x1D4BD, "M", "h"), + (0x1D4BE, "M", "i"), + (0x1D4BF, "M", "j"), + (0x1D4C0, "M", "k"), + (0x1D4C1, "M", "l"), + (0x1D4C2, "M", "m"), + ] + + +def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D4C3, "M", "n"), + (0x1D4C4, "X"), + (0x1D4C5, "M", "p"), + (0x1D4C6, "M", "q"), + (0x1D4C7, "M", "r"), + (0x1D4C8, "M", "s"), + (0x1D4C9, "M", "t"), + (0x1D4CA, "M", "u"), + (0x1D4CB, "M", "v"), + (0x1D4CC, "M", "w"), + (0x1D4CD, "M", "x"), + (0x1D4CE, "M", "y"), + (0x1D4CF, "M", "z"), + (0x1D4D0, "M", "a"), + (0x1D4D1, "M", "b"), + (0x1D4D2, "M", "c"), + (0x1D4D3, "M", "d"), + (0x1D4D4, "M", "e"), + (0x1D4D5, "M", "f"), + (0x1D4D6, "M", "g"), + (0x1D4D7, "M", "h"), + (0x1D4D8, "M", "i"), + (0x1D4D9, "M", "j"), + (0x1D4DA, "M", "k"), + (0x1D4DB, "M", "l"), + (0x1D4DC, "M", "m"), + (0x1D4DD, "M", "n"), + (0x1D4DE, "M", "o"), + (0x1D4DF, "M", "p"), + (0x1D4E0, "M", "q"), + (0x1D4E1, "M", "r"), + (0x1D4E2, "M", "s"), + (0x1D4E3, "M", "t"), + (0x1D4E4, "M", "u"), + (0x1D4E5, "M", "v"), + (0x1D4E6, "M", "w"), + (0x1D4E7, "M", "x"), + (0x1D4E8, "M", "y"), + (0x1D4E9, "M", "z"), + (0x1D4EA, "M", "a"), + (0x1D4EB, "M", "b"), + (0x1D4EC, "M", "c"), + (0x1D4ED, "M", "d"), + (0x1D4EE, "M", "e"), + (0x1D4EF, "M", "f"), + (0x1D4F0, "M", "g"), + (0x1D4F1, "M", "h"), + (0x1D4F2, "M", "i"), + (0x1D4F3, "M", "j"), + (0x1D4F4, "M", "k"), + (0x1D4F5, "M", "l"), + (0x1D4F6, "M", "m"), + (0x1D4F7, "M", "n"), + (0x1D4F8, "M", "o"), + (0x1D4F9, "M", "p"), + (0x1D4FA, "M", "q"), + (0x1D4FB, "M", "r"), + (0x1D4FC, "M", "s"), + (0x1D4FD, "M", "t"), + (0x1D4FE, "M", "u"), + (0x1D4FF, "M", "v"), + (0x1D500, "M", "w"), + (0x1D501, "M", "x"), + (0x1D502, "M", "y"), + (0x1D503, "M", "z"), + (0x1D504, "M", "a"), + (0x1D505, "M", "b"), + (0x1D506, "X"), + (0x1D507, "M", "d"), + (0x1D508, "M", "e"), + (0x1D509, "M", "f"), + (0x1D50A, "M", "g"), + (0x1D50B, "X"), + (0x1D50D, "M", "j"), + (0x1D50E, "M", "k"), + (0x1D50F, "M", "l"), + (0x1D510, "M", "m"), + (0x1D511, "M", "n"), + (0x1D512, "M", "o"), + (0x1D513, "M", "p"), + (0x1D514, "M", "q"), + (0x1D515, "X"), + (0x1D516, "M", "s"), + (0x1D517, "M", "t"), + (0x1D518, "M", "u"), + (0x1D519, "M", "v"), + (0x1D51A, "M", "w"), + (0x1D51B, "M", "x"), + (0x1D51C, "M", "y"), + (0x1D51D, "X"), + (0x1D51E, "M", "a"), + (0x1D51F, "M", "b"), + (0x1D520, "M", "c"), + (0x1D521, "M", "d"), + (0x1D522, "M", "e"), + (0x1D523, "M", "f"), + (0x1D524, "M", "g"), + (0x1D525, "M", "h"), + (0x1D526, "M", "i"), + (0x1D527, "M", "j"), + ] + + +def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D528, "M", "k"), + (0x1D529, "M", "l"), + (0x1D52A, "M", "m"), + (0x1D52B, "M", "n"), + (0x1D52C, "M", "o"), + (0x1D52D, "M", "p"), + (0x1D52E, "M", "q"), + (0x1D52F, "M", "r"), + (0x1D530, "M", "s"), + (0x1D531, "M", "t"), + (0x1D532, "M", "u"), + (0x1D533, "M", "v"), + (0x1D534, "M", "w"), + (0x1D535, "M", "x"), + (0x1D536, "M", "y"), + (0x1D537, "M", "z"), + (0x1D538, "M", "a"), + (0x1D539, "M", "b"), + (0x1D53A, "X"), + (0x1D53B, "M", "d"), + (0x1D53C, "M", "e"), + (0x1D53D, "M", "f"), + (0x1D53E, "M", "g"), + (0x1D53F, "X"), + (0x1D540, "M", "i"), + (0x1D541, "M", "j"), + (0x1D542, "M", "k"), + (0x1D543, "M", "l"), + (0x1D544, "M", "m"), + (0x1D545, "X"), + (0x1D546, "M", "o"), + (0x1D547, "X"), + (0x1D54A, "M", "s"), + (0x1D54B, "M", "t"), + (0x1D54C, "M", "u"), + (0x1D54D, "M", "v"), + (0x1D54E, "M", "w"), + (0x1D54F, "M", "x"), + (0x1D550, "M", "y"), + (0x1D551, "X"), + (0x1D552, "M", "a"), + (0x1D553, "M", "b"), + (0x1D554, "M", "c"), + (0x1D555, "M", "d"), + (0x1D556, "M", "e"), + (0x1D557, "M", "f"), + (0x1D558, "M", "g"), + (0x1D559, "M", "h"), + (0x1D55A, "M", "i"), + (0x1D55B, "M", "j"), + (0x1D55C, "M", "k"), + (0x1D55D, "M", "l"), + (0x1D55E, "M", "m"), + (0x1D55F, "M", "n"), + (0x1D560, "M", "o"), + (0x1D561, "M", "p"), + (0x1D562, "M", "q"), + (0x1D563, "M", "r"), + (0x1D564, "M", "s"), + (0x1D565, "M", "t"), + (0x1D566, "M", "u"), + (0x1D567, "M", "v"), + (0x1D568, "M", "w"), + (0x1D569, "M", "x"), + (0x1D56A, "M", "y"), + (0x1D56B, "M", "z"), + (0x1D56C, "M", "a"), + (0x1D56D, "M", "b"), + (0x1D56E, "M", "c"), + (0x1D56F, "M", "d"), + (0x1D570, "M", "e"), + (0x1D571, "M", "f"), + (0x1D572, "M", "g"), + (0x1D573, "M", "h"), + (0x1D574, "M", "i"), + (0x1D575, "M", "j"), + (0x1D576, "M", "k"), + (0x1D577, "M", "l"), + (0x1D578, "M", "m"), + (0x1D579, "M", "n"), + (0x1D57A, "M", "o"), + (0x1D57B, "M", "p"), + (0x1D57C, "M", "q"), + (0x1D57D, "M", "r"), + (0x1D57E, "M", "s"), + (0x1D57F, "M", "t"), + (0x1D580, "M", "u"), + (0x1D581, "M", "v"), + (0x1D582, "M", "w"), + (0x1D583, "M", "x"), + (0x1D584, "M", "y"), + (0x1D585, "M", "z"), + (0x1D586, "M", "a"), + (0x1D587, "M", "b"), + (0x1D588, "M", "c"), + (0x1D589, "M", "d"), + (0x1D58A, "M", "e"), + (0x1D58B, "M", "f"), + (0x1D58C, "M", "g"), + (0x1D58D, "M", "h"), + ] + + +def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D58E, "M", "i"), + (0x1D58F, "M", "j"), + (0x1D590, "M", "k"), + (0x1D591, "M", "l"), + (0x1D592, "M", "m"), + (0x1D593, "M", "n"), + (0x1D594, "M", "o"), + (0x1D595, "M", "p"), + (0x1D596, "M", "q"), + (0x1D597, "M", "r"), + (0x1D598, "M", "s"), + (0x1D599, "M", "t"), + (0x1D59A, "M", "u"), + (0x1D59B, "M", "v"), + (0x1D59C, "M", "w"), + (0x1D59D, "M", "x"), + (0x1D59E, "M", "y"), + (0x1D59F, "M", "z"), + (0x1D5A0, "M", "a"), + (0x1D5A1, "M", "b"), + (0x1D5A2, "M", "c"), + (0x1D5A3, "M", "d"), + (0x1D5A4, "M", "e"), + (0x1D5A5, "M", "f"), + (0x1D5A6, "M", "g"), + (0x1D5A7, "M", "h"), + (0x1D5A8, "M", "i"), + (0x1D5A9, "M", "j"), + (0x1D5AA, "M", "k"), + (0x1D5AB, "M", "l"), + (0x1D5AC, "M", "m"), + (0x1D5AD, "M", "n"), + (0x1D5AE, "M", "o"), + (0x1D5AF, "M", "p"), + (0x1D5B0, "M", "q"), + (0x1D5B1, "M", "r"), + (0x1D5B2, "M", "s"), + (0x1D5B3, "M", "t"), + (0x1D5B4, "M", "u"), + (0x1D5B5, "M", "v"), + (0x1D5B6, "M", "w"), + (0x1D5B7, "M", "x"), + (0x1D5B8, "M", "y"), + (0x1D5B9, "M", "z"), + (0x1D5BA, "M", "a"), + (0x1D5BB, "M", "b"), + (0x1D5BC, "M", "c"), + (0x1D5BD, "M", "d"), + (0x1D5BE, "M", "e"), + (0x1D5BF, "M", "f"), + (0x1D5C0, "M", "g"), + (0x1D5C1, "M", "h"), + (0x1D5C2, "M", "i"), + (0x1D5C3, "M", "j"), + (0x1D5C4, "M", "k"), + (0x1D5C5, "M", "l"), + (0x1D5C6, "M", "m"), + (0x1D5C7, "M", "n"), + (0x1D5C8, "M", "o"), + (0x1D5C9, "M", "p"), + (0x1D5CA, "M", "q"), + (0x1D5CB, "M", "r"), + (0x1D5CC, "M", "s"), + (0x1D5CD, "M", "t"), + (0x1D5CE, "M", "u"), + (0x1D5CF, "M", "v"), + (0x1D5D0, "M", "w"), + (0x1D5D1, "M", "x"), + (0x1D5D2, "M", "y"), + (0x1D5D3, "M", "z"), + (0x1D5D4, "M", "a"), + (0x1D5D5, "M", "b"), + (0x1D5D6, "M", "c"), + (0x1D5D7, "M", "d"), + (0x1D5D8, "M", "e"), + (0x1D5D9, "M", "f"), + (0x1D5DA, "M", "g"), + (0x1D5DB, "M", "h"), + (0x1D5DC, "M", "i"), + (0x1D5DD, "M", "j"), + (0x1D5DE, "M", "k"), + (0x1D5DF, "M", "l"), + (0x1D5E0, "M", "m"), + (0x1D5E1, "M", "n"), + (0x1D5E2, "M", "o"), + (0x1D5E3, "M", "p"), + (0x1D5E4, "M", "q"), + (0x1D5E5, "M", "r"), + (0x1D5E6, "M", "s"), + (0x1D5E7, "M", "t"), + (0x1D5E8, "M", "u"), + (0x1D5E9, "M", "v"), + (0x1D5EA, "M", "w"), + (0x1D5EB, "M", "x"), + (0x1D5EC, "M", "y"), + (0x1D5ED, "M", "z"), + (0x1D5EE, "M", "a"), + (0x1D5EF, "M", "b"), + (0x1D5F0, "M", "c"), + (0x1D5F1, "M", "d"), + ] + + +def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D5F2, "M", "e"), + (0x1D5F3, "M", "f"), + (0x1D5F4, "M", "g"), + (0x1D5F5, "M", "h"), + (0x1D5F6, "M", "i"), + (0x1D5F7, "M", "j"), + (0x1D5F8, "M", "k"), + (0x1D5F9, "M", "l"), + (0x1D5FA, "M", "m"), + (0x1D5FB, "M", "n"), + (0x1D5FC, "M", "o"), + (0x1D5FD, "M", "p"), + (0x1D5FE, "M", "q"), + (0x1D5FF, "M", "r"), + (0x1D600, "M", "s"), + (0x1D601, "M", "t"), + (0x1D602, "M", "u"), + (0x1D603, "M", "v"), + (0x1D604, "M", "w"), + (0x1D605, "M", "x"), + (0x1D606, "M", "y"), + (0x1D607, "M", "z"), + (0x1D608, "M", "a"), + (0x1D609, "M", "b"), + (0x1D60A, "M", "c"), + (0x1D60B, "M", "d"), + (0x1D60C, "M", "e"), + (0x1D60D, "M", "f"), + (0x1D60E, "M", "g"), + (0x1D60F, "M", "h"), + (0x1D610, "M", "i"), + (0x1D611, "M", "j"), + (0x1D612, "M", "k"), + (0x1D613, "M", "l"), + (0x1D614, "M", "m"), + (0x1D615, "M", "n"), + (0x1D616, "M", "o"), + (0x1D617, "M", "p"), + (0x1D618, "M", "q"), + (0x1D619, "M", "r"), + (0x1D61A, "M", "s"), + (0x1D61B, "M", "t"), + (0x1D61C, "M", "u"), + (0x1D61D, "M", "v"), + (0x1D61E, "M", "w"), + (0x1D61F, "M", "x"), + (0x1D620, "M", "y"), + (0x1D621, "M", "z"), + (0x1D622, "M", "a"), + (0x1D623, "M", "b"), + (0x1D624, "M", "c"), + (0x1D625, "M", "d"), + (0x1D626, "M", "e"), + (0x1D627, "M", "f"), + (0x1D628, "M", "g"), + (0x1D629, "M", "h"), + (0x1D62A, "M", "i"), + (0x1D62B, "M", "j"), + (0x1D62C, "M", "k"), + (0x1D62D, "M", "l"), + (0x1D62E, "M", "m"), + (0x1D62F, "M", "n"), + (0x1D630, "M", "o"), + (0x1D631, "M", "p"), + (0x1D632, "M", "q"), + (0x1D633, "M", "r"), + (0x1D634, "M", "s"), + (0x1D635, "M", "t"), + (0x1D636, "M", "u"), + (0x1D637, "M", "v"), + (0x1D638, "M", "w"), + (0x1D639, "M", "x"), + (0x1D63A, "M", "y"), + (0x1D63B, "M", "z"), + (0x1D63C, "M", "a"), + (0x1D63D, "M", "b"), + (0x1D63E, "M", "c"), + (0x1D63F, "M", "d"), + (0x1D640, "M", "e"), + (0x1D641, "M", "f"), + (0x1D642, "M", "g"), + (0x1D643, "M", "h"), + (0x1D644, "M", "i"), + (0x1D645, "M", "j"), + (0x1D646, "M", "k"), + (0x1D647, "M", "l"), + (0x1D648, "M", "m"), + (0x1D649, "M", "n"), + (0x1D64A, "M", "o"), + (0x1D64B, "M", "p"), + (0x1D64C, "M", "q"), + (0x1D64D, "M", "r"), + (0x1D64E, "M", "s"), + (0x1D64F, "M", "t"), + (0x1D650, "M", "u"), + (0x1D651, "M", "v"), + (0x1D652, "M", "w"), + (0x1D653, "M", "x"), + (0x1D654, "M", "y"), + (0x1D655, "M", "z"), + ] + + +def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D656, "M", "a"), + (0x1D657, "M", "b"), + (0x1D658, "M", "c"), + (0x1D659, "M", "d"), + (0x1D65A, "M", "e"), + (0x1D65B, "M", "f"), + (0x1D65C, "M", "g"), + (0x1D65D, "M", "h"), + (0x1D65E, "M", "i"), + (0x1D65F, "M", "j"), + (0x1D660, "M", "k"), + (0x1D661, "M", "l"), + (0x1D662, "M", "m"), + (0x1D663, "M", "n"), + (0x1D664, "M", "o"), + (0x1D665, "M", "p"), + (0x1D666, "M", "q"), + (0x1D667, "M", "r"), + (0x1D668, "M", "s"), + (0x1D669, "M", "t"), + (0x1D66A, "M", "u"), + (0x1D66B, "M", "v"), + (0x1D66C, "M", "w"), + (0x1D66D, "M", "x"), + (0x1D66E, "M", "y"), + (0x1D66F, "M", "z"), + (0x1D670, "M", "a"), + (0x1D671, "M", "b"), + (0x1D672, "M", "c"), + (0x1D673, "M", "d"), + (0x1D674, "M", "e"), + (0x1D675, "M", "f"), + (0x1D676, "M", "g"), + (0x1D677, "M", "h"), + (0x1D678, "M", "i"), + (0x1D679, "M", "j"), + (0x1D67A, "M", "k"), + (0x1D67B, "M", "l"), + (0x1D67C, "M", "m"), + (0x1D67D, "M", "n"), + (0x1D67E, "M", "o"), + (0x1D67F, "M", "p"), + (0x1D680, "M", "q"), + (0x1D681, "M", "r"), + (0x1D682, "M", "s"), + (0x1D683, "M", "t"), + (0x1D684, "M", "u"), + (0x1D685, "M", "v"), + (0x1D686, "M", "w"), + (0x1D687, "M", "x"), + (0x1D688, "M", "y"), + (0x1D689, "M", "z"), + (0x1D68A, "M", "a"), + (0x1D68B, "M", "b"), + (0x1D68C, "M", "c"), + (0x1D68D, "M", "d"), + (0x1D68E, "M", "e"), + (0x1D68F, "M", "f"), + (0x1D690, "M", "g"), + (0x1D691, "M", "h"), + (0x1D692, "M", "i"), + (0x1D693, "M", "j"), + (0x1D694, "M", "k"), + (0x1D695, "M", "l"), + (0x1D696, "M", "m"), + (0x1D697, "M", "n"), + (0x1D698, "M", "o"), + (0x1D699, "M", "p"), + (0x1D69A, "M", "q"), + (0x1D69B, "M", "r"), + (0x1D69C, "M", "s"), + (0x1D69D, "M", "t"), + (0x1D69E, "M", "u"), + (0x1D69F, "M", "v"), + (0x1D6A0, "M", "w"), + (0x1D6A1, "M", "x"), + (0x1D6A2, "M", "y"), + (0x1D6A3, "M", "z"), + (0x1D6A4, "M", "ı"), + (0x1D6A5, "M", "ȷ"), + (0x1D6A6, "X"), + (0x1D6A8, "M", "α"), + (0x1D6A9, "M", "β"), + (0x1D6AA, "M", "γ"), + (0x1D6AB, "M", "δ"), + (0x1D6AC, "M", "ε"), + (0x1D6AD, "M", "ζ"), + (0x1D6AE, "M", "η"), + (0x1D6AF, "M", "θ"), + (0x1D6B0, "M", "ι"), + (0x1D6B1, "M", "κ"), + (0x1D6B2, "M", "λ"), + (0x1D6B3, "M", "μ"), + (0x1D6B4, "M", "ν"), + (0x1D6B5, "M", "ξ"), + (0x1D6B6, "M", "ο"), + (0x1D6B7, "M", "π"), + (0x1D6B8, "M", "ρ"), + (0x1D6B9, "M", "θ"), + (0x1D6BA, "M", "σ"), + ] + + +def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D6BB, "M", "τ"), + (0x1D6BC, "M", "υ"), + (0x1D6BD, "M", "φ"), + (0x1D6BE, "M", "χ"), + (0x1D6BF, "M", "ψ"), + (0x1D6C0, "M", "ω"), + (0x1D6C1, "M", "∇"), + (0x1D6C2, "M", "α"), + (0x1D6C3, "M", "β"), + (0x1D6C4, "M", "γ"), + (0x1D6C5, "M", "δ"), + (0x1D6C6, "M", "ε"), + (0x1D6C7, "M", "ζ"), + (0x1D6C8, "M", "η"), + (0x1D6C9, "M", "θ"), + (0x1D6CA, "M", "ι"), + (0x1D6CB, "M", "κ"), + (0x1D6CC, "M", "λ"), + (0x1D6CD, "M", "μ"), + (0x1D6CE, "M", "ν"), + (0x1D6CF, "M", "ξ"), + (0x1D6D0, "M", "ο"), + (0x1D6D1, "M", "π"), + (0x1D6D2, "M", "ρ"), + (0x1D6D3, "M", "σ"), + (0x1D6D5, "M", "τ"), + (0x1D6D6, "M", "υ"), + (0x1D6D7, "M", "φ"), + (0x1D6D8, "M", "χ"), + (0x1D6D9, "M", "ψ"), + (0x1D6DA, "M", "ω"), + (0x1D6DB, "M", "∂"), + (0x1D6DC, "M", "ε"), + (0x1D6DD, "M", "θ"), + (0x1D6DE, "M", "κ"), + (0x1D6DF, "M", "φ"), + (0x1D6E0, "M", "ρ"), + (0x1D6E1, "M", "π"), + (0x1D6E2, "M", "α"), + (0x1D6E3, "M", "β"), + (0x1D6E4, "M", "γ"), + (0x1D6E5, "M", "δ"), + (0x1D6E6, "M", "ε"), + (0x1D6E7, "M", "ζ"), + (0x1D6E8, "M", "η"), + (0x1D6E9, "M", "θ"), + (0x1D6EA, "M", "ι"), + (0x1D6EB, "M", "κ"), + (0x1D6EC, "M", "λ"), + (0x1D6ED, "M", "μ"), + (0x1D6EE, "M", "ν"), + (0x1D6EF, "M", "ξ"), + (0x1D6F0, "M", "ο"), + (0x1D6F1, "M", "π"), + (0x1D6F2, "M", "ρ"), + (0x1D6F3, "M", "θ"), + (0x1D6F4, "M", "σ"), + (0x1D6F5, "M", "τ"), + (0x1D6F6, "M", "υ"), + (0x1D6F7, "M", "φ"), + (0x1D6F8, "M", "χ"), + (0x1D6F9, "M", "ψ"), + (0x1D6FA, "M", "ω"), + (0x1D6FB, "M", "∇"), + (0x1D6FC, "M", "α"), + (0x1D6FD, "M", "β"), + (0x1D6FE, "M", "γ"), + (0x1D6FF, "M", "δ"), + (0x1D700, "M", "ε"), + (0x1D701, "M", "ζ"), + (0x1D702, "M", "η"), + (0x1D703, "M", "θ"), + (0x1D704, "M", "ι"), + (0x1D705, "M", "κ"), + (0x1D706, "M", "λ"), + (0x1D707, "M", "μ"), + (0x1D708, "M", "ν"), + (0x1D709, "M", "ξ"), + (0x1D70A, "M", "ο"), + (0x1D70B, "M", "π"), + (0x1D70C, "M", "ρ"), + (0x1D70D, "M", "σ"), + (0x1D70F, "M", "τ"), + (0x1D710, "M", "υ"), + (0x1D711, "M", "φ"), + (0x1D712, "M", "χ"), + (0x1D713, "M", "ψ"), + (0x1D714, "M", "ω"), + (0x1D715, "M", "∂"), + (0x1D716, "M", "ε"), + (0x1D717, "M", "θ"), + (0x1D718, "M", "κ"), + (0x1D719, "M", "φ"), + (0x1D71A, "M", "ρ"), + (0x1D71B, "M", "π"), + (0x1D71C, "M", "α"), + (0x1D71D, "M", "β"), + (0x1D71E, "M", "γ"), + (0x1D71F, "M", "δ"), + (0x1D720, "M", "ε"), + ] + + +def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D721, "M", "ζ"), + (0x1D722, "M", "η"), + (0x1D723, "M", "θ"), + (0x1D724, "M", "ι"), + (0x1D725, "M", "κ"), + (0x1D726, "M", "λ"), + (0x1D727, "M", "μ"), + (0x1D728, "M", "ν"), + (0x1D729, "M", "ξ"), + (0x1D72A, "M", "ο"), + (0x1D72B, "M", "π"), + (0x1D72C, "M", "ρ"), + (0x1D72D, "M", "θ"), + (0x1D72E, "M", "σ"), + (0x1D72F, "M", "τ"), + (0x1D730, "M", "υ"), + (0x1D731, "M", "φ"), + (0x1D732, "M", "χ"), + (0x1D733, "M", "ψ"), + (0x1D734, "M", "ω"), + (0x1D735, "M", "∇"), + (0x1D736, "M", "α"), + (0x1D737, "M", "β"), + (0x1D738, "M", "γ"), + (0x1D739, "M", "δ"), + (0x1D73A, "M", "ε"), + (0x1D73B, "M", "ζ"), + (0x1D73C, "M", "η"), + (0x1D73D, "M", "θ"), + (0x1D73E, "M", "ι"), + (0x1D73F, "M", "κ"), + (0x1D740, "M", "λ"), + (0x1D741, "M", "μ"), + (0x1D742, "M", "ν"), + (0x1D743, "M", "ξ"), + (0x1D744, "M", "ο"), + (0x1D745, "M", "π"), + (0x1D746, "M", "ρ"), + (0x1D747, "M", "σ"), + (0x1D749, "M", "τ"), + (0x1D74A, "M", "υ"), + (0x1D74B, "M", "φ"), + (0x1D74C, "M", "χ"), + (0x1D74D, "M", "ψ"), + (0x1D74E, "M", "ω"), + (0x1D74F, "M", "∂"), + (0x1D750, "M", "ε"), + (0x1D751, "M", "θ"), + (0x1D752, "M", "κ"), + (0x1D753, "M", "φ"), + (0x1D754, "M", "ρ"), + (0x1D755, "M", "π"), + (0x1D756, "M", "α"), + (0x1D757, "M", "β"), + (0x1D758, "M", "γ"), + (0x1D759, "M", "δ"), + (0x1D75A, "M", "ε"), + (0x1D75B, "M", "ζ"), + (0x1D75C, "M", "η"), + (0x1D75D, "M", "θ"), + (0x1D75E, "M", "ι"), + (0x1D75F, "M", "κ"), + (0x1D760, "M", "λ"), + (0x1D761, "M", "μ"), + (0x1D762, "M", "ν"), + (0x1D763, "M", "ξ"), + (0x1D764, "M", "ο"), + (0x1D765, "M", "π"), + (0x1D766, "M", "ρ"), + (0x1D767, "M", "θ"), + (0x1D768, "M", "σ"), + (0x1D769, "M", "τ"), + (0x1D76A, "M", "υ"), + (0x1D76B, "M", "φ"), + (0x1D76C, "M", "χ"), + (0x1D76D, "M", "ψ"), + (0x1D76E, "M", "ω"), + (0x1D76F, "M", "∇"), + (0x1D770, "M", "α"), + (0x1D771, "M", "β"), + (0x1D772, "M", "γ"), + (0x1D773, "M", "δ"), + (0x1D774, "M", "ε"), + (0x1D775, "M", "ζ"), + (0x1D776, "M", "η"), + (0x1D777, "M", "θ"), + (0x1D778, "M", "ι"), + (0x1D779, "M", "κ"), + (0x1D77A, "M", "λ"), + (0x1D77B, "M", "μ"), + (0x1D77C, "M", "ν"), + (0x1D77D, "M", "ξ"), + (0x1D77E, "M", "ο"), + (0x1D77F, "M", "π"), + (0x1D780, "M", "ρ"), + (0x1D781, "M", "σ"), + (0x1D783, "M", "τ"), + (0x1D784, "M", "υ"), + (0x1D785, "M", "φ"), + (0x1D786, "M", "χ"), + ] + + +def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D787, "M", "ψ"), + (0x1D788, "M", "ω"), + (0x1D789, "M", "∂"), + (0x1D78A, "M", "ε"), + (0x1D78B, "M", "θ"), + (0x1D78C, "M", "κ"), + (0x1D78D, "M", "φ"), + (0x1D78E, "M", "ρ"), + (0x1D78F, "M", "π"), + (0x1D790, "M", "α"), + (0x1D791, "M", "β"), + (0x1D792, "M", "γ"), + (0x1D793, "M", "δ"), + (0x1D794, "M", "ε"), + (0x1D795, "M", "ζ"), + (0x1D796, "M", "η"), + (0x1D797, "M", "θ"), + (0x1D798, "M", "ι"), + (0x1D799, "M", "κ"), + (0x1D79A, "M", "λ"), + (0x1D79B, "M", "μ"), + (0x1D79C, "M", "ν"), + (0x1D79D, "M", "ξ"), + (0x1D79E, "M", "ο"), + (0x1D79F, "M", "π"), + (0x1D7A0, "M", "ρ"), + (0x1D7A1, "M", "θ"), + (0x1D7A2, "M", "σ"), + (0x1D7A3, "M", "τ"), + (0x1D7A4, "M", "υ"), + (0x1D7A5, "M", "φ"), + (0x1D7A6, "M", "χ"), + (0x1D7A7, "M", "ψ"), + (0x1D7A8, "M", "ω"), + (0x1D7A9, "M", "∇"), + (0x1D7AA, "M", "α"), + (0x1D7AB, "M", "β"), + (0x1D7AC, "M", "γ"), + (0x1D7AD, "M", "δ"), + (0x1D7AE, "M", "ε"), + (0x1D7AF, "M", "ζ"), + (0x1D7B0, "M", "η"), + (0x1D7B1, "M", "θ"), + (0x1D7B2, "M", "ι"), + (0x1D7B3, "M", "κ"), + (0x1D7B4, "M", "λ"), + (0x1D7B5, "M", "μ"), + (0x1D7B6, "M", "ν"), + (0x1D7B7, "M", "ξ"), + (0x1D7B8, "M", "ο"), + (0x1D7B9, "M", "π"), + (0x1D7BA, "M", "ρ"), + (0x1D7BB, "M", "σ"), + (0x1D7BD, "M", "τ"), + (0x1D7BE, "M", "υ"), + (0x1D7BF, "M", "φ"), + (0x1D7C0, "M", "χ"), + (0x1D7C1, "M", "ψ"), + (0x1D7C2, "M", "ω"), + (0x1D7C3, "M", "∂"), + (0x1D7C4, "M", "ε"), + (0x1D7C5, "M", "θ"), + (0x1D7C6, "M", "κ"), + (0x1D7C7, "M", "φ"), + (0x1D7C8, "M", "ρ"), + (0x1D7C9, "M", "π"), + (0x1D7CA, "M", "ϝ"), + (0x1D7CC, "X"), + (0x1D7CE, "M", "0"), + (0x1D7CF, "M", "1"), + (0x1D7D0, "M", "2"), + (0x1D7D1, "M", "3"), + (0x1D7D2, "M", "4"), + (0x1D7D3, "M", "5"), + (0x1D7D4, "M", "6"), + (0x1D7D5, "M", "7"), + (0x1D7D6, "M", "8"), + (0x1D7D7, "M", "9"), + (0x1D7D8, "M", "0"), + (0x1D7D9, "M", "1"), + (0x1D7DA, "M", "2"), + (0x1D7DB, "M", "3"), + (0x1D7DC, "M", "4"), + (0x1D7DD, "M", "5"), + (0x1D7DE, "M", "6"), + (0x1D7DF, "M", "7"), + (0x1D7E0, "M", "8"), + (0x1D7E1, "M", "9"), + (0x1D7E2, "M", "0"), + (0x1D7E3, "M", "1"), + (0x1D7E4, "M", "2"), + (0x1D7E5, "M", "3"), + (0x1D7E6, "M", "4"), + (0x1D7E7, "M", "5"), + (0x1D7E8, "M", "6"), + (0x1D7E9, "M", "7"), + (0x1D7EA, "M", "8"), + (0x1D7EB, "M", "9"), + (0x1D7EC, "M", "0"), + (0x1D7ED, "M", "1"), + ] + + +def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D7EE, "M", "2"), + (0x1D7EF, "M", "3"), + (0x1D7F0, "M", "4"), + (0x1D7F1, "M", "5"), + (0x1D7F2, "M", "6"), + (0x1D7F3, "M", "7"), + (0x1D7F4, "M", "8"), + (0x1D7F5, "M", "9"), + (0x1D7F6, "M", "0"), + (0x1D7F7, "M", "1"), + (0x1D7F8, "M", "2"), + (0x1D7F9, "M", "3"), + (0x1D7FA, "M", "4"), + (0x1D7FB, "M", "5"), + (0x1D7FC, "M", "6"), + (0x1D7FD, "M", "7"), + (0x1D7FE, "M", "8"), + (0x1D7FF, "M", "9"), + (0x1D800, "V"), + (0x1DA8C, "X"), + (0x1DA9B, "V"), + (0x1DAA0, "X"), + (0x1DAA1, "V"), + (0x1DAB0, "X"), + (0x1DF00, "V"), + (0x1DF1F, "X"), + (0x1DF25, "V"), + (0x1DF2B, "X"), + (0x1E000, "V"), + (0x1E007, "X"), + (0x1E008, "V"), + (0x1E019, "X"), + (0x1E01B, "V"), + (0x1E022, "X"), + (0x1E023, "V"), + (0x1E025, "X"), + (0x1E026, "V"), + (0x1E02B, "X"), + (0x1E030, "M", "а"), + (0x1E031, "M", "б"), + (0x1E032, "M", "в"), + (0x1E033, "M", "г"), + (0x1E034, "M", "д"), + (0x1E035, "M", "е"), + (0x1E036, "M", "ж"), + (0x1E037, "M", "з"), + (0x1E038, "M", "и"), + (0x1E039, "M", "к"), + (0x1E03A, "M", "л"), + (0x1E03B, "M", "м"), + (0x1E03C, "M", "о"), + (0x1E03D, "M", "п"), + (0x1E03E, "M", "р"), + (0x1E03F, "M", "с"), + (0x1E040, "M", "т"), + (0x1E041, "M", "у"), + (0x1E042, "M", "ф"), + (0x1E043, "M", "х"), + (0x1E044, "M", "ц"), + (0x1E045, "M", "ч"), + (0x1E046, "M", "ш"), + (0x1E047, "M", "ы"), + (0x1E048, "M", "э"), + (0x1E049, "M", "ю"), + (0x1E04A, "M", "ꚉ"), + (0x1E04B, "M", "ә"), + (0x1E04C, "M", "і"), + (0x1E04D, "M", "ј"), + (0x1E04E, "M", "ө"), + (0x1E04F, "M", "ү"), + (0x1E050, "M", "ӏ"), + (0x1E051, "M", "а"), + (0x1E052, "M", "б"), + (0x1E053, "M", "в"), + (0x1E054, "M", "г"), + (0x1E055, "M", "д"), + (0x1E056, "M", "е"), + (0x1E057, "M", "ж"), + (0x1E058, "M", "з"), + (0x1E059, "M", "и"), + (0x1E05A, "M", "к"), + (0x1E05B, "M", "л"), + (0x1E05C, "M", "о"), + (0x1E05D, "M", "п"), + (0x1E05E, "M", "с"), + (0x1E05F, "M", "у"), + (0x1E060, "M", "ф"), + (0x1E061, "M", "х"), + (0x1E062, "M", "ц"), + (0x1E063, "M", "ч"), + (0x1E064, "M", "ш"), + (0x1E065, "M", "ъ"), + (0x1E066, "M", "ы"), + (0x1E067, "M", "ґ"), + (0x1E068, "M", "і"), + (0x1E069, "M", "ѕ"), + (0x1E06A, "M", "џ"), + (0x1E06B, "M", "ҫ"), + (0x1E06C, "M", "ꙑ"), + (0x1E06D, "M", "ұ"), + ] + + +def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E06E, "X"), + (0x1E08F, "V"), + (0x1E090, "X"), + (0x1E100, "V"), + (0x1E12D, "X"), + (0x1E130, "V"), + (0x1E13E, "X"), + (0x1E140, "V"), + (0x1E14A, "X"), + (0x1E14E, "V"), + (0x1E150, "X"), + (0x1E290, "V"), + (0x1E2AF, "X"), + (0x1E2C0, "V"), + (0x1E2FA, "X"), + (0x1E2FF, "V"), + (0x1E300, "X"), + (0x1E4D0, "V"), + (0x1E4FA, "X"), + (0x1E5D0, "V"), + (0x1E5FB, "X"), + (0x1E5FF, "V"), + (0x1E600, "X"), + (0x1E7E0, "V"), + (0x1E7E7, "X"), + (0x1E7E8, "V"), + (0x1E7EC, "X"), + (0x1E7ED, "V"), + (0x1E7EF, "X"), + (0x1E7F0, "V"), + (0x1E7FF, "X"), + (0x1E800, "V"), + (0x1E8C5, "X"), + (0x1E8C7, "V"), + (0x1E8D7, "X"), + (0x1E900, "M", "𞤢"), + (0x1E901, "M", "𞤣"), + (0x1E902, "M", "𞤤"), + (0x1E903, "M", "𞤥"), + (0x1E904, "M", "𞤦"), + (0x1E905, "M", "𞤧"), + (0x1E906, "M", "𞤨"), + (0x1E907, "M", "𞤩"), + (0x1E908, "M", "𞤪"), + (0x1E909, "M", "𞤫"), + (0x1E90A, "M", "𞤬"), + (0x1E90B, "M", "𞤭"), + (0x1E90C, "M", "𞤮"), + (0x1E90D, "M", "𞤯"), + (0x1E90E, "M", "𞤰"), + (0x1E90F, "M", "𞤱"), + (0x1E910, "M", "𞤲"), + (0x1E911, "M", "𞤳"), + (0x1E912, "M", "𞤴"), + (0x1E913, "M", "𞤵"), + (0x1E914, "M", "𞤶"), + (0x1E915, "M", "𞤷"), + (0x1E916, "M", "𞤸"), + (0x1E917, "M", "𞤹"), + (0x1E918, "M", "𞤺"), + (0x1E919, "M", "𞤻"), + (0x1E91A, "M", "𞤼"), + (0x1E91B, "M", "𞤽"), + (0x1E91C, "M", "𞤾"), + (0x1E91D, "M", "𞤿"), + (0x1E91E, "M", "𞥀"), + (0x1E91F, "M", "𞥁"), + (0x1E920, "M", "𞥂"), + (0x1E921, "M", "𞥃"), + (0x1E922, "V"), + (0x1E94C, "X"), + (0x1E950, "V"), + (0x1E95A, "X"), + (0x1E95E, "V"), + (0x1E960, "X"), + (0x1EC71, "V"), + (0x1ECB5, "X"), + (0x1ED01, "V"), + (0x1ED3E, "X"), + (0x1EE00, "M", "ا"), + (0x1EE01, "M", "ب"), + (0x1EE02, "M", "ج"), + (0x1EE03, "M", "د"), + (0x1EE04, "X"), + (0x1EE05, "M", "و"), + (0x1EE06, "M", "ز"), + (0x1EE07, "M", "ح"), + (0x1EE08, "M", "ط"), + (0x1EE09, "M", "ي"), + (0x1EE0A, "M", "ك"), + (0x1EE0B, "M", "ل"), + (0x1EE0C, "M", "م"), + (0x1EE0D, "M", "ن"), + (0x1EE0E, "M", "س"), + (0x1EE0F, "M", "ع"), + (0x1EE10, "M", "ف"), + (0x1EE11, "M", "ص"), + (0x1EE12, "M", "ق"), + (0x1EE13, "M", "ر"), + (0x1EE14, "M", "ش"), + ] + + +def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EE15, "M", "ت"), + (0x1EE16, "M", "ث"), + (0x1EE17, "M", "خ"), + (0x1EE18, "M", "ذ"), + (0x1EE19, "M", "ض"), + (0x1EE1A, "M", "ظ"), + (0x1EE1B, "M", "غ"), + (0x1EE1C, "M", "ٮ"), + (0x1EE1D, "M", "ں"), + (0x1EE1E, "M", "ڡ"), + (0x1EE1F, "M", "ٯ"), + (0x1EE20, "X"), + (0x1EE21, "M", "ب"), + (0x1EE22, "M", "ج"), + (0x1EE23, "X"), + (0x1EE24, "M", "ه"), + (0x1EE25, "X"), + (0x1EE27, "M", "ح"), + (0x1EE28, "X"), + (0x1EE29, "M", "ي"), + (0x1EE2A, "M", "ك"), + (0x1EE2B, "M", "ل"), + (0x1EE2C, "M", "م"), + (0x1EE2D, "M", "ن"), + (0x1EE2E, "M", "س"), + (0x1EE2F, "M", "ع"), + (0x1EE30, "M", "ف"), + (0x1EE31, "M", "ص"), + (0x1EE32, "M", "ق"), + (0x1EE33, "X"), + (0x1EE34, "M", "ش"), + (0x1EE35, "M", "ت"), + (0x1EE36, "M", "ث"), + (0x1EE37, "M", "خ"), + (0x1EE38, "X"), + (0x1EE39, "M", "ض"), + (0x1EE3A, "X"), + (0x1EE3B, "M", "غ"), + (0x1EE3C, "X"), + (0x1EE42, "M", "ج"), + (0x1EE43, "X"), + (0x1EE47, "M", "ح"), + (0x1EE48, "X"), + (0x1EE49, "M", "ي"), + (0x1EE4A, "X"), + (0x1EE4B, "M", "ل"), + (0x1EE4C, "X"), + (0x1EE4D, "M", "ن"), + (0x1EE4E, "M", "س"), + (0x1EE4F, "M", "ع"), + (0x1EE50, "X"), + (0x1EE51, "M", "ص"), + (0x1EE52, "M", "ق"), + (0x1EE53, "X"), + (0x1EE54, "M", "ش"), + (0x1EE55, "X"), + (0x1EE57, "M", "خ"), + (0x1EE58, "X"), + (0x1EE59, "M", "ض"), + (0x1EE5A, "X"), + (0x1EE5B, "M", "غ"), + (0x1EE5C, "X"), + (0x1EE5D, "M", "ں"), + (0x1EE5E, "X"), + (0x1EE5F, "M", "ٯ"), + (0x1EE60, "X"), + (0x1EE61, "M", "ب"), + (0x1EE62, "M", "ج"), + (0x1EE63, "X"), + (0x1EE64, "M", "ه"), + (0x1EE65, "X"), + (0x1EE67, "M", "ح"), + (0x1EE68, "M", "ط"), + (0x1EE69, "M", "ي"), + (0x1EE6A, "M", "ك"), + (0x1EE6B, "X"), + (0x1EE6C, "M", "م"), + (0x1EE6D, "M", "ن"), + (0x1EE6E, "M", "س"), + (0x1EE6F, "M", "ع"), + (0x1EE70, "M", "ف"), + (0x1EE71, "M", "ص"), + (0x1EE72, "M", "ق"), + (0x1EE73, "X"), + (0x1EE74, "M", "ش"), + (0x1EE75, "M", "ت"), + (0x1EE76, "M", "ث"), + (0x1EE77, "M", "خ"), + (0x1EE78, "X"), + (0x1EE79, "M", "ض"), + (0x1EE7A, "M", "ظ"), + (0x1EE7B, "M", "غ"), + (0x1EE7C, "M", "ٮ"), + (0x1EE7D, "X"), + (0x1EE7E, "M", "ڡ"), + (0x1EE7F, "X"), + (0x1EE80, "M", "ا"), + (0x1EE81, "M", "ب"), + (0x1EE82, "M", "ج"), + (0x1EE83, "M", "د"), + ] + + +def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EE84, "M", "ه"), + (0x1EE85, "M", "و"), + (0x1EE86, "M", "ز"), + (0x1EE87, "M", "ح"), + (0x1EE88, "M", "ط"), + (0x1EE89, "M", "ي"), + (0x1EE8A, "X"), + (0x1EE8B, "M", "ل"), + (0x1EE8C, "M", "م"), + (0x1EE8D, "M", "ن"), + (0x1EE8E, "M", "س"), + (0x1EE8F, "M", "ع"), + (0x1EE90, "M", "ف"), + (0x1EE91, "M", "ص"), + (0x1EE92, "M", "ق"), + (0x1EE93, "M", "ر"), + (0x1EE94, "M", "ش"), + (0x1EE95, "M", "ت"), + (0x1EE96, "M", "ث"), + (0x1EE97, "M", "خ"), + (0x1EE98, "M", "ذ"), + (0x1EE99, "M", "ض"), + (0x1EE9A, "M", "ظ"), + (0x1EE9B, "M", "غ"), + (0x1EE9C, "X"), + (0x1EEA1, "M", "ب"), + (0x1EEA2, "M", "ج"), + (0x1EEA3, "M", "د"), + (0x1EEA4, "X"), + (0x1EEA5, "M", "و"), + (0x1EEA6, "M", "ز"), + (0x1EEA7, "M", "ح"), + (0x1EEA8, "M", "ط"), + (0x1EEA9, "M", "ي"), + (0x1EEAA, "X"), + (0x1EEAB, "M", "ل"), + (0x1EEAC, "M", "م"), + (0x1EEAD, "M", "ن"), + (0x1EEAE, "M", "س"), + (0x1EEAF, "M", "ع"), + (0x1EEB0, "M", "ف"), + (0x1EEB1, "M", "ص"), + (0x1EEB2, "M", "ق"), + (0x1EEB3, "M", "ر"), + (0x1EEB4, "M", "ش"), + (0x1EEB5, "M", "ت"), + (0x1EEB6, "M", "ث"), + (0x1EEB7, "M", "خ"), + (0x1EEB8, "M", "ذ"), + (0x1EEB9, "M", "ض"), + (0x1EEBA, "M", "ظ"), + (0x1EEBB, "M", "غ"), + (0x1EEBC, "X"), + (0x1EEF0, "V"), + (0x1EEF2, "X"), + (0x1F000, "V"), + (0x1F02C, "X"), + (0x1F030, "V"), + (0x1F094, "X"), + (0x1F0A0, "V"), + (0x1F0AF, "X"), + (0x1F0B1, "V"), + (0x1F0C0, "X"), + (0x1F0C1, "V"), + (0x1F0D0, "X"), + (0x1F0D1, "V"), + (0x1F0F6, "X"), + (0x1F101, "M", "0,"), + (0x1F102, "M", "1,"), + (0x1F103, "M", "2,"), + (0x1F104, "M", "3,"), + (0x1F105, "M", "4,"), + (0x1F106, "M", "5,"), + (0x1F107, "M", "6,"), + (0x1F108, "M", "7,"), + (0x1F109, "M", "8,"), + (0x1F10A, "M", "9,"), + (0x1F10B, "V"), + (0x1F110, "M", "(a)"), + (0x1F111, "M", "(b)"), + (0x1F112, "M", "(c)"), + (0x1F113, "M", "(d)"), + (0x1F114, "M", "(e)"), + (0x1F115, "M", "(f)"), + (0x1F116, "M", "(g)"), + (0x1F117, "M", "(h)"), + (0x1F118, "M", "(i)"), + (0x1F119, "M", "(j)"), + (0x1F11A, "M", "(k)"), + (0x1F11B, "M", "(l)"), + (0x1F11C, "M", "(m)"), + (0x1F11D, "M", "(n)"), + (0x1F11E, "M", "(o)"), + (0x1F11F, "M", "(p)"), + (0x1F120, "M", "(q)"), + (0x1F121, "M", "(r)"), + (0x1F122, "M", "(s)"), + (0x1F123, "M", "(t)"), + (0x1F124, "M", "(u)"), + (0x1F125, "M", "(v)"), + ] + + +def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F126, "M", "(w)"), + (0x1F127, "M", "(x)"), + (0x1F128, "M", "(y)"), + (0x1F129, "M", "(z)"), + (0x1F12A, "M", "〔s〕"), + (0x1F12B, "M", "c"), + (0x1F12C, "M", "r"), + (0x1F12D, "M", "cd"), + (0x1F12E, "M", "wz"), + (0x1F12F, "V"), + (0x1F130, "M", "a"), + (0x1F131, "M", "b"), + (0x1F132, "M", "c"), + (0x1F133, "M", "d"), + (0x1F134, "M", "e"), + (0x1F135, "M", "f"), + (0x1F136, "M", "g"), + (0x1F137, "M", "h"), + (0x1F138, "M", "i"), + (0x1F139, "M", "j"), + (0x1F13A, "M", "k"), + (0x1F13B, "M", "l"), + (0x1F13C, "M", "m"), + (0x1F13D, "M", "n"), + (0x1F13E, "M", "o"), + (0x1F13F, "M", "p"), + (0x1F140, "M", "q"), + (0x1F141, "M", "r"), + (0x1F142, "M", "s"), + (0x1F143, "M", "t"), + (0x1F144, "M", "u"), + (0x1F145, "M", "v"), + (0x1F146, "M", "w"), + (0x1F147, "M", "x"), + (0x1F148, "M", "y"), + (0x1F149, "M", "z"), + (0x1F14A, "M", "hv"), + (0x1F14B, "M", "mv"), + (0x1F14C, "M", "sd"), + (0x1F14D, "M", "ss"), + (0x1F14E, "M", "ppv"), + (0x1F14F, "M", "wc"), + (0x1F150, "V"), + (0x1F16A, "M", "mc"), + (0x1F16B, "M", "md"), + (0x1F16C, "M", "mr"), + (0x1F16D, "V"), + (0x1F190, "M", "dj"), + (0x1F191, "V"), + (0x1F1AE, "X"), + (0x1F1E6, "V"), + (0x1F200, "M", "ほか"), + (0x1F201, "M", "ココ"), + (0x1F202, "M", "サ"), + (0x1F203, "X"), + (0x1F210, "M", "手"), + (0x1F211, "M", "字"), + (0x1F212, "M", "双"), + (0x1F213, "M", "デ"), + (0x1F214, "M", "二"), + (0x1F215, "M", "多"), + (0x1F216, "M", "解"), + (0x1F217, "M", "天"), + (0x1F218, "M", "交"), + (0x1F219, "M", "映"), + (0x1F21A, "M", "無"), + (0x1F21B, "M", "料"), + (0x1F21C, "M", "前"), + (0x1F21D, "M", "後"), + (0x1F21E, "M", "再"), + (0x1F21F, "M", "新"), + (0x1F220, "M", "初"), + (0x1F221, "M", "終"), + (0x1F222, "M", "生"), + (0x1F223, "M", "販"), + (0x1F224, "M", "声"), + (0x1F225, "M", "吹"), + (0x1F226, "M", "演"), + (0x1F227, "M", "投"), + (0x1F228, "M", "捕"), + (0x1F229, "M", "一"), + (0x1F22A, "M", "三"), + (0x1F22B, "M", "遊"), + (0x1F22C, "M", "左"), + (0x1F22D, "M", "中"), + (0x1F22E, "M", "右"), + (0x1F22F, "M", "指"), + (0x1F230, "M", "走"), + (0x1F231, "M", "打"), + (0x1F232, "M", "禁"), + (0x1F233, "M", "空"), + (0x1F234, "M", "合"), + (0x1F235, "M", "満"), + (0x1F236, "M", "有"), + (0x1F237, "M", "月"), + (0x1F238, "M", "申"), + (0x1F239, "M", "割"), + (0x1F23A, "M", "営"), + (0x1F23B, "M", "配"), + (0x1F23C, "X"), + ] + + +def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F240, "M", "〔本〕"), + (0x1F241, "M", "〔三〕"), + (0x1F242, "M", "〔二〕"), + (0x1F243, "M", "〔安〕"), + (0x1F244, "M", "〔点〕"), + (0x1F245, "M", "〔打〕"), + (0x1F246, "M", "〔盗〕"), + (0x1F247, "M", "〔勝〕"), + (0x1F248, "M", "〔敗〕"), + (0x1F249, "X"), + (0x1F250, "M", "得"), + (0x1F251, "M", "可"), + (0x1F252, "X"), + (0x1F260, "V"), + (0x1F266, "X"), + (0x1F300, "V"), + (0x1F6D8, "X"), + (0x1F6DC, "V"), + (0x1F6ED, "X"), + (0x1F6F0, "V"), + (0x1F6FD, "X"), + (0x1F700, "V"), + (0x1F777, "X"), + (0x1F77B, "V"), + (0x1F7DA, "X"), + (0x1F7E0, "V"), + (0x1F7EC, "X"), + (0x1F7F0, "V"), + (0x1F7F1, "X"), + (0x1F800, "V"), + (0x1F80C, "X"), + (0x1F810, "V"), + (0x1F848, "X"), + (0x1F850, "V"), + (0x1F85A, "X"), + (0x1F860, "V"), + (0x1F888, "X"), + (0x1F890, "V"), + (0x1F8AE, "X"), + (0x1F8B0, "V"), + (0x1F8BC, "X"), + (0x1F8C0, "V"), + (0x1F8C2, "X"), + (0x1F900, "V"), + (0x1FA54, "X"), + (0x1FA60, "V"), + (0x1FA6E, "X"), + (0x1FA70, "V"), + (0x1FA7D, "X"), + (0x1FA80, "V"), + (0x1FA8A, "X"), + (0x1FA8F, "V"), + (0x1FAC7, "X"), + (0x1FACE, "V"), + (0x1FADD, "X"), + (0x1FADF, "V"), + (0x1FAEA, "X"), + (0x1FAF0, "V"), + (0x1FAF9, "X"), + (0x1FB00, "V"), + (0x1FB93, "X"), + (0x1FB94, "V"), + (0x1FBF0, "M", "0"), + (0x1FBF1, "M", "1"), + (0x1FBF2, "M", "2"), + (0x1FBF3, "M", "3"), + (0x1FBF4, "M", "4"), + (0x1FBF5, "M", "5"), + (0x1FBF6, "M", "6"), + (0x1FBF7, "M", "7"), + (0x1FBF8, "M", "8"), + (0x1FBF9, "M", "9"), + (0x1FBFA, "X"), + (0x20000, "V"), + (0x2A6E0, "X"), + (0x2A700, "V"), + (0x2B73A, "X"), + (0x2B740, "V"), + (0x2B81E, "X"), + (0x2B820, "V"), + (0x2CEA2, "X"), + (0x2CEB0, "V"), + (0x2EBE1, "X"), + (0x2EBF0, "V"), + (0x2EE5E, "X"), + (0x2F800, "M", "丽"), + (0x2F801, "M", "丸"), + (0x2F802, "M", "乁"), + (0x2F803, "M", "𠄢"), + (0x2F804, "M", "你"), + (0x2F805, "M", "侮"), + (0x2F806, "M", "侻"), + (0x2F807, "M", "倂"), + (0x2F808, "M", "偺"), + (0x2F809, "M", "備"), + (0x2F80A, "M", "僧"), + (0x2F80B, "M", "像"), + (0x2F80C, "M", "㒞"), + (0x2F80D, "M", "𠘺"), + (0x2F80E, "M", "免"), + ] + + +def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F80F, "M", "兔"), + (0x2F810, "M", "兤"), + (0x2F811, "M", "具"), + (0x2F812, "M", "𠔜"), + (0x2F813, "M", "㒹"), + (0x2F814, "M", "內"), + (0x2F815, "M", "再"), + (0x2F816, "M", "𠕋"), + (0x2F817, "M", "冗"), + (0x2F818, "M", "冤"), + (0x2F819, "M", "仌"), + (0x2F81A, "M", "冬"), + (0x2F81B, "M", "况"), + (0x2F81C, "M", "𩇟"), + (0x2F81D, "M", "凵"), + (0x2F81E, "M", "刃"), + (0x2F81F, "M", "㓟"), + (0x2F820, "M", "刻"), + (0x2F821, "M", "剆"), + (0x2F822, "M", "割"), + (0x2F823, "M", "剷"), + (0x2F824, "M", "㔕"), + (0x2F825, "M", "勇"), + (0x2F826, "M", "勉"), + (0x2F827, "M", "勤"), + (0x2F828, "M", "勺"), + (0x2F829, "M", "包"), + (0x2F82A, "M", "匆"), + (0x2F82B, "M", "北"), + (0x2F82C, "M", "卉"), + (0x2F82D, "M", "卑"), + (0x2F82E, "M", "博"), + (0x2F82F, "M", "即"), + (0x2F830, "M", "卽"), + (0x2F831, "M", "卿"), + (0x2F834, "M", "𠨬"), + (0x2F835, "M", "灰"), + (0x2F836, "M", "及"), + (0x2F837, "M", "叟"), + (0x2F838, "M", "𠭣"), + (0x2F839, "M", "叫"), + (0x2F83A, "M", "叱"), + (0x2F83B, "M", "吆"), + (0x2F83C, "M", "咞"), + (0x2F83D, "M", "吸"), + (0x2F83E, "M", "呈"), + (0x2F83F, "M", "周"), + (0x2F840, "M", "咢"), + (0x2F841, "M", "哶"), + (0x2F842, "M", "唐"), + (0x2F843, "M", "啓"), + (0x2F844, "M", "啣"), + (0x2F845, "M", "善"), + (0x2F847, "M", "喙"), + (0x2F848, "M", "喫"), + (0x2F849, "M", "喳"), + (0x2F84A, "M", "嗂"), + (0x2F84B, "M", "圖"), + (0x2F84C, "M", "嘆"), + (0x2F84D, "M", "圗"), + (0x2F84E, "M", "噑"), + (0x2F84F, "M", "噴"), + (0x2F850, "M", "切"), + (0x2F851, "M", "壮"), + (0x2F852, "M", "城"), + (0x2F853, "M", "埴"), + (0x2F854, "M", "堍"), + (0x2F855, "M", "型"), + (0x2F856, "M", "堲"), + (0x2F857, "M", "報"), + (0x2F858, "M", "墬"), + (0x2F859, "M", "𡓤"), + (0x2F85A, "M", "売"), + (0x2F85B, "M", "壷"), + (0x2F85C, "M", "夆"), + (0x2F85D, "M", "多"), + (0x2F85E, "M", "夢"), + (0x2F85F, "M", "奢"), + (0x2F860, "M", "𡚨"), + (0x2F861, "M", "𡛪"), + (0x2F862, "M", "姬"), + (0x2F863, "M", "娛"), + (0x2F864, "M", "娧"), + (0x2F865, "M", "姘"), + (0x2F866, "M", "婦"), + (0x2F867, "M", "㛮"), + (0x2F868, "M", "㛼"), + (0x2F869, "M", "嬈"), + (0x2F86A, "M", "嬾"), + (0x2F86C, "M", "𡧈"), + (0x2F86D, "M", "寃"), + (0x2F86E, "M", "寘"), + (0x2F86F, "M", "寧"), + (0x2F870, "M", "寳"), + (0x2F871, "M", "𡬘"), + (0x2F872, "M", "寿"), + (0x2F873, "M", "将"), + (0x2F874, "M", "当"), + (0x2F875, "M", "尢"), + (0x2F876, "M", "㞁"), + ] + + +def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F877, "M", "屠"), + (0x2F878, "M", "屮"), + (0x2F879, "M", "峀"), + (0x2F87A, "M", "岍"), + (0x2F87B, "M", "𡷤"), + (0x2F87C, "M", "嵃"), + (0x2F87D, "M", "𡷦"), + (0x2F87E, "M", "嵮"), + (0x2F87F, "M", "嵫"), + (0x2F880, "M", "嵼"), + (0x2F881, "M", "巡"), + (0x2F882, "M", "巢"), + (0x2F883, "M", "㠯"), + (0x2F884, "M", "巽"), + (0x2F885, "M", "帨"), + (0x2F886, "M", "帽"), + (0x2F887, "M", "幩"), + (0x2F888, "M", "㡢"), + (0x2F889, "M", "𢆃"), + (0x2F88A, "M", "㡼"), + (0x2F88B, "M", "庰"), + (0x2F88C, "M", "庳"), + (0x2F88D, "M", "庶"), + (0x2F88E, "M", "廊"), + (0x2F88F, "M", "𪎒"), + (0x2F890, "M", "廾"), + (0x2F891, "M", "𢌱"), + (0x2F893, "M", "舁"), + (0x2F894, "M", "弢"), + (0x2F896, "M", "㣇"), + (0x2F897, "M", "𣊸"), + (0x2F898, "M", "𦇚"), + (0x2F899, "M", "形"), + (0x2F89A, "M", "彫"), + (0x2F89B, "M", "㣣"), + (0x2F89C, "M", "徚"), + (0x2F89D, "M", "忍"), + (0x2F89E, "M", "志"), + (0x2F89F, "M", "忹"), + (0x2F8A0, "M", "悁"), + (0x2F8A1, "M", "㤺"), + (0x2F8A2, "M", "㤜"), + (0x2F8A3, "M", "悔"), + (0x2F8A4, "M", "𢛔"), + (0x2F8A5, "M", "惇"), + (0x2F8A6, "M", "慈"), + (0x2F8A7, "M", "慌"), + (0x2F8A8, "M", "慎"), + (0x2F8A9, "M", "慌"), + (0x2F8AA, "M", "慺"), + (0x2F8AB, "M", "憎"), + (0x2F8AC, "M", "憲"), + (0x2F8AD, "M", "憤"), + (0x2F8AE, "M", "憯"), + (0x2F8AF, "M", "懞"), + (0x2F8B0, "M", "懲"), + (0x2F8B1, "M", "懶"), + (0x2F8B2, "M", "成"), + (0x2F8B3, "M", "戛"), + (0x2F8B4, "M", "扝"), + (0x2F8B5, "M", "抱"), + (0x2F8B6, "M", "拔"), + (0x2F8B7, "M", "捐"), + (0x2F8B8, "M", "𢬌"), + (0x2F8B9, "M", "挽"), + (0x2F8BA, "M", "拼"), + (0x2F8BB, "M", "捨"), + (0x2F8BC, "M", "掃"), + (0x2F8BD, "M", "揤"), + (0x2F8BE, "M", "𢯱"), + (0x2F8BF, "M", "搢"), + (0x2F8C0, "M", "揅"), + (0x2F8C1, "M", "掩"), + (0x2F8C2, "M", "㨮"), + (0x2F8C3, "M", "摩"), + (0x2F8C4, "M", "摾"), + (0x2F8C5, "M", "撝"), + (0x2F8C6, "M", "摷"), + (0x2F8C7, "M", "㩬"), + (0x2F8C8, "M", "敏"), + (0x2F8C9, "M", "敬"), + (0x2F8CA, "M", "𣀊"), + (0x2F8CB, "M", "旣"), + (0x2F8CC, "M", "書"), + (0x2F8CD, "M", "晉"), + (0x2F8CE, "M", "㬙"), + (0x2F8CF, "M", "暑"), + (0x2F8D0, "M", "㬈"), + (0x2F8D1, "M", "㫤"), + (0x2F8D2, "M", "冒"), + (0x2F8D3, "M", "冕"), + (0x2F8D4, "M", "最"), + (0x2F8D5, "M", "暜"), + (0x2F8D6, "M", "肭"), + (0x2F8D7, "M", "䏙"), + (0x2F8D8, "M", "朗"), + (0x2F8D9, "M", "望"), + (0x2F8DA, "M", "朡"), + (0x2F8DB, "M", "杞"), + (0x2F8DC, "M", "杓"), + ] + + +def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F8DD, "M", "𣏃"), + (0x2F8DE, "M", "㭉"), + (0x2F8DF, "M", "柺"), + (0x2F8E0, "M", "枅"), + (0x2F8E1, "M", "桒"), + (0x2F8E2, "M", "梅"), + (0x2F8E3, "M", "𣑭"), + (0x2F8E4, "M", "梎"), + (0x2F8E5, "M", "栟"), + (0x2F8E6, "M", "椔"), + (0x2F8E7, "M", "㮝"), + (0x2F8E8, "M", "楂"), + (0x2F8E9, "M", "榣"), + (0x2F8EA, "M", "槪"), + (0x2F8EB, "M", "檨"), + (0x2F8EC, "M", "𣚣"), + (0x2F8ED, "M", "櫛"), + (0x2F8EE, "M", "㰘"), + (0x2F8EF, "M", "次"), + (0x2F8F0, "M", "𣢧"), + (0x2F8F1, "M", "歔"), + (0x2F8F2, "M", "㱎"), + (0x2F8F3, "M", "歲"), + (0x2F8F4, "M", "殟"), + (0x2F8F5, "M", "殺"), + (0x2F8F6, "M", "殻"), + (0x2F8F7, "M", "𣪍"), + (0x2F8F8, "M", "𡴋"), + (0x2F8F9, "M", "𣫺"), + (0x2F8FA, "M", "汎"), + (0x2F8FB, "M", "𣲼"), + (0x2F8FC, "M", "沿"), + (0x2F8FD, "M", "泍"), + (0x2F8FE, "M", "汧"), + (0x2F8FF, "M", "洖"), + (0x2F900, "M", "派"), + (0x2F901, "M", "海"), + (0x2F902, "M", "流"), + (0x2F903, "M", "浩"), + (0x2F904, "M", "浸"), + (0x2F905, "M", "涅"), + (0x2F906, "M", "𣴞"), + (0x2F907, "M", "洴"), + (0x2F908, "M", "港"), + (0x2F909, "M", "湮"), + (0x2F90A, "M", "㴳"), + (0x2F90B, "M", "滋"), + (0x2F90C, "M", "滇"), + (0x2F90D, "M", "𣻑"), + (0x2F90E, "M", "淹"), + (0x2F90F, "M", "潮"), + (0x2F910, "M", "𣽞"), + (0x2F911, "M", "𣾎"), + (0x2F912, "M", "濆"), + (0x2F913, "M", "瀹"), + (0x2F914, "M", "瀞"), + (0x2F915, "M", "瀛"), + (0x2F916, "M", "㶖"), + (0x2F917, "M", "灊"), + (0x2F918, "M", "災"), + (0x2F919, "M", "灷"), + (0x2F91A, "M", "炭"), + (0x2F91B, "M", "𠔥"), + (0x2F91C, "M", "煅"), + (0x2F91D, "M", "𤉣"), + (0x2F91E, "M", "熜"), + (0x2F91F, "M", "𤎫"), + (0x2F920, "M", "爨"), + (0x2F921, "M", "爵"), + (0x2F922, "M", "牐"), + (0x2F923, "M", "𤘈"), + (0x2F924, "M", "犀"), + (0x2F925, "M", "犕"), + (0x2F926, "M", "𤜵"), + (0x2F927, "M", "𤠔"), + (0x2F928, "M", "獺"), + (0x2F929, "M", "王"), + (0x2F92A, "M", "㺬"), + (0x2F92B, "M", "玥"), + (0x2F92C, "M", "㺸"), + (0x2F92E, "M", "瑇"), + (0x2F92F, "M", "瑜"), + (0x2F930, "M", "瑱"), + (0x2F931, "M", "璅"), + (0x2F932, "M", "瓊"), + (0x2F933, "M", "㼛"), + (0x2F934, "M", "甤"), + (0x2F935, "M", "𤰶"), + (0x2F936, "M", "甾"), + (0x2F937, "M", "𤲒"), + (0x2F938, "M", "異"), + (0x2F939, "M", "𢆟"), + (0x2F93A, "M", "瘐"), + (0x2F93B, "M", "𤾡"), + (0x2F93C, "M", "𤾸"), + (0x2F93D, "M", "𥁄"), + (0x2F93E, "M", "㿼"), + (0x2F93F, "M", "䀈"), + (0x2F940, "M", "直"), + (0x2F941, "M", "𥃳"), + ] + + +def _seg_81() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F942, "M", "𥃲"), + (0x2F943, "M", "𥄙"), + (0x2F944, "M", "𥄳"), + (0x2F945, "M", "眞"), + (0x2F946, "M", "真"), + (0x2F948, "M", "睊"), + (0x2F949, "M", "䀹"), + (0x2F94A, "M", "瞋"), + (0x2F94B, "M", "䁆"), + (0x2F94C, "M", "䂖"), + (0x2F94D, "M", "𥐝"), + (0x2F94E, "M", "硎"), + (0x2F94F, "M", "碌"), + (0x2F950, "M", "磌"), + (0x2F951, "M", "䃣"), + (0x2F952, "M", "𥘦"), + (0x2F953, "M", "祖"), + (0x2F954, "M", "𥚚"), + (0x2F955, "M", "𥛅"), + (0x2F956, "M", "福"), + (0x2F957, "M", "秫"), + (0x2F958, "M", "䄯"), + (0x2F959, "M", "穀"), + (0x2F95A, "M", "穊"), + (0x2F95B, "M", "穏"), + (0x2F95C, "M", "𥥼"), + (0x2F95D, "M", "𥪧"), + (0x2F95F, "M", "竮"), + (0x2F960, "M", "䈂"), + (0x2F961, "M", "𥮫"), + (0x2F962, "M", "篆"), + (0x2F963, "M", "築"), + (0x2F964, "M", "䈧"), + (0x2F965, "M", "𥲀"), + (0x2F966, "M", "糒"), + (0x2F967, "M", "䊠"), + (0x2F968, "M", "糨"), + (0x2F969, "M", "糣"), + (0x2F96A, "M", "紀"), + (0x2F96B, "M", "𥾆"), + (0x2F96C, "M", "絣"), + (0x2F96D, "M", "䌁"), + (0x2F96E, "M", "緇"), + (0x2F96F, "M", "縂"), + (0x2F970, "M", "繅"), + (0x2F971, "M", "䌴"), + (0x2F972, "M", "𦈨"), + (0x2F973, "M", "𦉇"), + (0x2F974, "M", "䍙"), + (0x2F975, "M", "𦋙"), + (0x2F976, "M", "罺"), + (0x2F977, "M", "𦌾"), + (0x2F978, "M", "羕"), + (0x2F979, "M", "翺"), + (0x2F97A, "M", "者"), + (0x2F97B, "M", "𦓚"), + (0x2F97C, "M", "𦔣"), + (0x2F97D, "M", "聠"), + (0x2F97E, "M", "𦖨"), + (0x2F97F, "M", "聰"), + (0x2F980, "M", "𣍟"), + (0x2F981, "M", "䏕"), + (0x2F982, "M", "育"), + (0x2F983, "M", "脃"), + (0x2F984, "M", "䐋"), + (0x2F985, "M", "脾"), + (0x2F986, "M", "媵"), + (0x2F987, "M", "𦞧"), + (0x2F988, "M", "𦞵"), + (0x2F989, "M", "𣎓"), + (0x2F98A, "M", "𣎜"), + (0x2F98B, "M", "舁"), + (0x2F98C, "M", "舄"), + (0x2F98D, "M", "辞"), + (0x2F98E, "M", "䑫"), + (0x2F98F, "M", "芑"), + (0x2F990, "M", "芋"), + (0x2F991, "M", "芝"), + (0x2F992, "M", "劳"), + (0x2F993, "M", "花"), + (0x2F994, "M", "芳"), + (0x2F995, "M", "芽"), + (0x2F996, "M", "苦"), + (0x2F997, "M", "𦬼"), + (0x2F998, "M", "若"), + (0x2F999, "M", "茝"), + (0x2F99A, "M", "荣"), + (0x2F99B, "M", "莭"), + (0x2F99C, "M", "茣"), + (0x2F99D, "M", "莽"), + (0x2F99E, "M", "菧"), + (0x2F99F, "M", "著"), + (0x2F9A0, "M", "荓"), + (0x2F9A1, "M", "菊"), + (0x2F9A2, "M", "菌"), + (0x2F9A3, "M", "菜"), + (0x2F9A4, "M", "𦰶"), + (0x2F9A5, "M", "𦵫"), + (0x2F9A6, "M", "𦳕"), + (0x2F9A7, "M", "䔫"), + ] + + +def _seg_82() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F9A8, "M", "蓱"), + (0x2F9A9, "M", "蓳"), + (0x2F9AA, "M", "蔖"), + (0x2F9AB, "M", "𧏊"), + (0x2F9AC, "M", "蕤"), + (0x2F9AD, "M", "𦼬"), + (0x2F9AE, "M", "䕝"), + (0x2F9AF, "M", "䕡"), + (0x2F9B0, "M", "𦾱"), + (0x2F9B1, "M", "𧃒"), + (0x2F9B2, "M", "䕫"), + (0x2F9B3, "M", "虐"), + (0x2F9B4, "M", "虜"), + (0x2F9B5, "M", "虧"), + (0x2F9B6, "M", "虩"), + (0x2F9B7, "M", "蚩"), + (0x2F9B8, "M", "蚈"), + (0x2F9B9, "M", "蜎"), + (0x2F9BA, "M", "蛢"), + (0x2F9BB, "M", "蝹"), + (0x2F9BC, "M", "蜨"), + (0x2F9BD, "M", "蝫"), + (0x2F9BE, "M", "螆"), + (0x2F9BF, "M", "䗗"), + (0x2F9C0, "M", "蟡"), + (0x2F9C1, "M", "蠁"), + (0x2F9C2, "M", "䗹"), + (0x2F9C3, "M", "衠"), + (0x2F9C4, "M", "衣"), + (0x2F9C5, "M", "𧙧"), + (0x2F9C6, "M", "裗"), + (0x2F9C7, "M", "裞"), + (0x2F9C8, "M", "䘵"), + (0x2F9C9, "M", "裺"), + (0x2F9CA, "M", "㒻"), + (0x2F9CB, "M", "𧢮"), + (0x2F9CC, "M", "𧥦"), + (0x2F9CD, "M", "䚾"), + (0x2F9CE, "M", "䛇"), + (0x2F9CF, "M", "誠"), + (0x2F9D0, "M", "諭"), + (0x2F9D1, "M", "變"), + (0x2F9D2, "M", "豕"), + (0x2F9D3, "M", "𧲨"), + (0x2F9D4, "M", "貫"), + (0x2F9D5, "M", "賁"), + (0x2F9D6, "M", "贛"), + (0x2F9D7, "M", "起"), + (0x2F9D8, "M", "𧼯"), + (0x2F9D9, "M", "𠠄"), + (0x2F9DA, "M", "跋"), + (0x2F9DB, "M", "趼"), + (0x2F9DC, "M", "跰"), + (0x2F9DD, "M", "𠣞"), + (0x2F9DE, "M", "軔"), + (0x2F9DF, "M", "輸"), + (0x2F9E0, "M", "𨗒"), + (0x2F9E1, "M", "𨗭"), + (0x2F9E2, "M", "邔"), + (0x2F9E3, "M", "郱"), + (0x2F9E4, "M", "鄑"), + (0x2F9E5, "M", "𨜮"), + (0x2F9E6, "M", "鄛"), + (0x2F9E7, "M", "鈸"), + (0x2F9E8, "M", "鋗"), + (0x2F9E9, "M", "鋘"), + (0x2F9EA, "M", "鉼"), + (0x2F9EB, "M", "鏹"), + (0x2F9EC, "M", "鐕"), + (0x2F9ED, "M", "𨯺"), + (0x2F9EE, "M", "開"), + (0x2F9EF, "M", "䦕"), + (0x2F9F0, "M", "閷"), + (0x2F9F1, "M", "𨵷"), + (0x2F9F2, "M", "䧦"), + (0x2F9F3, "M", "雃"), + (0x2F9F4, "M", "嶲"), + (0x2F9F5, "M", "霣"), + (0x2F9F6, "M", "𩅅"), + (0x2F9F7, "M", "𩈚"), + (0x2F9F8, "M", "䩮"), + (0x2F9F9, "M", "䩶"), + (0x2F9FA, "M", "韠"), + (0x2F9FB, "M", "𩐊"), + (0x2F9FC, "M", "䪲"), + (0x2F9FD, "M", "𩒖"), + (0x2F9FE, "M", "頋"), + (0x2FA00, "M", "頩"), + (0x2FA01, "M", "𩖶"), + (0x2FA02, "M", "飢"), + (0x2FA03, "M", "䬳"), + (0x2FA04, "M", "餩"), + (0x2FA05, "M", "馧"), + (0x2FA06, "M", "駂"), + (0x2FA07, "M", "駾"), + (0x2FA08, "M", "䯎"), + (0x2FA09, "M", "𩬰"), + (0x2FA0A, "M", "鬒"), + (0x2FA0B, "M", "鱀"), + (0x2FA0C, "M", "鳽"), + ] + + +def _seg_83() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2FA0D, "M", "䳎"), + (0x2FA0E, "M", "䳭"), + (0x2FA0F, "M", "鵧"), + (0x2FA10, "M", "𪃎"), + (0x2FA11, "M", "䳸"), + (0x2FA12, "M", "𪄅"), + (0x2FA13, "M", "𪈎"), + (0x2FA14, "M", "𪊑"), + (0x2FA15, "M", "麻"), + (0x2FA16, "M", "䵖"), + (0x2FA17, "M", "黹"), + (0x2FA18, "M", "黾"), + (0x2FA19, "M", "鼅"), + (0x2FA1A, "M", "鼏"), + (0x2FA1B, "M", "鼖"), + (0x2FA1C, "M", "鼻"), + (0x2FA1D, "M", "𪘀"), + (0x2FA1E, "X"), + (0x30000, "V"), + (0x3134B, "X"), + (0x31350, "V"), + (0x323B0, "X"), + (0xE0100, "I"), + (0xE01F0, "X"), + ] + + +uts46data = tuple( + _seg_0() + + _seg_1() + + _seg_2() + + _seg_3() + + _seg_4() + + _seg_5() + + _seg_6() + + _seg_7() + + _seg_8() + + _seg_9() + + _seg_10() + + _seg_11() + + _seg_12() + + _seg_13() + + _seg_14() + + _seg_15() + + _seg_16() + + _seg_17() + + _seg_18() + + _seg_19() + + _seg_20() + + _seg_21() + + _seg_22() + + _seg_23() + + _seg_24() + + _seg_25() + + _seg_26() + + _seg_27() + + _seg_28() + + _seg_29() + + _seg_30() + + _seg_31() + + _seg_32() + + _seg_33() + + _seg_34() + + _seg_35() + + _seg_36() + + _seg_37() + + _seg_38() + + _seg_39() + + _seg_40() + + _seg_41() + + _seg_42() + + _seg_43() + + _seg_44() + + _seg_45() + + _seg_46() + + _seg_47() + + _seg_48() + + _seg_49() + + _seg_50() + + _seg_51() + + _seg_52() + + _seg_53() + + _seg_54() + + _seg_55() + + _seg_56() + + _seg_57() + + _seg_58() + + _seg_59() + + _seg_60() + + _seg_61() + + _seg_62() + + _seg_63() + + _seg_64() + + _seg_65() + + _seg_66() + + _seg_67() + + _seg_68() + + _seg_69() + + _seg_70() + + _seg_71() + + _seg_72() + + _seg_73() + + _seg_74() + + _seg_75() + + _seg_76() + + _seg_77() + + _seg_78() + + _seg_79() + + _seg_80() + + _seg_81() + + _seg_82() + + _seg_83() +) # type: Tuple[Union[Tuple[int, str], Tuple[int, str, str]], ...] diff --git a/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/yarl/__pycache__/_parse.cpython-312.pyc b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/yarl/__pycache__/_parse.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6432860a597de7bbf8a25f50e4aa907e07cd8f1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/_private/runtime_env/agent/thirdparty_files/yarl/__pycache__/_parse.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/__init__.py b/lib/python3.12/site-packages/ray/llm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b78d3ba4e1464bbeb6b99b2eb3811a0c5d7b60ae Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1a304e84ded93babf08de6fd5d90e4e66d4203e0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/__init__.py @@ -0,0 +1,13 @@ +from ray.llm._internal.batch.processor import ( + HttpRequestProcessorConfig, + Processor, + ProcessorBuilder, + ProcessorConfig, +) + +__all__ = [ + "Processor", + "ProcessorConfig", + "ProcessorBuilder", + "HttpRequestProcessorConfig", +] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62cd19ecfa7089130dade7b9330694201f2a9502 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ce2d10b5c1c732fcb1b8cf9408d70a458e9dba0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3257786be13189e80c402b5892ef9cc444e4e87e Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/benchmark_processor.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/benchmark_processor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26f09573950801fa1f7f8172d6ce051ccf14f0c3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/benchmark_processor.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/dataset.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/dataset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69ee25a3ba6a766144192765ae3007645547baa0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/__pycache__/dataset.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/benchmark_processor.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/benchmark_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..e0eee68e976a38a09d7fa5214a6e10e02929dfee --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/benchmark_processor.py @@ -0,0 +1,556 @@ +#!/usr/bin/env python +""" +Benchmark Ray Data LLM offline batch inference throughput. + +Sample usage: +python ray.llm._internal.batch.benchmark.benchmark_processor --mode vllm_engine --batch-size 64 --concurrency 1 --num-prompts 10000 --model facebook/opt-1.3b + --tensor-parallel-size 2 --pipeline-parallel-size 2 --distributed-executor-backend ray +""" + +import argparse +import sys +from dataclasses import dataclass +from enum import Enum +from time import perf_counter, sleep + +import ray +from .dataset import ShareGPTDataset +from ray import data, serve +from ray.data.llm import ( + ServeDeploymentProcessorConfig, + build_llm_processor, + vLLMEngineProcessorConfig, +) +from ray.serve.llm import ( + LLMConfig, + ModelLoadingConfig, + build_llm_deployment, +) +from ray.serve.llm.openai_api_models import CompletionRequest + + +class Mode(Enum): + """Processor to benchmark.""" + + VLLM_ENGINE = "vllm_engine" + SHARED_VLLM_ENGINE = "shared_vllm_engine" + SERVE_DEPLOYMENT = "serve_deployment" + SHARED_SERVE_DEPLOYMENT = "shared_serve_deployment" + + +# Default sampling parameters -- ensure a fair comparison by omitting sampling-induced variance +VLLM_SAMPLING_PARAMS = { + "temperature": 1.0, + "max_tokens": 100, + "top_p": 1.0, + "ignore_eos": True, +} + +# Default vLLM engine kwargs +VLLM_ENGINE_KWARGS = { + "enable_prefix_caching": True, + "enable_chunked_prefill": True, + "max_num_batched_tokens": 4096, +} + + +def build_vllm_engine_kwargs(**kwargs) -> dict: + """Build vLLM engine kwargs from command line arguments.""" + engine_kwargs = VLLM_ENGINE_KWARGS.copy() + engine_kwargs.update({k: v for k, v in kwargs.items() if v is not None}) + return engine_kwargs + + +def _build_vllm_engine_config( + model: str, + batch_size: int, + concurrency: int, + pipeline_parallel_size: int = None, + tensor_parallel_size: int = None, + distributed_executor_backend: str = None, +) -> vLLMEngineProcessorConfig: + """Helper to create vLLMEngineProcessorConfig.""" + return vLLMEngineProcessorConfig( + model_source=model, + batch_size=batch_size, + concurrency=concurrency, + apply_chat_template=False, + tokenize=False, + detokenize=False, + engine_kwargs=build_vllm_engine_kwargs( + pipeline_parallel_size=pipeline_parallel_size, + tensor_parallel_size=tensor_parallel_size, + distributed_executor_backend=distributed_executor_backend, + ), + ) + + +def _build_serve_deployment_config( + batch_size: int, + concurrency: int, + deployment_name: str = None, + app_name: str = None, +) -> ServeDeploymentProcessorConfig: + """Helper to create ServeDeploymentProcessorConfig.""" + return ServeDeploymentProcessorConfig( + deployment_name=deployment_name, + app_name=app_name, + dtype_mapping={ + "CompletionRequest": CompletionRequest, + }, + batch_size=batch_size, + concurrency=concurrency, + ) + + +@dataclass(slots=True) +class BenchmarkResult: + mode: Mode + batch_size: int + concurrency: int + samples: int + elapsed_s: float + + @property + def throughput(self) -> float: + return self.samples / self.elapsed_s if self.elapsed_s else 0.0 + + def show(self) -> None: + print("\n" + "=" * 60) + print(f"BENCHMARK - {self.mode}") + print("=" * 60) + print(f"Samples : {self.samples}") + print(f"Batch size : {self.batch_size}") + print(f"Concurrency : {self.concurrency}") + print(f"Time (s) : {self.elapsed_s:.2f}") + print(f"Throughput : {self.throughput:.2f} req/s") + print("=" * 60) + + +def build_single_vllm_engine_processor( + batch_size: int, + concurrency: int, + model: str, + sampling_params: dict = VLLM_SAMPLING_PARAMS, + pipeline_parallel_size: int = None, + tensor_parallel_size: int = None, + distributed_executor_backend: str = None, +): + """Build vLLM engine processor for single-turn benchmark.""" + config = _build_vllm_engine_config( + model, + batch_size, + concurrency, + pipeline_parallel_size, + tensor_parallel_size, + distributed_executor_backend, + ) + return build_llm_processor( + config, + preprocess=lambda row: dict( + prompt=row["prompt"], + sampling_params=sampling_params, + ), + postprocess=lambda row: row, + ) + + +def build_shared_vllm_engine_processor( + batch_size: int, + concurrency: int, + model: str, + sampling_params: dict = VLLM_SAMPLING_PARAMS, + pipeline_parallel_size: int = None, + tensor_parallel_size: int = None, + distributed_executor_backend: str = None, +): + """Build vLLM engine processor for multi-turn benchmark.""" + config = _build_vllm_engine_config( + model, + batch_size, + concurrency, + pipeline_parallel_size, + tensor_parallel_size, + distributed_executor_backend, + ) + + processor1 = build_llm_processor( + config, + preprocess=lambda row: dict( + prompt=row["prompt"], + sampling_params=sampling_params, + ), + postprocess=lambda row: { + "prompt": row["generated_text"] + if str(row.get("generated_text", "")).strip() + else row["prompt"] + }, + ) + + processor2 = build_llm_processor( + config, + preprocess=lambda row: dict( + prompt=row["prompt"], + sampling_params=sampling_params, + ), + postprocess=lambda row: row, + ) + + def multi_turn_processor(dataset): + return processor2(processor1(dataset)) + + return multi_turn_processor + + +def setup_serve_deployment(model: str, concurrency: int) -> tuple[str, str]: + """Set up Ray Serve deployment for hosting the LLM model.""" + deployment_name = "benchmark_deployment" + app_name = "benchmark_app" + + llm_config = LLMConfig( + model_loading_config=ModelLoadingConfig( + model_id=model, + model_source=model, + ), + deployment_config=dict( + name=deployment_name, + # To fairly compare with vLLM engine processor, fix the number of replicas to the concurrency level + autoscaling_config=dict( + min_replicas=concurrency, + max_replicas=concurrency, + ), + ), + engine_kwargs=dict( + enable_prefix_caching=True, + enable_chunked_prefill=True, + max_num_batched_tokens=4096, + ), + ) + + override_serve_options = dict(name=deployment_name) + llm_app = build_llm_deployment( + llm_config, override_serve_options=override_serve_options + ) + serve.run(llm_app, name=app_name) + + print("Waiting for Serve deployment to be ready...") + max_wait_time = 120 # seconds + wait_time = 0 + while not _is_app_ready(app_name) and wait_time < max_wait_time: + sleep(5) + wait_time += 5 + + if wait_time >= max_wait_time: + raise TimeoutError("Deployment failed to become ready within timeout") + + print("Deployment is ready!") + return deployment_name, app_name + + +def _is_app_ready(app_name: str) -> bool: + try: + serve_status = serve.status() + + if app_name in serve_status.applications: + app_status = serve_status.applications[app_name] + if app_status.status == "RUNNING": + print(f"Application '{app_name}' is RUNNING.") + return True + else: + print(f"Application '{app_name}' status: {app_status.status}") + return False + else: + print(f"Application '{app_name}' not found in Serve status.") + return False + except Exception as e: + print(f"Error checking app status: {e}") + return False + + +def build_single_serve_deployment_processor( + batch_size: int, + concurrency: int, + model: str, + sampling_params: dict = VLLM_SAMPLING_PARAMS, + deployment_name: str = None, + app_name: str = None, + **kwargs, +): + """Build Serve deployment processor for single-turn benchmark.""" + config = _build_serve_deployment_config( + batch_size, + concurrency, + deployment_name, + app_name, + ) + return build_llm_processor( + config, + preprocess=lambda row: dict( + method="completions", + dtype="CompletionRequest", + request_kwargs=dict( + model=model, + prompt=row["prompt"], + **sampling_params, + ), + ), + postprocess=lambda row: row, + ) + + +def build_shared_serve_deployment_processor( + batch_size: int, + concurrency: int, + model: str, + sampling_params: dict = VLLM_SAMPLING_PARAMS, + deployment_name: str = None, + app_name: str = None, + **kwargs, +): + """Build Serve deployment processor for multi-turn benchmark.""" + config = _build_serve_deployment_config( + batch_size, + concurrency, + deployment_name, + app_name, + ) + + processor1 = build_llm_processor( + config, + preprocess=lambda row: dict( + method="completions", + dtype="CompletionRequest", + request_kwargs=dict( + model=model, + prompt=row["prompt"], + stream=False, + ), + ), + postprocess=lambda row: { + # Fall back to original prompt if generated text is empty + "prompt": ( + row["choices"][0]["text"] + if row.get("choices") and str(row["choices"][0].get("text", "")).strip() + else row["prompt"] + ) + }, + ) + + processor2 = build_llm_processor( + config, + preprocess=lambda row: dict( + method="completions", + dtype="CompletionRequest", + request_kwargs=dict( + model=model, + prompt=row["prompt"], + stream=False, + ), + ), + postprocess=lambda row: row, + ) + + def multi_turn_processor(dataset): + return processor2(processor1(dataset)) + + return multi_turn_processor + + +# ----------------------------------------------------------------------------- +# Benchmark execution +# ----------------------------------------------------------------------------- +def run_processor( + mode: Mode, + dataset: data.Dataset, + builder, + **kwargs, +) -> BenchmarkResult: + processor = builder(**kwargs) + + total_samples = dataset.count() + + start = perf_counter() + processor(dataset).materialize() + elapsed = perf_counter() - start + + return BenchmarkResult( + mode=mode, + batch_size=kwargs.get("batch_size"), + concurrency=kwargs.get("concurrency"), + samples=total_samples, + elapsed_s=elapsed, + ) + + +def benchmark( + mode: Mode, + dataset: data.Dataset, + *, + batch_size: int, + concurrency: int, + model: str, + sampling_params: dict = VLLM_SAMPLING_PARAMS, + pipeline_parallel_size: int = None, + tensor_parallel_size: int = None, + distributed_executor_backend: str = None, +) -> BenchmarkResult: + mode_to_builder = { + Mode.VLLM_ENGINE: build_single_vllm_engine_processor, + Mode.SHARED_VLLM_ENGINE: build_shared_vllm_engine_processor, + Mode.SERVE_DEPLOYMENT: build_single_serve_deployment_processor, + Mode.SHARED_SERVE_DEPLOYMENT: build_shared_serve_deployment_processor, + } + + if mode not in mode_to_builder: + raise ValueError(f"Unknown benchmark mode: {mode}") + + builder = mode_to_builder[mode] + + if mode in [Mode.SERVE_DEPLOYMENT, Mode.SHARED_SERVE_DEPLOYMENT]: + deployment_name, app_name = setup_serve_deployment(model, concurrency) + try: + return run_processor( + mode, + dataset, + builder, + batch_size=batch_size, + concurrency=concurrency, + model=model, + sampling_params=sampling_params, + deployment_name=deployment_name, + app_name=app_name, + ) + finally: + serve.delete(app_name) + else: + return run_processor( + mode, + dataset, + builder, + batch_size=batch_size, + concurrency=concurrency, + model=model, + sampling_params=sampling_params, + pipeline_parallel_size=pipeline_parallel_size, + tensor_parallel_size=tensor_parallel_size, + distributed_executor_backend=distributed_executor_backend, + ) + + +# ----------------------------------------------------------------------------- +# CLI +# ----------------------------------------------------------------------------- +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="vLLM throughput benchmark") + parser.add_argument( + "--mode", + choices=[mode.value for mode in Mode], + default=Mode.VLLM_ENGINE.value, + help="Ray Data LLM processor to run benchmarks for", + ) + # Dataset configuration + parser.add_argument( + "--dataset-path", + type=str, + default="/home/ubuntu/datasets/Code-feedback-sharegpt-renamed", + help="Path to dataset on disk", + ) + parser.add_argument( + "--num-prompts", type=int, default=1000, help="Number of prompts to process" + ) + parser.add_argument( + "--hf-dataset-id", + type=str, + default="Crystalcareai/Code-feedback-sharegpt-renamed", + help="Hugging Face dataset ID to download", + ) + parser.add_argument( + "--hf-split", + type=str, + default="train", + help="Hugging Face dataset split to load", + ) + parser.add_argument( + "--seed", + type=int, + default=0, + help="Random seed for dataset sampling", + ) + parser.add_argument( + "--truncate-prompt", + type=int, + default=2048, + help="Maximum prompt length", + ) + # Engine configuration + parser.add_argument( + "--model", + type=str, + required=True, + help="LLM model to use", + ) + parser.add_argument( + "--pipeline-parallel-size", + type=int, + default=1, + help="Pipeline parallel size for vLLM engine", + ) + parser.add_argument( + "--tensor-parallel-size", + type=int, + default=1, + help="Tensor parallel size for vLLM engine", + ) + parser.add_argument( + "--distributed-executor-backend", + type=str, + default="mp", + choices=["ray", "mp"], + help="Distributed executor backend for vLLM engine", + ) + # Ray Data worker configuration + parser.add_argument( + "--batch-size", + type=int, + required=True, + help="Ray Data batch size for processing", + ) + parser.add_argument( + "--concurrency", type=int, required=True, help="Ray Data concurrency level" + ) + return parser.parse_args(argv) + + +def main() -> None: + args = parse_args(sys.argv[1:]) + + ray.init() + try: + dataset = ShareGPTDataset( + dataset_path=args.dataset_path, + seed=args.seed, + hf_dataset_id=args.hf_dataset_id, + hf_split=args.hf_split, + truncate_prompt=args.truncate_prompt, + ) + prompts = dataset.sample(args.num_prompts) + + dataset = data.from_items(prompts) + result = benchmark( + Mode(args.mode), + dataset, + batch_size=args.batch_size, + concurrency=args.concurrency, + model=args.model, + sampling_params=VLLM_SAMPLING_PARAMS, + pipeline_parallel_size=args.pipeline_parallel_size, + tensor_parallel_size=args.tensor_parallel_size, + distributed_executor_backend=args.distributed_executor_backend, + ) + result.show() + finally: + ray.shutdown() + + +if __name__ == "__main__": + main() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/dataset.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..d58027cffefc0d565aa8d66d6d6a801454d2d26d --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/benchmark/dataset.py @@ -0,0 +1,169 @@ +""" +This module defines a dataset framework for sampling benchmark requests. +""" + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Dict, List, Optional + +from datasets import load_dataset, load_from_disk + + +class BenchmarkDataset(ABC): + DEFAULT_RANDOM_SEED = 0 + + def __init__( + self, + dataset_path: Optional[str] = None, + random_seed: int = DEFAULT_RANDOM_SEED, + ) -> None: + """ + Abstract base class for benchmark datasets. + + All benchmark datasets should inherit from this class and implement + the required abstract methods. + + Args: + dataset_path: The path to the dataset on disk. + random_seed: The seed for the random number generator. + """ + self._dataset_path = dataset_path + self._random_seed = random_seed + + @abstractmethod + def load_data(self) -> None: + """ + Load data from the dataset source into memory. + + Raises: + NotImplementedError: If the method is not implemented in subclasses. + """ + raise NotImplementedError("load_data must be implemented in subclasses.") + + @abstractmethod + def sample(self, num_requests: int) -> List[Dict]: + """ + Sample prompts from the loaded dataset. + + Args: + num_requests: The number of prompts to sample from the dataset. + + Returns: + A list of sampled request dictionaries. + + Raises: + NotImplementedError: If the method is not implemented in subclasses. + """ + raise NotImplementedError("sample must be implemented in subclasses.") + + +class ShareGPTDataset(BenchmarkDataset): + """Implements the ShareGPT dataset. The first human message of each conversation is used to build a prompt.""" + + def __init__( + self, + dataset_path: str, + seed: int, + hf_dataset_id: str = "Crystalcareai/Code-feedback-sharegpt-renamed", + hf_split: str = "train", + truncate_prompt: Optional[int] = None, + ) -> None: + """ + Initializes the ShareGPTDataset. + + Args: + dataset_path: The path to the dataset on disk. + seed: The seed for the random number generator. + hf_dataset_id: The Hugging Face dataset ID to download if the dataset is not found on disk. + hf_split: The Hugging Face split to load from the dataset. + truncate_prompt: Maximum prompt length so that the prompt fits in the model's context window. + """ + super().__init__(dataset_path, seed) + self._seed = seed + + self._hf_dataset_id = hf_dataset_id + self._hf_split = hf_split + self._truncate_prompt = truncate_prompt + + self._data: list[Dict] | None = None + + def load_data(self) -> None: + """Load data from the dataset path into memory.""" + if self._data is None: + self._data = self._load_dataset_data() + + def sample(self, num_requests: int) -> List[Dict]: + """Sample prompts from the loaded dataset.""" + if self._data is None: + self.load_data() + + prompts = [] + for item in self._data: + if len(prompts) >= num_requests: + break + + prompt_data = self._extract_prompt(item) + if prompt_data is not None: + prompts.append(prompt_data) + + if not prompts: + raise ValueError("ShareGPT dataset yielded no usable prompts") + return prompts + + def _load_dataset(self): + """Load dataset from disk or Hugging Face.""" + path = Path(self._dataset_path) + print(f"Attempting to load dataset from {path}") + print(f"Dataset exists on disk: {path.exists()}") + + try: + if path.exists(): + dataset = load_from_disk(str(path)) + else: + print( + f"Dataset not found on disk, downloading from Hugging Face: {self._hf_dataset_id}" + ) + + path.parent.mkdir(parents=True, exist_ok=True) + dataset = load_dataset(self._hf_dataset_id, split=self._hf_split) + dataset.save_to_disk(str(path)) + return dataset + + except Exception as e: + raise RuntimeError(f"Error loading ShareGPT dataset: {e}") + + def _load_dataset_data(self) -> List[Dict]: + """Load and process dataset data into a list of dictionaries.""" + ds = self._load_dataset().shuffle(seed=self._seed) + data = [] + + for i, row in enumerate(ds): + data.append(row) + + print(f"Loaded {len(data)} samples from dataset") + return data + + def _extract_prompt(self, item: Dict) -> Dict | None: + """ + Extracts the first human message of a conversation or None. + + The ShareGPT schema uses {"role": "human", "value": ...} for user + turns. + """ + messages = item.get("messages") or item.get("conversations") or [] + prompt = next( + ( + str(msg.get("value", "")).strip() + for msg in messages + if msg.get("role") in {"human", "user"} + ), + None, + ) + + # Only return a valid prompt if it's not empty + if prompt and prompt.strip(): + if self._truncate_prompt: + prompt = prompt[: self._truncate_prompt] + return {"prompt": prompt} + + return None diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a6b16daae6757e4368cec4063decb3a92a90d923 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__init__.py @@ -0,0 +1,23 @@ +from ray.llm._internal.batch.observability.logging.setup import ( + setup_logging, +) +from ray.llm._internal.common.observability.logging_utils import ( + disable_datasets_logger, + disable_vllm_custom_ops_logger_on_cpu_nodes, +) +from ray.llm._internal.common.observability.telemetry_utils import Once + +_setup_observability_once = Once() + + +def _setup_observability(): + setup_logging() + disable_datasets_logger() + disable_vllm_custom_ops_logger_on_cpu_nodes() + + +def setup_observability(): + _setup_observability_once.do_once(_setup_observability) + + +__all__ = ["setup_observability"] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e8eb69e3beaa20bc885d9bf05b58358735e3c2f Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..04cd4d26101f34f3d4dcd116a9d56c7ef945ddf7 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__init__.py @@ -0,0 +1,40 @@ +import logging +from typing import Optional + +from ray._common.filters import CoreContextFilter + + +def _setup_logger(logger_name: str): + """Setup logger given the logger name. + + This function is idempotent and won't set up the same logger multiple times. It will + also skip the setup if Data logger is already setup and has handlers. + + Args: + logger_name: logger name used to get the logger. + """ + logger = logging.getLogger(logger_name) + llm_logger = logging.getLogger("ray.data") + + # Skip setup if the logger already has handlers setup or if the parent (Data + # logger) has handlers. + if logger.handlers or llm_logger.handlers: + return + + # Set up stream handler, which logs to console as plaintext. + stream_handler = logging.StreamHandler() + stream_handler.addFilter(CoreContextFilter()) + logger.addHandler(stream_handler) + logger.setLevel(logging.INFO) + logger.propagate = False + + +def get_logger(name: Optional[str] = None): + """Get a structured logger inherited from the Ray Data logger. + + Loggers by default are logging to stdout, and are expected to be scraped by an + external process. + """ + logger_name = f"ray.data.{name}" + _setup_logger(logger_name) + return logging.getLogger(logger_name) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1185f72d93f6d2cbe4bd5262f5640563c3e1f24c Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/setup.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/setup.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..453379522d02e8f2ca16176e7936c5ee5be69417 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/__pycache__/setup.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/setup.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..75edff6649399cd45349e113129f7b4fecf5c3df --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/logging/setup.py @@ -0,0 +1,26 @@ +import logging + +from ray._common.filters import CoreContextFilter +from ray._common.formatters import JSONFormatter + + +def _configure_stdlib_logging(): + """Configures stdlib root logger to make sure stdlib loggers (created as + `logging.getLogger(...)`) are using Ray's `JSONFormatter` with Core and Serve + context filters. + """ + + handler = logging.StreamHandler() + handler.addFilter(CoreContextFilter()) + handler.setFormatter(JSONFormatter()) + + root_logger = logging.getLogger() + # NOTE: It's crucial we reset all the handlers of the root logger, + # to make sure that logs aren't emitted twice + root_logger.handlers = [] + root_logger.addHandler(handler) + root_logger.setLevel(logging.INFO) + + +def setup_logging(): + _configure_stdlib_logging() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47d41bbb1f3028640eb1396749d34586266027a5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abe6ee2befdbaa004a7c9bd0ce5d23392937811b Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/usage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/usage.py new file mode 100644 index 0000000000000000000000000000000000000000..32d72a87a3574a77bd4bb3032b275b20caf43af5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/observability/usage_telemetry/usage.py @@ -0,0 +1,131 @@ +from enum import Enum +from typing import Callable, Dict, List, Tuple, Union + +import ray +from ray._common.usage.usage_lib import record_extra_usage_tag +from ray.llm._internal.batch.observability.logging import get_logger +from ray.llm._internal.common.base_pydantic import BaseModelExtended + +LLM_BATCH_TELEMETRY_NAMESPACE = "llm_batch_telemetry" +LLM_BATCH_TELEMETRY_ACTOR_NAME = "llm_batch_telemetry" + +logger = get_logger(__name__) + + +class BatchModelTelemetry(BaseModelExtended): + processor_config_name: str = "" + model_architecture: str = "" + batch_size: int = 0 + accelerator_type: str = "" + concurrency: Union[int, Tuple[int, int]] = 0 + task_type: str = "" + + # For the parallel size, 0 means not supported. + pipeline_parallel_size: int = 0 + tensor_parallel_size: int = 0 + data_parallel_size: int = 0 + + +class BatchTelemetryTags(str, Enum): + """Telemetry tags for RayLLM Batch.""" + + LLM_BATCH_PROCESSOR_CONFIG_NAME = "LLM_BATCH_PROCESSOR_CONFIG_NAME" + LLM_BATCH_MODEL_ARCHITECTURE = "LLM_BATCH_MODEL_ARCHITECTURE" + LLM_BATCH_SIZE = "LLM_BATCH_SIZE" + LLM_BATCH_ACCELERATOR_TYPE = "LLM_BATCH_ACCELERATOR_TYPE" + LLM_BATCH_CONCURRENCY = "LLM_BATCH_CONCURRENCY" + LLM_BATCH_TASK_TYPE = "LLM_BATCH_TASK_TYPE" + LLM_BATCH_PIPELINE_PARALLEL_SIZE = "LLM_BATCH_PIPELINE_PARALLEL_SIZE" + LLM_BATCH_TENSOR_PARALLEL_SIZE = "LLM_BATCH_TENSOR_PARALLEL_SIZE" + LLM_BATCH_DATA_PARALLEL_SIZE = "LLM_BATCH_DATA_PARALLEL_SIZE" + + +@ray.remote( + name=LLM_BATCH_TELEMETRY_ACTOR_NAME, + namespace=LLM_BATCH_TELEMETRY_NAMESPACE, + num_cpus=0, + lifetime="detached", +) +class _TelemetryAgent: + """Named Actor to keep the state of all deployed models and record telemetry.""" + + def __init__(self): + self._tracking_telemetries: List[BatchModelTelemetry] = [] + self._record_tag_func = record_extra_usage_tag + + def _update_record_tag_func(self, record_tag_func: Callable) -> None: + self._record_tag_func = record_tag_func + + def generate_report(self) -> Dict[str, str]: + return { + BatchTelemetryTags.LLM_BATCH_PROCESSOR_CONFIG_NAME: ",".join( + [t.processor_config_name for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_MODEL_ARCHITECTURE: ",".join( + [t.model_architecture for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_SIZE: ",".join( + [str(t.batch_size) for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_ACCELERATOR_TYPE: ",".join( + [t.accelerator_type for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_CONCURRENCY: ",".join( + [str(t.concurrency) for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_TASK_TYPE: ",".join( + [t.task_type for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_PIPELINE_PARALLEL_SIZE: ",".join( + [str(t.pipeline_parallel_size) for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_TENSOR_PARALLEL_SIZE: ",".join( + [str(t.tensor_parallel_size) for t in self._tracking_telemetries] + ), + BatchTelemetryTags.LLM_BATCH_DATA_PARALLEL_SIZE: ",".join( + [str(t.data_parallel_size) for t in self._tracking_telemetries] + ), + } + + def record(self, telemetry: BatchModelTelemetry) -> None: + """Append and record telemetries.""" + from ray._common.usage.usage_lib import TagKey + + self._tracking_telemetries.append(telemetry) + telemetry_dict = self.generate_report() + for key, value in telemetry_dict.items(): + self._record_tag_func(TagKey.Value(key), value) + + +class TelemetryAgent: + """Wrapper around the telemetry agent that calls the remote method until + push_telemetry_report is called.""" + + def __init__(self): + try: + self.remote_telemetry_agent = ray.get_actor( + LLM_BATCH_TELEMETRY_ACTOR_NAME, namespace=LLM_BATCH_TELEMETRY_NAMESPACE + ) + except ValueError: + from ray._common.constants import HEAD_NODE_RESOURCE_NAME + from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy + + self.remote_telemetry_agent = _TelemetryAgent.options( + # Ensure the actor is created on the head node. + resources={HEAD_NODE_RESOURCE_NAME: 0.001}, + # Ensure the actor is not scheduled with the existing placement group. + scheduling_strategy=PlacementGroupSchedulingStrategy( + placement_group=None + ), + ).remote() + + def _update_record_tag_func(self, record_tag_func: Callable): + self.remote_telemetry_agent._update_record_tag_func.remote(record_tag_func) + + def push_telemetry_report(self, telemetry: BatchModelTelemetry): + ray.get(self.remote_telemetry_agent.record.remote(telemetry)) + + +def get_or_create_telemetry_agent() -> TelemetryAgent: + """Helper to get or create the telemetry agent.""" + return TelemetryAgent() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fed7d021fe8e185933985870590889f18a5c786e --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__init__.py @@ -0,0 +1,15 @@ +from .base import Processor, ProcessorBuilder, ProcessorConfig +from .http_request_proc import HttpRequestProcessorConfig +from .serve_deployment_proc import ServeDeploymentProcessorConfig +from .sglang_engine_proc import SGLangEngineProcessorConfig +from .vllm_engine_proc import vLLMEngineProcessorConfig + +__all__ = [ + "ProcessorConfig", + "ProcessorBuilder", + "HttpRequestProcessorConfig", + "vLLMEngineProcessorConfig", + "SGLangEngineProcessorConfig", + "ServeDeploymentProcessorConfig", + "Processor", +] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02ca7c765debd1a1d4012a305a55c3c16ed6c716 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/base.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..007706921621aa4ea42abf879fe821e5fe0c27f9 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/base.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/http_request_proc.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/http_request_proc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25d17e8b96140a18c1fc307f0114d59ec9071613 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/http_request_proc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/serve_deployment_proc.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/serve_deployment_proc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de2614cfb036894b9b8c95254433f542fa6b2928 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/serve_deployment_proc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/sglang_engine_proc.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/sglang_engine_proc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eca27593f72296d5ce7e12d87b139f6f62c1d660 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/sglang_engine_proc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/vllm_engine_proc.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/vllm_engine_proc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..129cc535257e54412eb6e39118998222cdbb118c Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/__pycache__/vllm_engine_proc.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/base.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/base.py new file mode 100644 index 0000000000000000000000000000000000000000..8babcf629b5b5c7d227b6ee34cb7e2c7b05a5966 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/base.py @@ -0,0 +1,414 @@ +import logging +from collections import OrderedDict +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union + +from pydantic import Field, field_validator + +import ray +from ray.data import Dataset +from ray.data.block import UserDefinedFunction +from ray.llm._internal.batch.stages import ( + StatefulStage, + wrap_postprocess, + wrap_preprocess, +) +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.util.annotations import DeveloperAPI, PublicAPI + +logger = logging.getLogger(__name__) + + +# Higher values here are better for prefetching and locality. It's ok for this to be +# fairly high since streaming backpressure prevents us from overloading actors. +DEFAULT_MAX_TASKS_IN_FLIGHT = 4 + + +class ProcessorConfig(BaseModelExtended): + """The processor configuration.""" + + batch_size: int = Field( + default=32, + description="Large batch sizes are likely to saturate the compute resources " + "and could achieve higher throughput. On the other hand, small batch sizes " + "are more fault-tolerant and could reduce bubbles in the data pipeline. " + "You can tune the batch size to balance the throughput and fault-tolerance " + "based on your use case. Defaults to 32.", + ) + resources_per_bundle: Optional[Dict[str, float]] = Field( + default=None, + description="[DEPRECATED] This parameter is deprecated and will be removed in a future version. ", + deprecated=True, + ) + accelerator_type: Optional[str] = Field( + default=None, + description="The accelerator type used by the LLM stage in a processor. " + "Default to None, meaning that only the CPU will be used.", + ) + concurrency: Union[int, Tuple[int, int]] = Field( + default=1, + description="The number of workers for data parallelism. Default to 1. " + "If ``concurrency`` is a ``tuple`` ``(m, n)``, Ray creates an autoscaling " + "actor pool that scales between ``m`` and ``n`` workers (``1 <= m <= n``). " + "If ``concurrency`` is an ``int`` ``n``, Ray uses either a fixed pool of ``n`` " + "workers or an autoscaling pool from ``1`` to ``n`` workers, depending on " + "the processor and stage.", + ) + + experimental: Dict[str, Any] = Field( + default_factory=dict, + description="[Experimental] Experimental configurations." + "Supported keys:\n" + "`max_tasks_in_flight_per_actor`: The maximum number of tasks in flight per actor. Default to 4.", + ) + + @field_validator("concurrency") + def validate_concurrency( + cls, concurrency: Union[int, Tuple[int, int]] + ) -> Union[int, Tuple[int, int]]: + """Validate that `concurrency` is either: + - a positive int, or + - a 2-tuple `(min, max)` of positive ints with `min <= max`. + """ + + def require(condition: bool, message: str) -> None: + if not condition: + raise ValueError(message) + + if isinstance(concurrency, int): + require( + concurrency > 0, + f"A positive integer for `concurrency` is expected! Got: `{concurrency}`.", + ) + elif isinstance(concurrency, tuple): + require( + all(c > 0 for c in concurrency), + f"`concurrency` tuple items must be positive integers! Got: `{concurrency}`.", + ) + + min_concurrency, max_concurrency = concurrency + require( + min_concurrency <= max_concurrency, + f"min > max in the concurrency tuple `{concurrency}`!", + ) + return concurrency + + def get_concurrency(self, autoscaling_enabled: bool = True) -> Tuple[int, int]: + """Return a normalized `(min, max)` worker range from `self.concurrency`. + + Behavior: + - If `concurrency` is an int `n`: + - `autoscaling_enabled` is True -> return `(1, n)` (autoscaling). + - `autoscaling_enabled` is False -> return `(n, n)` (fixed-size pool). + - If `concurrency` is a 2-tuple `(m, n)`, return it unchanged + (the `autoscaling_enabled` flag is ignored). + + Args: + autoscaling_enabled: When False, treat an integer `concurrency` as fixed `(n, n)`; + otherwise treat it as a range `(1, n)`. Defaults to True. + + Returns: + tuple[int, int]: The allowed worker range `(min, max)`. + + Examples: + >>> self.concurrency = (2, 4) + >>> self.get_concurrency() + (2, 4) + >>> self.concurrency = 4 + >>> self.get_concurrency() + (1, 4) + >>> self.get_concurrency(autoscaling_enabled=False) + (4, 4) + """ + if isinstance(self.concurrency, int): + if autoscaling_enabled: + return 1, self.concurrency + else: + return self.concurrency, self.concurrency + return self.concurrency + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +class OfflineProcessorConfig(ProcessorConfig): + """The processor configuration for offline processing.""" + + model_source: str = Field( + description="The model source to use for the offline processing.", + ) + runtime_env: Optional[Dict[str, Any]] = Field( + default=None, + description="The runtime environment to use for the offline processing.", + ) + max_pending_requests: Optional[int] = Field( + default=None, + description="The maximum number of pending requests. If not specified, " + "will use the default value from the backend engine.", + ) + max_concurrent_batches: int = Field( + default=8, + description="The maximum number of concurrent batches in the engine. " + "This is to overlap the batch processing to avoid the tail latency of " + "each batch. The default value may not be optimal when the batch size " + "or the batch processing latency is too small, but it should be good " + "enough for batch size >= 32.", + ) + + # Processor stage configurations. + apply_chat_template: bool = Field( + default=True, description="Whether to apply chat template." + ) + chat_template: Optional[str] = Field( + default=None, + description="The chat template to use. This is usually not needed if the " + "model checkpoint already contains the chat template.", + ) + tokenize: bool = Field( + default=True, + description="Whether to tokenize the input before passing it to the " + "backend engine. If not, the backend engine will tokenize the prompt.", + ) + detokenize: bool = Field( + default=True, + description="Whether to detokenize the output.", + ) + has_image: bool = Field( + default=False, + description="Whether the input messages have images.", + ) + + +@PublicAPI(stability="alpha") +class Processor: + """A processor is composed of a preprocess stage, followed by one or more + processing stages, and finally a postprocess stage. We use processor as a + paradigm for processing data using LLMs. + + Args: + config: The processor config. + stages: List of processing stages. + preprocess: An optional lambda function that takes a row (dict) as input + and returns a preprocessed row (dict). The output row must contain the + required fields for the following processing stages. + postprocess: An optional lambda function that takes a row (dict) as input + and returns a postprocessed row (dict). + preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + preprocess stage (e.g., num_cpus, memory, concurrency). + postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + postprocess stage (e.g., num_cpus, memory, concurrency). + """ + + # The internal used data column name ("__data"). Your input + # dataset should not contain this column. If you want to use this column + # in your input dataset, you have to derive and customize Processor. + DATA_COLUMN: str = "__data" + + def __init__( + self, + config: ProcessorConfig, + stages: List[StatefulStage], + preprocess: Optional[UserDefinedFunction] = None, + postprocess: Optional[UserDefinedFunction] = None, + preprocess_map_kwargs: Optional[Dict[str, Any]] = None, + postprocess_map_kwargs: Optional[Dict[str, Any]] = None, + ): + self.config = config + self.preprocess = None + self.postprocess = None + self.preprocess_map_kwargs = preprocess_map_kwargs or {} + self.postprocess_map_kwargs = postprocess_map_kwargs or {} + self.stages: OrderedDict[str, StatefulStage] = OrderedDict() + + # FIXES: https://github.com/ray-project/ray/issues/53124 + # TODO (Kourosh): Remove this once the issue is fixed + data_context = ray.data.DataContext.get_current() + data_context.wait_for_min_actors_s = 600 + # TODO: Remove this when https://github.com/ray-project/ray/issues/53169 + # is fixed. + data_context._enable_actor_pool_on_exit_hook = True + + # NOTE (Kourosh): If pre/postprocess is not provided, use the identity function. + # Wrapping is required even if they are identity functions, b/c data_column + # gets inserted/removed via wrap_preprocess/wrap_postprocess. + preprocess = preprocess or (lambda row: row) + postprocess = postprocess or (lambda row: row) + + self.preprocess = wrap_preprocess( + preprocess, + self.DATA_COLUMN, + ) + + self.postprocess = wrap_postprocess( + postprocess, + self.DATA_COLUMN, + ) + + for stage in stages: + self._append_stage(stage) + + def __call__(self, dataset: Dataset) -> Dataset: + """Execute the processor: + preprocess -> stages -> postprocess. + Note that the dataset won't be materialized during the execution. + + Args: + dataset: The input dataset. + + Returns: + The output dataset. + """ + if self.preprocess is not None: + dataset = dataset.map(self.preprocess, **self.preprocess_map_kwargs) + + # Apply stages. + for stage in self.stages.values(): + kwargs = stage.get_dataset_map_batches_kwargs( + batch_size=self.config.batch_size, + data_column=self.DATA_COLUMN, + ) + dataset = dataset.map_batches(stage.fn, **kwargs) + + if self.postprocess is not None: + dataset = dataset.map(self.postprocess, **self.postprocess_map_kwargs) + return dataset + + def _append_stage(self, stage: StatefulStage) -> None: + """Append a stage before postprocess. The stage class name will be used as + the stage name. If there are multiple stages with the same type, a suffix + will be added to the stage name to avoid conflicts. + + Args: + stage: The stage to append. + """ + stage_name = type(stage).__name__ + + # When a processor has multiple stages with the same type, + # append a index suffix to the stage name to avoid conflicts. + if stage_name in self.stages: + num_same_type_stage = len([s for s in self.stages.values() if s is stage]) + stage_name = f"{stage_name}_{num_same_type_stage + 1}" + self.stages[stage_name] = stage + + def list_stage_names(self) -> List[str]: + """List the stage names of this processor in order. Preprocess and postprocess + are not included. + + Returns: + A list of stage names. + """ + return list(self.stages.keys()) + + def get_stage_by_name(self, name: str) -> StatefulStage: + """Get a particular stage by its name. If the stage is not found, + a ValueError will be raised. + + Args: + name: The stage name. + + Returns: + The pipeline stage. + """ + if name in self.stages: + return self.stages[name] + raise ValueError(f"Stage {name} not found") + + def log_input_column_names(self): + """Log.info the input stage and column names of this processor. + If the input dataset does not contain these columns, you have to + provide a preprocess function to bridge the gap. + """ + name, stage = list(self.stages.items())[0] + expected_input_keys = stage.get_required_input_keys() + optional_input_keys = stage.get_optional_input_keys() + + message = f"The first stage of the processor is {name}." + if expected_input_keys: + message += "\nRequired input columns:\n" + message += "\n".join(f"\t{k}: {v}" for k, v in expected_input_keys.items()) + if optional_input_keys: + message += "\nOptional input columns:\n" + message += "\n".join(f"\t{k}: {v}" for k, v in optional_input_keys.items()) + + logger.info(message) + + +@DeveloperAPI +class ProcessorBuilder: + """Build a processor based on the configuration.""" + + _registry: Dict[str, Callable] = {} + + @classmethod + def register(cls, config_type: Type[ProcessorConfig], builder: Callable) -> None: + """A decorator to associate a particular pipeline config + with its build function. + """ + type_name = config_type.__name__ + if type_name in cls._registry: + raise ValueError(f"Processor config type {type_name} already registered.") + cls._registry[type_name] = builder + + @classmethod + def clear_registry(cls) -> None: + """Clear the processor builder registry.""" + cls._registry.clear() + + @classmethod + def validate_builder_kwargs(cls, builder_kwargs: Optional[Dict[str, Any]]) -> None: + """Validate builder kwargs for conflicts with reserved keys. + + Args: + builder_kwargs: Optional additional kwargs to pass to the processor builder + function. + + Raises: + ValueError: If builder_kwargs contains reserved keys that conflict with + explicit arguments. + """ + if builder_kwargs is not None: + # Check for conflicts with explicitly passed arguments + reserved_keys = { + "preprocess", + "postprocess", + "preprocess_map_kwargs", + "postprocess_map_kwargs", + } + conflicting_keys = reserved_keys & builder_kwargs.keys() + if conflicting_keys: + raise ValueError( + f"builder_kwargs cannot contain {conflicting_keys} as these are " + "passed as explicit arguments to build_llm_processor. " + "Please pass these directly instead of in builder_kwargs." + ) + + @classmethod + def build( + cls, + config: ProcessorConfig, + override_stage_config_fn: Optional[Callable] = None, + **kwargs, + ) -> Processor: + """Build a processor. + + Args: + config: The processor config. + override_stage_config_fn: Custom stages configurations. + **kwargs: Additional keyword arguments to pass through to the + registered builder function. The builder function must accept + these kwargs in its signature, otherwise a TypeError will be raised. + + Returns: + The built processor. + """ + type_name = type(config).__name__ + if type_name not in cls._registry: + raise ValueError( + f"Processor config type {type_name} not registered. " + f"Available types: {cls._registry.keys()}" + ) + processor = cls._registry[type_name](config, **kwargs) + if override_stage_config_fn is not None: + for name, stage in processor.stages.items(): + override_stage_config_fn(name, stage) + return processor diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/http_request_proc.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/http_request_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..c48ccfb2cd068f63a1f7b04dcd8993401a0f0162 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/http_request_proc.py @@ -0,0 +1,115 @@ +"""The HTTP request processor.""" + +from typing import Any, Dict, Optional + +from pydantic import Field + +from ray.data.block import UserDefinedFunction +from ray.llm._internal.batch.observability.usage_telemetry.usage import ( + BatchModelTelemetry, + get_or_create_telemetry_agent, +) +from ray.llm._internal.batch.processor.base import ( + Processor, + ProcessorBuilder, + ProcessorConfig, +) +from ray.llm._internal.batch.stages import HttpRequestStage + + +class HttpRequestProcessorConfig(ProcessorConfig): + """The configuration for the HTTP request processor.""" + + batch_size: int = Field( + default=64, + description="The batch size.", + ) + url: str = Field( + description="The URL to query.", + ) + headers: Optional[Dict[str, Any]] = Field( + default=None, + description="The query header. Note that we will add " + "'Content-Type: application/json' to be the header for sure " + "because we only deal with requests body in JSON.", + ) + qps: Optional[int] = Field( + default=None, + description="The maximum number of requests per second to avoid rate limit. " + "If None, the request will be sent sequentially.", + ) + max_retries: int = Field( + default=0, + description="The maximum number of retries per request in the event of failures.", + ) + base_retry_wait_time_in_s: float = Field( + default=1, + description="The base wait time for a retry during exponential backoff.", + ) + # Since `session_factory` is a callable, we use type Any to avoid pydantic serialization issues + session_factory: Optional[Any] = Field( + default=None, + description="Optional session factory to be used for initializing a client session. Type: Callable[[], ClientSession]", + # exclude from JSON serialization since `session_factory` is a callable + exclude=True, + ) + + +def build_http_request_processor( + config: HttpRequestProcessorConfig, + preprocess: Optional[UserDefinedFunction] = None, + postprocess: Optional[UserDefinedFunction] = None, + preprocess_map_kwargs: Optional[Dict[str, Any]] = None, + postprocess_map_kwargs: Optional[Dict[str, Any]] = None, +) -> Processor: + """Construct a Processor and configure stages. + + Args: + config: The configuration for the processor. + preprocess: An optional lambda function that takes a row (dict) as input + and returns a preprocessed row (dict). The output row must contain the + required fields for the following processing stages. + postprocess: An optional lambda function that takes a row (dict) as input + and returns a postprocessed row (dict). + preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + preprocess stage (e.g., num_cpus, memory, concurrency). + postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + postprocess stage (e.g., num_cpus, memory, concurrency). + + Returns: + The constructed processor. + """ + stages = [ + HttpRequestStage( + fn_constructor_kwargs=dict( + url=config.url, + additional_header=config.headers, + qps=config.qps, + max_retries=config.max_retries, + base_retry_wait_time_in_s=config.base_retry_wait_time_in_s, + session_factory=config.session_factory, + ), + map_batches_kwargs=dict( + concurrency=config.concurrency, + ), + ) + ] + telemetry_agent = get_or_create_telemetry_agent() + telemetry_agent.push_telemetry_report( + BatchModelTelemetry( + processor_config_name=type(config).__name__, + concurrency=config.concurrency, + ) + ) + processor = Processor( + config, + stages, + preprocess=preprocess, + postprocess=postprocess, + preprocess_map_kwargs=preprocess_map_kwargs, + postprocess_map_kwargs=postprocess_map_kwargs, + ) + return processor + + +ProcessorBuilder.register(HttpRequestProcessorConfig, build_http_request_processor) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/serve_deployment_proc.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/serve_deployment_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..78ccd084f4d65eb48ce2a43c25abe7ec96e6262c --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/serve_deployment_proc.py @@ -0,0 +1,85 @@ +"""The processor that runs serve deployment.""" + +from typing import Any, Dict, Optional, Type + +from pydantic import Field + +from ray.data.block import UserDefinedFunction +from ray.llm._internal.batch.processor.base import ( + Processor, + ProcessorBuilder, + ProcessorConfig, +) +from ray.llm._internal.batch.stages import ( + ServeDeploymentStage, +) + + +class ServeDeploymentProcessorConfig(ProcessorConfig): + """The configuration for the serve deployment processor.""" + + # Configurations used to build the serve deployment + deployment_name: str = Field( + description="The name of the serve deployment to use.", + ) + app_name: str = Field( + description="The name of the serve application to use.", + default="default", + ) + dtype_mapping: Dict[str, Type[Any]] = Field( + description="A dictionary mapping data type names to their corresponding request classes for the serve deployment.", + default=None, + ) + + +def build_serve_deployment_processor( + config: ServeDeploymentProcessorConfig, + preprocess: Optional[UserDefinedFunction] = None, + postprocess: Optional[UserDefinedFunction] = None, + preprocess_map_kwargs: Optional[Dict[str, Any]] = None, + postprocess_map_kwargs: Optional[Dict[str, Any]] = None, +) -> Processor: + """Construct a processor that runs a serve deployment. + + Args: + config: The configuration for the processor. + preprocess: An optional lambda function that takes a row (dict) as input + and returns a preprocessed row (dict). The output row must contain the + required fields for the following processing stages. + postprocess: An optional lambda function that takes a row (dict) as input + and returns a postprocessed row (dict). + preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + preprocess stage (e.g., num_cpus, memory, concurrency). + postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + postprocess stage (e.g., num_cpus, memory, concurrency). + + Returns: + The constructed processor. + """ + stages = [ + ServeDeploymentStage( + fn_constructor_kwargs=dict( + deployment_name=config.deployment_name, + app_name=config.app_name, + dtype_mapping=config.dtype_mapping, + ), + map_batches_kwargs=dict( + concurrency=config.concurrency, + ), + ) + ] + # TODO (Kourosh): Add telemetry for ServeDeploymentStage + processor = Processor( + config, + stages, + preprocess=preprocess, + postprocess=postprocess, + preprocess_map_kwargs=preprocess_map_kwargs, + postprocess_map_kwargs=postprocess_map_kwargs, + ) + return processor + + +ProcessorBuilder.register( + ServeDeploymentProcessorConfig, build_serve_deployment_processor +) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/sglang_engine_proc.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/sglang_engine_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..c7b0aa46e1515bd78944be86f5067ea57170be68 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/sglang_engine_proc.py @@ -0,0 +1,195 @@ +"""The SGLang engine processor.""" + +from typing import Any, Dict, Optional + +import transformers +from pydantic import Field, root_validator + +import ray +from ray.data.block import UserDefinedFunction +from ray.llm._internal.batch.observability.usage_telemetry.usage import ( + BatchModelTelemetry, + TelemetryAgent, + get_or_create_telemetry_agent, +) +from ray.llm._internal.batch.processor.base import ( + DEFAULT_MAX_TASKS_IN_FLIGHT, + OfflineProcessorConfig, + Processor, + ProcessorBuilder, +) +from ray.llm._internal.batch.stages import ( + ChatTemplateStage, + DetokenizeStage, + SGLangEngineStage, + TokenizeStage, +) +from ray.llm._internal.batch.stages.sglang_engine_stage import SGLangTaskType +from ray.llm._internal.common.observability.telemetry_utils import DEFAULT_GPU_TYPE + +DEFAULT_MODEL_ARCHITECTURE = "UNKNOWN_MODEL_ARCHITECTURE" + + +class SGLangEngineProcessorConfig(OfflineProcessorConfig): + """The configuration for the SGLang engine processor.""" + + # SGLang stage configurations. + engine_kwargs: Dict[str, Any] = Field( + default_factory=dict, + description="The kwargs to pass to the SGLang engine. See " + "https://docs.sglang.ai/backend/server_arguments.html " + "for more details.", + ) + task_type: SGLangTaskType = Field( + default=SGLangTaskType.GENERATE, + description="The task type to use. If not specified, will use " + "'generate' by default.", + ) + + @root_validator(pre=True) + def validate_task_type(cls, values): + task_type_str = values.get("task_type", "generate") + values["task_type"] = SGLangTaskType(task_type_str) + return values + + +def build_sglang_engine_processor( + config: SGLangEngineProcessorConfig, + chat_template_kwargs: Optional[Dict[str, Any]] = None, + preprocess: Optional[UserDefinedFunction] = None, + postprocess: Optional[UserDefinedFunction] = None, + preprocess_map_kwargs: Optional[Dict[str, Any]] = None, + postprocess_map_kwargs: Optional[Dict[str, Any]] = None, + telemetry_agent: Optional[TelemetryAgent] = None, +) -> Processor: + """Construct a Processor and configure stages. + + Args: + config: The configuration for the processor. + chat_template_kwargs: The optional kwargs to pass to apply_chat_template. + preprocess: An optional lambda function that takes a row (dict) as input + and returns a preprocessed row (dict). The output row must contain the + required fields for the following processing stages. + postprocess: An optional lambda function that takes a row (dict) as input + and returns a postprocessed row (dict). + preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + preprocess stage (e.g., num_cpus, memory, concurrency). + postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + postprocess stage (e.g., num_cpus, memory, concurrency). + telemetry_agent: An optional telemetry agent for collecting usage telemetry. + + Returns: + The constructed processor. + """ + ray.init(runtime_env=config.runtime_env, ignore_reinit_error=True) + + stages = [] + + if config.apply_chat_template: + stages.append( + ChatTemplateStage( + fn_constructor_kwargs=dict( + model=config.model_source, + chat_template=config.chat_template, + chat_template_kwargs=chat_template_kwargs, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + if config.tokenize: + stages.append( + TokenizeStage( + fn_constructor_kwargs=dict( + model=config.model_source, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + # Core stage -- the SGLang engine. + stages.append( + SGLangEngineStage( + fn_constructor_kwargs=dict( + model=config.model_source, + engine_kwargs=config.engine_kwargs, + task_type=config.task_type, + max_pending_requests=config.max_pending_requests, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + # The number of running replicas. This is a deprecated field, but + # we need to set `max_tasks_in_flight_per_actor` through `compute`, + # which initiates enough many overlapping UDF calls per actor, to + # saturate `max_concurrency`. + compute=ray.data.ActorPoolStrategy( + min_size=config.get_concurrency(autoscaling_enabled=False)[0], + max_size=config.get_concurrency(autoscaling_enabled=False)[1], + max_tasks_in_flight_per_actor=config.experimental.get( + "max_tasks_in_flight_per_actor", DEFAULT_MAX_TASKS_IN_FLIGHT + ), + ), + # The number of running batches "per actor" in Ray Core level. + # This is used to make sure we overlap batches to avoid the tail + # latency of each batch. + max_concurrency=config.max_concurrent_batches, + accelerator_type=config.accelerator_type, + runtime_env=config.runtime_env, + ), + ) + ) + + if config.detokenize: + stages.append( + DetokenizeStage( + fn_constructor_kwargs=dict( + model=config.model_source, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + hf_config = transformers.AutoConfig.from_pretrained(config.model_source) + architecture = getattr(hf_config, "architectures", [DEFAULT_MODEL_ARCHITECTURE])[0] + + telemetry_agent = get_or_create_telemetry_agent() + telemetry_agent.push_telemetry_report( + BatchModelTelemetry( + processor_config_name=type(config).__name__, + model_architecture=architecture, + batch_size=config.batch_size, + accelerator_type=config.accelerator_type or DEFAULT_GPU_TYPE, + concurrency=config.concurrency, + task_type=SGLangTaskType(config.task_type), + tensor_parallel_size=config.engine_kwargs.get("tp_size", 1), + data_parallel_size=config.engine_kwargs.get("dp_size", 1), + ) + ) + + processor = Processor( + config, + stages, + preprocess=preprocess, + postprocess=postprocess, + preprocess_map_kwargs=preprocess_map_kwargs, + postprocess_map_kwargs=postprocess_map_kwargs, + ) + return processor + + +ProcessorBuilder.register(SGLangEngineProcessorConfig, build_sglang_engine_processor) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/vllm_engine_proc.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/vllm_engine_proc.py new file mode 100644 index 0000000000000000000000000000000000000000..c4316f7cf98b999ea9bba282c9bdc7064197feb5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/processor/vllm_engine_proc.py @@ -0,0 +1,277 @@ +"""The vLLM engine processor.""" + +from typing import Any, Dict, List, Literal, Optional + +import transformers +from pydantic import ConfigDict, Field, root_validator + +import ray +from ray.data.block import UserDefinedFunction +from ray.llm._internal.batch.observability.usage_telemetry.usage import ( + BatchModelTelemetry, + TelemetryAgent, + get_or_create_telemetry_agent, +) +from ray.llm._internal.batch.processor.base import ( + DEFAULT_MAX_TASKS_IN_FLIGHT, + OfflineProcessorConfig, + Processor, + ProcessorBuilder, +) +from ray.llm._internal.batch.stages import ( + ChatTemplateStage, + DetokenizeStage, + PrepareImageStage, + TokenizeStage, + vLLMEngineStage, +) +from ray.llm._internal.batch.stages.vllm_engine_stage import vLLMTaskType +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.llm._internal.common.observability.telemetry_utils import DEFAULT_GPU_TYPE +from ray.llm._internal.common.utils.download_utils import ( + STREAMING_LOAD_FORMATS, + NodeModelDownloadable, + download_model_files, +) + +DEFAULT_MODEL_ARCHITECTURE = "UNKNOWN_MODEL_ARCHITECTURE" + + +class BundleSchema(BaseModelExtended): + model_config = ConfigDict(extra="allow") + CPU: Optional[int] = Field(default=1, description="The number of CPUs per bundle.") + GPU: Optional[int] = Field(default=1, description="The number of GPUs per bundle.") + + +class PlacementGroupSchema(BaseModelExtended): + bundles: List[BundleSchema] = Field( + default_factory=list, description="The bundles for the placement group." + ) + strategy: Literal["PACK", "STRICT_PACK", "SPREAD", "STRICT_SPREAD"] = Field( + default="PACK", description="The strategy for the placement group." + ) + + +class vLLMEngineProcessorConfig(OfflineProcessorConfig): + """The configuration for the vLLM engine processor.""" + + # vLLM stage configurations. + engine_kwargs: Dict[str, Any] = Field( + default_factory=dict, + description="The kwargs to pass to the vLLM engine. See " + "https://docs.vllm.ai/en/latest/serving/engine_args.html " + "for more details.", + ) + task_type: vLLMTaskType = Field( + default=vLLMTaskType.GENERATE, + description="The task type to use. If not specified, will use " + "'generate' by default.", + ) + # LoRA configurations. + dynamic_lora_loading_path: Optional[str] = Field( + default=None, + description="The path to the dynamic LoRA adapter. It is expected " + "to hold subfolders each for a different lora checkpoint. If not " + "specified and LoRA is enabled, then the 'model' in LoRA " + "requests will be interpreted as model ID used by HF transformers.", + ) + # Custom placement group config for TP/PP. + placement_group_config: Optional[Dict[str, Any]] = Field( + default=None, + description="Ray placement group configuration for scheduling vLLM engine workers. " + "Should be a dictionary with 'bundles' (list of resource dicts, e.g., {'CPU': 1, 'GPU': 1}) " + "and an optional 'strategy' key ('PACK', 'STRICT_PACK', 'SPREAD', or 'STRICT_SPREAD'). " + "For ray distributed executor backend, each bundle must specify at most one GPU. " + "For mp backend, the 'strategy' field is ignored.", + ) + + @root_validator(pre=True) + def validate_task_type(cls, values): + task_type_str = values.get("task_type", "generate") + values["task_type"] = vLLMTaskType(task_type_str) + return values + + @root_validator(pre=True) + def validate_placement_group_config(cls, values): + placement_group_config = values.get("placement_group_config") + if placement_group_config is not None: + values["placement_group_config"] = PlacementGroupSchema( + **placement_group_config + ).model_dump() + return values + + +def build_vllm_engine_processor( + config: vLLMEngineProcessorConfig, + chat_template_kwargs: Optional[Dict[str, Any]] = None, + preprocess: Optional[UserDefinedFunction] = None, + postprocess: Optional[UserDefinedFunction] = None, + preprocess_map_kwargs: Optional[Dict[str, Any]] = None, + postprocess_map_kwargs: Optional[Dict[str, Any]] = None, + telemetry_agent: Optional[TelemetryAgent] = None, +) -> Processor: + """Construct a Processor and configure stages. + + Args: + config: The configuration for the processor. + chat_template_kwargs: The optional kwargs to pass to apply_chat_template. + preprocess: An optional lambda function that takes a row (dict) as input + and returns a preprocessed row (dict). The output row must contain the + required fields for the following processing stages. + postprocess: An optional lambda function that takes a row (dict) as input + and returns a postprocessed row (dict). + preprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + preprocess stage (e.g., num_cpus, memory, concurrency). + postprocess_map_kwargs: Optional kwargs to pass to Dataset.map() for the + postprocess stage (e.g., num_cpus, memory, concurrency). + telemetry_agent: An optional telemetry agent for collecting usage telemetry. + + Returns: + The constructed processor. + """ + ray.init(runtime_env=config.runtime_env, ignore_reinit_error=True) + + stages = [] + + if config.has_image: + stages.append( + PrepareImageStage( + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + ), + ) + ) + if config.apply_chat_template: + stages.append( + ChatTemplateStage( + fn_constructor_kwargs=dict( + model=config.model_source, + chat_template=config.chat_template, + chat_template_kwargs=chat_template_kwargs, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + if config.tokenize: + stages.append( + TokenizeStage( + fn_constructor_kwargs=dict( + model=config.model_source, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + # Core stage -- the vLLM engine. + + stages.append( + vLLMEngineStage( + fn_constructor_kwargs=dict( + batch_size=config.batch_size, + max_concurrent_batches=config.max_concurrent_batches, + model=config.model_source, + engine_kwargs=config.engine_kwargs, + task_type=config.task_type, + max_pending_requests=config.max_pending_requests, + dynamic_lora_loading_path=config.dynamic_lora_loading_path, + placement_group_config=config.placement_group_config, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + # The number of running replicas. This is a deprecated field, but + # we need to set `max_tasks_in_flight_per_actor` through `compute`, + # which initiates enough many overlapping UDF calls per actor, to + # saturate `max_concurrency`. + compute=ray.data.ActorPoolStrategy( + min_size=config.get_concurrency(autoscaling_enabled=False)[0], + max_size=config.get_concurrency(autoscaling_enabled=False)[1], + max_tasks_in_flight_per_actor=config.experimental.get( + "max_tasks_in_flight_per_actor", DEFAULT_MAX_TASKS_IN_FLIGHT + ), + ), + # The number of running batches "per actor" in Ray Core level. + # This is used to make sure we overlap batches to avoid the tail + # latency of each batch. + max_concurrency=config.max_concurrent_batches, + accelerator_type=config.accelerator_type, + runtime_env=config.runtime_env, + ), + ) + ) + + if config.detokenize: + stages.append( + DetokenizeStage( + fn_constructor_kwargs=dict( + model=config.model_source, + ), + map_batches_kwargs=dict( + zero_copy_batch=True, + concurrency=config.get_concurrency(), + batch_size=config.batch_size, + runtime_env=config.runtime_env, + ), + ) + ) + + # We download the config files here so that we can report the underlying architecture to the telemetry system. + # This should be a lightweight operation. + if config.engine_kwargs.get("load_format", None) in STREAMING_LOAD_FORMATS: + download_model_mode = NodeModelDownloadable.EXCLUDE_SAFETENSORS + else: + download_model_mode = NodeModelDownloadable.TOKENIZER_ONLY + model_path = download_model_files( + model_id=config.model_source, + mirror_config=None, + download_model=download_model_mode, + download_extra_files=False, + ) + hf_config = transformers.AutoConfig.from_pretrained( + model_path, + trust_remote_code=config.engine_kwargs.get("trust_remote_code", False), + ) + + architectures = getattr(hf_config, "architectures", []) + architecture = architectures[0] if architectures else DEFAULT_MODEL_ARCHITECTURE + + telemetry_agent = get_or_create_telemetry_agent() + telemetry_agent.push_telemetry_report( + BatchModelTelemetry( + processor_config_name=type(config).__name__, + model_architecture=architecture, + batch_size=config.batch_size, + accelerator_type=config.accelerator_type or DEFAULT_GPU_TYPE, + concurrency=config.concurrency, + task_type=vLLMTaskType(config.task_type), + pipeline_parallel_size=config.engine_kwargs.get( + "pipeline_parallel_size", 1 + ), + tensor_parallel_size=config.engine_kwargs.get("tensor_parallel_size", 1), + ) + ) + + processor = Processor( + config, + stages, + preprocess=preprocess, + postprocess=postprocess, + preprocess_map_kwargs=preprocess_map_kwargs, + postprocess_map_kwargs=postprocess_map_kwargs, + ) + return processor + + +ProcessorBuilder.register(vLLMEngineProcessorConfig, build_vllm_engine_processor) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a45d21fc76707b77ed2b16377e3ff59c721fef5d --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__init__.py @@ -0,0 +1,26 @@ +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + wrap_postprocess, + wrap_preprocess, +) +from ray.llm._internal.batch.stages.chat_template_stage import ChatTemplateStage +from ray.llm._internal.batch.stages.http_request_stage import HttpRequestStage +from ray.llm._internal.batch.stages.prepare_image_stage import PrepareImageStage +from ray.llm._internal.batch.stages.serve_deployment_stage import ServeDeploymentStage +from ray.llm._internal.batch.stages.sglang_engine_stage import SGLangEngineStage +from ray.llm._internal.batch.stages.tokenize_stage import DetokenizeStage, TokenizeStage +from ray.llm._internal.batch.stages.vllm_engine_stage import vLLMEngineStage + +__all__ = [ + "StatefulStage", + "HttpRequestStage", + "ChatTemplateStage", + "TokenizeStage", + "DetokenizeStage", + "vLLMEngineStage", + "SGLangEngineStage", + "ServeDeploymentStage", + "wrap_preprocess", + "wrap_postprocess", + "PrepareImageStage", +] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c8f59d9ff0289a6e482270104198521f469a7e4 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/base.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77c38387bcee964c3f5828e07788c31ea1e0652c Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/base.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/chat_template_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/chat_template_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..658c31ab13030362714fb74bc524847e1bfbd2f2 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/chat_template_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/common.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a8a5523067fad6f7cba7292011d23b3b58091f0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/common.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/http_request_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/http_request_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0879dc9ebc0d72dc84697d6431f47a8660339f69 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/http_request_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/prepare_image_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/prepare_image_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05d5244b7c6f851dbbd6ff06cd407abeda31ee69 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/prepare_image_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/serve_deployment_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/serve_deployment_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db900a923c7b1935aafc2ed12d22bd59470b7a64 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/serve_deployment_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/sglang_engine_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/sglang_engine_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f88d575758c4decb1392e1eaf1066541ce73f919 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/sglang_engine_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/tokenize_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/tokenize_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..866eebc220fbb17946d8b758277d1a318676c3c8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/tokenize_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/vllm_engine_stage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/vllm_engine_stage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f76b506b21dbe8a2e98a1b4a4baa286eb53e6b28 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/__pycache__/vllm_engine_stage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/base.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/base.py new file mode 100644 index 0000000000000000000000000000000000000000..8e57f173883892223a647a2200ef83bf73c52e90 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/base.py @@ -0,0 +1,287 @@ +"""The base class for all stages.""" +import logging +from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Type + +import pyarrow +from pydantic import BaseModel, Field + +from ray.data.block import UserDefinedFunction + +logger = logging.getLogger(__name__) + + +def wrap_preprocess( + fn: UserDefinedFunction, + processor_data_column: str, +) -> Callable: + """Wrap the preprocess function, so that the output schema of the + preprocess is normalized to {processor_data_column: fn(row), other input columns}. + + Args: + fn: The function to be applied. + processor_data_column: The internal data column name of the processor. + + Returns: + The wrapped function. + """ + + def _preprocess(row: dict[str, Any]) -> dict[str, Any]: + # First put everything into processor_data_column. + outputs = {processor_data_column: row} + + # Then apply the preprocess function and add its outputs. + preprocess_output = fn(row) + outputs[processor_data_column].update(preprocess_output) + return outputs + + return _preprocess + + +def wrap_postprocess( + fn: UserDefinedFunction, + processor_data_column: str, +) -> Callable: + """Wrap the postprocess function to remove the processor_data_column. + Note that we fully rely on users to determine which columns to carry over. + + Args: + fn: The function to be applied. + processor_data_column: The internal data column name of the processor. + + Returns: + The wrapped function. + """ + + def _postprocess(row: dict[str, Any]) -> dict[str, Any]: + if processor_data_column not in row: + raise ValueError( + f"[Internal] {processor_data_column} not found in row {row}" + ) + + return fn(row[processor_data_column]) + + return _postprocess + + +class StatefulStageUDF: + """A stage UDF wrapper that processes the input and output columns + before and after the UDF. + + Args: + data_column: The internal data column name of the processor. The + __call__ method takes the data column as the input of the UDF + method, and encapsulates the output of the UDF method into the data + column for the next stage. + expected_input_keys: The expected input keys of the stage. + """ + + # The internal column name for the index of the row in the batch. + # This is used to align the output of the UDF with the input batch. + IDX_IN_BATCH_COLUMN: str = "__idx_in_batch" + + def __init__( + self, data_column: str, expected_input_keys: Optional[List[str]] = None + ): + self.data_column = data_column + self.expected_input_keys = set(expected_input_keys or []) + + async def __call__(self, batch: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]: + """A stage UDF wrapper that processes the input and output columns + before and after the UDF. The expected schema of "batch" is: + {data_column: { + dataset columns, + other intermediate columns + }, + ...other metadata columns..., + }. + + The input of the UDF will then [dataset columns and other intermediate columns]. + In addition, while the output of the UDF depends on the UDF implementation, + the output schema is expected to be + {data_column: { + dataset columns, + other intermediate columns, + UDF output columns (will override above columns if they have the same name) + }, + ...other metadata columns..., + }. + And this will become the input of the next stage. + + Examples: + Input dataset columns: {A, B, C} + Preprocess: (lambda row: {"D": row["A"] + 1}) + Input: + UDF input: {A, B, C} + UDF output: {D} + Output: {__data: {A, B, C, D}} + Stage 1: + Input: {__data: {A, B, C, D}} + UDF input: {A, B, C, D} + UDF output: {E} + Output: {__data: {A, B, C, D, E}} + Stage 2: + Input: {__data: {A, B, C, D, E}} + UDF input: {A, B, C, D, E} + UDF output: {F, E} # E is in-place updated. + Output: {__data: {A, B, C, D, E, F}} + Postprocess: (lambda row: {"G": row["F"], "A": row["A"], "E": row["E"]}) + Input: {__data: {A, B, C, D, E, F}} + UDF input: {A, B, C, D, E, F} + UDF output: {G, A, E} + Output: {G, A, E} # User chooses to keep G, A, E. + + Args: + batch: The input batch. + + Returns: + An async iterator of the outputs. + """ + # Handle the case where the batch is empty. + # FIXME: This should not happen. + if isinstance(batch, pyarrow.lib.Table) and batch.num_rows == 0: + yield {} + return + + if self.data_column not in batch: + raise ValueError( + f"[Internal] {self.data_column} not found in batch {batch}" + ) + + inputs = batch.pop(self.data_column) + if hasattr(inputs, "tolist"): + inputs = inputs.tolist() + self.validate_inputs(inputs) + + # Assign the index of the row in the batch to the idx_in_batch_column. + # This is beacuse the UDF output may be out-of-order (if asyncio.as_completed + # is used interanlly for example), and we need to carry over unused input + # columns to the next stage. Thus, we use the row index in batch to match + # the output of the UDF with the input. + for idx, row in enumerate(inputs): + row[self.IDX_IN_BATCH_COLUMN] = idx + + # Collect all outputs first, then return them in the original order + # This is a requirement set by https://github.com/ray-project/ray/pull/54190/ + not_outputed_rows = set(range(len(inputs))) + async for output in self.udf(inputs): + if self.IDX_IN_BATCH_COLUMN not in output: + raise ValueError( + "The output of the UDF must contain the column " + f"{self.IDX_IN_BATCH_COLUMN}." + ) + idx_in_batch = output.pop(self.IDX_IN_BATCH_COLUMN) + if idx_in_batch not in not_outputed_rows: + raise ValueError( + f"The row {idx_in_batch} is outputed twice. " + "This is likely due to the UDF is not one-to-one." + ) + not_outputed_rows.remove(idx_in_batch) + + # Add stage outputs to the data column of the row. + inputs[idx_in_batch].pop(self.IDX_IN_BATCH_COLUMN) + inputs[idx_in_batch].update(output) + + if not_outputed_rows: + raise ValueError(f"The rows {not_outputed_rows} are not outputed.") + + # Return all updated inputs in the original order + yield {self.data_column: inputs} + + def validate_inputs(self, inputs: List[Dict[str, Any]]): + """Validate the inputs to make sure the required keys are present. + + Args: + inputs: The inputs. + + Raises: + ValueError: If the required keys are not found. + """ + for inp in inputs: + input_keys = set(inp.keys()) + + if self.IDX_IN_BATCH_COLUMN in input_keys: + raise ValueError( + f"The input column {self.IDX_IN_BATCH_COLUMN} is reserved " + "for internal use." + ) + + if not self.expected_input_keys: + continue + + missing_required = self.expected_input_keys - input_keys + if missing_required: + raise ValueError( + f"Required input keys {missing_required} not found at the input of " + f"{self.__class__.__name__}. Input keys: {input_keys}" + ) + + async def udf(self, rows: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + raise NotImplementedError("StageUDF must implement the udf method") + + +class StatefulStage(BaseModel): + """ + A basic building block to compose a Processor. + """ + + fn: Type[StatefulStageUDF] = Field( + description="The well-optimized stateful UDF for this stage." + ) + fn_constructor_kwargs: Dict[str, Any] = Field( + default_factory=dict, + description="The keyword arguments of the UDF constructor.", + ) + map_batches_kwargs: Dict[str, Any] = Field( + default_factory=lambda: {"concurrency": 1}, + description="The arguments of .map_batches(). Default {'concurrency': 1}.", + ) + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return {} + + def get_optional_input_keys(self) -> Dict[str, str]: + """The optional input keys of the stage and their descriptions.""" + return {} + + def get_dataset_map_batches_kwargs( + self, + batch_size: int, + data_column: str, + ) -> Dict[str, Any]: + """We separate fn and fn_constructor_kwargs in Stage for better UX, + so we combine them with other map_batches_kwargs together in this method. + + Args: + batch_size: The batch size set by the processor config. + data_column: The data column name set by the processor. + + Returns: + The dataset map_batches kwargs. + """ + kwargs = self.map_batches_kwargs.copy() + batch_size_in_kwargs = kwargs.get("batch_size", batch_size) + if batch_size_in_kwargs != batch_size: + logger.warning( + "batch_size is set to %d in map_batches_kwargs, but it will be " + "overridden by the batch size configured by the processor %d.", + batch_size_in_kwargs, + batch_size, + ) + kwargs["batch_size"] = batch_size + + kwargs.update({"fn_constructor_kwargs": self.fn_constructor_kwargs.copy()}) + if "data_column" in kwargs["fn_constructor_kwargs"]: + raise ValueError( + "'data_column' cannot be used as in fn_constructor_kwargs." + ) + + kwargs["fn_constructor_kwargs"]["data_column"] = data_column + kwargs["fn_constructor_kwargs"]["expected_input_keys"] = list( + self.get_required_input_keys().keys() + ) + return kwargs + + class Config: + arbitrary_types_allowed = True + validate_assignment = True diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/chat_template_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/chat_template_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..92d453dbb10336708077d0f2956e90e34873e3a9 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/chat_template_stage.py @@ -0,0 +1,130 @@ +"""Apply chat template stage""" + +from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional, Type, Union + +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) +from ray.llm._internal.common.utils.download_utils import ( + NodeModelDownloadable, + download_model_files, +) + + +class ChatTemplateUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + model: str, + chat_template: Optional[str] = None, + chat_template_kwargs: Optional[Dict[str, Any]] = None, + ): + """ + Initialize the ChatTemplateUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + model: The model to use for the chat template. + chat_template: The chat template in Jinja template format. This is + usually not needed if the model checkpoint already contains the + chat template. + chat_template_kwargs: The optional kwargs to pass apply_chat_template. + """ + from transformers import AutoProcessor + + super().__init__(data_column, expected_input_keys) + + # NOTE: We always use processor instead of tokenizer in this stage, + # because tokenizers of VLM models may not have chat template attribute. + # However, this may not be a reliable solution, because processors and + # tokenizers are not standardized across different models. + model_path = download_model_files( + model_id=model, + mirror_config=None, + download_model=NodeModelDownloadable.TOKENIZER_ONLY, + download_extra_files=False, + ) + if TYPE_CHECKING: + from transformers.processing_utils import ProcessorMixin + from transformers.tokenization_utils_base import PreTrainedTokenizerBase + + self.processor: Union[ + "PreTrainedTokenizerBase", "ProcessorMixin" + ] = AutoProcessor.from_pretrained(model_path, trust_remote_code=True) + self.chat_template = chat_template + self.chat_template_kwargs = chat_template_kwargs + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """ + Apply chat template to the given batch. + + Args: + batch: A list of rows to send. + + Yields: + A generator of rows with the chat template applied. + """ + prompts = [] + for row in batch: + # PyArrow cannot handle the messages with images, so Ray Data + # will fallback to use pickle for serialization. In this case, + # the "messages" column is already a list of dicts and does not + # have .tolist() method. + if hasattr(row["messages"], "tolist"): + conversation = row["messages"].tolist() + else: + conversation = row["messages"] + add_generation_prompt = self._should_add_generation_prompt(conversation) + # If we don't add a generation prompt, we should continue the final message. + continue_final_message = not add_generation_prompt + prompts.append( + self.processor.apply_chat_template( + conversation, + tokenize=False, + chat_template=self.chat_template, + add_generation_prompt=add_generation_prompt, + continue_final_message=continue_final_message, + **(self.chat_template_kwargs or {}), + ) + ) + assert len(batch) == len(prompts) + + for row, prompt in zip(batch, prompts): + yield { + self.IDX_IN_BATCH_COLUMN: row[self.IDX_IN_BATCH_COLUMN], + "prompt": prompt, + } + + def _should_add_generation_prompt(self, conversation: List[Dict[str, Any]]) -> bool: + """Determines if the generation prompt should be added for the given conversation. + + Adds the generation prompt only if the last message is from the user. + This is useful in cases where the user provides an assistant prefill + message. + + Args: + conversation: The conversation to check. + + Returns: + True if the generation prompt should be added, False otherwise. + """ + return conversation[-1]["role"] == "user" + + +class ChatTemplateStage(StatefulStage): + """ + A stage that applies chat template. + """ + + fn: Type[StatefulStageUDF] = ChatTemplateUDF + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return { + "messages": "A list of messages in OpenAI chat format. " + "See https://platform.openai.com/docs/api-reference/chat/create " + "for details." + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/common.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/common.py new file mode 100644 index 0000000000000000000000000000000000000000..fb0869642991a93c1bba932ec831f1550886cf8c --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/common.py @@ -0,0 +1,29 @@ +""" +Shared utilities for stages. +""" + +from typing import Any, Dict, List, Union + +import numpy as np + + +def maybe_convert_ndarray_to_list( + params: Union[np.ndarray, List[Any], Dict[str, Any]] +) -> Union[List[Any], Dict[str, Any]]: + """Convert all ndarray to list in the params. This is because Ray Data + by default converts all lists to ndarrays when passing data around, but + vLLM expects lists. + + Args: + params: The parameters to convert. + + Returns: + The converted parameters. + """ + if isinstance(params, dict): + return {k: maybe_convert_ndarray_to_list(v) for k, v in params.items()} + elif isinstance(params, list): + return [maybe_convert_ndarray_to_list(v) for v in params] + elif isinstance(params, np.ndarray): + return params.tolist() + return params diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/http_request_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/http_request_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..25240fbee89f33f38515342b8d91ceaacc29e1cc --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/http_request_stage.py @@ -0,0 +1,168 @@ +"""HTTP Request Stage""" + +import asyncio +import time +import traceback +from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Type + +import aiohttp +import aiohttp.web_exceptions +import numpy as np +from aiohttp.client_exceptions import ClientPayloadError + +from ray.llm._internal.batch.stages.base import StatefulStage, StatefulStageUDF + + +class HttpRequestUDF(StatefulStageUDF): + RETRYABLE_STATUS_CODES = [429, 408, 504, 502, 503] + + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + url: str, + additional_header: Optional[Dict[str, Any]] = None, + qps: Optional[int] = None, + max_retries: int = 0, + base_retry_wait_time_in_s: float = 1.0, + session_factory: Optional[Callable[[], aiohttp.ClientSession]] = None, + ): + """ + Initialize the HttpRequestUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + url: The URL to send the HTTP request to. + additional_header: The additional headers to send with the HTTP request. + qps: The maximum number of requests per second. + max_retries: The maximum number of retries per request in the event of failures. We retry with exponential backoff upto this specific maximum retries. + base_retry_wait_time_in_s: The base retry wait time during exponential backoff. + session_factory: Optional session factory to be used for initializing a client session. + """ + super().__init__(data_column, expected_input_keys) + self.url = url + self.additional_header = additional_header or {} + self.qps = qps + self.max_retries = max_retries + self.base_retry_wait_time_in_s = base_retry_wait_time_in_s + self.session_factory = session_factory or aiohttp.ClientSession + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """ + Send HTTP requests to the given URL. + + Args: + batch: A list of rows to send. + + Yields: + A generator of rows of the response of the HTTP request. + """ + # preprocess to get request body for the given batch + request_bodies = [None] * len(batch) + for row in batch: + # Normalize the row to a JSON body. + json_body = {} + for key, value in row["payload"].items(): + if isinstance(value, np.ndarray): + json_body[key] = value.tolist() + else: + json_body[key] = value + request_bodies[row[self.IDX_IN_BATCH_COLUMN]] = json_body + + async with self.session_factory() as session: + start_time = time.time() + request_count = 0 + pending_requests = [] + headers = { + "Content-Type": "application/json", + **self.additional_header, + } + + # First send all requests based on QPS + for row in batch: + # Rate limit based on qps if specified + if self.qps is not None: + request_count += 1 + expected_time = request_count / self.qps + elapsed = time.time() - start_time + if elapsed < expected_time: + await asyncio.sleep(expected_time - elapsed) + + # self.IDX_IN_BATCH_COLUMN is the index of row in the batch + json_body = request_bodies[row[self.IDX_IN_BATCH_COLUMN]] + # Create request but don't await it yet + request = session.post( + self.url, + headers=headers, + json=json_body, + ) + pending_requests.append((row[self.IDX_IN_BATCH_COLUMN], request)) + + # Now receive all responses + for idx_in_batch_column, request in pending_requests: + resp_json = None + last_exception = None + last_exception_traceback = None + for retry_count in range(self.max_retries + 1): + if retry_count > 0: + json_body = request_bodies[idx_in_batch_column] + request = session.post( + self.url, + headers=headers, + json=json_body, + ) + try: + async with await request as response: + status_code = response.status + # check status and see if it's retry worthy + if status_code in self.RETRYABLE_STATUS_CODES: + last_exception = aiohttp.web_exceptions.HTTPException( + reason=response.reason + ) + last_exception.status_code = status_code + wait_time = self.base_retry_wait_time_in_s * ( + 2**retry_count + ) + await asyncio.sleep(wait_time) + continue + resp_json = await response.json() + if self.IDX_IN_BATCH_COLUMN in resp_json: + raise ValueError( + "The response of the HTTP request must not contain " + f"the column {self.IDX_IN_BATCH_COLUMN}." + ) + break + except ( + asyncio.TimeoutError, + aiohttp.ClientConnectionError, + ClientPayloadError, + ) as e: + last_exception_traceback = traceback.format_exc() + last_exception = type(e).__name__ + wait_time = self.base_retry_wait_time_in_s * (2**retry_count) + await asyncio.sleep(wait_time) + continue + if not resp_json: + raise RuntimeError( + f"Reached maximum retries of {self.max_retries} for input row {batch[idx_in_batch_column]}. Previous Exception: {last_exception}. Full Traceback: \n{last_exception_traceback}" + ) + yield { + self.IDX_IN_BATCH_COLUMN: idx_in_batch_column, + "http_response": resp_json, + } + + +class HttpRequestStage(StatefulStage): + """ + A stage that sends HTTP requests. + """ + + fn: Type[StatefulStageUDF] = HttpRequestUDF + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return { + "payload": "The payload to send to the HTTP request. " + "It should be in JSON format." + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/prepare_image_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/prepare_image_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..dc20379a9bcb09bea638a71396c5e766b6498f8a --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/prepare_image_stage.py @@ -0,0 +1,408 @@ +"""Prepare Image Stage""" + +import asyncio +import base64 +import importlib +import logging +from io import BytesIO +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + AsyncIterator, + Dict, + List, + Mapping, + MutableMapping, + Optional, + Union, +) +from urllib.parse import urlparse + +import aiohttp +import requests + +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) + +# TODO: Remove the guard once Pillow is added into the dependencies. +if TYPE_CHECKING: + from PIL import Image + +logger = logging.getLogger(__name__) + +_ImageType = Union[str, "Image.Image"] + + +class HTTPConnection: + """Adapted from vllm.connections.HTTPConnection. + Helper class to send HTTP requests. + """ + + def __init__(self, *, reuse_client: bool = True) -> None: + super().__init__() + + self.reuse_client = reuse_client + + self._sync_client: Optional[requests.Session] = None + self._async_client: Optional[aiohttp.ClientSession] = None + + def get_sync_client(self) -> requests.Session: + if self._sync_client is None or not self.reuse_client: + self._sync_client = requests.Session() + + return self._sync_client + + # NOTE: We intentionally use an async function even though it is not + # required, so that the client is only accessible inside async event loop + async def get_async_client(self) -> aiohttp.ClientSession: + if self._async_client is None or not self.reuse_client: + self._async_client = aiohttp.ClientSession() + + return self._async_client + + def _validate_http_url(self, url: str): + parsed_url = urlparse(url) + + if parsed_url.scheme not in ("http", "https"): + raise ValueError( + "Invalid HTTP URL: A valid HTTP URL " + "must have scheme 'http' or 'https'." + ) + + def _headers(self, **extras: str) -> MutableMapping[str, str]: + return {"User-Agent": "RayLLM-Batch", **extras} + + def get_response( + self, + url: str, + *, + stream: bool = False, + timeout: Optional[float] = None, + extra_headers: Optional[Mapping[str, str]] = None, + ): + self._validate_http_url(url) + + client = self.get_sync_client() + extra_headers = extra_headers or {} + + return client.get( + url, headers=self._headers(**extra_headers), stream=stream, timeout=timeout + ) + + async def get_async_response( + self, + url: str, + *, + timeout: Optional[float] = None, + extra_headers: Optional[Mapping[str, str]] = None, + ): + self._validate_http_url(url) + + client = await self.get_async_client() + extra_headers = extra_headers or {} + + return client.get(url, headers=self._headers(**extra_headers), timeout=timeout) + + def get_bytes(self, url: str, *, timeout: Optional[float] = None) -> bytes: + with self.get_response(url, timeout=timeout) as r: + r.raise_for_status() + + return r.content + + async def async_get_bytes( + self, + url: str, + *, + timeout: Optional[float] = None, + ) -> bytes: + async with await self.get_async_response(url, timeout=timeout) as r: + r.raise_for_status() + + return await r.read() + + def get_text(self, url: str, *, timeout: Optional[float] = None) -> str: + with self.get_response(url, timeout=timeout) as r: + r.raise_for_status() + + return r.text + + async def async_get_text( + self, + url: str, + *, + timeout: Optional[float] = None, + ) -> str: + async with await self.get_async_response(url, timeout=timeout) as r: + r.raise_for_status() + + return await r.text() + + def get_json(self, url: str, *, timeout: Optional[float] = None) -> str: + with self.get_response(url, timeout=timeout) as r: + r.raise_for_status() + + return r.json() + + async def async_get_json( + self, + url: str, + *, + timeout: Optional[float] = None, + ) -> str: + async with await self.get_async_response(url, timeout=timeout) as r: + r.raise_for_status() + + return await r.json() + + def download_file( + self, + url: str, + save_path: Path, + *, + timeout: Optional[float] = None, + chunk_size: int = 128, + ) -> Path: + with self.get_response(url, timeout=timeout) as r: + r.raise_for_status() + + with save_path.open("wb") as f: + for chunk in r.iter_content(chunk_size): + f.write(chunk) + + return save_path + + async def async_download_file( + self, + url: str, + save_path: Path, + *, + timeout: Optional[float] = None, + chunk_size: int = 128, + ) -> Path: + async with await self.get_async_response(url, timeout=timeout) as r: + r.raise_for_status() + + with save_path.open("wb") as f: + async for chunk in r.content.iter_chunked(chunk_size): + f.write(chunk) + + return save_path + + +class ImageProcessor: + """Download and load images.""" + + def __init__(self): + self.Image = importlib.import_module("PIL.Image") + self.http_connection = HTTPConnection() + + async def download_image_from_url(self, image_url: str) -> Optional[bytes]: + """Download the image from the Internet with up to 3 retries. + + Args: + image_url: The image URL to download. + + Returns: + The image bytes (None if failed to download). + """ + for _ in range(3): + try: + image_raw = await self.http_connection.async_get_bytes( + image_url, timeout=5 + ) + return image_raw + except Exception: + await asyncio.sleep(1) + return None + + async def load_image_bytes_from_url(self, image_urls: List[str]) -> List[bytes]: + """Load an image from a URL. + + Args: + image_urls: The image URLs to load. + + Returns: + The image bytes. + """ + return await asyncio.gather( + *[self.download_image_from_url(image_url) for image_url in image_urls] + ) + + async def fetch_images( + self, image_urls: List[str], *, image_mode: Optional[str] = None + ) -> List["Image.Image"]: + """ + Adapted from vllm.multimodal.utils.fetch_image. + Load a PIL image from a HTTP or base64 data URL. + + Args: + image_urls: A list of URLs of the images. + image_mode: The mode of the image. If None, the image is not converted. + + Returns: + A list of loaded images. + """ + + def _load_image_from_bytes(b: bytes): + image = self.Image.open(BytesIO(b)) + image.load() + return image + + def _load_image_from_data_url(image_url: str): + # Only split once and assume the second part is the base64 encoded image + _, image_base64 = image_url.split(",", 1) + return _load_image_from_bytes(base64.b64decode(image_base64)) + + # Check if all image URLs are of the same type. + if image_urls[0].startswith("http"): + image_url_prefix = "http" + elif image_urls[0].startswith("data:image"): + image_url_prefix = "data:image" + else: + raise ValueError(f"Invalid image URL prefix: {image_urls[0]}") + + if not all(url.startswith(image_url_prefix) for url in image_urls): + raise ValueError( + f"All image URLs must have the same prefix, got {image_url_prefix=}" + ) + + if image_url_prefix == "http": + image_raws = await self.load_image_bytes_from_url(image_urls) + images = [_load_image_from_bytes(image_raw) for image_raw in image_raws] + elif image_url_prefix == "data:image": + images = [_load_image_from_data_url(image_url) for image_url in image_urls] + else: + raise ValueError( + "Invalid 'image_url': A valid 'image_url' must start " + "with either 'data:image' or 'http'." + ) + + if image_mode is not None and images[0].mode != image_mode: + images = [image.convert(image_mode) for image in images] + return images + + async def process(self, images: List[_ImageType]) -> List["Image.Image"]: + """Load and resize an image for the model. + Args: + image: A list of images. + + Returns: + A list of processed images. + """ + if not images: + return [] + + # Check if all images are of the same type. + image_type = type(images[0]) + if not all(isinstance(img, image_type) for img in images): + raise ValueError(f"All images must be of the same type, got {image_type=}") + + if not issubclass(image_type, self.Image.Image): + images = await self.fetch_images(images) + return images + + +class PrepareImageUDF(StatefulStageUDF): + def __init__(self, data_column: str, expected_input_keys: List[str]): + super().__init__(data_column, expected_input_keys) + self.Image = importlib.import_module("PIL.Image") + self.image_processor = ImageProcessor() + + def extract_image_info(self, messages: List[Dict]) -> List[_ImageType]: + """Extract image information from chat messages. + + Args: + messages: List of chat messages. + + Returns: + List of _ImageType. + + Note: + The optional 'detail' parameter from the OpenAI schema is not + passed forward to downstream templates. + """ + + image_info: List[_ImageType] = [] + for message in messages: + content = message["content"] + + # Convert PyArrow objects to Python objects if needed (like ChatTemplateStage). + # This handles the case where unform content types are serialized with PyArrow + # instead of pickle- happens when all messages have the same content structure + # (e.g., no system prompt + string content mixed with user messages with list content). + if hasattr(content, "tolist"): + content = content.tolist() + + if not isinstance(content, list): + continue + for content_item in content: + if content_item["type"] not in ("image", "image_url"): + continue + + image_data = content_item[content_item["type"]] + + if content_item["type"] == "image_url" and isinstance(image_data, dict): + # OpenAI nested format: {"image_url": {"url": "..."}} + image = image_data.get("url") + if not isinstance(image, str) or not image: + raise ValueError( + "image_url must be an object with a non-empty 'url' string" + ) + else: + # Simple format: {"image": "..."} or {"image_url": "..."} + image = image_data + + if not isinstance(image, str) and not isinstance( + image, self.Image.Image + ): + raise ValueError(f"Cannot handle image type {type(image)}") + image_info.append(image) + return image_info + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + messages = [row["messages"] for row in batch] + + # Process all images in this batch. + all_image_info = [self.extract_image_info(message) for message in messages] + flat_all_image_info = [img for imgs in all_image_info for img in imgs] + flat_all_images = await self.image_processor.process(flat_all_image_info) + + # TODO: We now use asyncio.gather to process all images in this batch, + # so the outputs here must be in order. However, it is more efficient + # to support out-of-order outputs so that we won't be blocked by slow + # downloaded images. + img_start_idx = 0 + idx_in_batch = 0 + for image_info_per_req in all_image_info: + num_images_in_req = len(image_info_per_req) + ret = {self.IDX_IN_BATCH_COLUMN: idx_in_batch} + idx_in_batch += 1 + if num_images_in_req > 0: + images = flat_all_images[ + img_start_idx : img_start_idx + num_images_in_req + ] + ret.update( + { + "image": images, + "image_sizes": [(img.width, img.height) for img in images], + } + ) + img_start_idx += num_images_in_req + yield ret + + +class PrepareImageStage(StatefulStage): + """A stage to prepare images from OpenAI chat template messages.""" + + fn: StatefulStageUDF = PrepareImageUDF + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return { + "messages": "A list of messages in OpenAI chat format. " + "See https://platform.openai.com/docs/api-reference/chat/create " + "for details." + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/serve_deployment_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/serve_deployment_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..04626e734cd7cb8f033ece71b736e6a31ba6cb1f --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/serve_deployment_stage.py @@ -0,0 +1,156 @@ +"""The stage that runs serve deployment.""" + +import asyncio +import logging +import time +import uuid +from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Type + +from pydantic import BaseModel + +from ray import serve +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) + +logger = logging.getLogger(__name__) + + +class ServeDeploymentStageUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + *, + deployment_name: str, + app_name: str, + dtype_mapping: Dict[str, Type[Any]], + ): + """ + Initialize the ServeDeploymentStageUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + deployment_name: The name of the deployment. + app_name: The name of the deployment app. + dtype_mapping: The mapping of the request class name to the request class. + """ + super().__init__(data_column, expected_input_keys) + self._dtype_mapping = dtype_mapping + + # Using stream=True as LLM serve deployments return async generators. + # TODO (Kourosh): Generalize this to support non-streaming deployments. + self._dh = serve.get_deployment_handle(deployment_name, app_name).options( + stream=True + ) + self.request_id = 0 + + def _prepare_request( + self, row: Dict[str, Any] + ) -> Tuple[Dict[str, Any], Optional[Type[Any]], str]: + """ + Decorate the request with metadata related to the batch. + + Args: + row: The row. + + Returns: + A tuple of (decorated_request, dtype, method_name). dtype is the class of the request object and + can be None if the serve deployment accepts a raw dict. method_name is the name of the method to + invoke on the serve deployment. + """ + method = row.get("method") + dtype_name = row.get("dtype") + + dtype = None + if dtype_name is not None: + if not self._dtype_mapping or dtype_name not in self._dtype_mapping: + raise ValueError( + f"{dtype_name} must be provided in ServeDeploymentProcessorConfig's dtype_mapping." + ) + dtype = self._dtype_mapping[dtype_name] + + request_kwargs = row.pop("request_kwargs") + request = { + "request_id": str(self.request_id), + "idx_in_batch": row[self.IDX_IN_BATCH_COLUMN], + **request_kwargs, + } + self.request_id += 1 + + return request, dtype, method + + async def generate_async( + self, row: Dict[str, Any] + ) -> Tuple[Dict[str, Any], Dict[str, Any], float]: + """ + Run the serve deployment. + + Args: + row: The row to run the serve deployment on. + + Returns: + The response from the serve deployment. + """ + request, dtype, method = self._prepare_request(row) + request_obj = dtype(**request) if dtype else request + + if getattr(self._dh, method) is None: + raise ValueError(f"Method {method} not found in the serve deployment.") + + t = time.perf_counter() + # Directly using anext() requires python3.10 and above + output_data = await getattr(self._dh, method).remote(request_obj).__anext__() + time_taken = time.perf_counter() - t + + # Convert the output data to a dict if it is a Pydantic model. + if isinstance(output_data, BaseModel): + output_data = output_data.model_dump() + + return request, output_data, time_taken + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """ + Run the serve deployment. + + Args: + batch: A list of rows to run the serve deployment on. + + Yields: + Dict[str, Any]: A dictionary containing the response from the serve deployment + along with processing metadata. + """ + batch_uuid = uuid.uuid4() + t = time.perf_counter() + tasks = [asyncio.create_task(self.generate_async(row)) for row in batch] + + for resp in asyncio.as_completed(tasks): + request, output, time_taken = await resp + + yield { + "request_id": request["request_id"], + self.IDX_IN_BATCH_COLUMN: request["idx_in_batch"], + "batch_uuid": batch_uuid.hex, + "time_taken": time_taken, + **output, + } + + batch_time_taken = time.perf_counter() - t + logger.info( + "[LLM Batch - Serve Deployment] Elapsed time for batch %s with size %d: %s", + batch_uuid.hex, + len(batch), + batch_time_taken, + ) + + +class ServeDeploymentStage(StatefulStage): + fn: Type[StatefulStageUDF] = ServeDeploymentStageUDF + + def get_required_input_keys(self) -> Dict[str, str]: + return { + "method": "Name of the method to invoke on the serve deployment.", + "request_kwargs": "The request_kwargs to construct the request to the serve deployment.", + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/sglang_engine_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/sglang_engine_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..ba2a69e80fe27c6fbb6f4225ac6bf85f311a697b --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/sglang_engine_stage.py @@ -0,0 +1,404 @@ +"""The stage that runs SGLang engine.""" + +import asyncio +import logging +import time +import uuid +from contextlib import nullcontext +from enum import Enum +from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Type + +from pydantic import BaseModel, root_validator + +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) +from ray.llm._internal.batch.stages.common import maybe_convert_ndarray_to_list + +logger = logging.getLogger(__name__) + + +class SGLangTaskType(str, Enum): + """The type of task to run on the SGLang engine.""" + + """Generate text.""" + GENERATE = "generate" + + +class SGLangEngineRequest(BaseModel): + """A request to the SGLang engine.""" + + # The request ID for the LLM engine (unique per replica). + request_id: int + # The index of the request in the batch. + idx_in_batch: int + # The input prompt. + prompt: Optional[str] + # Alternative to text. Specify the input as token IDs instead of text. + prompt_token_ids: Optional[List[int]] + # The sampling parameters (more details can be seen in https://docs.sglang.ai/backend/sampling_params.html). + params: Optional[Dict[str, Any]] + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +class SGLangOutputData(BaseModel): + """The output of the SGLang engine.""" + + prompt: Optional[str] + prompt_token_ids: Optional[List[int]] + num_input_tokens: int + + # Generate fields. + generated_tokens: Optional[List[int]] + generated_text: Optional[str] + num_generated_tokens: int + + # Metrics fields. + metrics: Optional[Dict[str, Any]] = None + + @classmethod + def from_sglang_engine_output(cls, output: Dict[str, Any]) -> "SGLangOutputData": + """Create a SGLangOutputData from a SGLang engine output.""" + + # Set by `_generate_async`. + assert "prompt" in output + assert "prompt_token_ids" in output + + # Returned in the native output of the SGLang engine. + assert "meta_info" in output + assert "prompt_tokens" in output["meta_info"] + assert "completion_tokens" in output["meta_info"] + + data = cls( + prompt=output["prompt"], + prompt_token_ids=output["prompt_token_ids"], + num_input_tokens=output["meta_info"]["prompt_tokens"], + generated_tokens=output["output_ids"] if "output_ids" in output else None, + generated_text=output["text"] if "text" in output else None, + num_generated_tokens=output["meta_info"]["completion_tokens"], + ) + + return data + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +class SGLangEngineWrapper: + """Wrapper around the SGLang engine to handle async requests. + + Args: + *args: The positional arguments for the engine. + max_pending_requests: The maximum number of pending requests in the queue. + **kwargs: The keyword arguments for the engine. + """ + + def __init__( + self, + idx_in_batch_column: str, + max_pending_requests: int = -1, + **kwargs, + ): + self.request_id = 0 + self.idx_in_batch_column = idx_in_batch_column + self.task_type = kwargs.pop("task", SGLangTaskType.GENERATE) + self.model = kwargs.pop("model", None) + assert self.model is not None + # We need to rename the `model` to `model_path` for SGLang. + kwargs["model_path"] = self.model + + # Set the skip_tokenizer_init to True by default for SGLang engine + # because we will not use the tokenizer/detokenizer in SGLang engine + # by default. + self.skip_tokenizer_init = kwargs.pop("skip_tokenizer_init", True) + kwargs["skip_tokenizer_init"] = self.skip_tokenizer_init + + try: + import sglang + except ImportError as e: + raise ImportError( + "SGLang is not installed or failed to import. Please run " + "`pip install sglang[all]` to install required dependencies." + ) from e + + # Initialize the SGLang engine + self.engine = sglang.Engine(**kwargs) + + # The performance gets really bad if there are too many requests in the pending queue. + # We work around it with semaphore to limit the number of concurrent requests in the engine. + self.max_pending_requests = max_pending_requests + if self.max_pending_requests > 0: + self.semaphore = asyncio.Semaphore(self.max_pending_requests) + else: + # Use contextlib.nullcontext which works for both sync and async contexts. + self.semaphore = nullcontext() + + async def _prepare_llm_request(self, row: Dict[str, Any]) -> SGLangEngineRequest: + """Prepare the inputs for LLM inference. + + Args: + row: The row. + + Returns: + A single SGLangEngineRequest. + """ + prompt = row.pop("prompt") + + if "tokenized_prompt" in row: + tokenized_prompt = row.pop("tokenized_prompt").tolist() + else: + tokenized_prompt = None + + # Prepare sampling parameters. + if self.task_type == SGLangTaskType.GENERATE: + params = maybe_convert_ndarray_to_list(row.pop("sampling_params")) + else: + raise ValueError(f"Unsupported task type: {self.task_type}") + + if tokenized_prompt is not None and not self.skip_tokenizer_init: + raise ValueError( + "To use a token-in-token-out mode of SGLang Engine, please set engine_kwargs['skip_tokenizer_init'] to True." + ) + + request = SGLangEngineRequest( + request_id=self.request_id, + idx_in_batch=row[self.idx_in_batch_column], + prompt=prompt, + prompt_token_ids=tokenized_prompt, + params=params, + ) + self.request_id += 1 + return request + + async def generate_async( + self, row: Dict[str, Any] + ) -> Tuple[SGLangEngineRequest, Dict[str, Any], float]: + """Process a single request. + + Args: + request: The request. + + Returns: + A tuple of index in batch, request output and bypassed custom fields, and time taken. + """ + request = await self._prepare_llm_request(row) + t = time.perf_counter() + + async with self.semaphore: + output = await self._generate_async(request) + + time_taken = time.perf_counter() - t + + output_data = SGLangOutputData.from_sglang_engine_output(output) + return request, output_data.model_dump(), time_taken + + async def _generate_async(self, request: SGLangEngineRequest) -> Any: + """Process a single request. + + Args: + request: The request. + + Returns: + The output of the request. + """ + + # Send the request to the LLM engine. + stream = await self.engine.async_generate( + prompt=request.prompt, + input_ids=request.prompt_token_ids, + sampling_params=request.params, + stream=True, + ) + + # Consume the stream until the request is finished. + async for output in stream: + if output["meta_info"]["finish_reason"] is not None: + output["prompt"] = request.prompt + output["prompt_token_ids"] = request.prompt_token_ids + return output + + raise RuntimeError( + "[SGLang] The request is not finished. This should not happen. Please report this issue to the Ray team." + ) + + def shutdown(self): + """Shutdown the SGLang engine.""" + if hasattr(self.engine, "shutdown"): + logger.info("Shutting down SGLang engine") + self.engine.shutdown() + + +class SGLangEngineStageUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + model: str, + engine_kwargs: Dict[str, Any], + task_type: SGLangTaskType = SGLangTaskType.GENERATE, + max_pending_requests: Optional[int] = None, + ): + """ + Initialize the SGLangEngineStageUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + model: The path to the model to use for the SGLang engine. + engine_kwargs: The kwargs to pass to the SGLang engine. + task_type: The task to use for the SGLang engine (e.g., "generate", "embed", "reward"). + max_pending_requests: The maximum number of pending requests. If None, + it will be set to a default value based on engine settings. + """ + super().__init__(data_column, expected_input_keys) + self.model = model + + # Setup SGLang engine kwargs. + self.task_type = task_type + self.engine_kwargs = self.normalize_engine_kwargs(task_type, engine_kwargs) + + # Set up the max pending requests. + # Disable the semaphore if max_pending_requests is not set. + self.max_pending_requests = max_pending_requests or -1 + if self.max_pending_requests > 0: + logger.info("Max pending requests is set to %d", self.max_pending_requests) + + # Create an LLM engine. + self.llm = SGLangEngineWrapper( + model=self.model, + idx_in_batch_column=self.IDX_IN_BATCH_COLUMN, + max_pending_requests=self.max_pending_requests, + **self.engine_kwargs, + ) + + def normalize_engine_kwargs( + self, + task_type: SGLangTaskType, + engine_kwargs: Dict[str, Any], + ) -> Dict[str, Any]: + """ + Normalize the engine kwargs. + + Args: + task_type: The task to use for the SGLang engine (e.g., "generate", etc). + engine_kwargs: The kwargs to normalize. + + Returns: + The normalized kwargs. + """ + # Remove model from engine kwargs if set. + model = engine_kwargs.pop("model", None) + if model is not None and model != self.model: + logger.warning( + "The model set in engine kwargs (%s) is different from the " + "stage (%s). Please remove 'model' from engine kwargs.", + model, + self.model, + ) + + # Override the task if it is different from the stage. + task = SGLangTaskType(engine_kwargs.get("task", task_type)) + if task != task_type: + logger.warning( + "The task set in engine kwargs (%s) is different from the " + "stage (%s). Overriding the task in engine kwargs to %s.", + task, + task_type, + task_type, + ) + engine_kwargs["task"] = task_type + return engine_kwargs + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """Run the SGLang engine. + + Args: + batch: A list of rows to run the SGLang engine on. + + Returns: + The response of the SGLang engine. + """ + batch_uuid = uuid.uuid4() + batch_start_time = time.perf_counter() + + tasks = [asyncio.create_task(self.llm.generate_async(row)) for row in batch] + + for resp in asyncio.as_completed(tasks): + request, output, time_taken_llm = await resp + + yield { + **output, + "request_id": request.request_id, + self.IDX_IN_BATCH_COLUMN: request.idx_in_batch, + "batch_uuid": batch_uuid.hex, + "time_taken_llm": time_taken_llm, + "params": str(request.params), + } + + batch_time_taken = time.perf_counter() - batch_start_time + logger.info( + "[SGLang] Elapsed time for batch %s with size %d: %s", + batch_uuid.hex, + len(batch), + batch_time_taken, + ) + + def __del__(self): + if hasattr(self, "llm"): + self.llm.shutdown() + + +class SGLangEngineStage(StatefulStage): + """ + A stage that runs SGLang engine. + """ + + fn: Type[StatefulStageUDF] = SGLangEngineStageUDF + + @root_validator(pre=True) + def post_init(cls, values): + """Post-initialize the stage. Specifically, + this function determines the num_gpus and Ray remote args + for the .map_batches() call in this stage. + + Args: + values: The raw stage values. + Returns: + The updated values. + """ + map_batches_kwargs = values["map_batches_kwargs"] + accelerator_type = map_batches_kwargs.get("accelerator_type", "") + fn_constructor_kwargs = values["fn_constructor_kwargs"] + engine_kwargs = fn_constructor_kwargs.get("engine_kwargs", {}) + + ray_remote_args = {} + if accelerator_type: + ray_remote_args["accelerator_type"] = accelerator_type + + # Set up num_gpus required + tp_size = engine_kwargs.get("tp_size", 1) + dp_size = engine_kwargs.get("dp_size", 1) + num_gpus = tp_size * dp_size + + ray_remote_args["num_gpus"] = num_gpus + map_batches_kwargs.update(ray_remote_args) + return values + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + ret = {"prompt": "The text prompt (str)."} + task_type = self.fn_constructor_kwargs.get("task_type", SGLangTaskType.GENERATE) + if task_type == SGLangTaskType.GENERATE: + ret[ + "sampling_params" + ] = "The sampling parameters. See https://docs.sglang.ai/backend/sampling_params.html for details." + return ret + + def get_optional_input_keys(self) -> Dict[str, str]: + """The optional input keys of the stage and their descriptions.""" + return {} diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/tokenize_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/tokenize_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..b6b64e29052e212b06d8a5ac4d6171b307fd3ec5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/tokenize_stage.py @@ -0,0 +1,142 @@ +"""Tokenize and detokenize stage""" + +from typing import Any, AsyncIterator, Dict, List, Type + +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) +from ray.llm._internal.batch.utils import get_cached_tokenizer +from ray.llm._internal.common.utils.download_utils import ( + NodeModelDownloadable, + download_model_files, +) + + +class TokenizeUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + model: str, + ): + """ + Initialize the TokenizeUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + model: The model to use for the chat template. + """ + from transformers import AutoTokenizer + + super().__init__(data_column, expected_input_keys) + model_path = download_model_files( + model_id=model, + mirror_config=None, + download_model=NodeModelDownloadable.TOKENIZER_ONLY, + download_extra_files=False, + ) + self.tokenizer = get_cached_tokenizer( + AutoTokenizer.from_pretrained( + model_path, + trust_remote_code=True, + ) + ) + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """ + Tokenize the given batch. + + Args: + batch: A list of rows to send. + + Yields: + A generator of rows with the tokenized prompt. + """ + for row, prompt_token_ids in zip( + batch, + self.tokenizer([row["prompt"] for row in batch])["input_ids"], + ): + yield { + self.IDX_IN_BATCH_COLUMN: row[self.IDX_IN_BATCH_COLUMN], + "tokenized_prompt": prompt_token_ids, + } + + +class TokenizeStage(StatefulStage): + """ + A stage that tokenizes the input. + """ + + fn: Type[StatefulStageUDF] = TokenizeUDF + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return {"prompt": "The text prompt (str) to tokenize."} + + +class DetokenizeUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + model: str, + ): + """ + Initialize the DetokenizeUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + model: The model to use for the chat template. + """ + from transformers import AutoTokenizer + + super().__init__(data_column, expected_input_keys) + model_path = download_model_files( + model_id=model, + mirror_config=None, + download_model=NodeModelDownloadable.TOKENIZER_ONLY, + download_extra_files=False, + ) + self.tokenizer = get_cached_tokenizer( + AutoTokenizer.from_pretrained( + model_path, + trust_remote_code=True, + ) + ) + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """ + Detokenize the given batch. + + Args: + batch: A list of rows to send. + + Yields: + A generator of rows with the detokenized prompt. + """ + for row, generated_text in zip( + batch, + self.tokenizer.batch_decode( + [row["generated_tokens"] for row in batch], + skip_special_tokens=True, + ), + ): + yield { + self.IDX_IN_BATCH_COLUMN: row[self.IDX_IN_BATCH_COLUMN], + "generated_text": generated_text, + } + + +class DetokenizeStage(StatefulStage): + """ + A stage that detokenizes the input. + """ + + fn: Type[StatefulStageUDF] = DetokenizeUDF + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + return {"generated_tokens": "A list of generated tokens (int) to detokenize."} diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/vllm_engine_stage.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/vllm_engine_stage.py new file mode 100644 index 0000000000000000000000000000000000000000..b69ab5b2610606cc783d990613a4daf32c04b173 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/stages/vllm_engine_stage.py @@ -0,0 +1,698 @@ +"""The stage that runs vLLM engine.""" + +import asyncio +import copy +import dataclasses +import logging +import math +import time +import uuid +from collections import Counter +from enum import Enum +from functools import partial +from typing import Any, AsyncIterator, Dict, List, Optional, Tuple, Type + +import numpy as np +import torch +from pydantic import BaseModel, Field, root_validator + +import ray +from ray.llm._internal.batch.stages.base import ( + StatefulStage, + StatefulStageUDF, +) +from ray.llm._internal.batch.stages.common import maybe_convert_ndarray_to_list +from ray.llm._internal.common.utils.cloud_utils import is_remote_path +from ray.llm._internal.common.utils.download_utils import ( + STREAMING_LOAD_FORMATS, + NodeModelDownloadable, + download_model_files, +) +from ray.llm._internal.common.utils.lora_utils import download_lora_adapter +from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy + +logger = logging.getLogger(__name__) + + +class vLLMTaskType(str, Enum): + """The type of task to run on the vLLM engine.""" + + """Generate text.""" + GENERATE = "generate" + + """Generate embeddings.""" + EMBED = "embed" + + +class vLLMEngineRequest(BaseModel): + """A request to the vLLM engine.""" + + # The request ID for the LLM engine (unique per replica). + request_id: int + # The index of the request in the batch. + idx_in_batch: int + # The full prompt string (with chat template applied if any). + prompt: str + # The images inputs for the multimodal model. Use Any to avoid importing PIL. + images: List[Any] + # The tokenized prompt IDs. If None, then the string prompt will be + # tokenized by the LLM engine. This is not recommended for performance reasons. + prompt_token_ids: Optional[List[int]] + # The sampling or pooling parameters. Use Any to avoid importing vLLM. + params: Any + # LoRA request. + lora_request: Optional[Any] = None + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +class vLLMOutputData(BaseModel): + """The output of the vLLM engine.""" + + prompt: str + prompt_token_ids: Optional[List[int]] + num_input_tokens: int + + # Generate fields. + generated_tokens: List[int] = Field(default_factory=list) + generated_text: str = Field(default="") + num_generated_tokens: int = Field(default=0) + + # Embed fields. The type should be torch.Tensor, but we use Any to avoid + # importing torch because of an error in sphinx-build with an unknown reason. + embeddings: Optional[Any] = None + + # Metrics fields. + metrics: Optional[Dict[str, Any]] = None + + @classmethod + def from_vllm_engine_output(cls, output: Any) -> "vLLMOutputData": + """Create a vLLMOutputData from a vLLM engine output.""" + + prompt_token_ids = output.prompt_token_ids + if isinstance(prompt_token_ids, np.ndarray): + prompt_token_ids = prompt_token_ids.tolist() + + data = cls( + prompt=output.prompt, + prompt_token_ids=prompt_token_ids, + num_input_tokens=len(prompt_token_ids), + ) + + import vllm + + if isinstance(output, vllm.outputs.RequestOutput): + metrics = {} + if output.metrics is not None: + metrics = dict(dataclasses.asdict(output.metrics)) + data.metrics = metrics + data.generated_tokens = output.outputs[0].token_ids + data.generated_text = output.outputs[0].text + data.num_generated_tokens = len(output.outputs[0].token_ids) + elif isinstance(output, vllm.outputs.PoolingRequestOutput): + data.embeddings = output.outputs.data.cpu() + if ( + isinstance(data.embeddings, torch.Tensor) + and data.embeddings.dtype == torch.bfloat16 + ): + data.embeddings = data.embeddings.to(torch.float32) + else: + raise ValueError(f"Unknown output type: {type(output)}") + + return data + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +class vLLMEngineWrapper: + """Wrapper around the vLLM engine to handle async requests. + + Args: + *args: The positional arguments for the engine. + max_pending_requests: The maximum number of pending requests in the queue. + dynamic_lora_loading_path: The S3 path to the dynamic LoRA adapter. + **kwargs: The keyword arguments for the engine. + """ + + def __init__( + self, + idx_in_batch_column: str, + max_pending_requests: int = -1, + dynamic_lora_loading_path: Optional[str] = None, + **kwargs, + ): + self.request_id = 0 + self.idx_in_batch_column = idx_in_batch_column + self.task_type = kwargs.get("task", vLLMTaskType.GENERATE) + + # Use model_source in kwargs["model"] because "model" is actually + # the model source in vLLM. + self.model = kwargs.pop("model", None) + self.model_source = kwargs.pop("model_source", None) + assert self.model is not None and self.model_source is not None + kwargs["model"] = self.model_source + + # LoRA related. + self.dynamic_lora_loading_path = dynamic_lora_loading_path + self.lora_lock = asyncio.Lock() + self.lora_name_to_request = {} + + # Convert the task type back to a string to pass to the engine. + kwargs["task"] = self.task_type.value + + try: + import vllm + except ImportError as e: + raise ImportError( + "vLLM is not installed or failed to import. Please run " + "`pip install ray[llm]` to install required dependencies." + ) from e + + # Construct PoolerConfig if override_pooler_config is specified. + if self.task_type == vLLMTaskType.EMBED and "override_pooler_config" in kwargs: + kwargs["override_pooler_config"] = vllm.config.PoolerConfig( + **kwargs["override_pooler_config"] + ) + + # Initialize the vLLM engine. + engine_args = vllm.AsyncEngineArgs( + **kwargs, + ) + # create_engine_config will set default values including `max_num_seqs`. + self._vllm_config = engine_args.create_engine_config() + self.engine = vllm.AsyncLLMEngine.from_engine_args(engine_args) + + # The performance gets really bad if there are too many requests in the pending queue. + # We work around it with semaphore to limit the number of concurrent requests in the engine. + self.max_pending_requests = max_pending_requests + if self.max_pending_requests > 0: + self.semaphore = asyncio.Semaphore(self.max_pending_requests) + else: + self.semaphore = asyncio.NullContext() + + async def _maybe_get_lora_request( + self, + row: Dict[str, Any], + ) -> Optional[Any]: + """Get the LoRA request for the given row. + Specifically, if the model name is given and is different from the model + set in the config, then this request has LoRA. + + Args: + row: The row. + + Returns: + The LoRA request (vllm.lora.request.LoRARequest), + or None if there is no LoRA. We use Any in type hint to + pass doc build in the environment without vLLM. + """ + import vllm + + lora_request = None + if "model" in row and row["model"] != self.model: + + lora_name = row["model"] + if lora_name not in self.lora_name_to_request: + if is_remote_path(lora_name): + raise ValueError( + "LoRA name cannot be a remote path (s3:// or gs://). " + "Please specify dynamic_lora_loading_path in the processor config." + ) + + async with self.lora_lock: + if lora_name not in self.lora_name_to_request: + # Load a new LoRA adapter if it is not loaded yet. + lora_path = download_lora_adapter( + lora_name, + remote_path=self.dynamic_lora_loading_path, + ) + logger.info( + "Downloaded LoRA adapter for %s to %s", lora_name, lora_path + ) + lora_request = vllm.lora.request.LoRARequest( + lora_name=lora_name, + # LoRA ID starts from 1. + lora_int_id=len(self.lora_name_to_request) + 1, + lora_path=lora_path, + ) + self.lora_name_to_request[lora_name] = lora_request + lora_request = self.lora_name_to_request[lora_name] + return lora_request + + async def _prepare_llm_request(self, row: Dict[str, Any]) -> vLLMEngineRequest: + """Prepare the inputs for LLM inference. + + Args: + row: The row. + + Returns: + A single vLLMEngineRequest. + """ + prompt = row.pop("prompt") + + if "tokenized_prompt" in row: + tokenized_prompt = maybe_convert_ndarray_to_list( + row.pop("tokenized_prompt") + ) + else: + tokenized_prompt = None + + # Extract image data from preprocessing output + # Note: Field name is 'image' (singular) not 'images' (plural). + if "image" in row: + image = row.pop("image") + else: + image = [] + + lora_request = await self._maybe_get_lora_request(row) + + # Prepare sampling parameters. + import vllm + + if self.task_type == vLLMTaskType.GENERATE: + sampling_params = row.pop("sampling_params") + if "guided_decoding" in sampling_params: + guided_decoding = vllm.sampling_params.GuidedDecodingParams( + **maybe_convert_ndarray_to_list( + sampling_params.pop("guided_decoding") + ) + ) + else: + guided_decoding = None + params = vllm.SamplingParams( + **maybe_convert_ndarray_to_list(sampling_params), + guided_decoding=guided_decoding, + ) + elif self.task_type == vLLMTaskType.EMBED: + params = vllm.PoolingParams(task=self.task_type.value) + else: + raise ValueError(f"Unsupported task type: {self.task_type}") + + request = vLLMEngineRequest( + request_id=self.request_id, + idx_in_batch=row[self.idx_in_batch_column], + prompt=prompt, + prompt_token_ids=tokenized_prompt, + images=image, + params=params, + lora_request=lora_request, + ) + self.request_id += 1 + return request + + async def generate_async( + self, row: Dict[str, Any] + ) -> Tuple[vLLMEngineRequest, Dict[str, Any], float]: + """Process a single request. + + Args: + request: The request. + + Returns: + A tuple of index in batch, request output and bypassed custom fields, and time taken. + """ + request = await self._prepare_llm_request(row) + t = time.perf_counter() + + async with self.semaphore: + output = await self._generate_async(request) + + time_taken = time.perf_counter() - t + + output_data = vLLMOutputData.from_vllm_engine_output(output) + return request, output_data.model_dump(), time_taken + + async def _generate_async(self, request: vLLMEngineRequest) -> Any: + """Process a single request. + + Args: + request: The request. + + Returns: + The output of the request. + """ + + # NOTE: vLLM v1 tighly couples tokenizer and detokenizer to the engine. + # We should investigate whether decoupling them could lead to better + # performance. Given that v1 tokenizer and detokenizer are already + # in a separate process, the benefit of decoupling them in the Processor + # may be limited. + assert request.prompt + import vllm + + multi_modal_data = {"image": request.images} if request.images else None + llm_prompt = vllm.inputs.data.TextPrompt( + prompt=request.prompt, multi_modal_data=multi_modal_data + ) + + # Send the request to the LLM engine. + stream = self.engine.generate( + request_id=str(request.request_id), + prompt=llm_prompt, + sampling_params=request.params, + ) + + # Consume the stream until the request is finished. + async for request_output in stream: + if request_output.finished: + # Bypass the original full prompt. + request_output.prompt = request.prompt + return request_output + + raise RuntimeError( + "[vLLM] The request is not finished. This should not happen. Please report this issue to the Ray team." + ) + + def shutdown(self): + """Shutdown the vLLM v1 engine. This kills child processes forked + by the vLLM engine. If not called, the child processes will be + orphaned and will not be killed when the parent process exits, + and they won't be able to be tracked by Ray anymore. + """ + if hasattr(self.engine, "shutdown"): + logger.info("Shutting down vLLM engine") + self.engine.shutdown() + + def get_scheduler_config(self): + return self._vllm_config.scheduler_config + + +class vLLMEngineStageUDF(StatefulStageUDF): + def __init__( + self, + data_column: str, + expected_input_keys: List[str], + batch_size: int, + max_concurrent_batches: int, + model: str, + engine_kwargs: Dict[str, Any], + task_type: vLLMTaskType = vLLMTaskType.GENERATE, + max_pending_requests: Optional[int] = None, + dynamic_lora_loading_path: Optional[str] = None, + ): + """ + Initialize the vLLMEngineStageUDF. + + Args: + data_column: The data column name. + expected_input_keys: The expected input keys of the stage. + model: The model to use for the vLLM engine. + engine_kwargs: The kwargs to pass to the vLLM engine. + task_type: The task to use for the vLLM engine (e.g., "generate", "embed", etc). + max_pending_requests: The maximum number of pending requests. If None, + it will be set to 1.1 * max_num_seqs * pipeline_parallel_size. + dynamic_lora_loading_path: The path to the dynamic LoRA adapter. It is expected + to hold subfolders each for a different lora checkpoint. + """ + super().__init__(data_column, expected_input_keys) + self.model = model + + # Setup vLLM engine kwargs. + self.task_type = task_type + self.engine_kwargs = self.normalize_engine_kwargs(task_type, engine_kwargs) + + # Set up the max pending requests. + pp_size = self.engine_kwargs.get("pipeline_parallel_size", 1) + self.max_pending_requests = max_pending_requests or math.ceil( + self.engine_kwargs.get("max_num_seqs", 128) * pp_size * 1.1 + ) + if self.max_pending_requests > 0: + logger.info("Max pending requests is set to %d", self.max_pending_requests) + + exclude_safetensors = ( + self.engine_kwargs.get("load_format") in STREAMING_LOAD_FORMATS + ) + if exclude_safetensors: + logger.info("Excluding safetensors files when downloading the model.") + download_model = NodeModelDownloadable.EXCLUDE_SAFETENSORS + else: + logger.info("Downloading model and tokenizer.") + download_model = NodeModelDownloadable.MODEL_AND_TOKENIZER + + # Download the model if needed. + model_source = download_model_files( + model_id=self.model, + mirror_config=None, + download_model=download_model, + download_extra_files=False, + ) + + # If we are using streaming load formats, we need to pass in self.model which is a remote cloud storage path. + source = model_source if not exclude_safetensors else self.model + self.llm = vLLMEngineWrapper( + model=self.model, + model_source=source, + idx_in_batch_column=self.IDX_IN_BATCH_COLUMN, + enable_log_requests=False, + max_pending_requests=self.max_pending_requests, + dynamic_lora_loading_path=dynamic_lora_loading_path, + **self.engine_kwargs, + ) + + max_num_seqs = self.llm.get_scheduler_config().max_num_seqs + if batch_size * max_concurrent_batches < max_num_seqs: + logger.warning( + f"The product of batch_size ({batch_size}) and " + f"max_concurrent_batches ({max_concurrent_batches}) is too small " + "to saturate vLLM engine. This may lead to suboptimal " + "throughput. Please increase max_concurrent_batches to at least " + f"{math.ceil(max_num_seqs / batch_size)}." + ) + + def normalize_engine_kwargs( + self, + task_type: vLLMTaskType, + engine_kwargs: Dict[str, Any], + ) -> Dict[str, Any]: + """ + Normalize the engine kwargs. + + Args: + task_type: The task to use for the vLLM engine (e.g., "generate", "embed", etc). + engine_kwargs: The kwargs to normalize. + + Returns: + The normalized kwargs. + """ + # Remove model from engine kwargs if set. + model = engine_kwargs.pop("model", None) + if model is not None and model != self.model: + logger.warning( + "The model set in engine kwargs (%s) is different from the " + "stage (%s). Please remove 'model' from engine kwargs.", + model, + self.model, + ) + + # Override the task if it is different from the stage. + task = vLLMTaskType(engine_kwargs.get("task", task_type)) + if task != task_type: + logger.warning( + "The task set in engine kwargs (%s) is different from the " + "stage (%s). Overriding the task in engine kwargs to %s.", + task, + task_type, + task_type, + ) + engine_kwargs["task"] = task_type + return engine_kwargs + + async def udf(self, batch: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]: + """Run the vLLM engine. + + Args: + batch: A list of rows to run the vLLM engine on. + + Returns: + The response of the vLLM engine. + """ + batch_uuid = uuid.uuid4() + batch_start_time = time.perf_counter() + + tasks = [asyncio.create_task(self.llm.generate_async(row)) for row in batch] + + for resp in asyncio.as_completed(tasks): + request, output, time_taken_llm = await resp + + yield { + **output, + "request_id": request.request_id, + self.IDX_IN_BATCH_COLUMN: request.idx_in_batch, + "batch_uuid": batch_uuid.hex, + "time_taken_llm": time_taken_llm, + "params": str(request.params), + } + + batch_time_taken = time.perf_counter() - batch_start_time + # TODO: Add metrics to the UDf wrapper so that we don't need + # timer in UDFs anymore. + logger.info( + "[vLLM] Elapsed time for batch %s with size %d: %s", + batch_uuid.hex, + len(batch), + batch_time_taken, + ) + + # Log engine stats after each batch is done conditioned on the flag + # passed to the engine. + if not self.engine_kwargs.get("disable_log_stats", False): + await self.llm.engine.do_log_stats() + + def __del__(self): + if hasattr(self, "llm"): + # Kill the engine processes. + self.llm.shutdown() + + +def _ray_scheduling_strategy_fn( + num_bundles_per_replica: int, + accelerator_type: Optional[str] = None, + placement_group_config: Optional[Dict[str, Any]] = None, +): + """Create a Ray scheduling strategy for the engine. + + Args: + num_bundles_per_replica: The number of device bundles per + engine replica. + accelerator_type: The accelerator type. If None, the + accelerator_type label will not be set. + placement_group_config: The custom placement group configuration. + If None, we use the default placement group configuration. + + Returns: + The Ray scheduling strategy. + """ + + def _get_bundle() -> Dict[str, float]: + # GPU bundles + bundle = {"GPU": 1, "CPU": 1} + + # Accelerator type + if accelerator_type: + bundle[f"accelerator_type:{accelerator_type}"] = 0.001 + return bundle + + if placement_group_config: + placement_group_config = copy.deepcopy(placement_group_config) + + if accelerator_type: + for bundle in placement_group_config["bundles"]: + bundle[f"accelerator_type:{accelerator_type}"] = 0.001 + + pg = ray.util.placement_group(**placement_group_config) + else: + pg = ray.util.placement_group( + [_get_bundle()] * num_bundles_per_replica, + strategy="PACK", + ) + return dict( + scheduling_strategy=PlacementGroupSchedulingStrategy( + pg, placement_group_capture_child_tasks=True + ) + ) + + +class vLLMEngineStage(StatefulStage): + """ + A stage that runs vLLM engine. + """ + + fn: Type[StatefulStageUDF] = vLLMEngineStageUDF + + @root_validator(pre=True) + def post_init(cls, values): + """Post-initialize the stage. Specifically, + this function determines the num_gpus and Ray remote args + for the .map_batches() call in this stage. + + Args: + values: The raw stage values. + Returns: + The updated values. + """ + map_batches_kwargs = values["map_batches_kwargs"] + accelerator_type = map_batches_kwargs.get("accelerator_type", "") + fn_constructor_kwargs = values["fn_constructor_kwargs"] + engine_kwargs = fn_constructor_kwargs.get("engine_kwargs", {}) + + ray_remote_args = {} + if accelerator_type: + ray_remote_args["accelerator_type"] = accelerator_type + + # Setup num_workers required per vLLM engine. + tp_size = engine_kwargs.get("tensor_parallel_size", 1) + pp_size = engine_kwargs.get("pipeline_parallel_size", 1) + num_bundles_per_replica = tp_size * pp_size + + # Use the MP backend by default. + engine_kwargs.setdefault("distributed_executor_backend", "mp") + executor_backend = engine_kwargs.get("distributed_executor_backend") + + # When Ray is used in the vLLM engine, we set num_devices to 0 so that + # Ray Data won't reserve GPUs in advance. Instead, we specify scheduling + # strategy in .map_batches() arguments and let vLLM Ray executor to + # create placement groups for each TP/PP worker. + placement_group_config = fn_constructor_kwargs.pop( + "placement_group_config", None + ) + if executor_backend == "ray": + # Note that we have to use partial() to pass a function + # instead of an object. + map_batches_kwargs["ray_remote_args_fn"] = partial( + _ray_scheduling_strategy_fn, + num_bundles_per_replica, + accelerator_type, + placement_group_config, + ) + ray_remote_args["num_gpus"] = 0 + else: + if not placement_group_config: + # Default to GPUs per bundle if placement group is not specified. + ray_remote_args["num_gpus"] = num_bundles_per_replica + else: + bundles = placement_group_config["bundles"] + resource_counter = Counter() + for bundle in bundles: + resource_counter.update(bundle) + + total_cpus = resource_counter.pop("CPU", 0) + total_gpus = resource_counter.pop("GPU", 0) + + # Ray Data expects CPU/GPU to be specified via num_cpus/num_gpus, + # not inside the resources dict. + if total_cpus: + ray_remote_args["num_cpus"] = total_cpus + if total_gpus: + ray_remote_args["num_gpus"] = total_gpus + + # Keep only non-CPU/GPU custom resources, if any. + if resource_counter: + ray_remote_args["resources"] = dict(resource_counter) + + map_batches_kwargs.update(ray_remote_args) + return values + + def get_required_input_keys(self) -> Dict[str, str]: + """The required input keys of the stage and their descriptions.""" + ret = {"prompt": "The text prompt (str)."} + task_type = self.fn_constructor_kwargs.get("task_type", vLLMTaskType.GENERATE) + if task_type == vLLMTaskType.GENERATE: + ret["sampling_params"] = ( + "The sampling parameters. See " + "https://docs.vllm.ai/en/latest/api/inference_params.html#sampling-parameters " + "for details." + ) + return ret + + def get_optional_input_keys(self) -> Dict[str, str]: + """The optional input keys of the stage and their descriptions.""" + return { + "tokenized_prompt": "The tokenized prompt. If provided, the prompt will not be tokenized by the vLLM engine.", + "image": "The image(s) for multimodal input. Accepts a single image or list of images.", + "model": "The model to use for this request. If the model is different from the " + "model set in the stage, then this is a LoRA request.", + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/batch/utils.py b/lib/python3.12/site-packages/ray/llm/_internal/batch/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5cca5d3dd880cfaa635b40d2cfa43219b9c9c53c --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/batch/utils.py @@ -0,0 +1,62 @@ +"""Utility functions for batch processing.""" +import logging +from typing import TYPE_CHECKING, Any, Union + +if TYPE_CHECKING: + from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast + +AnyTokenizer = Union["PreTrainedTokenizer", "PreTrainedTokenizerFast", Any] + +logger = logging.getLogger(__name__) + + +def get_cached_tokenizer(tokenizer: AnyTokenizer) -> AnyTokenizer: + """Get tokenizer with cached properties. + This will patch the tokenizer object in place. + By default, transformers will recompute multiple tokenizer properties + each time they are called, leading to a significant slowdown. This + function caches these properties for faster access. + Args: + tokenizer: The tokenizer object. + Returns: + The patched tokenizer object. + """ + chat_template = getattr(tokenizer, "chat_template", None) + # For VLM, the text tokenizer is wrapped by a processor. + if hasattr(tokenizer, "tokenizer"): + tokenizer = tokenizer.tokenizer + # Some VLM's tokenizer has chat_template attribute (e.g. Qwen/Qwen2-VL-7B-Instruct), + # however some other VLM's tokenizer does not have chat_template attribute (e.g. + # mistral-community/pixtral-12b). Therefore, we cache the processor's chat_template. + if chat_template is None: + chat_template = getattr(tokenizer, "chat_template", None) + + tokenizer_all_special_ids = set(tokenizer.all_special_ids) + tokenizer_all_special_tokens_extended = tokenizer.all_special_tokens_extended + tokenizer_all_special_tokens = set(tokenizer.all_special_tokens) + tokenizer_len = len(tokenizer) + + class CachedTokenizer(tokenizer.__class__): # type: ignore + @property + def all_special_ids(self): + return tokenizer_all_special_ids + + @property + def all_special_tokens(self): + return tokenizer_all_special_tokens + + @property + def all_special_tokens_extended(self): + return tokenizer_all_special_tokens_extended + + @property + def chat_template(self): + return chat_template + + def __len__(self): + return tokenizer_len + + CachedTokenizer.__name__ = f"Cached{tokenizer.__class__.__name__}" + + tokenizer.__class__ = CachedTokenizer + return tokenizer diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7a44581bf5f39a2b7c3307863450d512c8867f7e --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/__init__.py @@ -0,0 +1,14 @@ +from ray.llm._internal.common.observability.logging_utils import ( + disable_vllm_custom_ops_logger_on_cpu_nodes, +) +from ray.llm._internal.serve.observability import setup_observability + +# Set up observability +disable_vllm_custom_ops_logger_on_cpu_nodes() +setup_observability() + + +def _worker_process_setup_hook(): + """Noop setup hook used for ENABLE_WORKER_PROCESS_SETUP_HOOK + (see python/ray/llm/_internal/serve/configs/constants.py).""" + pass diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/base_configs/templates/default_deployment_configs.yaml b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/base_configs/templates/default_deployment_configs.yaml new file mode 100644 index 0000000000000000000000000000000000000000..66da8729b54986d4389009d47940dfbe0321a721 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/base_configs/templates/default_deployment_configs.yaml @@ -0,0 +1,83 @@ +model_id_to_gpu_deployment_configs: + meta-llama/Meta-Llama-3.1-8B-Instruct: + A10G: &base_llama_31_8b_config + model_loading_config: + model_id: meta-llama/Meta-Llama-3.1-8B-Instruct + engine_kwargs: + # Llama 3.1's context length is 128k, but we set a lower one to avoid GPU OOM. + max_model_len: 8192 + max_num_batched_tokens: 2048 + tensor_parallel_size: 1 + enable_chunked_prefill: true + L4: *base_llama_31_8b_config + L40S: *base_llama_31_8b_config + A100-40G: *base_llama_31_8b_config + A100-80G: *base_llama_31_8b_config + H100: *base_llama_31_8b_config + + meta-llama/Meta-Llama-3.1-70B-Instruct: + A100-40G: + model_loading_config: &meta_llama_31_70b_model_loading_config + model_id: meta-llama/Meta-Llama-3.1-70B-Instruct + engine_kwargs: + # Llama 3.1's context length is 128k, but we set a lower one to avoid GPU OOM. + max_model_len: 8192 + max_num_batched_tokens: 2048 + tensor_parallel_size: 8 + enable_chunked_prefill: true + A100-80G: + model_loading_config: *meta_llama_31_70b_model_loading_config + engine_kwargs: + # Llama 3.1's context length is 128k, but we set a lower one to avoid GPU OOM. + max_model_len: 8192 + max_num_batched_tokens: 2048 + tensor_parallel_size: 4 + enable_chunked_prefill: true + H100: + model_loading_config: *meta_llama_31_70b_model_loading_config + engine_kwargs: + # Llama 3.1's context length is 128k, but we set a lower one to avoid GPU OOM. + max_model_len: 8192 + max_num_batched_tokens: 2048 + tensor_parallel_size: 4 + enable_chunked_prefill: true + + meta-llama/Meta-Llama-3.1-405B-Instruct-FP8: + H100: + model_loading_config: &meta_llama_31_405b_model_loading_config + model_id: meta-llama/Meta-Llama-3.1-405B-Instruct-FP8 + engine_kwargs: + # Llama 3.1's context length is 128k, but we set a lower one to avoid GPU OOM. + max_model_len: 8192 + max_num_batched_tokens: 2048 + tensor_parallel_size: 8 + enable_chunked_prefill: true + + mistral-community/pixtral-12b: + L40S: &pixtral_base_12b_config + model_loading_config: &pixtral_base_12b_model_loading_config + model_id: mistral-community/pixtral-12b + engine_kwargs: + tensor_parallel_size: 1 + A100-40G: *pixtral_base_12b_config + A100-80G: *pixtral_base_12b_config + H100: *pixtral_base_12b_config + + meta-llama/Llama-3.2-11B-Vision-Instruct: + L40S: &llama_32_base_11b_config + model_loading_config: &llama_32_base_11b_model_loading_config + model_id: meta-llama/Llama-3.2-11B-Vision-Instruct + engine_kwargs: + tensor_parallel_size: 1 + A100-40G: *llama_32_base_11b_config + A100-80G: *llama_32_base_11b_config + H100: *llama_32_base_11b_config + + meta-llama/Llama-3.2-90B-Vision-Instruct: + A100-40G: &llama_32_base_90b_config + model_loading_config: &llama_32_base_90b_model_loading_config + model_id: meta-llama/Llama-3.2-90B-Vision-Instruct + engine_kwargs: + tensor_parallel_size: 1 + A100-80G: *llama_32_base_90b_config + H100: *llama_32_base_90b_config diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/generator.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/generator.py new file mode 100644 index 0000000000000000000000000000000000000000..2b00f9f85417b05b0d1fe6707571ffa1444ecfae --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/generator.py @@ -0,0 +1,124 @@ +import copy +import os +from typing import Any, Dict + +import yaml + +from ray.llm._internal.serve.config_generator.utils.constants import TEMPLATE_DIR +from ray.llm._internal.serve.config_generator.utils.models import ( + TextCompletionModelConfig, +) +from ray.llm._internal.serve.config_generator.utils.overrides import ( + ask_and_merge_model_overrides, +) +from ray.llm._internal.serve.config_generator.utils.text_completion import ( + populate_text_completion_model_config, +) +from ray.serve._private.constants import DEFAULT_TARGET_ONGOING_REQUESTS +from ray.serve.config import AutoscalingConfig + + +def get_model_base_config( + input_model_config: TextCompletionModelConfig, +) -> Dict[str, Any]: + """ + This method returns the base model config based on user inputs. + """ + return populate_text_completion_model_config(input_model_config) + + +def get_model_config(input_model_config: TextCompletionModelConfig) -> Dict[str, Any]: + """ + This method decorates the base model config based on serving type, such as Lora or function calling. + """ + + model_config = get_model_base_config(input_model_config) + + model_config = maybe_get_lora_enabled_config( + input_model_config, + model_config, + ) + + return model_config + + +def get_serve_config( + model_config_path: str, +): + """ + This util method constructs the final serve config. + + It doesn't inline the model config and, instead, just puts the file path based on the type of models. + """ + serve_config = get_serve_base_config() + serve_config["applications"][0]["args"]["llm_configs"] = [model_config_path] + return serve_config + + +def override_model_configs(base_model_config: Dict[str, Any]) -> Dict[str, Any]: + existing_autoscaling_config = base_model_config["deployment_config"].setdefault( + "autoscaling_config", {} + ) + # Getting the default target ongoing requests from the base model config. + # If not present, default to 2 (same as Ray Serve's default). + default_ongoing_requests = ( + base_model_config.get("deployment_config", {}) + .get("autoscaling_config", {}) + .get("target_ongoing_requests", DEFAULT_TARGET_ONGOING_REQUESTS) + ) + final_autoscaling_config = ask_and_merge_model_overrides( + existing_configs=existing_autoscaling_config, + model_cls=AutoscalingConfig, + fields=["min_replicas", "max_replicas", "target_ongoing_requests"], + defaults={"target_ongoing_requests": default_ongoing_requests}, + ) + final_autoscaling_config["initial_replicas"] = final_autoscaling_config[ + "min_replicas" + ] + base_model_config["deployment_config"][ + "autoscaling_config" + ] = final_autoscaling_config + + # Make sure max_ongoing request is always double of target_ongoing_requests + base_model_config["deployment_config"]["max_ongoing_requests"] = ( + 2 * final_autoscaling_config["target_ongoing_requests"] + ) + + return base_model_config + + +def maybe_get_lora_enabled_config( + input_model_config: TextCompletionModelConfig, + base_config: Dict[str, Any], +): + """ + If the input model contains lora config, this method decorates the base + config with lora-specific configurations. + """ + if input_model_config.lora_config: + max_num_adapters_per_replica = ( + input_model_config.lora_config.max_num_lora_per_replica + ) + + res = copy.deepcopy(base_config) + res["lora_config"] = { + "dynamic_lora_loading_path": input_model_config.lora_config.uri, + "max_num_adapters_per_replica": max_num_adapters_per_replica, + } + + res.setdefault("engine_kwargs", {})["enable_lora"] = True + res.setdefault("engine_kwargs", {})["max_lora_rank"] = 32 + res.setdefault("engine_kwargs", {})["max_loras"] = max_num_adapters_per_replica + return res + else: + return base_config + + +_BASE_SERVE_CONFIG = "base_serve_config.yaml" + + +def get_serve_base_config() -> Dict[str, Any]: + # Construct the full path to the YAML file + file_path = os.path.join(TEMPLATE_DIR, _BASE_SERVE_CONFIG) + with open(file_path, "r") as stream: + return yaml.safe_load(stream) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/inputs.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..706fbb2502c7a1eeba67e24d537a732ac21545d9 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/inputs.py @@ -0,0 +1,220 @@ +from typing import List, Optional + +from huggingface_hub import repo_info +from pydantic import BaseModel +from rich import print +from rich.prompt import Confirm, Prompt + +from ray.llm._internal.serve.config_generator.utils.constants import ( + get_lora_storage_uri, +) +from ray.llm._internal.serve.config_generator.utils.files import ( + clear_hf_token, + get_hf_token, +) +from ray.llm._internal.serve.config_generator.utils.gpu import ( + DEFAULT_MODEL_ID_TO_GPU, + GPUType, + get_available_gpu_types, +) +from ray.llm._internal.serve.config_generator.utils.input_converter import ( + convert_inputs_to_text_completion_model, +) +from ray.llm._internal.serve.config_generator.utils.models import ( + TEXT_COMPLETION_MODEL_TYPE, + ModelType, + TextCompletionLoraModelConfig, + TextCompletionModelConfig, +) +from ray.llm._internal.serve.config_generator.utils.prompt import ( + BoldIntPrompt, + BoldPrompt, +) +from ray.llm._internal.serve.config_generator.utils.text_completion import ( + get_default_llm_config, +) + + +def _get_user_input_from_list(prompt: str, options: List[GPUType]): + while True: + user_input = BoldPrompt.ask( + f"{prompt}", + choices=[option.value for option in options], + ) + for option in options: + if user_input.lower() == option.value.lower(): + return option + print(f"Please enter a valid option from {options}") + + +class BaseModelInfo(BaseModel): + id: str + hf_token: Optional[str] = None + type: ModelType + import_model_storage_uri: Optional[str] = None + reference_model_id: Optional[str] = None + + +def _get_user_input_from_lists( + prompt: str, options: List[str], allow_any_user_input: bool +): + while True: + res = Prompt.ask(prompt) + if allow_any_user_input or res in options: + return res + else: + print("Please enter a valid option.") + + +def _get_model_id(): + model_list = list(DEFAULT_MODEL_ID_TO_GPU.keys()) + model_ids = "\n".join(model_list) + prompt_message = f"""[bold]We have provided the defaults for the following models[not bold]: +{model_ids} +[bold]Please enter the model ID you would like to serve, or enter your own custom model ID. For multi-lora serving, enter only the base model ID""" + + model_id = _get_user_input_from_lists( + prompt_message, model_list, allow_any_user_input=True + ) + return model_id + + +def _get_validated_model_info(): + """ + If the model is supposed to be loaded from huggingface, we make an API call to confirm that + user's credentials work. + """ + while True: + ref_model_id = None + + model_id = _get_model_id() + model_type = TEXT_COMPLETION_MODEL_TYPE + if model_id not in DEFAULT_MODEL_ID_TO_GPU: + ref_model_id = "other" + import_model = Confirm.ask( + "Do you have the model weights stored in your cloud buckets? " + "If not, model weights will be downloaded from HuggingFace.", + default=False, + ) + + if import_model: + # No need to validate since the model is loaded from remote uri + remote_storage_uri = BoldPrompt.ask("Storage uri of your model: ") + return BaseModelInfo( + id=model_id, + hf_token=None, + type=model_type, + import_model_storage_uri=remote_storage_uri, + reference_model_id=ref_model_id, + ) + + hf_token = get_hf_token() + + # Need to validate + try: + repo_info(model_id, token=hf_token) + print( + f"Verified that you can access {model_id} with your HuggingFace token." + ) + return BaseModelInfo( + id=model_id, + hf_token=hf_token, + type=model_type, + reference_model_id=ref_model_id, + ) + except Exception as e: + print(f"An error occurred: {e}") + # Clear cached tf token in case it was incorrectly entered + clear_hf_token() + + print( + "\nUnable to access the HuggingFace repo based on the " + "provided information. Please confirm that you have access " + "to the model and try again." + ) + + +def _get_default_tensor_parallelism(model_id: str, gpu_type: GPUType) -> int: + if model_id in DEFAULT_MODEL_ID_TO_GPU: + llm_config = get_default_llm_config(model_id=model_id, gpu_type=gpu_type) + return llm_config.engine_kwargs.setdefault("tensor_parallel_size", 1) + + return 1 + + +_DEFAULT_NUM_LORA_PER_REPLICA = 16 + + +def get_input_model_via_args( + model_id: str, + hf_token: Optional[str], + gpu_type: GPUType, + tensor_parallelism: int, + enable_lora: Optional[bool], + num_loras_per_replica: Optional[int], +) -> TextCompletionModelConfig: + if enable_lora: + max_num_lora_per_replica = ( + num_loras_per_replica or _DEFAULT_NUM_LORA_PER_REPLICA + ) + default_lora_uri = get_lora_storage_uri() + lora_config = TextCompletionLoraModelConfig( + max_num_lora_per_replica=max_num_lora_per_replica, + uri=default_lora_uri, + ) + else: + lora_config = None + + return convert_inputs_to_text_completion_model( + model_id=model_id, + hf_token=hf_token, + gpu_type=gpu_type, + tensor_parallelism=tensor_parallelism, + lora_config=lora_config, + ) + + +def get_input_model_via_interactive_inputs() -> TextCompletionModelConfig: + """ + This method constructs a corresponding model object based on user inputs. + + This model object should be used to construct the final serve config. + """ + base_model_info = _get_validated_model_info() + + available_gpu_types = get_available_gpu_types(base_model_info.id) + gpu_type = _get_user_input_from_list("GPU type", available_gpu_types) + + default_tensor_parallelism = _get_default_tensor_parallelism( + base_model_info.id, gpu_type=gpu_type + ) + + tensor_parallelism = BoldIntPrompt.ask( + "Tensor parallelism", default=default_tensor_parallelism + ) + + is_lora_enabled = Confirm.ask("[bold]Enable LoRA serving", default=False) + + if is_lora_enabled: + default_lora_uri = get_lora_storage_uri() + max_num_lora_per_replica = BoldIntPrompt.ask( + "Maximum number of LoRA models per replica", + default=_DEFAULT_NUM_LORA_PER_REPLICA, + ) + lora_uri = BoldPrompt.ask("LoRA storage uri", default=default_lora_uri) + lora_config = TextCompletionLoraModelConfig( + max_num_lora_per_replica=max_num_lora_per_replica, + uri=lora_uri, + ) + else: + lora_config = None + + return convert_inputs_to_text_completion_model( + model_id=base_model_info.id, + hf_token=base_model_info.hf_token, + remote_storage_uri=base_model_info.import_model_storage_uri, + gpu_type=gpu_type, + tensor_parallelism=tensor_parallelism, + lora_config=lora_config, + reference_model_id=base_model_info.reference_model_id, + ) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/start.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/start.py new file mode 100644 index 0000000000000000000000000000000000000000..97c8e6d512669abd02ede09c30af92431f1674bb --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/config_generator/start.py @@ -0,0 +1,224 @@ +import datetime +import os +from typing import Any, Dict, Optional + +import typer +import yaml +from rich import print +from rich.prompt import Confirm +from typing_extensions import Annotated + +from ray.llm._internal.serve.config_generator.generator import ( + get_model_config, + get_serve_config, + override_model_configs, +) +from ray.llm._internal.serve.config_generator.inputs import ( + get_input_model_via_args, + get_input_model_via_interactive_inputs, +) +from ray.llm._internal.serve.config_generator.utils.gpu import ( + DEFAULT_MODEL_ID_TO_GPU, + GPUType, +) + + +def _format_yaml_dumper(): + """Changes the YAML dumper's format.""" + + # Prompt formats often contain newline characters. We modify the YAML + # dumper here, so it prints newline characters instead of actual newlines. + def newline_representer(dumper, data): + if "\n" in data: + # Only adjust the dumper if a newline is present. + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style='"') + else: + # Use the default dumper otherwise. + return dumper.represent_scalar("tag:yaml.org,2002:str", data) + + yaml.SafeDumper.add_representer(str, newline_representer) + + +def _write_model_config_to_disk( + model_config: Dict[str, Any], model_id: str, timestamp: str +) -> str: + # Set the filename with prefix, timestamp, and suffix + sanitized_model_id = model_id.replace("/", "--").replace(".", "_") + file_name = f"{sanitized_model_id}_{timestamp}.yaml" + file_path = "./model_config" # Directory where the file will be saved + + os.makedirs(file_path, exist_ok=True) + + # Create the full path for the file + full_file_path = f"{file_path}/{file_name}" + + # Dump the configuration to the file + with open(full_file_path, "w+") as file: + yaml.safe_dump(model_config, file) + return full_file_path + + +def _write_config_to_disk(serve_config: Dict[str, Any], timestamp: str): + # Set the filename with prefix, timestamp, and suffix + file_name = f"serve_{timestamp}.yaml" + file_path = "./" # Directory where the file will be saved + + # Create the full path for the file + full_file_path = f"{file_path}{file_name}" + + # Dump the configuration to the file + with open(full_file_path, "w+") as file: + yaml.safe_dump(serve_config, file) + + return full_file_path + + +def _get_model_with_validation( + model_id: Optional[str], + hf_token: Optional[str], + gpu_type: Optional[GPUType], + tensor_parallelism: Optional[int], + enable_lora: Optional[bool], + num_loras_per_replica: Optional[int], +): + if model_id and model_id not in DEFAULT_MODEL_ID_TO_GPU: + raise RuntimeError( + "In non-interactive CLI mode, please only specify model ID whose configs are provided by default." + ) + + if model_id and gpu_type and tensor_parallelism: + return get_input_model_via_args( + model_id, + hf_token=hf_token, + gpu_type=gpu_type, + tensor_parallelism=tensor_parallelism, + enable_lora=enable_lora, + num_loras_per_replica=num_loras_per_replica, + ) + else: + raise RuntimeError( + "If specifying args, please make sure all arguments are specified." + ) + + +def gen_config( + model_id: Annotated[ + Optional[str], + typer.Option( + help="HuggingFace model ID to download and run", + hidden=True, + ), + ] = None, + hf_token: Annotated[ + Optional[str], typer.Option(help="Huggingface token", hidden=True) + ] = None, + gpu_type: Annotated[ + Optional[GPUType], typer.Option(help="Type of GPU", hidden=True) + ] = None, + tensor_parallelism: Annotated[ + Optional[int], typer.Option(help="Number of tensor parallelism", hidden=True) + ] = None, + enable_lora: Annotated[ + Optional[bool], typer.Option(help="Whether to enable LoRA serving", hidden=True) + ] = False, + # TODO (shrekris): Expose these hidden options after the API stabilizes. + external_model_id: Annotated[ + Optional[str], + typer.Option( + help="[EXPERIMENTAL] Model ID to expose to end users. Defaults to model_id.", + hidden=True, + ), + ] = None, + override_lora_uri: Annotated[ + Optional[str], + typer.Option(help="[EXPERIMENTAL] URI to LoRA weights", hidden=True), + ] = None, + num_loras_per_replica: Annotated[ + Optional[int], + typer.Option( + help="[EXPERIMENTAL] Number of LoRA weights per replica", hidden=True + ), + ] = None, + enable_chunk_prefill: Annotated[ + Optional[bool], + typer.Option( + "--enable-chunked-prefill/--disable-chunked-prefill", + help="Whether to enable chunked prefill", + hidden=True, + ), + ] = None, + max_num_batched_tokens: Annotated[ + Optional[int], + typer.Option(help="Max Number of batched tokens", hidden=True), + ] = None, +): + """Starts an interactive session to generate RayLLM YAML configs.""" + + if (override_lora_uri or num_loras_per_replica) and not enable_lora: + raise ValueError( + "--enable-lora is not set. --enable-lora must be set " + "to use --override-lora-uri or --num-loras-per-replica." + ) + + if model_id or gpu_type or tensor_parallelism: + interactive_mode = False + input_model_config = _get_model_with_validation( + model_id=model_id, + hf_token=hf_token, + gpu_type=gpu_type, + tensor_parallelism=tensor_parallelism, + enable_lora=enable_lora, + num_loras_per_replica=num_loras_per_replica, + ) + else: + # sets up conditional vars for the interactive mode to not skip prompts + interactive_mode = True + input_model_config = get_input_model_via_interactive_inputs() + + model_config = get_model_config(input_model_config) + if interactive_mode and Confirm.ask( + "[bold]Further customize the auto-scaling config", default=False + ): + model_config = override_model_configs(model_config) + + if external_model_id: + model_config["model_loading_config"]["model_id"] = external_model_id + + if override_lora_uri: + model_config["lora_config"]["dynamic_lora_loading_path"] = override_lora_uri + + if enable_chunk_prefill is not None: + model_config["engine_kwargs"]["enable_chunked_prefill"] = enable_chunk_prefill + + if max_num_batched_tokens is not None: + model_config["engine_kwargs"]["max_num_batched_tokens"] = max_num_batched_tokens + + timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + + _format_yaml_dumper() + + # Write the model config file to disk + model_config_path = _write_model_config_to_disk( + model_config, input_model_config.id, timestamp + ) + serve_config = get_serve_config(model_config_path) + + full_file_path = _write_config_to_disk(serve_config, timestamp) + + print( + f"\nYour serve configuration file is successfully written to {full_file_path}\n" + ) + + if input_model_config.id not in DEFAULT_MODEL_ID_TO_GPU: + print( + f"Note: we don't have serving defaults for {input_model_config.id} " + "and we generally recommend you going over engine_kwargs in the yaml before serving. " + ) + + # Define the serve run command + command_str = f"serve run {full_file_path}" + print(f"Start the RayLLM app on this workspace by running: {command_str}") + + +if __name__ == "__main__": + typer.run(gen_config) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/constants.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..68307ca54a67cdcb897220c599951bf54cf540de --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/constants.py @@ -0,0 +1,103 @@ +import os + +ALLOW_NEW_PLACEMENT_GROUPS_IN_DEPLOYMENT = int( + os.getenv("RAYLLM_ALLOW_NEW_PLACEMENT_GROUPS_IN_DEPLOYMENT", "1") +) + + +# Timeout before download in multiplex deployment fails. <=0 means no timeout. +DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S = float( + os.getenv("DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S", "30") +) +if DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S <= 0: + DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S = None + + +# Number of retries for downloading a model in multiplex deployment. +DEFAULT_MULTIPLEX_DOWNLOAD_TRIES = int( + os.getenv("DEFAULT_MULTIPLEX_DOWNLOAD_RETRIES", "3") +) + + +# If true, a default runtime_env will be injected to import rayllm on worker startup. +# This is a startup time optimization to avoid the latency penalty of sequentially +# importing rayllm in multiple layers of worker processes. +ENABLE_WORKER_PROCESS_SETUP_HOOK = ( + os.environ.get("RAYLLM_ENABLE_WORKER_PROCESS_SETUP_HOOK", "1") == "1" +) + + +CLOUD_OBJECT_MISSING_EXPIRE_S = 30 +CLOUD_OBJECT_EXISTS_EXPIRE_S = 60 * 60 + +# Sentinel object used to indicate that a LoRA adapter config file is missing. +LORA_ADAPTER_CONFIG_NAME = "adapter_config.json" + +DEFAULT_HEALTH_CHECK_PERIOD_S = int( + os.getenv("RAY_SERVE_LLM_DEFAULT_HEALTH_CHECK_PERIOD_S", "10") +) +DEFAULT_HEALTH_CHECK_TIMEOUT_S = int( + os.getenv("RAY_SERVE_LLM_DEFAULT_HEALTH_CHECK_TIMEOUT_S", "10") +) +DEFAULT_MAX_ONGOING_REQUESTS = int( + os.getenv("RAY_SERVE_LLM_DEFAULT_MAX_ONGOING_REQUESTS", str(int(1e9))) +) +DEFAULT_MAX_REPLICAS = int(os.getenv("RAY_SERVE_LLM_DEFAULT_MAX_REPLICAS", "10")) +DEFAULT_MAX_TARGET_ONGOING_REQUESTS = int( + os.getenv("RAY_SERVE_LLM_DEFAULT_MAX_TARGET_ONGOING_REQUESTS", str(int(1e9))) +) + + +ENGINE_START_TIMEOUT_S = int(os.getenv("RAYLLM_ENGINE_START_TIMEOUT_S", str(60 * 60))) + +MIN_NUM_TOPLOGPROBS_ALLOWED = 0 +MAX_NUM_TOPLOGPROBS_ALLOWED = 5 +MODEL_RESPONSE_BATCH_TIMEOUT_MS = float( + os.getenv("RAYLLM_MODEL_RESPONSE_BATCH_TIMEOUT_MS", "50") +) +RAYLLM_ENABLE_REQUEST_PROMPT_LOGS = ( + os.environ.get("RAYLLM_ENABLE_REQUEST_PROMPT_LOGS", "1") == "1" +) +RAYLLM_GUIDED_DECODING_BACKEND = os.environ.get( + "RAYLLM_GUIDED_DECODING_BACKEND", "xgrammar" +) + +MAX_NUM_STOPPING_SEQUENCES = int(os.getenv("RAYLLM_MAX_NUM_STOPPING_SEQUENCES", "8")) +ENV_VARS_TO_PROPAGATE = { + "HUGGING_FACE_HUB_TOKEN", + "HF_TOKEN", +} +# timeout in 10 minutes. Streaming can take longer than 3 min +DEFAULT_LLM_ROUTER_HTTP_TIMEOUT = float( + os.environ.get("RAY_SERVE_LLM_ROUTER_HTTP_TIMEOUT", 600) +) + +ENABLE_VERBOSE_TELEMETRY = bool(int(os.getenv("RAYLLM_ENABLE_VERBOSE_TELEMETRY", "0"))) + +RAYLLM_VLLM_ENGINE_CLS_ENV = "RAYLLM_VLLM_ENGINE_CLS" + +# The ratio of number of router replicas to number of model replicas. +# Default to 2 meaning that there are 2 router replicas for every model replica. +DEFAULT_ROUTER_TO_MODEL_REPLICA_RATIO = float( + os.getenv("RAY_SERVE_LLM_ROUTER_TO_MODEL_REPLICA_RATIO", "2") +) + +DEFAULT_LLM_ROUTER_MIN_REPLICAS = int( + os.environ.get("RAY_SERVE_LLM_ROUTER_MIN_REPLICAS", 2) +) +DEFAULT_LLM_ROUTER_INITIAL_REPLICAS = int( + os.environ.get("RAY_SERVE_LLM_ROUTER_INITIAL_REPLICAS", 2) +) +DEFAULT_LLM_ROUTER_MAX_REPLICAS = int( + os.environ.get("RAY_SERVE_LLM_ROUTER_MAX_REPLICAS", 1000) +) +DEFAULT_LLM_ROUTER_TARGET_ONGOING_REQUESTS = int( + os.environ.get( + "RAY_SERVE_LLM_ROUTER_TARGET_ONGOING_REQUESTS", + DEFAULT_MAX_TARGET_ONGOING_REQUESTS, + ) +) + + +# HOME DIR +RAYLLM_HOME_DIR = os.environ.get("RAYLLM_HOME_DIR", os.path.expanduser("~/.ray/llm")) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2203755e8c3722dbbff5fc43bcf5d4f435a7828d Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/protocol.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..871b69dc8b9d3aef589d4fe87b85e1fbca30a02f Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/__pycache__/protocol.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce6a765c84728b16a5990bfc24615709094a59f1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/llm_config.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/llm_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0588bdd5e150e0a28d6616fc0a6a88144bbbae57 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/llm_config.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/openai_api_models.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/openai_api_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84e51863c731c518745f10268003a919899f102d Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/__pycache__/openai_api_models.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/llm_config.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/llm_config.py new file mode 100644 index 0000000000000000000000000000000000000000..c4d25a6978efdd55312a1b4c070635955d8f5153 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/llm_config.py @@ -0,0 +1,516 @@ +from enum import Enum +from typing import ( + Any, + Dict, + Optional, + TypeVar, + Union, +) + +from pydantic import ( + BaseModel, + Field, + PositiveInt, + PrivateAttr, + field_validator, + model_validator, +) + +import ray.util.accelerators.accelerators as accelerators +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.llm._internal.common.callbacks.base import ( + CallbackBase, + CallbackConfig, +) +from ray.llm._internal.common.utils.cloud_utils import ( + CloudMirrorConfig, + is_remote_path, +) +from ray.llm._internal.common.utils.download_utils import ( + STREAMING_LOAD_FORMATS, + NodeModelDownloadable, +) +from ray.llm._internal.common.utils.import_utils import load_class, try_import +from ray.llm._internal.serve.constants import ( + DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S, + DEFAULT_MULTIPLEX_DOWNLOAD_TRIES, + MODEL_RESPONSE_BATCH_TIMEOUT_MS, +) +from ray.llm._internal.serve.engines.vllm.kv_transfer.factory import ( + KVConnectorBackendFactory, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.serve._private.config import DeploymentConfig + +transformers = try_import("transformers") + + +GPUType = Enum("GPUType", vars(accelerators)) +ModelT = TypeVar("ModelT", bound=BaseModel) + + +logger = get_logger(__name__) + + +class ServeMultiplexConfig(BaseModelExtended): + max_num_models_per_replica: PositiveInt = Field( + ..., description="The maximum number of models to be loaded on each replica." + ) + download_timeout_s: Optional[float] = Field( + DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S, + description="How much time the download subprocess has to download a single LoRA before a timeout. None means no timeout.", + ) + max_download_tries: int = Field( + DEFAULT_MULTIPLEX_DOWNLOAD_TRIES, + description="The maximum number of download retries.", + ) + + +class InputModality(str, Enum): + text = "text" + image = "image" + + +class LLMEngine(str, Enum): + """Enum that represents an LLMEngine.""" + + vLLM = "vLLM" + + +class LoraConfig(BaseModelExtended): + dynamic_lora_loading_path: Optional[str] = Field( + default=None, + description="Cloud storage path where LoRA adapter weights are stored.", + ) + max_num_adapters_per_replica: PositiveInt = Field( + default=16, + description="The maximum number of adapters load on each replica.", + ) + download_timeout_s: Optional[float] = Field( + DEFAULT_MULTIPLEX_DOWNLOAD_TIMEOUT_S, + description=( + "How much time the download subprocess has to download a single " + "LoRA before a timeout. None means no timeout." + ), + ) + max_download_tries: int = Field( + DEFAULT_MULTIPLEX_DOWNLOAD_TRIES, + description="The maximum number of download retries.", + ) + + @field_validator("dynamic_lora_loading_path") + def validate_dynamic_lora_loading_path(cls, value: Optional[str]): + if value is None: + return value + + assert is_remote_path(value), ( + "Only AWS S3, Google Cloud Storage, and Azure Storage are supported. The " + 'dynamic_lora_loading_path must start with "s3://", "gs://", "abfss://", or "azure://". ' + f'Got "{value}" instead.' + ) + return value.rstrip("/") + + +class ModelLoadingConfig(BaseModelExtended): + + model_id: str = Field( + description="The ID that should be used by end users to access this model.", + ) + model_source: Optional[Union[str, CloudMirrorConfig]] = Field( + default=None, + description=( + "Where to obtain the model weights from. " + "Should be a HuggingFace model ID, S3 mirror config, GCS mirror config, " + "or a local path. When omitted, defaults to the model_id as a " + "HuggingFace model ID." + ), + ) + tokenizer_source: Optional[str] = Field( + default=None, + description=( + "Where to obtain the tokenizer from. If None, tokenizer is " + "obtained from the model source. Only HuggingFace IDs are " + "supported for now." + ), + ) + + +EngineConfigType = Union[None, "VLLMEngineConfig"] # noqa: F821 + + +class LLMConfig(BaseModelExtended): + + runtime_env: Optional[Dict[str, Any]] = Field( + default=None, + description=( + "The runtime_env to use for the model deployment replica " + "and the engine workers." + ), + ) + + model_loading_config: Union[Dict[str, Any], ModelLoadingConfig] = Field( + description="The settings for how to download and expose the model. Validated against ModelLoadingConfig." + ) + + llm_engine: str = Field( + default=LLMEngine.vLLM.value, + description=f"The LLMEngine that should be used to run the model. Only the following values are supported: {str([t.value for t in LLMEngine])}", + ) + + engine_kwargs: Dict[str, Any] = Field( + default={}, + description=( + "Additional keyword arguments for the engine. In case of vLLM, " + "this will include all the configuration knobs they provide out " + "of the box, except for tensor-parallelism which is set " + "automatically from Ray Serve configs." + ), + ) + + accelerator_type: Optional[str] = Field( + default=None, + description=f"The type of accelerator runs the model on. Only the following values are supported: {str([t.value for t in GPUType])}", + ) + + placement_group_config: Optional[Dict[str, Any]] = Field( + default=None, + description=( + "Ray placement group configuration for scheduling vLLM engine workers. " + "Defines resource bundles and placement strategy for multi-node deployments. " + "Should contain 'bundles' (list of resource dicts) and optionally 'strategy' " + "(defaults to 'PACK'). Example: {'bundles': [{'GPU': 1, 'CPU': 2}], 'strategy': 'PACK'}" + ), + ) + + lora_config: Optional[Union[Dict[str, Any], LoraConfig]] = Field( + default=None, + description="Settings for LoRA adapter. Validated against LoraConfig.", + ) + + deployment_config: Dict[str, Any] = Field( + default_factory=dict, + description=""" + The Ray @server.deployment options. + Supported fields are: + `name`, `num_replicas`, `ray_actor_options`, `max_ongoing_requests`, + `autoscaling_config`, `max_queued_requests`, `user_config`, + `health_check_period_s`, `health_check_timeout_s`, + `graceful_shutdown_wait_loop_s`, `graceful_shutdown_timeout_s`, + `logging_config`, `request_router_config`. + For more details, see the `Ray Serve Documentation `_. + """, + ) + + experimental_configs: Dict[str, Any] = Field( + default_factory=dict, + description="Experimental configurations for Ray Serve LLM. This is a " + "dictionary of key-value pairs. Current supported keys are:\n" + "- `stream_batching_interval_ms`: Ray Serve LLM batches streaming " + "requests together. This config decides how long to wait for the " + "batch before processing the requests. Defaults to " + f"{MODEL_RESPONSE_BATCH_TIMEOUT_MS}.\n" + "- `num_ingress_replicas`: The number of replicas for the router. Ray " + "Serve will take the max amount all the replicas. Default would be 2 " + "router replicas per model replica.\n", + ) + + log_engine_metrics: Optional[bool] = Field( + default=True, + description="Enable additional engine metrics via Ray Prometheus port.", + ) + + callback_config: CallbackConfig = Field( + default_factory=CallbackConfig, + description="Callback configuration to use for model initialization. Can be a string path to a class or a Callback subclass.", + ) + + _supports_vision: bool = PrivateAttr(False) + _model_architecture: str = PrivateAttr("UNSPECIFIED") + _engine_config: EngineConfigType = PrivateAttr(None) + _callback_instance: Optional[CallbackBase] = PrivateAttr(None) + + def _infer_supports_vision(self, model_id_or_path: str) -> None: + """Called in llm node initializer together with other transformers calls. It + loads the model config from huggingface and sets the supports_vision + attribute based on whether the config has `vision_config`. All LVM models has + `vision_config` setup. + """ + try: + hf_config = transformers.PretrainedConfig.from_pretrained(model_id_or_path) + self._supports_vision = hasattr(hf_config, "vision_config") + except Exception as e: + raise ValueError( + f"Failed to load Hugging Face config for model_id='{model_id_or_path}'.\ + Ensure `model_id` is a valid Hugging Face repo or a local path that \ + contains a valid `config.json` file. " + f"Original error: {repr(e)}" + ) from e + + def _set_model_architecture( + self, + model_id_or_path: Optional[str] = None, + model_architecture: Optional[str] = None, + ) -> None: + """Called in llm node initializer together with other transformers calls. It + loads the model config from huggingface and sets the model_architecture + attribute based on whether the config has `architectures`. + """ + if model_id_or_path: + try: + hf_config = transformers.PretrainedConfig.from_pretrained( + model_id_or_path + ) + if ( + hf_config + and hasattr(hf_config, "architectures") + and hf_config.architectures + ): + self._model_architecture = hf_config.architectures[0] + except Exception as e: + raise ValueError( + f"Failed to load Hugging Face config for model_id='{model_id_or_path}'.\ + Ensure `model_id` is a valid Hugging Face repo or a local path that \ + contains a valid `config.json` file. " + f"Original error: {repr(e)}" + ) from e + + if model_architecture: + self._model_architecture = model_architecture + + def apply_checkpoint_info( + self, model_id_or_path: str, trust_remote_code: bool = False + ) -> None: + """Apply the checkpoint info to the model config.""" + self._infer_supports_vision(model_id_or_path) + self._set_model_architecture(model_id_or_path) + + def get_or_create_callback(self) -> Optional[CallbackBase]: + """Get or create the callback instance for this process. + + This ensures one callback instance per process (singleton pattern). + The instance is cached so the same object is used across all hooks. + + Returns: + Instance of class that implements Callback + """ # Return cached instance if exists + if self._callback_instance is not None: + return self._callback_instance + + engine_config = self.get_engine_config() + assert engine_config is not None + pg = engine_config.get_or_create_pg() + runtime_env = engine_config.get_runtime_env_with_local_env_vars() + if self.engine_kwargs.get("load_format", None) in STREAMING_LOAD_FORMATS: + worker_node_download_model = NodeModelDownloadable.NONE + else: + worker_node_download_model = NodeModelDownloadable.MODEL_AND_TOKENIZER + + # Create new instance + if isinstance(self.callback_config.callback_class, str): + callback_class = load_class(self.callback_config.callback_class) + else: + callback_class = self.callback_config.callback_class + + self._callback_instance = callback_class( + raise_error_on_callback=self.callback_config.raise_error_on_callback, + llm_config=self, + ctx_kwargs={ + "worker_node_download_model": worker_node_download_model, + "placement_group": pg, + "runtime_env": runtime_env, + }, + **self.callback_config.callback_kwargs, + ) + return self._callback_instance + + @property + def supports_vision(self) -> bool: + return self._supports_vision + + @property + def model_architecture(self) -> str: + return self._model_architecture + + @property + def input_modality(self) -> str: + """Returns the input modality of the model. There could be more types in the + future. Right now assumes if the model doesn't support version, it'll be text. + """ + if self.supports_vision: + return InputModality.image.value + + return InputModality.text.value + + @property + def model_id(self) -> str: + return self.model_loading_config.model_id + + @property + def max_request_context_length(self) -> Optional[int]: + return self.engine_kwargs.get("max_model_len") + + @field_validator("accelerator_type") + def validate_accelerator_type(cls, value: Optional[str]): + if value is None: + return value + + # Ensure A10 is converted to A10G. + if value == "A10": + value = "A10G" + + if value not in [t.value for t in GPUType]: + raise ValueError(f"Unsupported accelerator type: {value}") + + return value + + @field_validator("llm_engine") + def validate_llm_engine(cls, value: str) -> str: + """Validates the llm_engine string value.""" + try: + # Validate the engine + LLMEngine(value) + except ValueError as e: + raise ValueError(f"Unsupported engine: {value}") from e + return value + + @field_validator("deployment_config") + def validate_deployment_config(cls, value: Dict[str, Any]) -> Dict[str, Any]: + """Validates the deployment config dictionary.""" + try: + DeploymentConfig(**value) + except Exception as e: + raise ValueError(f"Invalid deployment config: {value}") from e + + return value + + @field_validator("model_loading_config") + def validate_model_loading_config( + cls, value: Union[Dict[str, Any], ModelLoadingConfig] + ) -> ModelLoadingConfig: + """Validates the model loading config dictionary.""" + if isinstance(value, ModelLoadingConfig): + return value + + try: + model_loading_config = ModelLoadingConfig(**value) + except Exception as e: + raise ValueError(f"Invalid model_loading_config: {value}") from e + + return model_loading_config + + @field_validator("lora_config") + def validate_lora_config( + cls, value: Optional[Union[Dict[str, Any], LoraConfig]] + ) -> Optional[LoraConfig]: + """Validates the lora config dictionary.""" + if value is None or isinstance(value, LoraConfig): + return value + + try: + lora_config = LoraConfig(**value) + except Exception as e: + raise ValueError(f"Invalid lora_config: {value}") from e + + return lora_config + + @field_validator("experimental_configs") + def validate_experimental_configs(cls, value: Dict[str, Any]) -> Dict[str, Any]: + """Validates the experimental configs dictionary.""" + # TODO(Kourosh): Remove this deprecation check after users have + # migrated. + if "num_router_replicas" in value: + raise ValueError( + "The 'num_router_replicas' key in experimental_configs has " + "been renamed to 'num_ingress_replicas'. Please update " + "your configuration to use 'num_ingress_replicas' instead." + ) + return value + + @model_validator(mode="after") + def _check_log_stats_with_metrics(self): + """Validate that disable_log_stats isn't enabled when log_engine_metrics is enabled.""" + if self.log_engine_metrics and self.engine_kwargs.get("disable_log_stats"): + raise ValueError( + "disable_log_stats cannot be set to True when log_engine_metrics is enabled. " + "Engine metrics require log stats to be enabled." + ) + + return self + + def multiplex_config(self) -> ServeMultiplexConfig: + multiplex_config = None + if self.lora_config: + multiplex_config = ServeMultiplexConfig( + max_num_models_per_replica=self.lora_config.max_num_adapters_per_replica, + download_timeout_s=self.lora_config.download_timeout_s, + max_download_tries=self.lora_config.max_download_tries, + ) + return multiplex_config + + def get_engine_config(self) -> EngineConfigType: + """Returns the engine config for the given LLM config. + + LLMConfig not only has engine config but also deployment config, etc. + """ + # Note (genesu): This is important that we cache the engine config as the + # `hf_model_id` attribute on the engine config will be set based on whether + # the model is downloaded from a remote storage and will be set to the + # local path of the model. This is important for vLLM not going to Hugging + # Face to download the model again after it's already downloaded during node + # initialization step. + if self._engine_config: + return self._engine_config + + if self.llm_engine == LLMEngine.vLLM: + from ray.llm._internal.serve.engines.vllm.vllm_models import ( + VLLMEngineConfig, + ) + + self._engine_config = VLLMEngineConfig.from_llm_config(self) + else: + # Note (genesu): This should never happen because we validate the engine + # in the config. + raise ValueError(f"Unsupported engine: {self.llm_engine}") + + return self._engine_config + + def update_engine_kwargs(self, **kwargs: Any) -> None: + """Update the engine_kwargs and the engine_config engine_kwargs. + + This is typically called during engine starts, when certain engine_kwargs + (e.g., data_parallel_rank) become available. + """ + self.engine_kwargs.update(kwargs) + # engine_config may be created before engine starts, this makes sure + # the engine_config is updated with the latest engine_kwargs. + if self._engine_config: + self._engine_config.engine_kwargs.update(kwargs) + + def setup_engine_backend(self): + self._setup_kv_connector_backend() + + def _setup_kv_connector_backend(self): + """Private method to setup kv connector depending on the local deployment state""" + # 1. validate that the backend is one of the backends supported (Nixl or LMCache) + kv_transfer_config = self.engine_kwargs.get("kv_transfer_config") + if not kv_transfer_config: + return + + kv_connector = kv_transfer_config.get("kv_connector") + if not kv_connector: + raise ValueError("Connector type is not specified.") + + # 2. Setup the backend using factory + kv_connector_backend = KVConnectorBackendFactory.create_backend( + kv_connector, self + ) + kv_connector_backend.setup() + + +class DiskMultiplexConfig(BaseModelExtended): + model_id: str + max_total_tokens: Optional[int] + local_path: str + + # this is a per process id assigned to the model + lora_assigned_int_id: int diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/openai_api_models.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/openai_api_models.py new file mode 100644 index 0000000000000000000000000000000000000000..9fc708ce0bc64981fa4bbb998bce32be86f4d079 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/configs/openai_api_models.py @@ -0,0 +1,234 @@ +"""This module contains the wrapper classes for vLLM's OpenAI implementation. + +If there are any major differences in the interface, the expectation is that +they will be upstreamed to vLLM. +""" + +from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union + +from pydantic import ( + BaseModel, + ConfigDict, + Field, +) +from vllm.entrypoints.openai.protocol import ( + ChatCompletionRequest as vLLMChatCompletionRequest, + ChatCompletionResponse as vLLMChatCompletionResponse, + ChatCompletionStreamResponse as vLLMChatCompletionStreamResponse, + CompletionRequest as vLLMCompletionRequest, + CompletionResponse as vLLMCompletionResponse, + CompletionStreamResponse as vLLMCompletionStreamResponse, + EmbeddingChatRequest as vLLMEmbeddingChatRequest, + EmbeddingCompletionRequest as vLLMEmbeddingCompletionRequest, + EmbeddingResponse as vLLMEmbeddingResponse, + ErrorInfo as vLLMErrorInfo, + ErrorResponse as vLLMErrorResponse, + ScoreRequest as vLLMScoreRequest, + ScoreResponse as vLLMScoreResponse, + TranscriptionRequest as vLLMTranscriptionRequest, + TranscriptionResponse as vLLMTranscriptionResponse, + TranscriptionStreamResponse as vLLMTranscriptionStreamResponse, +) +from vllm.utils import random_uuid + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + + +class ChatCompletionRequest(vLLMChatCompletionRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ChatCompletionResponse(vLLMChatCompletionResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ChatCompletionStreamResponse(vLLMChatCompletionStreamResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ErrorInfo(vLLMErrorInfo): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ErrorResponse(vLLMErrorResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +# TODO (Kourosh): Upstream +class CompletionRequest(vLLMCompletionRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + request_id: str = Field( + default_factory=lambda: f"{random_uuid()}", + description=( + "The request_id related to this request. If the caller does " + "not set it, a random_uuid will be generated. This id is used " + "through out the inference process and return in response." + ), + ) + + +class CompletionResponse(vLLMCompletionResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class CompletionStreamResponse(vLLMCompletionStreamResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +# TODO (Kourosh): Upstream +class EmbeddingCompletionRequest(vLLMEmbeddingCompletionRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + request_id: str = Field( + default_factory=lambda: f"{random_uuid()}", + description=( + "The request_id related to this request. If the caller does " + "not set it, a random_uuid will be generated. This id is used " + "through out the inference process and return in response." + ), + ) + + +class EmbeddingChatRequest(vLLMEmbeddingChatRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class EmbeddingResponse(vLLMEmbeddingResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class TranscriptionRequest(vLLMTranscriptionRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + request_id: str = Field( + default_factory=lambda: f"{random_uuid()}", + description=( + "The request_id related to this request. If the caller does " + "not set it, a random_uuid will be generated. This id is used " + "through out the inference process and return in response." + ), + ) + + +class TranscriptionResponse(vLLMTranscriptionResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class TranscriptionStreamResponse(vLLMTranscriptionStreamResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ScoreRequest(vLLMScoreRequest): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class ScoreResponse(vLLMScoreResponse): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +EmbeddingRequest = Union[EmbeddingCompletionRequest, EmbeddingChatRequest] + +LLMEmbeddingsResponse = Union[ + AsyncGenerator[Union[EmbeddingResponse, ErrorResponse], None], +] + +LLMScoreResponse = Union[ + AsyncGenerator[Union[ScoreResponse, ErrorResponse], None], +] + +LLMChatResponse = Union[ + AsyncGenerator[ + Union[str, ChatCompletionStreamResponse, ChatCompletionResponse, ErrorResponse], + None, + ], +] + +LLMCompletionsResponse = Union[ + AsyncGenerator[ + Union[str, CompletionStreamResponse, CompletionResponse, ErrorResponse], None + ], +] + +LLMTranscriptionResponse = Union[ + AsyncGenerator[ + Union[str, TranscriptionStreamResponse, TranscriptionResponse, ErrorResponse], + None, + ], +] + + +# TODO: remove this class +class OpenAIHTTPException(Exception): + def __init__( + self, + status_code: int, + message: str, + type: str = "Unknown", + internal_message: Optional[str] = None, + ) -> None: + self.status_code = status_code + self.message = message + self.type = type + self.internal_message = internal_message + + +# TODO: upstream metadata for ModelData +# Compared to vLLM this has a metadata field. +class ModelCard(BaseModel): + model_config = ConfigDict( + protected_namespaces=tuple(), arbitrary_types_allowed=True + ) + + id: str + object: str + owned_by: str + permission: List[str] + metadata: Dict[str, Any] + + @property + def model_type(self) -> str: + return self.metadata["engine_config"]["model_type"] + + +class ModelList(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + data: List[ModelCard] + object: str = "list" + + +def to_model_metadata( + model_id: str, + model_config: "LLMConfig", + overrides: Optional[Dict[str, Any]] = None, +) -> ModelCard: + """Creates an OpenAI-compatible ModelData object. + + Args: + model_id: The ID of the model. Should contain the suffix if the model + is LoRA fine-tuned. For example: + meta-llama/Llama-2-7b-chat-hf:my_suffix:aBc1234 + model_config: The model's YAML config. + overrides: should only be set for LoRA fine-tuned models. The + overrides of the fine-tuned model metadata. + + Returns: + A ModelCard object. + """ + metadata = { + "model_id": model_config.model_id, + "input_modality": model_config.input_modality, + "max_request_context_length": model_config.max_request_context_length, + } + + if overrides: + metadata.update(overrides) + + return ModelCard( + id=model_id, + object="model", + owned_by="organization-owner", + permission=[], + metadata=metadata, + ) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7d840c437e2bc3089dc6c378109ef859c86f2f3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/protocol.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cf735079328320db870278dda4c7e0dbee347c0 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/__pycache__/protocol.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/protocol.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..c36b8073d0dab53c5886b1291b5a9e95f5b71eda --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/engine/protocol.py @@ -0,0 +1,178 @@ +import abc +from typing import TYPE_CHECKING, AsyncGenerator, Union + +from ray.llm._internal.serve.core.configs.llm_config import ( + DiskMultiplexConfig, + LLMConfig, +) + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.openai_api_models import ( + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + EmbeddingRequest, + EmbeddingResponse, + ErrorResponse, + TranscriptionRequest, + TranscriptionResponse, + ) + + +class LLMEngine(abc.ABC): + """Base protocol class for all LLM engines.""" + + @abc.abstractmethod + def __init__(self, llm_config: LLMConfig): + """Initialize the engine with the llm config""" + pass + + @abc.abstractmethod + async def start(self): + """Start the engine""" + pass + + @abc.abstractmethod + async def resolve_lora(self, lora_model: DiskMultiplexConfig): + """Mounts the LoRA model on the engine, given the local disk path.""" + pass + + @abc.abstractmethod + async def reset_prefix_cache(self) -> None: + """Reset the prefix cache of the underlying engine""" + + @abc.abstractmethod + async def chat( + self, request: "ChatCompletionRequest" + ) -> AsyncGenerator[Union[str, "ChatCompletionResponse", "ErrorResponse"], None]: + """Run a ChatCompletion with the engine. + + To implement this method, you need to take a openAI compatible chat request, internally cast it to the target engine request type, and then call the engine's chat method. + + This method is an async generator, so it yields chunks of response and when it is done, it returns None. We have the following convention: + + - In case of streaming, yield a string representing data: \n\n for each chunk. This should be already openAI compatible, so the higher level can just yield it to the client. + - In case of non-streaming, yield a single object of type ChatCompletionResponse. + - In case of error, yield a single object of type ErrorResponse. + + Args: + request: The chat completion request. + + Yields: + Union[str, ChatCompletionResponse, ErrorResponse]: A string representing a chunk of the response, a ChatCompletionResponse object, or an ErrorResponse object. + + Returns: + None when the generator is done. + """ + pass + + @abc.abstractmethod + async def completions( + self, request: "CompletionRequest" + ) -> AsyncGenerator[Union[str, "CompletionResponse", "ErrorResponse"], None]: + """Run a Completion with the engine. + + Similar to chat, this method is an async generator, so it yields chunks + of response and when it is done, it returns None. We have the following + convention: + + * In case of streaming, yield a string representing data: + \n\n for each chunk. This should be already openAI compatible + with completion response format, so the higher level can just yield it + directly to the client. + * In case of non-streaming, yield a single object of type + CompletionResponse. + * In case of error, yield a single object of type ErrorResponse. + + Args: + request: The completion request. + + Yields: + Union[str, CompletionResponse, ErrorResponse]: A string + representing a chunk of the response, a CompletionResponse object, + or an ErrorResponse object. + + Returns: + None when the generator is done. + """ + pass + + @abc.abstractmethod + async def embeddings( + self, request: "EmbeddingRequest" + ) -> AsyncGenerator[Union["EmbeddingResponse", "ErrorResponse"], None]: + """Run an Embedding with the engine. + + This method is different from chat and completion in that it does not + have streaming, but still it is an async generator that yields response + objects and when it is done, it returns None. We have the following + convention: + + * yield a single object of type EmbeddingResponse. + * For errors, yield a single object of type ErrorResponse. + + Args: + request: The embedding request. + + Returns: + An async generator that yields EmbeddingResponse objects or ErrorResponse objects, and returns None when the generator is done. + """ + pass + + @abc.abstractmethod + async def transcriptions( + self, request: "TranscriptionRequest" + ) -> AsyncGenerator[Union[str, "TranscriptionResponse", "ErrorResponse"], None]: + """Run a Transcription with the engine. + + Similar to chat and completion, this method is an async generator, + so it yields chunks of response and when it is done, it returns None. + We have the following convention: + + * In case of streaming, yield a string representing data: + \n\n for each chunk. This should be already openAI compatible, + so the higher level can just yield it to the client. + * In case of non-streaming, yield a single object of type TranscriptionResponse. + * In case of error, yield a single object of type ErrorResponse. + + Args: + request: The transcription request. + + Yields: + Union[str, TranscriptionResponse, ErrorResponse]: A string + representing a chunk of the response, a TranscriptionResponse object, + or an ErrorResponse object. + + Returns: + None when the generator is done. + """ + pass + + async def check_health(self) -> None: + """Check the health of the engine. + + Does not return anything. Raise error when the engine is dead and needs + to be restarted. + """ + return + + ############################################################## + # Optional methods + # These methods will be implemented in the future to allow + # more granular life-cycle management of the engine. + # e.g. in usecases like RL training, we need to put the engine + # to sleep during training and wake up during rollouts. + ############################################################## + + async def sleep(self): + """Puts the engine to sleep""" + pass + + async def wakeup(self): + """Wakes up the engine""" + pass + + def shutdown(self): + """Shuts down the engine""" + pass diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8587bea84822cbaa3529993a9a4ea473958a323 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/builder.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa0daaff24e0a2206dcb28b707770544b829521b Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/builder.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/ingress.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/ingress.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55dbc0e0b434d6c4ad5e5c5f5a5e5e52e49b3cd7 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/ingress.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/middleware.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/middleware.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c150c43145fb2622b94d245ab888db3211eccfa Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/__pycache__/middleware.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/builder.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..b083b7133f5af3001060493d34341498b19ee9ce --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/builder.py @@ -0,0 +1,136 @@ +import os +import pprint +from typing import Any, Dict, List, Optional, Type, Union + +from pydantic import Field, field_validator, model_validator + +from ray import serve +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.llm._internal.common.dict_utils import deep_merge_dicts +from ray.llm._internal.common.utils.import_utils import load_class +from ray.llm._internal.serve.core.configs.llm_config import LLMConfig +from ray.llm._internal.serve.core.ingress.ingress import ( + OpenAiIngress, + make_fastapi_ingress, +) +from ray.llm._internal.serve.core.server.builder import ( + build_llm_deployment, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.serve.deployment import Application + +logger = get_logger(__name__) + + +class IngressClsConfig(BaseModelExtended): + ingress_cls: Union[str, Type[OpenAiIngress]] = Field( + default=OpenAiIngress, + description="The class name of the ingress to use. It can be in form of `module_name.class_name` or `module_name:class_name` or the class itself. The class constructor should take the following arguments: `(llm_deployments: List[DeploymentHandle], **extra_kwargs)` where `llm_deployments` is a list of DeploymentHandle objects from `LLMServer` deployments.", + ) + + ingress_extra_kwargs: Optional[dict] = Field( + default_factory=dict, + description="""The kwargs to bind to the ingress deployment. This will be passed to the ingress class constructor.""", + ) + + @field_validator("ingress_cls") + @classmethod + def validate_class( + cls, value: Union[str, Type[OpenAiIngress]] + ) -> Type[OpenAiIngress]: + if isinstance(value, str): + return load_class(value) + return value + + +class LLMServingArgs(BaseModelExtended): + llm_configs: List[Union[str, dict, LLMConfig]] = Field( + description="A list of LLMConfigs, or dicts representing LLMConfigs, or paths to yaml files defining LLMConfigs.", + ) + ingress_cls_config: Union[dict, IngressClsConfig] = Field( + default_factory=IngressClsConfig, + description="The configuration for the ingress class. It can be a dict representing the ingress class configuration, or an IngressClsConfig object.", + ) + ingress_deployment_config: Dict[str, Any] = Field( + default_factory=dict, + description=""" + The Ray @server.deployment options for the ingress server. + """, + ) + + @field_validator("ingress_cls_config") + @classmethod + def _validate_ingress_cls_config( + cls, value: Union[dict, IngressClsConfig] + ) -> IngressClsConfig: + if isinstance(value, dict): + return IngressClsConfig.model_validate(value) + return value + + @field_validator("llm_configs") + @classmethod + def _validate_llm_configs( + cls, value: List[Union[str, dict, LLMConfig]] + ) -> List[LLMConfig]: + llm_configs = [] + for config in value: + if isinstance(config, str): + if not os.path.exists(config): + raise ValueError( + f"Could not load model config from {config}, as the file does not exist." + ) + llm_configs.append(LLMConfig.from_file(config)) + elif isinstance(config, dict): + llm_configs.append(LLMConfig.model_validate(config)) + elif isinstance(config, LLMConfig): + llm_configs.append(config) + else: + raise TypeError(f"Invalid LLMConfig type: {type(config)}") + return llm_configs + + @model_validator(mode="after") + def _validate_model_ids(self): + """Validate that model IDs are unique and at least one model is configured.""" + if len({m.model_id for m in self.llm_configs}) != len(self.llm_configs): + raise ValueError("Duplicate models found. Make sure model ids are unique.") + + if len(self.llm_configs) == 0: + raise ValueError( + "List of models is empty. Maybe some parameters cannot be parsed into the LLMConfig config." + ) + return self + + +def build_openai_app(builder_config: dict) -> Application: + """Build an OpenAI compatible app with the llm deployment setup from + the given builder configuration. + + Args: + builder_config: The configuration for the builder. It has to conform + to the LLMServingArgs pydantic model. + + Returns: + The configured Ray Serve Application router. + """ + + builder_config = LLMServingArgs.model_validate(builder_config) + llm_configs = builder_config.llm_configs + + llm_deployments = [build_llm_deployment(c) for c in llm_configs] + + ingress_cls_config = builder_config.ingress_cls_config + ingress_options = ingress_cls_config.ingress_cls.get_deployment_options(llm_configs) + + if builder_config.ingress_deployment_config: + ingress_options = deep_merge_dicts( + ingress_options, builder_config.ingress_deployment_config + ) + + ingress_cls = make_fastapi_ingress(ingress_cls_config.ingress_cls) + + logger.info("============== Ingress Options ==============") + logger.info(pprint.pformat(ingress_options)) + + return serve.deployment(ingress_cls, **ingress_options).bind( + llm_deployments=llm_deployments, **ingress_cls_config.ingress_extra_kwargs + ) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/ingress.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/ingress.py new file mode 100644 index 0000000000000000000000000000000000000000..29a9e17ada4dcdd43af354287f88d1affaa46ad0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/ingress.py @@ -0,0 +1,665 @@ +import asyncio +import json +import sys +from contextlib import asynccontextmanager +from enum import Enum +from typing import ( + Annotated, + Any, + AsyncGenerator, + Awaitable, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) + +from fastapi import FastAPI, Form, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware +from starlette.responses import JSONResponse, Response, StreamingResponse + +from ray import serve +from ray._common.utils import get_or_create_event_loop +from ray.llm._internal.common.utils.lora_utils import ( + get_base_model_id, + get_lora_model_ids, +) +from ray.llm._internal.serve.constants import ( + DEFAULT_LLM_ROUTER_HTTP_TIMEOUT, + DEFAULT_MAX_ONGOING_REQUESTS, +) +from ray.llm._internal.serve.core.configs.llm_config import LLMConfig +from ray.llm._internal.serve.core.configs.openai_api_models import ( + ChatCompletionRequest, + ChatCompletionResponse, + ChatCompletionStreamResponse, + CompletionRequest, + CompletionResponse, + CompletionStreamResponse, + EmbeddingRequest, + EmbeddingResponse, + ErrorResponse, + LLMChatResponse, + LLMCompletionsResponse, + LLMEmbeddingsResponse, + LLMScoreResponse, + LLMTranscriptionResponse, + ModelCard, + ModelList, + OpenAIHTTPException, + ScoreRequest, + ScoreResponse, + TranscriptionRequest, + TranscriptionResponse, + TranscriptionStreamResponse, + to_model_metadata, +) +from ray.llm._internal.serve.core.ingress.middleware import ( + SetRequestIdMiddleware, + add_exception_handling_middleware, +) +from ray.llm._internal.serve.core.protocol import DeploymentProtocol +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.observability.metrics.fast_api_metrics import ( + add_http_metrics_middleware, + metrics_lifespan, +) +from ray.llm._internal.serve.utils.lora_serve_utils import ( + get_lora_model_metadata, +) +from ray.llm._internal.serve.utils.server_utils import replace_prefix +from ray.serve.handle import DeploymentHandle + +# Import asyncio timeout depends on python version +if sys.version_info >= (3, 11): + from asyncio import timeout +else: + from async_timeout import timeout + +logger = get_logger(__name__) + +T = TypeVar("T") + + +DEFAULT_INGRESS_OPTIONS = { + "max_ongoing_requests": DEFAULT_MAX_ONGOING_REQUESTS, +} + +# These methods correspond to functions defined in the LLMEngine class in python/ray/llm/_internal/serve/deployments/llm/llm_engine.py +class CallMethod(Enum): + CHAT = "chat" + COMPLETIONS = "completions" + TRANSCRIPTIONS = "transcriptions" + + +NON_STREAMING_RESPONSE_TYPES = ( + ChatCompletionResponse, + CompletionResponse, + TranscriptionResponse, +) + + +def _sanitize_chat_completion_request( + request: ChatCompletionRequest, +) -> ChatCompletionRequest: + """Sanitize ChatCompletionRequest to fix Pydantic ValidatorIterator serialization issue. + + This addresses a known Pydantic bug where tool_calls fields become ValidatorIterator + objects that cannot be pickled for Ray remote calls. + + References: + - vLLM PR that introduces the workaround: https://github.com/vllm-project/vllm/pull/9951 + - Pydantic Issue: https://github.com/pydantic/pydantic/issues/9467 + - Related Issue: https://github.com/pydantic/pydantic/issues/9541 + - Official Workaround: https://github.com/pydantic/pydantic/issues/9467#issuecomment-2442097291 + + TODO(seiji): Remove when we update to Pydantic v2.11+ with the fix. + """ + from vllm.transformers_utils.tokenizers.mistral import maybe_serialize_tool_calls + + maybe_serialize_tool_calls(request) + + return request + + +StreamResponseType = Union[ + ChatCompletionStreamResponse, CompletionStreamResponse, TranscriptionStreamResponse +] +BatchedStreamResponseType = List[StreamResponseType] + + +DEFAULT_ENDPOINTS = { + "models": lambda app: app.get("/v1/models", response_model=ModelList), + "model_data": lambda app: app.get( + "/v1/models/{model:path}", response_model=ModelCard + ), + "completions": lambda app: app.post("/v1/completions"), + "chat": lambda app: app.post("/v1/chat/completions"), + "embeddings": lambda app: app.post("/v1/embeddings"), + "transcriptions": lambda app: app.post( + "/v1/audio/transcriptions", + ), + "score": lambda app: app.post("/v1/score"), +} + + +def init() -> FastAPI: + _fastapi_router_app = FastAPI(lifespan=metrics_lifespan) + + # NOTE: PLEASE READ CAREFULLY BEFORE MODIFYING + # + # FastAPI middleware is executed in LIFO (last-in, first-out) order, + # hence maintaining current ordering is crucial as some of the middleware + # might have data dependency on the other: for ex, telemetry middleware + # depends on middleware generating request-id + # + # Add exception handling middleware + # NOTE: This middleware should be added first such that it's intercepting + # exceptions from the handlers, avoiding them propagating to other + # middleware (for ex, telemetry) + add_exception_handling_middleware(_fastapi_router_app) + # Configure CORS middleware + _fastapi_router_app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + # Add HTTP metrics middleware + add_http_metrics_middleware(_fastapi_router_app) + + # Inject unique per-request ID + # + # NOTE: This middleware should be executed among the last (since + # middleware is executed in LIFO). + _fastapi_router_app.add_middleware(SetRequestIdMiddleware) + + return _fastapi_router_app + + +def make_fastapi_ingress( + cls: Type, + *, + endpoint_map: Optional[Dict[str, Callable[[FastAPI], Callable]]] = None, + app: Optional[FastAPI] = None, +): + """ + Create a Ray Serve ingress deployment from a class and endpoint mapping. + + Args: + cls: The class to convert into an ingress deployment + endpoint_map: Dictionary mapping method names to FastAPI route + decorators. Each value is a lambda that takes a FastAPI app and + returns a route decorator. + app: Optional FastAPI app to use for the ingress deployment. If not + provided, a new FastAPI app will be created. + + Returns: + A class decorated with @serve.ingress + + Example: + endpoint_map = { + "increment": lambda app: app.post("/increment"), + "get_counter": lambda app: app.get("/counter"), + } + + # With additional FastAPI parameters: + endpoint_map = { + "increment": lambda app: app.post("/increment", status_code=201, tags=["counter"]), + "get_counter": lambda app: app.get("/counter", response_model=CounterResponse), + } + """ + + if app is None: + app = init() + + if endpoint_map is None: + endpoint_map = DEFAULT_ENDPOINTS + + # Create a new class that inherits from the original to avoid modifying it + # in-place. We populate the new class's __dict__ with decorated methods. + class_dict = {} + + # Apply route decorators to the class methods and store them in class_dict + for method_name, route_factory in endpoint_map.items(): + # Get the route decorator from the lambda + route_decorator = route_factory(app) + # Get the original method from the class + original_method = getattr(cls, method_name) + # Apply the decorator to the original method + decorated_method = route_decorator(original_method) + # Store in the class dict so it will be properly bound to new_cls + class_dict[method_name] = decorated_method + + # Create new class with the decorated methods in its __dict__. + # IMPORTANT: We keep the same __name__ and __qualname__ as the original + # class so that make_fastapi_class_based_view can properly identify the routes + # (it checks if cls.__qualname__ is in route.endpoint.__qualname__). + new_cls = type(cls.__name__, (cls,), class_dict) + new_cls.__qualname__ = cls.__qualname__ + + # Apply the serve.ingress decorator to the new class + return serve.ingress(app)(new_cls) + + +def _apply_openai_json_format( + response: Union[StreamResponseType, BatchedStreamResponseType], +) -> str: + """Converts the stream response to OpenAI format. + + Each model response is converted to the string: + data: \n\n + + The converted strings are concatenated and returned: + data: \n\ndata: \n\n... + """ + if isinstance(response, list): + first_response = next(iter(response)) + if isinstance(first_response, str): + return "".join(response) + if isinstance(first_response, dict): + return "".join(f"data: {json.dumps(r)}\n\n" for r in response) + if hasattr(first_response, "model_dump_json"): + return "".join(f"data: {r.model_dump_json()}\n\n" for r in response) + raise ValueError( + f"Unexpected response type: {type(first_response)}, {first_response=}" + ) + if hasattr(response, "model_dump_json"): + return f"data: {response.model_dump_json()}\n\n" + if isinstance(response, str): + return response + raise ValueError(f"Unexpected response type: {type(response)}, {response=}") + + +async def _peek_at_generator( + gen: AsyncGenerator[T, None], +) -> Tuple[T, AsyncGenerator[T, None]]: + # Peek at the first element + first_item = await gen.__anext__() + + # Create a new generator that yields the peeked item first + async def new_generator() -> AsyncGenerator[T, None]: + yield first_item + async for item in gen: + yield item + + return first_item, new_generator() + + +async def _openai_json_wrapper( + generator: AsyncGenerator[ + Union[StreamResponseType, BatchedStreamResponseType], None + ], +) -> AsyncGenerator[str, None]: + """Wrapper that converts stream responses into OpenAI JSON strings. + + Args: + generator: an async generator that yields either individual stream responses + (StreamResponseType) or batches of stream responses (BatchedStreamResponseType). + Each response is converted into OpenAI JSON format and streamed to the client. + For batched responses, the items are concatenated together as a single string. + + Yields: + String chunks in OpenAI SSE format: "data: {json}\n\n", with a final + "data: [DONE]\n\n" to indicate completion. + """ + async for response in generator: + packet = _apply_openai_json_format(response) + yield packet + + yield "data: [DONE]\n\n" + + +@asynccontextmanager +async def router_request_timeout(timeout_duration: float): + try: + async with timeout(timeout_duration): + yield + except asyncio.TimeoutError as e: + raise OpenAIHTTPException( + status_code=status.HTTP_408_REQUEST_TIMEOUT, + message="Request server side timeout", + internal_message=str(e), + ) + + +class OpenAiIngress(DeploymentProtocol): + def __init__( + self, + llm_deployments: List[DeploymentHandle], + *, + _get_lora_model_metadata_func: Optional[ + Callable[[str, LLMConfig], Awaitable[Dict[str, Any]]] + ] = None, + ): + self._default_serve_handles: Dict[str, DeploymentHandle] = {} + self._llm_configs: Dict[str, LLMConfig] = {} + + # Configuring a ServeHandle with .options() creates a new ServeHandle + # object, which contains a new metrics pusher and long-polling call. + # Creating too many ServeHandles can impact event-loop and Serve Controller + # performance, so we save configured ServeHandles here and reuse them. + self._configured_serve_handles: Dict[str, DeploymentHandle] = {} + self._get_lora_model_metadata_func = ( + _get_lora_model_metadata_func or self._default_get_lora_model_metadata_func + ) + + # Setup _default_serve_handles and _llm_configs asynchronously. + self._init_completed = asyncio.Event() + self.running_setup_task = get_or_create_event_loop().create_task( + self._setup_handle_and_config_maps(llm_deployments=llm_deployments) + ) + + async def _default_get_lora_model_metadata_func( + self, model_id: str, llm_config: LLMConfig + ) -> Dict[str, Any]: + return await get_lora_model_metadata(model_id, llm_config) + + async def _setup_handle_and_config_maps( + self, llm_deployments: List[DeploymentHandle] + ): + for handle in llm_deployments: + llm_config = await handle.llm_config.remote() + self._default_serve_handles[llm_config.model_id] = handle + self._llm_configs[llm_config.model_id] = llm_config + + # Note (genesu): Even though we have already checked model id uniqueness in + # `router_application()` under run.py. When we OSS this router component, users + # would be able to directly use the lower level api and bypass that check. We + # check it again here to ensure all the model ids are unique. + if len(llm_deployments) != len(self._llm_configs): + raise ValueError("Duplicate models found. Make sure model ids are unique.") + + self._init_completed.set() + + async def check_health(self): + await self._init_completed.wait() + + def _get_configured_serve_handle(self, model_id: str): + """Gets a ServeHandle to a model deployment. + + Configures the handle's options, and stores it in a cache. + + If the model_id includes LoRA suffix, we set the model ID as + the multiplexed_model_id, so the request uses Serve's multiplexed + routing logic. + + If the model_id is a base model- even if the model has LoRA + adapters- we don't set multiplexed_model_id. Setting + multiplexed_model_id would cause base model requests to be + sent to a single model replica, instead of being load + balanced across all replicas. This is undesirable for base + model requests (unlike LoRA requests) because all the replicas + have a copy of the base model. + """ + + if model_id not in self._configured_serve_handles: + base_model_id = get_base_model_id(model_id) + if base_model_id in self._default_serve_handles: + if model_id == base_model_id: + default_handle = self._default_serve_handles[model_id] + configured_handle = default_handle.options(stream=True) + self._configured_serve_handles[model_id] = configured_handle + else: + default_handle = self._default_serve_handles[base_model_id] + configured_handle = default_handle.options( + stream=True, + multiplexed_model_id=model_id, + ) + self._configured_serve_handles[model_id] = configured_handle + else: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + f'Could not find model with id "{model_id}".', + ) + + return self._configured_serve_handles[model_id] + + async def _get_response( + self, + *, + body: Union[ + CompletionRequest, + ChatCompletionRequest, + EmbeddingRequest, + TranscriptionRequest, + ScoreRequest, + ], + call_method: str, + ) -> AsyncGenerator[ + Union[ + LLMChatResponse, + LLMCompletionsResponse, + LLMEmbeddingsResponse, + LLMTranscriptionResponse, + LLMScoreResponse, + ], + None, + ]: + """Calls the model deployment and returns the stream.""" + model: str = body.model + base_model_id = get_base_model_id(model) + if base_model_id not in self._llm_configs: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + f'Got request for model "{model}". ' + f'Could not find base model with ID "{base_model_id}".', + ) + + model_handle = self._get_configured_serve_handle(model) + + # TODO(seiji): Remove when we update to Pydantic v2.11+ with the fix + # for tool calling ValidatorIterator serialization issue. + if isinstance(body, ChatCompletionRequest): + body = _sanitize_chat_completion_request(body) + + async for response in getattr(model_handle, call_method).remote(body): + yield response + + async def model(self, model_id: str) -> Optional[ModelCard]: + if model_id in self._llm_configs: + return to_model_metadata(model_id, self._llm_configs[model_id]) + + base_model_id = get_base_model_id(model_id) + if ( + base_model_id in self._llm_configs + and self._llm_configs[base_model_id].lora_config + ): + try: + overrides = await self._get_lora_model_metadata_func( + model_id, self._llm_configs[base_model_id] + ) + + return to_model_metadata( + model_id=model_id, + model_config=self._llm_configs[base_model_id], + overrides=overrides, + ) + except HTTPException: + logger.exception( + "Unable to retrieve LoRA adapter config file for " + f'"{model_id}". Omitting it from list of available models. ' + "Check that adapter config file exists in cloud bucket." + ) + + async def models(self) -> ModelList: + """OpenAI API-compliant endpoint to get all rayllm models.""" + all_models = dict() + for base_model_id, llm_config in self._llm_configs.items(): + # Add the base model. + all_models[base_model_id] = await self.model(base_model_id) + + if llm_config.lora_config is not None: + # Add all the fine-tuned models. + lora_model_ids = get_lora_model_ids( + dynamic_lora_loading_path=llm_config.lora_config.dynamic_lora_loading_path, + base_model_id=base_model_id, + ) + for lora_id in lora_model_ids: + model_data = await self.model(lora_id) + if model_data is not None: + all_models[lora_id] = model_data + + return ModelList(data=list(all_models.values())) + + async def model_data(self, model: str) -> ModelCard: + """OpenAI API-compliant endpoint to get one rayllm model. + + :param model: The model ID (e.g. "amazon/LightGPT") + """ + model = replace_prefix(model) + model_data = await self.model(model) + if model_data is None: + raise OpenAIHTTPException( + message=f"Unable to find {model}. Please ensure that the model exists and you have permission.", + status_code=status.HTTP_404_NOT_FOUND, + type="InvalidModel", + ) + return model_data + + async def _process_llm_request( + self, + body: Union[CompletionRequest, ChatCompletionRequest, TranscriptionRequest], + call_method: str, + ) -> Response: + + async with router_request_timeout(DEFAULT_LLM_ROUTER_HTTP_TIMEOUT): + + gen = self._get_response(body=body, call_method=call_method) + + # In streaming with batching enabled, this first response can be a list of chunks. + initial_response, gen = await _peek_at_generator(gen) + + if isinstance(initial_response, list): + first_chunk = initial_response[0] + else: + first_chunk = initial_response + + if isinstance(first_chunk, ErrorResponse): + raise OpenAIHTTPException( + message=first_chunk.error.message, + status_code=first_chunk.error.code, + type=first_chunk.error.type, + ) + + if isinstance(first_chunk, NON_STREAMING_RESPONSE_TYPES): + # Not streaming, first chunk should be a single response + return JSONResponse(content=first_chunk.model_dump()) + + # In case of streaming we need to iterate over the chunks and yield them + openai_stream_generator = _openai_json_wrapper(gen) + + return StreamingResponse( + openai_stream_generator, media_type="text/event-stream" + ) + + async def completions(self, body: CompletionRequest) -> Response: + """Given a prompt, the model will return one or more predicted completions, + and can also return the probabilities of alternative tokens at each position. + + Args: + body: The CompletionRequest object. + + Returns: + A response object with completions. + """ + return await self._process_llm_request( + body, call_method=CallMethod.COMPLETIONS.value + ) + + async def chat(self, body: ChatCompletionRequest) -> Response: + """Given a prompt, the model will return one or more predicted completions, + and can also return the probabilities of alternative tokens at each position. + + Args: + body: The ChatCompletionRequest object. + + Returns: + A response object with completions. + """ + + return await self._process_llm_request(body, call_method=CallMethod.CHAT.value) + + async def embeddings(self, body: EmbeddingRequest) -> Response: + """Create embeddings for the provided input. + + Args: + body: The EmbeddingRequest object. + + Returns: + A response object with embeddings. + """ + async with router_request_timeout(DEFAULT_LLM_ROUTER_HTTP_TIMEOUT): + results = self._get_response(body=body, call_method="embeddings") + result = await results.__anext__() + if isinstance(result, ErrorResponse): + raise OpenAIHTTPException( + message=result.error.message, + status_code=result.error.code, + type=result.error.type, + ) + + if isinstance(result, EmbeddingResponse): + return JSONResponse(content=result.model_dump()) + + # Annotated[..., Form()] is wrapper that is used to handle multiple form data, which is how audio is sent in transcription requests. + # vLLM implementation for handling transcription requests: https://github.com/vllm-project/vllm/blob/0825197bee8dea547f2ab25f48afd8aea0cd2578/vllm/entrypoints/openai/api_server.py#L839. + async def transcriptions( + self, body: Annotated[TranscriptionRequest, Form()] + ) -> Response: + """Create transcription for the provided audio input. + + Args: + body: The TranscriptionRequest object. + + Returns: + A response object with transcriptions. + """ + + return await self._process_llm_request( + body, call_method=CallMethod.TRANSCRIPTIONS.value + ) + + async def score(self, body: ScoreRequest) -> Response: + """Create scores for the provided text pairs. + + Note: This is a vLLM specific endpoint. + + Args: + body: The score request containing input text pairs to score. + + Returns: + A response object with scores. + """ + + async with router_request_timeout(DEFAULT_LLM_ROUTER_HTTP_TIMEOUT): + results = self._get_response(body=body, call_method="score") + result = await results.__anext__() + if isinstance(result, ErrorResponse): + raise OpenAIHTTPException( + message=result.message, + status_code=result.code, + type=result.type, + ) + + if isinstance(result, ScoreResponse): + return JSONResponse(content=result.model_dump()) + + @classmethod + def get_deployment_options( + cls, llm_configs: Optional[List[LLMConfig]] = None + ) -> Dict[str, Any]: + """Get the deployment options for the ingress deployment. + + Args: + llm_configs: The LLM configs to infer the number of ingress replicas from. + + Returns: + A dictionary containing the deployment options for the ingress deployment. + """ + return DEFAULT_INGRESS_OPTIONS diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/middleware.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..dcb0021cd97f33b5776dfc82a0c0526f9139e28a --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/ingress/middleware.py @@ -0,0 +1,165 @@ +import uuid +from asyncio import CancelledError +from typing import Optional + +from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import JSONResponse, Response + +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.utils.server_utils import ( + get_response_for_error, +) + +logger = get_logger(__file__) + + +def get_request_id(request) -> str: + """Fetches request-id from Starlette's request object. + + NOTE: This method relies on "request_id" value to be injected into the + Starlette's `request.state` via `inject_request_id` middleware + + :param request: Starlette request object + :return: (optional) Id allowing to identify particular user + """ + return getattr(request.state, "request_id", None) + + +async def _handle_validation_error( + request: Request, exc: RequestValidationError +) -> JSONResponse: + """Handle pydantic validation errors in an OpenAI-like format.""" + error_details = exc.errors()[0] if exc.errors() else {"msg": "Invalid request"} + + error_msg = error_details.get("msg", "Unknown validation error") + error_loc = error_details.get("loc", ("body")) + error_input = error_details.get("input", None) + msg = f"Invalid request format: {error_msg} at {error_loc}" + + error_response = { + "error": { + "message": msg, + "type": error_details.get("type", "invalid_request_error"), + "param": error_input, + "code": "invalid_parameter", + } + } + + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=error_response) + + +def _uncaught_exception_handler(request: Request, e: Exception): + """This method serves as an uncaught exception handler being + the last resort to return properly formatted response. + + NOTE: Exceptions from application handlers should NOT be reaching this point, + this handler is here to intercept "fly-away" exceptions and should not + be handled for handling of converting application exceptions into + appropriate responses + """ + + if isinstance(e, CancelledError): + return JSONResponse(content={}, status_code=204) + + request_id = get_request_id(request) + + logger.error(f"Uncaught exception while handling request {request_id}", exc_info=e) + + error_response = get_response_for_error(e, request_id) + + return JSONResponse( + content=error_response.model_dump(), status_code=error_response.error.code + ) + + +def add_exception_handling_middleware(router: FastAPI): + # NOTE: PLEASE READ CAREFULLY BEFORE CHANGING + # + # Starlette has different behavior depending on the Exception class being handled + # that we unfortunately have to take into account here: + # + # - Handler for `Exception` will be added as uncaught exception handler (of last resort) + # that is going to be executed absolute last, making sure that in case of any fly-away + # (uncaught) exception + # - Handlers for any other classes of exceptions will be executed as last middleware layer, + # therefore being to intercept any exceptions originating from the handler before it + # propagates to the middleware above it + # + # As such we're aiming for 2 goals here: + # - Intercepting exceptions from the handlers, converting them into proper user-facing + # response (avoiding exception propagation up the middleware stack) + # - Adding uncaught exception handler (of last resort) to intercept any exceptions that + # might be originating from the middleware itself + + async def _handle_application_exceptions( + request: Request, call_next: RequestResponseEndpoint + ) -> Response: + """This method intercepts application level exceptions not handled by the + application code converting them into appropriately formatted (JSON) response + """ + + try: + return await call_next(request) + except CancelledError as ce: + # NOTE: We re-raise CancelledError as is to let other middleware handle it. + # Since no response is expected in this case, it's deferred to uncaught + # exception handler to ultimately handle it + raise ce + except RequestValidationError as e: + return await _handle_validation_error(request, e) + except Exception as e: + request_id = get_request_id(request) + error_response = get_response_for_error(e, request_id) + + return JSONResponse( + content=error_response.model_dump(), + status_code=error_response.error.code, + ) + + # This adds last-resort uncaught exception handler into Starlette + router.add_exception_handler(Exception, _uncaught_exception_handler) + # Add validation error handler + router.add_exception_handler(RequestValidationError, _handle_validation_error) + # This adds application exception handler, allowing to convert application + # exceptions into properly formatted responses + router.add_middleware( + BaseHTTPMiddleware, + dispatch=_handle_application_exceptions, + ) + + +class SetRequestIdMiddleware: + """Injects request ID into the request's state. + + The ID is either: + 1. the value of the request's "x-request-id" header, set by Ray + Serve's Proxy, or + 2. if "x-request-id" header is unavailable, this middleware creates + a UUIDv4 request ID. + """ + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] == "http": + request = Request(scope) + request.state.request_id = request.headers.get("x-request-id", None) or str( + uuid.uuid4() + ) + + return await self.app(scope, receive, send) + + +def get_user_id(request) -> Optional[str]: + """Fetches user id inside Starlette's request object. + + NOTE: This method relies on "user_id" value to be injected into the + Starlette's `request.state` via authentication middleware + + :param request: Starlette request object + :return: (optional) Id identifying particular user + """ + return getattr(request.state, "user_id", None) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/protocol.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..cc468c86179f411e76f770e3d91ef838f1433603 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/protocol.py @@ -0,0 +1,78 @@ +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Dict, + List, + Optional, + Protocol, + Union, +) + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + from ray.llm._internal.serve.core.configs.openai_api_models import ( + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + ErrorResponse, + ) + + +class DeploymentProtocol(Protocol): + @classmethod + def get_deployment_options(cls, **kwargs) -> Dict[str, Any]: + """Get the default deployment options for the this deployment.""" + + +class LLMServerProtocol(DeploymentProtocol): + """ + This is the common interface between all the llm deployment. All llm deployments + need to implement a sync constructor, an async start method, and check_health method. + """ + + def __init__(self): + """ + Constructor takes basic setup that doesn't require async operations. + """ + + async def start(self) -> None: + """ + Start the underlying engine. This handles async initialization. + """ + + async def chat( + self, request: "ChatCompletionRequest" + ) -> AsyncGenerator[Union[str, "ChatCompletionResponse", "ErrorResponse"], None]: + """ + Inferencing to the engine for chat, and return the response. + """ + + async def completions( + self, request: "CompletionRequest" + ) -> AsyncGenerator[ + Union[List[Union[str, "ErrorResponse"]], "CompletionResponse"], None + ]: + """ + Inferencing to the engine for completion api, and return the response. + """ + + async def check_health(self) -> None: + """ + Check the health of the replica. Does not return anything. + Raise error when the engine is dead and needs to be restarted. + """ + + async def reset_prefix_cache(self) -> None: + """Reset the prefix cache of the underlying engine""" + + async def start_profile(self) -> None: + """Start profiling""" + + async def stop_profile(self) -> None: + """Stop profiling""" + + # TODO (Kourosh): This does not belong here. + async def llm_config(self) -> Optional["LLMConfig"]: + """Get the LLM config""" diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..656530bd6439130bc1357e1d28a49b395ddc170f Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/builder.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0d2e536e26a2dc2a39ca5de16bf0509cb7ded5d Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/builder.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/llm_server.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/llm_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37d2ff989641869418adf77768cdc54e6ff184c8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/__pycache__/llm_server.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/builder.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..032e195784a2f28aa7c8cab7362f269a580ca92d --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/builder.py @@ -0,0 +1,78 @@ +import pprint +from typing import Optional, Type + +from ray import serve +from ray.llm._internal.common.dict_utils import deep_merge_dicts +from ray.llm._internal.serve.constants import ( + DEFAULT_HEALTH_CHECK_PERIOD_S, + DEFAULT_HEALTH_CHECK_TIMEOUT_S, + DEFAULT_MAX_ONGOING_REQUESTS, +) +from ray.llm._internal.serve.core.configs.llm_config import ( + LLMConfig, +) +from ray.llm._internal.serve.core.server.llm_server import LLMServer +from ray.llm._internal.serve.observability.logging import get_logger +from ray.serve.deployment import Application + +logger = get_logger(__name__) + + +DEFAULT_DEPLOYMENT_OPTIONS = { + "max_ongoing_requests": DEFAULT_MAX_ONGOING_REQUESTS, + "health_check_period_s": DEFAULT_HEALTH_CHECK_PERIOD_S, + "health_check_timeout_s": DEFAULT_HEALTH_CHECK_TIMEOUT_S, +} + + +def _get_deployment_name(llm_config: LLMConfig) -> str: + return llm_config.model_id.replace("/", "--").replace(".", "_") + + +def build_llm_deployment( + llm_config: LLMConfig, + *, + name_prefix: Optional[str] = None, + bind_kwargs: Optional[dict] = None, + override_serve_options: Optional[dict] = None, + deployment_cls: Optional[Type[LLMServer]] = None, +) -> Application: + """Build an LLMServer deployment. + + Args: + llm_config: The LLMConfig to build the deployment. + name_prefix: The prefix to add to the deployment name. + bind_kwargs: The optional extra kwargs to pass to the deployment. + Used for customizing the deployment. + override_serve_options: The optional serve options to override the + default options. + deployment_cls: The deployment class to use. Defaults to LLMServer. + + Returns: + The Ray Serve Application for the LLMServer deployment. + """ + deployment_cls = deployment_cls or LLMServer + name_prefix = name_prefix or f"{deployment_cls.__name__}:" + bind_kwargs = bind_kwargs or {} + + deployment_options = deployment_cls.get_deployment_options(llm_config) + + # Set the name of the deployment config to map to the model ID. + deployment_name = deployment_options.get("name", _get_deployment_name(llm_config)) + + if name_prefix: + deployment_options["name"] = name_prefix + deployment_name + + if override_serve_options: + deployment_options.update(override_serve_options) + + deployment_options = deep_merge_dicts( + DEFAULT_DEPLOYMENT_OPTIONS, deployment_options + ) + + logger.info("============== Deployment Options ==============") + logger.info(pprint.pformat(deployment_options)) + + return serve.deployment(deployment_cls, **deployment_options).bind( + llm_config=llm_config, **bind_kwargs + ) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/llm_server.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/llm_server.py new file mode 100644 index 0000000000000000000000000000000000000000..0da17151cc12fe1b391b31956d6ae851e504d6cf --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/core/server/llm_server.py @@ -0,0 +1,523 @@ +import asyncio +import copy +import os +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Dict, + List, + Optional, + Type, + TypeVar, + Union, +) + +import ray +from ray import serve +from ray._common.utils import import_attr +from ray.llm._internal.serve.constants import ( + ENABLE_WORKER_PROCESS_SETUP_HOOK, + ENGINE_START_TIMEOUT_S, + MODEL_RESPONSE_BATCH_TIMEOUT_MS, + RAYLLM_VLLM_ENGINE_CLS_ENV, +) +from ray.llm._internal.serve.core.configs.llm_config import ( + DiskMultiplexConfig, + LLMConfig, +) +from ray.llm._internal.serve.core.engine.protocol import LLMEngine +from ray.llm._internal.serve.core.protocol import LLMServerProtocol +from ray.llm._internal.serve.engines.vllm.vllm_engine import VLLMEngine +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.observability.usage_telemetry.usage import ( + push_telemetry_report_for_all_models, +) +from ray.llm._internal.serve.utils.batcher import Batcher +from ray.llm._internal.serve.utils.lora_serve_utils import ( + LoraModelLoader, +) +from ray.llm._internal.serve.utils.server_utils import ( + get_serve_request_id, +) + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.openai_api_models import ( + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + EmbeddingRequest, + EmbeddingResponse, + ErrorResponse, + ScoreRequest, + ScoreResponse, + TranscriptionRequest, + TranscriptionResponse, + ) + +logger = get_logger(__name__) +T = TypeVar("T") + + +def _merge_replica_actor_and_child_actor_bundles( + child_actor_bundles: List[Dict[str, float]], + replica_actor_bundle: Dict[str, float], +) -> List[Dict[str, float]]: + """Sum up the bundles from replica actor bundles with the first bundle from child actor bundles. + + This is because the replica actor will use the first bundle in the list, and we want to collocate the replica actor with the child actor. + So we need to group them together. + + So for example: + child_actor_bundles = [{"GPU": 1, "CPU": 1}, {"GPU": 1, "CPU": 1}] + replica_actor_bundle = {"GPU": 0, "CPU": 1, "memory": 100} + return [{"GPU": 1, "CPU": 2, "memory": 100}, {"GPU": 1, "CPU": 1}] + """ + + if not child_actor_bundles: + return [copy.copy(replica_actor_bundle)] + + if not replica_actor_bundle: + return [copy.copy(bundle) for bundle in child_actor_bundles] + + original_first_bundle = child_actor_bundles[0] + bundle_key_set = set(original_first_bundle.keys()) | set( + replica_actor_bundle.keys() + ) + + merged_first_bundle = { + key: original_first_bundle.get(key, 0) + replica_actor_bundle.get(key, 0) + for key in bundle_key_set + } + + return [merged_first_bundle] + [ + copy.copy(bundle) for bundle in child_actor_bundles[1:] + ] + + +class LLMServer(LLMServerProtocol): + """This is a shim layer to decouple the LLM engine from the ingress + deployment. + + It has a very similar API as the engine. Almost all of the abstractions are + implemented by the engine. This class just a little bit more logic on top: + + 1. Logic for serve multiplexing (e.g. LoRA loading). + 2. Request id handing from serve context. + 3. Batching in case of streaming (only for chat and completions). + 4. Telemetry reporting. + + Usage Patterns: + + 1. Basic pattern (for testing): + server = LLMServer.sync_init(llm_config) # Sync constructor, unstarted + await server.start() # Must explicitly start + + 2. Async context (default, used by Ray Serve): + server = await LLMServer(llm_config) # Async constructor, fully started + + 3. Ray Serve deployment: + # Ray Serve calls the async constructor directly + deployment = serve.deployment(LLMServer).bind(llm_config) + """ + + _default_engine_cls = VLLMEngine + + async def __init__( + self, + llm_config: LLMConfig, + *, + engine_cls: Optional[Type[LLMEngine]] = None, + model_downloader: Optional[Type[LoraModelLoader]] = None, + ): + """Asynchronous constructor that returns a fully started instance. + + This is the default constructor used by Ray Serve deployments. + + Args: + llm_config: LLMConfig for the model. + engine_cls: Dependency injection for the vllm engine class. + Defaults to `VLLMEngine`. + model_downloader: Dependency injection for the model downloader. + Defaults to `LoraModelLoader`. + """ + super().__init__() + self._init_shared(llm_config, engine_cls, model_downloader) + await self.start() + + def _init_shared( + self, + llm_config: LLMConfig, + engine_cls: Optional[Type[LLMEngine]] = None, + model_downloader: Optional[Type[LoraModelLoader]] = None, + ): + """Shared initialization logic between constructors.""" + self._llm_config = llm_config + self._engine_cls = engine_cls or self._get_default_engine_class() + self.engine: Optional[LLMEngine] = None + self._init_multiplex_loader(model_downloader) + + @classmethod + def sync_init( + cls, + llm_config: LLMConfig, + *, + engine_cls: Optional[Type[LLMEngine]] = None, + model_downloader: Optional[Type[LoraModelLoader]] = None, + ) -> "LLMServer": + """Synchronous constructor that returns an unstarted instance. + + This is used for testing the new pattern where initialization + and starting are explicitly separated. + + Args: + llm_config: LLMConfig for the model. + engine_cls: Dependency injection for the vllm engine class. + Defaults to `VLLMEngine`. + model_downloader: Dependency injection for the model downloader. + Defaults to `LoraModelLoader`. + + Returns: + An unstarted LLMServer instance. Caller must call await start(). + """ + instance = cls.__new__(cls) + LLMServerProtocol.__init__(instance) + instance._init_shared(llm_config, engine_cls, model_downloader) + return instance + + async def start(self): + """Start the underlying engine. This handles async initialization.""" + if self._engine_cls is not None: + self.engine = self._engine_cls(self._llm_config) + await asyncio.wait_for(self._start_engine(), timeout=ENGINE_START_TIMEOUT_S) + + def _init_multiplex_loader( + self, model_downloader_cls: Optional[Type[LoraModelLoader]] = None + ): + """Initialize the multiplex loader.""" + + model_downloader_cls = model_downloader_cls or LoraModelLoader + mx_config = self._llm_config.multiplex_config() + + if mx_config is not None: + model_downloader = model_downloader_cls( + download_timeout_s=mx_config.download_timeout_s, + max_tries=mx_config.max_download_tries, + ) + + async def _load_model(lora_model_id: str) -> DiskMultiplexConfig: + return await model_downloader.load_model_from_config( + lora_model_id=lora_model_id, + llm_config=self._llm_config, + ) + + self._load_model = serve.multiplexed( + max_num_models_per_replica=mx_config.max_num_models_per_replica + )(_load_model) + else: + + async def _load_model(lora_model_id: str) -> DiskMultiplexConfig: + raise ValueError("LoRA config is not set in the LLMConfig") + + self._load_model = _load_model + + def _get_default_engine_class(self) -> Type[LLMEngine]: + """Helper to load the engine class from the environment variable. + This is used for testing or escape-hatch for patching purposes. + If env variable is not set, it will fallback to the default engine class. + """ + engine_cls_path = os.environ.get(RAYLLM_VLLM_ENGINE_CLS_ENV) + if engine_cls_path: + return import_attr(engine_cls_path) + return self._default_engine_cls + + async def _start_engine(self): + if self.engine is None: + raise ValueError("Engine is not set") + + await self.engine.start() + + # Push telemetry reports for the model in the current deployment. + push_telemetry_report_for_all_models(all_models=[self._llm_config]) + + def _get_batch_interval_ms(self, stream: bool = True) -> int: + """Calculate the batching interval for responses.""" + stream_batching_interval_ms = self._llm_config.experimental_configs.get( + "stream_batching_interval_ms" + ) + if stream_batching_interval_ms is None: + stream_batching_interval_ms = MODEL_RESPONSE_BATCH_TIMEOUT_MS + return stream_batching_interval_ms if stream else None + + async def _maybe_add_request_id_to_request( + self, + request: Union[ + "ChatCompletionRequest", + "CompletionRequest", + "EmbeddingRequest", + "TranscriptionRequest", + ], + ): + """Add the request id to the request.""" + request_id = get_serve_request_id() + if request_id: + request.request_id = request_id + + async def _maybe_resolve_lora_from_multiplex(self) -> None: + """Handle the lora model for the request.""" + multiplexed_model_id = serve.get_multiplexed_model_id() + if multiplexed_model_id: + if self._llm_config.lora_config is None: + raise ValueError("Must setup lora config for multiplexed requests.") + disk_lora_model = await self._load_model(multiplexed_model_id) + await self.engine.resolve_lora(disk_lora_model) + + def _batch_output_stream( + self, generator: AsyncGenerator[T, None] + ) -> AsyncGenerator[List[T], None]: + return Batcher( + generator, + interval_ms=self._get_batch_interval_ms(), + ).stream() + + async def _run_request( + self, + request: Union[ + "ChatCompletionRequest", + "CompletionRequest", + "EmbeddingRequest", + "TranscriptionRequest", + "ScoreRequest", + ], + *, + engine_method: str, + batch_output_stream: bool = False, + ) -> AsyncGenerator[Any, None]: + """Run the engine method on the request + perform batching when stream=True. + + Args: + request: The request to run. + engine_method: The method to call on the engine. + batch_output_stream: Whether to batch the output stream. + + Returns: + An AsyncGenerator of the response. If stream is True and batching is enabled, then the generator will yield a list of streaming responses (strings of the format data: {response_json}\n\n). Otherwise, it will yield the non-streaming response from engine directly. + """ + + await self._maybe_add_request_id_to_request(request) + await self._maybe_resolve_lora_from_multiplex() + + is_stream = hasattr(request, "stream") and request.stream + if is_stream and batch_output_stream: + stream = self._batch_output_stream( + getattr(self.engine, engine_method)(request) + ) + else: + stream = getattr(self.engine, engine_method)(request) + + return stream + + async def chat( + self, request: "ChatCompletionRequest" + ) -> AsyncGenerator[ + Union[List[Union[str, "ErrorResponse"]], "ChatCompletionResponse"], None + ]: + """Runs a chat request to the LLM engine and returns the response. + + Args: + request: A ChatCompletionRequest object. + + Returns: + An AsyncGenerator of the response. If stream is True and batching is enabled, then the generator will yield a list of chat streaming responses (strings of the format data: {response_json}\n\n). Otherwise, it will yield the ChatCompletionResponse object directly. + """ + return await self._run_request( + request, + engine_method="chat", + batch_output_stream=True, + ) + + async def completions( + self, request: "CompletionRequest" + ) -> AsyncGenerator[ + Union[List[Union[str, "ErrorResponse"]], "CompletionResponse"], None + ]: + """Runs a completion request to the LLM engine and returns the response. + + Args: + request: A CompletionRequest object. + + Returns: + An AsyncGenerator of the response. If stream is True and batching is enabled, then the generator will yield a list of completion streaming responses (strings of the format data: {response_json}\n\n). Otherwise, it will yield the CompletionResponse object directly. + """ + return await self._run_request( + request, + engine_method="completions", + batch_output_stream=True, + ) + + async def embeddings( + self, request: "EmbeddingRequest" + ) -> AsyncGenerator[Union[List["ErrorResponse"], "EmbeddingResponse"], None]: + """Runs an embeddings request to the engine and returns the response. + + Returns an AsyncGenerator over the EmbeddingResponse object. This is so that the caller can have a consistent interface across all the methods of chat, completions, embeddings and transcriptions. + + Args: + request: An EmbeddingRequest object. + + Returns: + An AsyncGenerator over the EmbeddingResponse object. + """ + # NOTE: Embeddings does not need batching. + return await self._run_request( + request, + engine_method="embeddings", + batch_output_stream=False, + ) + + async def transcriptions( + self, request: "TranscriptionRequest" + ) -> AsyncGenerator[ + Union[List[Union[str, "ErrorResponse"]], "TranscriptionResponse"], None + ]: + """Runs an transcriptions request to the engine and returns the response. + + Returns an AsyncGenerator over the TranscriptionResponse object. This is so that the caller can have a consistent interface across all the methods of chat, completions, embeddings and transcriptions. + + Args: + request: An TranscriptionRequest object. + + Returns: + An AsyncGenerator over the TranscriptionResponse object. + """ + return await self._run_request( + request, + engine_method="transcriptions", + batch_output_stream=True, + ) + + async def score( + self, request: "ScoreRequest" + ) -> AsyncGenerator[Union["ScoreResponse", "ErrorResponse"], None]: + """Runs a score request to the engine and returns the response. + + Returns an AsyncGenerator over the ScoreResponse object. This is so that the caller can have a consistent interface across all the methods of chat, completions, embeddings, and score. + + Args: + request: A ScoreRequest object. + + Returns: + An AsyncGenerator over the ScoreResponse object. + """ + # NOTE: Score does not need batching, similar to embeddings. + return await self._run_request( + request, engine_method="score", batch_output_stream=False + ) + + async def check_health(self) -> None: + """ + Check the health of the replica. Does not return anything. Raise error when + the engine is dead and needs to be restarted. + """ + if self.engine is None: + return + try: + return await self.engine.check_health() + except Exception as e: + logger.error("Engine health check failed in LLMServer.check_health: %s", e) + raise e + + async def reset_prefix_cache(self) -> None: + """Reset the prefix cache of the underlying engine""" + if self.engine is None: + return + try: + await self.engine.reset_prefix_cache() + except Exception as e: + logger.error( + "Engine reset prefix cache failed in LLMServer.reset_prefix_cache: %s", + e, + ) + raise e + + async def start_profile(self) -> None: + """Start profiling""" + if self.engine is None: + return + try: + await self.engine.start_profile() + except Exception as e: + logger.error( + "Engine start profile failed in LLMServer.start_profile: %s", e + ) + raise e + + async def stop_profile(self) -> None: + """Stop profiling""" + if self.engine is None: + return + try: + await self.engine.stop_profile() + except Exception as e: + logger.error("Engine stop profile failed in LLMServer.stop_profile: %s", e) + raise e + + async def llm_config(self) -> Optional[LLMConfig]: + return self._llm_config + + @classmethod + def get_deployment_options(cls, llm_config: "LLMConfig"): + engine_config = llm_config.get_engine_config() + deployment_options = copy.deepcopy(llm_config.deployment_config) + + # Handle the ray_actor_options that could be passed in to + # deployment_options + ray_actor_options = deployment_options.get("ray_actor_options", {}) + + replica_actor_resources = { + "CPU": ray_actor_options.get("num_cpus", 1), + "GPU": ray_actor_options.get("num_gpus", 0), + **ray_actor_options.get("resources", {}), + } + if "memory" in ray_actor_options: + replica_actor_resources["memory"] = ray_actor_options["memory"] + + if ( + "placement_group_bundles" in llm_config.deployment_config + or "placement_group_strategy" in llm_config.deployment_config + ): + raise ValueError( + "placement_group_bundles and placement_group_strategy must not be specified in deployment_config. You can override the default values by setting the `placement_group_config` in the LLMConfig." + ) + + # TODO: Move this _merge_replica_actor_and_child_actor_bundles to a + # more generic place. + pg_bundles = _merge_replica_actor_and_child_actor_bundles( + engine_config.placement_bundles, replica_actor_resources + ) + + deployment_options.update( + { + "placement_group_bundles": pg_bundles, + "placement_group_strategy": engine_config.placement_strategy, + } + ) + + # Handle env vars from runtime_env + default_runtime_env = ray.get_runtime_context().runtime_env + if ENABLE_WORKER_PROCESS_SETUP_HOOK: + default_runtime_env[ + "worker_process_setup_hook" + ] = "ray.llm._internal.serve._worker_process_setup_hook" + + ray_actor_options = deployment_options.get("ray_actor_options", {}) + ray_actor_options["runtime_env"] = { + **default_runtime_env, + # Existing runtime_env should take precedence over the default. + **ray_actor_options.get("runtime_env", {}), + **(llm_config.runtime_env if llm_config.runtime_env else {}), + } + deployment_options["ray_actor_options"] = ray_actor_options + + return deployment_options diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fe0209ade3c1e84bbf9f845a59b514f1bc65aef Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9daffe5d3b9e86de588cd6d83578c6d87282b429 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_engine.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38cd1c285f71b84d810376d619864db8074ccc09 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_engine.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_models.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53ae67086cca84f8065c2903a310c6e6fda08346 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/__pycache__/vllm_models.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f3fd0377f8e7680725208b45d6b23ecf5d6d9106 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__init__.py @@ -0,0 +1,5 @@ +"""KV Transfer connector backends for Ray Serve LLM. + +This package provides connector backends for KV cache transfer in vLLM. +All backends are lazily loaded through the factory to avoid circular imports. +""" diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..792dae4e0423f4e3cc1a873c9300963a5a240955 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/base.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..799c53686d101601c8555d2f2fd7c442641095be Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/base.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/factory.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/factory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20f7348ab88205d2e0f89af8ec803498f1e45d0c Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/factory.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/lmcache.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/lmcache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8096021f9e6af29082af93ce4fea2cebce742e7 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/lmcache.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/multi_connector.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/multi_connector.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6c4ce104b625f6f2b19654206eb4c19d9931dc9 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/multi_connector.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/nixl.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/nixl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aed1cc65db0d5be5e8e71fb620a4eeeba4d2478f Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/__pycache__/nixl.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/base.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/base.py new file mode 100644 index 0000000000000000000000000000000000000000..527d3fd7ec14d21345e5641b8434748e0129a045 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/base.py @@ -0,0 +1,81 @@ +import abc +import random +import string +from typing import TYPE_CHECKING, Any, Dict + +from ray import serve + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + + +class BaseConnectorBackend(abc.ABC): + def __init__(self, llm_config: "LLMConfig"): + """Base class for connector backends. + + Args: + llm_config: The llm configuration for this engine + """ + self.llm_config = llm_config + + @property + def kv_transfer_config(self) -> Dict[str, Any]: + engine_kwargs = self.llm_config.engine_kwargs + kv_transfer_config = engine_kwargs.get("kv_transfer_config") + assert ( + kv_transfer_config is not None + ), "In Connector backend, kv_transfer_config is not set" + return kv_transfer_config + + def _get_unique_suffix(self, len: int = 6) -> str: + """Generates unique alphanumeric suffix. + + Args: + len: Length of the suffix to generate. + Returns: + A unique alphanumeric suffix string of specified length. + """ + return "".join(random.choices(string.ascii_letters + string.digits, k=len)) + + def _compute_port_offset(self) -> int: + """Compute a deterministic port offset for this replica. + + Uses data_parallel_rank if DP case, otherwise falls back to + the replica rank assigned by Ray Serve (TP/PP case). + + For TP/PP cases, multiply by num_devices (tp × pp) to reserve + sufficient port space, since each worker needs a unique port. + Each TP worker adds its tp_rank (0, 1, ..., tp_size-1) to the + base port at bind time, and PP stages also need separate ports. + + Returns: + Non-negative integer offset to add to a base port. + """ + # Prefer explicit DP rank when available + dp_rank = self.llm_config.engine_kwargs.get("data_parallel_rank") + if isinstance(dp_rank, int) and dp_rank >= 0: + # vLLM already accounts for TP spacing in DP offset calculation + # (data_parallel_rank × tp_size), don't multiply here + return dp_rank + + # Fall back to Serve replica rank for TP/PP cases + try: + rc = serve.get_replica_context() + if rc and hasattr(rc, "rank"): + # Use num_devices (tp × pp) to reserve ports for all workers + # Each replica spawns num_devices workers, each needing a unique port + engine_config = self.llm_config.get_engine_config() + num_devices = engine_config.num_devices + return rc.rank * num_devices + except Exception: + # Best-effort fallback; avoid introducing failures in setup paths + pass + + return 0 + + def setup(self) -> None: + """Setup the connector backend. + + This method is called to setup the connector backend. + """ + pass diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/factory.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/factory.py new file mode 100644 index 0000000000000000000000000000000000000000..742955e6f6e2d6ce7e424309858fd00183772dbe --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/factory.py @@ -0,0 +1,136 @@ +"""Factory for lazy-loading KV connector backends. + +This module provides a factory pattern for registering and instantiating +KV connector backends without eagerly importing all implementations. +This avoids circular import issues and improves startup performance. +""" + +from typing import TYPE_CHECKING, Type, Union + +from ray.llm._internal.serve.engines.vllm.kv_transfer.base import ( + BaseConnectorBackend, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.utils.registry import get_registry + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + + +logger = get_logger(__name__) + +# Get the registry instance for KV connector backends +_kv_backend_registry = get_registry("kv_connector_backend") + + +class KVConnectorBackendFactory: + """Factory for creating KV connector backend instances with lazy loading.""" + + @classmethod + def register_backend( + cls, + name: str, + backend_class_or_path: Union[Type["BaseConnectorBackend"], str], + ) -> None: + """Register a connector backend. + + This enables the backend to be accessed on every Ray process in the cluster. + + Args: + name: The name of the connector (e.g., "LMCacheConnectorV1") + backend_class_or_path: Either: + - The backend class object directly (preferred), or + - A string in the format "module_path:class_name" for lazy loading + + Examples: + # Register with class directly (recommended): + KVConnectorBackendFactory.register_backend("MyConnector", MyConnectorClass) + + # Register with module path string (for lazy loading): + KVConnectorBackendFactory.register_backend("MyConnector", "my.module:MyClass") + """ + _kv_backend_registry.register(name, backend_class_or_path) + + @classmethod + def get_backend_class(cls, name: str) -> Type["BaseConnectorBackend"]: + """Get the connector backend class by name. + + For registered connectors, returns the registered backend class. + For unregistered connectors, returns BaseConnectorBackend which has + a no-op setup() method, allowing connectors that don't require + Ray Serve orchestration to work without registration. + + Args: + name: The name of the connector backend + + Returns: + The connector backend class + + Raises: + ImportError: If a registered backend fails to load + """ + try: + return _kv_backend_registry.get(name) + except ValueError: + logger.warning( + f"Unsupported connector backend: {name}. " + f"Using default: {BaseConnectorBackend.__name__}." + ) + return BaseConnectorBackend + except Exception as e: + raise ImportError( + f"Failed to load connector backend '{name}': {type(e).__name__}: {e}" + ) from e + + @classmethod + def create_backend( + cls, name: str, llm_config: "LLMConfig" + ) -> "BaseConnectorBackend": + """Create a connector backend instance. + + Args: + name: The name of the connector backend + llm_config: The LLM configuration + + Returns: + An instance of the connector backend + """ + return cls.get_backend_class(name)(llm_config) + + @classmethod + def is_registered(cls, name: str) -> bool: + """Check if a connector backend is registered.""" + return _kv_backend_registry.contains(name) + + @classmethod + def unregister_backend(cls, name: str) -> None: + """Unregister a connector backend. + + Removes the backend from the registry across all Ray processes. + + Args: + name: The name of the connector backend to unregister + """ + _kv_backend_registry.unregister(name) + + +BUILTIN_BACKENDS = { + "LMCacheConnectorV1": "ray.llm._internal.serve.engines.vllm.kv_transfer.lmcache:LMCacheConnectorV1Backend", + "NixlConnector": "ray.llm._internal.serve.engines.vllm.kv_transfer.nixl:NixlConnectorBackend", + "MultiConnector": "ray.llm._internal.serve.engines.vllm.kv_transfer.multi_connector:MultiConnectorBackend", +} + + +def _initialize_registry() -> None: + """Initialize the registry with built-in backends. + + This function is called when the module is imported to ensure + built-in backends are registered. + """ + for name, backend_path in BUILTIN_BACKENDS.items(): + if not KVConnectorBackendFactory.is_registered(name): + KVConnectorBackendFactory.register_backend(name, backend_path) + + +# Initialize registry when module is imported +_initialize_registry() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/lmcache.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/lmcache.py new file mode 100644 index 0000000000000000000000000000000000000000..b60e85a4764f38e115c097be3155f6c5b60524c5 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/lmcache.py @@ -0,0 +1,61 @@ +from ray.llm._internal.serve.engines.vllm.kv_transfer.base import ( + BaseConnectorBackend, +) +from ray.llm._internal.serve.observability.logging import get_logger + +logger = get_logger(__name__) + + +def _check_lmcache_installed(): + try: + import lmcache # noqa: F401 + except ImportError: + raise ImportError( + "LMCache is not installed. Please install it with `pip install lmcache`." + ) + + +class LMCacheConnectorV1Backend(BaseConnectorBackend): + + KV_CONNECTOR_EXTRA_CONFIG_FIELD_NAME = "kv_connector_extra_config" + LMCACHE_RPC_PORT_FIELD_NAME = "lmcache_rpc_port" + DEFAULT_LMCACHE_RPC_PORT_NAME = "lmcache_rpc_port" + + def setup(self) -> None: + """Initialize the LMCache connector backend. + + Creates a unique LMCache RPC port name across replicas by appending + a random suffix to the base port name. + + Raises: + ImportError: If LMCache is not installed. + """ + _check_lmcache_installed() + + if ( + LMCacheConnectorV1Backend.KV_CONNECTOR_EXTRA_CONFIG_FIELD_NAME + not in self.kv_transfer_config + ): + return + + kv_connector_extra_config = self.kv_transfer_config[ + LMCacheConnectorV1Backend.KV_CONNECTOR_EXTRA_CONFIG_FIELD_NAME + ] + base_value = kv_connector_extra_config.get( + LMCacheConnectorV1Backend.LMCACHE_RPC_PORT_FIELD_NAME, + LMCacheConnectorV1Backend.DEFAULT_LMCACHE_RPC_PORT_NAME, + ) + + # Append random suffix for uniqueness + lmcache_rpc_port_value = str(base_value) + self._get_unique_suffix() + if ( + LMCacheConnectorV1Backend.LMCACHE_RPC_PORT_FIELD_NAME + in kv_connector_extra_config + ): + logger.info( + f"Setting unique lmcache_rpc_port={lmcache_rpc_port_value} for current replica." + ) + + kv_connector_extra_config[ + LMCacheConnectorV1Backend.LMCACHE_RPC_PORT_FIELD_NAME + ] = lmcache_rpc_port_value diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/multi_connector.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/multi_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..56e556bbbfc19d3f8123a106f4206c09456d9349 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/multi_connector.py @@ -0,0 +1,51 @@ +import copy +from typing import TYPE_CHECKING + +from ray.llm._internal.serve.engines.vllm.kv_transfer.base import ( + BaseConnectorBackend, +) +from ray.llm._internal.serve.engines.vllm.kv_transfer.factory import ( + KVConnectorBackendFactory, +) + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + + +class MultiConnectorBackend(BaseConnectorBackend): + def __init__(self, llm_config: "LLMConfig"): + super().__init__(llm_config) + + def setup(self) -> None: + """Setup all connectors listed in the kv_transfer_config.""" + kv_transfer_config = self.kv_transfer_config + connectors = kv_transfer_config.get("kv_connector_extra_config", {}).get( + "connectors", [] + ) + + for connector in connectors: + connector_backend_str = connector.get("kv_connector") + if connector_backend_str is None: + raise ValueError("kv_connector is not set in the connector") + + if connector_backend_str == "MultiConnector": + raise ValueError( + "Nesting MultiConnector within MultiConnector is not supported." + ) + + # Merge parent config with connector-specific config + sub_llm_config = copy.deepcopy(self.llm_config) + sub_llm_config.engine_kwargs["kv_transfer_config"] = { + **{ + k: v + for k, v in kv_transfer_config.items() + if k != "kv_connector_extra_config" + }, + **connector, + } + + # Use factory to get backend class lazily + connector_backend = KVConnectorBackendFactory.create_backend( + connector_backend_str, sub_llm_config + ) + connector_backend.setup() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/nixl.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/nixl.py new file mode 100644 index 0000000000000000000000000000000000000000..7f95546d3df424f0fe208adde27383ec4910fc78 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/kv_transfer/nixl.py @@ -0,0 +1,61 @@ +import os + +from ray.llm._internal.serve.engines.vllm.kv_transfer.base import ( + BaseConnectorBackend, +) + + +class NixlConnectorBackend(BaseConnectorBackend): + def _set_side_channel_port(self): + from vllm import envs as vllm_envs, utils as vllm_utils + + if not vllm_envs.is_set("VLLM_NIXL_SIDE_CHANNEL_PORT"): + base_port: int = int( + self.llm_config.experimental_configs.get( + "NIXL_SIDE_CHANNEL_PORT_BASE", vllm_utils.get_open_port() + ) + ) + # Use a deterministic rank-based offset (DP rank if set; else replica hash) + port = base_port + self._compute_port_offset() + os.environ["VLLM_NIXL_SIDE_CHANNEL_PORT"] = str(port) + + def _set_side_channel_host(self): + from vllm import envs as vllm_envs, utils as vllm_utils + + if not vllm_envs.is_set("VLLM_NIXL_SIDE_CHANNEL_HOST"): + os.environ["VLLM_NIXL_SIDE_CHANNEL_HOST"] = vllm_utils.get_ip() + + def setup(self) -> None: + """Initialize the NIXL connector backend. + + This method sets up the NIXL (NVIDIA Inference Xfer Library) connector by: + 1. Verifying that the required vLLM environment variables are supported + 2. Configuring the side channel port and host if not already set + 3. Creating a unique engine ID across replicas + + The side channel is used for KV cache transfer between vLLM instances. + + Raises: + ValueError: If the current vLLM version doesn't support the required + NIXL environment variables. + """ + from vllm import envs as vllm_envs + + if ( + "VLLM_NIXL_SIDE_CHANNEL_PORT" not in vllm_envs.environment_variables + or "VLLM_NIXL_SIDE_CHANNEL_HOST" not in vllm_envs.environment_variables + ): + raise ValueError( + "This vLLM version does not support VLLM_NIXL_SIDE_CHANNEL_PORT" + "or VLLM_NIXL_SIDE_CHANNEL_HOST environment variable. It's likely" + "that you are using an older version of vLLM." + ) + + self._set_side_channel_port() + self._set_side_channel_host() + + # We need to overwrite the engine_id to make it unique across replicas. + engine_id = self.kv_transfer_config.get("engine_id", self._get_unique_suffix()) + host = vllm_envs.VLLM_NIXL_SIDE_CHANNEL_HOST + port = vllm_envs.VLLM_NIXL_SIDE_CHANNEL_PORT + self.kv_transfer_config["engine_id"] = "-".join([engine_id, host, str(port)]) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_engine.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..13be7465f885e283a47a9552c0b004708f74dbba --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_engine.py @@ -0,0 +1,531 @@ +import argparse +import os +from typing import TYPE_CHECKING, AsyncGenerator, Optional, Tuple, Union + +from starlette.datastructures import State +from starlette.requests import Request +from vllm.engine.arg_utils import AsyncEngineArgs +from vllm.entrypoints.openai.cli_args import FrontendArgs +from vllm.entrypoints.openai.protocol import ErrorResponse as VLLMErrorResponse + +import ray +from ray.llm._internal.common.callbacks.base import CallbackCtx +from ray.llm._internal.common.utils.import_utils import try_import +from ray.llm._internal.serve.core.configs.llm_config import ( + DiskMultiplexConfig, + LLMConfig, +) +from ray.llm._internal.serve.core.configs.openai_api_models import ( + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + EmbeddingRequest, + EmbeddingResponse, + ErrorInfo, + ErrorResponse, + ScoreRequest, + ScoreResponse, + TranscriptionRequest, + TranscriptionResponse, +) +from ray.llm._internal.serve.core.engine.protocol import LLMEngine +from ray.llm._internal.serve.engines.vllm.vllm_models import ( + VLLMEngineConfig, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.utils.node_initialization_utils import ( + initialize_node, +) +from ray.util.placement_group import PlacementGroup +from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy + +if TYPE_CHECKING: + from vllm.config import VllmConfig + from vllm.engine.protocol import EngineClient + from vllm.entrypoints.openai.serving_chat import OpenAIServingChat + from vllm.entrypoints.openai.serving_completion import OpenAIServingCompletion + from vllm.entrypoints.openai.serving_embedding import OpenAIServingEmbedding + from vllm.entrypoints.openai.serving_models import OpenAIServingModels + from vllm.entrypoints.openai.serving_score import ServingScores + from vllm.entrypoints.openai.serving_transcription import OpenAIServingTranscription + +vllm = try_import("vllm") +logger = get_logger(__name__) + + +def _get_vllm_engine_config( + llm_config: LLMConfig, +) -> Tuple["AsyncEngineArgs", "VllmConfig"]: + engine_config = llm_config.get_engine_config() + + # Resolve to local cache path if model was downloaded from S3/GCS mirror + # Only do this if mirror_config was specified (intentional S3/GCS download) + if engine_config.mirror_config: + from ray.llm._internal.common.utils.download_utils import ( + get_model_location_on_disk, + ) + + local_path = get_model_location_on_disk(engine_config.actual_hf_model_id) + if local_path and local_path != engine_config.actual_hf_model_id: + engine_config.hf_model_id = local_path + logger.info(f"Resolved model from mirror to local path: {local_path}") + + async_engine_args = vllm.engine.arg_utils.AsyncEngineArgs( + **engine_config.get_initialization_kwargs() + ) + from vllm.usage.usage_lib import UsageContext + + vllm_engine_config = async_engine_args.create_engine_config( + usage_context=UsageContext.OPENAI_API_SERVER + ) + return async_engine_args, vllm_engine_config + + +def _clear_current_platform_cache(): + """Clear the cache of the current platform. + + vllm current has an lru cache for getting device compatibility + that will not have the correct returned value if + CUDA_VISIBLE_DEVICES is not set properly. In RayLLM eventually + when we want to create the engine the env will be set properly, + but till then, upon the import of vllm somewhere + (which is a mystery) the lru cache will have the wrong value. + This function will clear the cache so that the next time the + cache is accessed, it will be re-evaluated. + + Related issues: + https://github.com/vllm-project/vllm/issues/8402 + https://github.com/vllm-project/vllm/issues/7890 + """ + from vllm.platforms import current_platform + + # TODO(seiji): remove this once https://github.com/vllm-project/vllm/pull/18979 is merged + if ( + "CUDA_VISIBLE_DEVICES" in os.environ + and os.environ["CUDA_VISIBLE_DEVICES"] == "" + ): + del os.environ["CUDA_VISIBLE_DEVICES"] + + # This check is just to future proof this implementation + # in case vllm removes their lru_cache decorator + if hasattr(current_platform.get_device_capability, "cache_clear"): + logger.info("Clearing the current platform cache ...") + current_platform.get_device_capability.cache_clear() + + +class VLLMEngine(LLMEngine): + def __init__( + self, + llm_config: LLMConfig, + ): + """Create a vLLM Engine class + + Args: + llm_config: The llm configuration for this engine + """ + super().__init__(llm_config) + + self.llm_config = llm_config + + if vllm is None: + raise ImportError( + "vLLM is not installed. Please install it with `pip install ray[llm]`." + ) + from vllm import envs as vllm_envs + + # TODO (Kourosh): Remove this after a few releases. + if not vllm_envs.VLLM_USE_V1: + logger.error( + "vLLM v0 is fully deprecated. As a result in Ray Serve LLM only v1 is supported." + ) + + self.llm_config.setup_engine_backend() + + self._running = False + + # vLLM Integration points. Will be set through .start() + self._engine_client = None + self._oai_models: Optional["OpenAIServingModels"] = None + self._oai_serving_chat: Optional["OpenAIServingChat"] = None + self._oai_serving_completion: Optional["OpenAIServingCompletion"] = None + self._oai_serving_embedding: Optional["OpenAIServingEmbedding"] = None + self._oai_serving_transcription: Optional["OpenAIServingTranscription"] = None + self._oai_serving_scores: Optional["ServingScores"] = None + + async def start(self) -> None: + """Start the vLLM engine. + + If the engine is already running, do nothing. + """ + + if self._running: + # The engine is already running! + logger.info("Skipping engine restart because the engine is already running") + return + + from vllm.entrypoints.openai.api_server import init_app_state + + callback = self.llm_config.get_or_create_callback() + await callback.run_callback("on_before_node_init") + if callback.ctx.run_init_node: + await initialize_node(self.llm_config) + await callback.run_callback("on_after_node_init") + + ( + vllm_engine_args, + vllm_frontend_args, + vllm_engine_config, + ) = self._prepare_engine_config(callback.ctx) + + # Apply checkpoint info to the llm_config. + # This is needed for capturing model capabilities + # (e.g. supports vision, etc.) on the llm_config. + config = self.llm_config.get_engine_config() + self.llm_config.apply_checkpoint_info( + vllm_engine_config.model_config.model, + trust_remote_code=config.trust_remote_code, + ) + + self._engine_client = self._start_async_llm_engine( + vllm_engine_args, + vllm_engine_config, + callback.ctx.placement_group, + ) + + state = State() + # TODO (Kourosh): There might be some variables that needs protection? + args = argparse.Namespace( + **vllm_frontend_args.__dict__, + **vllm_engine_args.__dict__, + ) + + await init_app_state( + engine_client=self._engine_client, + # TODO (ahao): remove vllm_config for vllm v1.12 + vllm_config=vllm_engine_config, + state=state, + args=args, + ) + + self._oai_models = state.openai_serving_models + self._oai_serving_chat = state.openai_serving_chat + self._oai_serving_completion = state.openai_serving_completion + self._oai_serving_embedding = state.openai_serving_embedding + self._oai_serving_transcription = state.openai_serving_transcription + self._oai_serving_scores = state.openai_serving_scores + + self._validate_openai_serving_models() + self._validate_engine_client() + + self._running = True + + logger.info("Started vLLM engine.") + + def _validate_openai_serving_models(self): + assert self._oai_models is not None, "oai_models is not initialized" + assert hasattr( + self._oai_models, "lora_requests" + ), "oai_models must have a lora_requests attribute" + assert hasattr( + self._oai_models, "load_lora_adapter" + ), "oai_models must have a load_lora_adapter attribute" + + def _validate_openai_serving_chat(self): + assert hasattr( + self._oai_serving_chat, "create_chat_completion" + ), "oai_serving_chat must have a create_chat_completion attribute" + + def _validate_openai_serving_completion(self): + assert hasattr( + self._oai_serving_completion, "create_completion" + ), "oai_serving_completion must have a create_completion attribute" + + def _validate_openai_serving_embedding(self): + assert hasattr( + self._oai_serving_embedding, "create_embedding" + ), "oai_serving_embedding must have a create_embedding attribute" + + def _validate_openai_serving_transcription(self): + assert hasattr( + self._oai_serving_transcription, "create_transcription" + ), "oai_serving_transcription must have a create_transcription attribute" + + def _validate_openai_serving_scores(self): + assert hasattr( + self._oai_serving_scores, "create_score" + ), "oai_serving_scores must have a create_score attribute" + + def _validate_engine_client(self): + assert hasattr( + self._engine_client, "check_health" + ), "engine_client must have a check_health attribute" + + def _prepare_engine_config( + self, callback_ctx: CallbackCtx + ) -> Tuple["AsyncEngineArgs", "FrontendArgs", "VllmConfig"]: + """Prepare the engine config to start the engine. + + Args: + callback_ctx: The callback context. + + Returns: + A tuple of: + engine_args: The vLLM's internal engine arguments that is flattened. + frontend_args: The vLLM's internal frontend arguments that is flattened. + engine_config: The vLLM's internal engine config that is nested. + """ + + engine_config: VLLMEngineConfig = self.llm_config.get_engine_config() + + if engine_config.use_gpu: + # Create engine config on a task with access to GPU, + # as GPU capability may be queried. + ref = ( + ray.remote( + num_cpus=0, + num_gpus=0.001, + accelerator_type=self.llm_config.accelerator_type, + )(_get_vllm_engine_config) + .options( + runtime_env=callback_ctx.runtime_env, + scheduling_strategy=PlacementGroupSchedulingStrategy( + placement_group=callback_ctx.placement_group, + ), + ) + .remote(self.llm_config) + ) + vllm_engine_args, vllm_engine_config = ray.get(ref) + else: + vllm_engine_args, vllm_engine_config = _get_vllm_engine_config( + self.llm_config + ) + + vllm_frontend_args = FrontendArgs(**engine_config.frontend_kwargs) + return vllm_engine_args, vllm_frontend_args, vllm_engine_config + + def _start_async_llm_engine( + self, + vllm_engine_args: "AsyncEngineArgs", + vllm_engine_config: "VllmConfig", + placement_group: PlacementGroup, + ) -> "EngineClient": + """Creates an async LLM engine from the engine arguments.""" + + from vllm.v1.engine.async_llm import AsyncLLM + from vllm.v1.executor.abstract import Executor + + vllm_engine_config.parallel_config.placement_group = placement_group + + _clear_current_platform_cache() + + custom_stat_loggers = None + if self.llm_config.log_engine_metrics: + from vllm.v1.metrics.ray_wrappers import RayPrometheusStatLogger + + # V1 AsyncLLM does not yet support add_logger: https://github.com/vllm-project/vllm/issues/17702 + # Use `disable_log_stats: False` and `log_engine_metrics: False` as + # a workaround to enable PrometheusStatLogger instead. + custom_stat_loggers = [RayPrometheusStatLogger] + + executor_class = Executor.get_class(vllm_engine_config) + logger.info(f"Using executor class: {executor_class}") + engine_client = AsyncLLM( + vllm_config=vllm_engine_config, + executor_class=executor_class, + log_stats=not vllm_engine_args.disable_log_stats, + stat_loggers=custom_stat_loggers, + ) + + return engine_client + + async def resolve_lora(self, disk_lora_model: DiskMultiplexConfig): + from vllm.entrypoints.openai.protocol import LoadLoRAAdapterRequest + + self._validate_openai_serving_models() + + if disk_lora_model.model_id in self._oai_models.lora_requests: + # Lora is already loaded, return + return + + lora_request = await self._oai_models.load_lora_adapter( # type: ignore[attr-defined] + request=LoadLoRAAdapterRequest( + lora_name=disk_lora_model.model_id, + lora_path=disk_lora_model.local_path, + ) + ) + + if isinstance(lora_request, VLLMErrorResponse): + raise ValueError(f"Failed to load lora model: {lora_request.error.message}") + + def _create_raw_request( + self, + request: Union[ + CompletionRequest, + ChatCompletionRequest, + EmbeddingRequest, + TranscriptionRequest, + ScoreRequest, + ], + path: str, + ) -> Request: + scope = { + "type": "http", + "method": "POST", + "path": path, + "headers": [(b"x-request-id", getattr(request, "request_id", "").encode())], + "query_string": b"", + } + return Request(scope) + + async def chat( + self, request: ChatCompletionRequest + ) -> AsyncGenerator[Union[str, ChatCompletionResponse, ErrorResponse], None]: + self._validate_openai_serving_chat() + + # TODO (Kourosh): Remove when we upstream request_id attribute to vLLM. + # PR: https://github.com/vllm-project/vllm/pull/21009 + # Create a fake starlette.Request object with the x-request-id header + # so that the create_chat_completion API can assign the request_id properly. + raw_request = self._create_raw_request(request, "/chat/completions") + + chat_response = await self._oai_serving_chat.create_chat_completion( # type: ignore[attr-defined] + request, raw_request=raw_request + ) + + if isinstance(chat_response, AsyncGenerator): + async for response in chat_response: + if not isinstance(response, str): + raise ValueError( + f"Expected create_chat_completion to return a stream of strings, got an item with type {type(response)}" + ) + yield response + else: + if isinstance(chat_response, VLLMErrorResponse): + yield ErrorResponse(error=ErrorInfo(**chat_response.error.model_dump())) + else: + yield ChatCompletionResponse(**chat_response.model_dump()) + + async def completions( + self, request: CompletionRequest + ) -> AsyncGenerator[Union[str, CompletionResponse, ErrorResponse], None]: + self._validate_openai_serving_completion() + + # TODO (Kourosh): Remove when we upstream request_id attribute to vLLM. + # PR: https://github.com/vllm-project/vllm/pull/21009 + # Create a fake starlette.Request object with the x-request-id header + # so that the create_completion API can assign the request_id properly. + raw_request = self._create_raw_request(request, "/completions") + + completion_response = await self._oai_serving_completion.create_completion( # type: ignore[attr-defined] + request, + raw_request=raw_request, + ) + + if isinstance(completion_response, AsyncGenerator): + async for response in completion_response: + if not isinstance(response, str): + raise ValueError( + f"Expected create_completion to return a stream of strings, got an item with type {type(response)}" + ) + yield response + else: + if isinstance(completion_response, VLLMErrorResponse): + yield ErrorResponse( + error=ErrorInfo(**completion_response.error.model_dump()) + ) + else: + yield CompletionResponse(**completion_response.model_dump()) + + async def embeddings( + self, request: EmbeddingRequest + ) -> AsyncGenerator[Union[EmbeddingResponse, ErrorResponse], None]: + self._validate_openai_serving_embedding() + + # TODO (Kourosh): Remove when upstream is fixed to accept req_id. + # Create a fake starlette.Request object with the x-request-id header + # so that the create_embedding API can assign the request_id properly. + raw_request = self._create_raw_request(request, "/embeddings") + + embedding_response = await self._oai_serving_embedding.create_embedding( # type: ignore[attr-defined] + request, raw_request=raw_request + ) + + if isinstance(embedding_response, VLLMErrorResponse): + yield ErrorResponse( + error=ErrorInfo(**embedding_response.error.model_dump()) + ) + else: + yield EmbeddingResponse(**embedding_response.model_dump()) + + async def transcriptions( + self, request: TranscriptionRequest + ) -> AsyncGenerator[Union[str, TranscriptionResponse, ErrorResponse], None]: + self._validate_openai_serving_transcription() + + # TODO (Kourosh): Remove when we upstream request_id attribute to vLLM. + # PR: https://github.com/vllm-project/vllm/pull/21009 + # Create a fake starlette.Request object with the x-request-id header + # so that the create_transcription API can assign the request_id properly. + raw_request = self._create_raw_request(request, "/audio/transcriptions") + + # Extract audio data from the request file + audio_data = await request.file.read() + + transcription_response = await self._oai_serving_transcription.create_transcription( # type: ignore[attr-defined] + audio_data, + request, + raw_request=raw_request, + ) + + if isinstance(transcription_response, AsyncGenerator): + async for response in transcription_response: + if not isinstance(response, str): + raise ValueError( + f"Expected create_transcription to return a stream of strings, got an item with type {type(response)}" + ) + yield response + else: + if isinstance(transcription_response, VLLMErrorResponse): + yield ErrorResponse( + error=ErrorInfo(**transcription_response.error.model_dump()) + ) + else: + yield TranscriptionResponse(**transcription_response.model_dump()) + + async def score( + self, request: ScoreRequest + ) -> AsyncGenerator[Union[ScoreResponse, ErrorResponse], None]: + self._validate_openai_serving_scores() + + raw_request = self._create_raw_request(request, "/score") + + score_response = await self._oai_serving_scores.create_score( + request, raw_request=raw_request + ) + + if isinstance(score_response, VLLMErrorResponse): + yield ErrorResponse(**score_response.model_dump()) + else: + yield ScoreResponse(**score_response.model_dump()) + + async def check_health(self) -> None: + assert self._engine_client is not None, "engine_client is not initialized" + + try: + await self._engine_client.check_health() + except BaseException as e: + logger.error("Healthcheck failed. The replica will be restarted") + raise e from None + + async def reset_prefix_cache(self) -> None: + assert self._engine_client is not None, "engine_client is not initialized" + await self._engine_client.reset_prefix_cache() + + async def start_profile(self) -> None: + assert self._engine_client is not None, "engine_client is not initialized" + await self._engine_client.start_profile() + + async def stop_profile(self) -> None: + assert self._engine_client is not None, "engine_client is not initialized" + await self._engine_client.stop_profile() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_models.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_models.py new file mode 100644 index 0000000000000000000000000000000000000000..7af47e8b588a14bd81872d16f1b42e706c572fc2 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/engines/vllm/vllm_models.py @@ -0,0 +1,320 @@ +import copy +import dataclasses +import os +from typing import Any, Dict, List, Literal, Optional + +from pydantic import ConfigDict, Field, field_validator, model_validator +from vllm.engine.arg_utils import AsyncEngineArgs +from vllm.entrypoints.openai.cli_args import FrontendArgs + +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.llm._internal.common.utils.cloud_utils import CloudMirrorConfig +from ray.llm._internal.common.utils.import_utils import try_import +from ray.llm._internal.serve.constants import ( + ALLOW_NEW_PLACEMENT_GROUPS_IN_DEPLOYMENT, + ENV_VARS_TO_PROPAGATE, +) +from ray.llm._internal.serve.core.configs.llm_config import ( + GPUType, + LLMConfig, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.util.placement_group import ( + PlacementGroup, + get_current_placement_group, + placement_group, + placement_group_table, +) + +# The key for the kv_transfer_params in the internal metadata. +KV_TRANSFER_PARAMS_KEY = "kv_transfer_params" +vllm = try_import("vllm") +logger = get_logger(__name__) + + +class BundleConfig(BaseModelExtended): + """Configuration for placement group bundle. + + Note: Counts are floats to align with Ray resource typing. + """ + + CPU: float = Field(default=0.0, ge=0.0, description="Number of CPUs per bundle") + GPU: float = Field(default=1.0, ge=0.0, description="Number of GPUs per bundle") + + class Config: + extra = "allow" # Allow arbitrary resource types + + +class PlacementGroupConfig(BaseModelExtended): + """Configuration for placement group.""" + + bundles: List[BundleConfig] = Field(description="List of resource bundles") + strategy: Literal["PACK", "SPREAD", "STRICT_PACK", "STRICT_SPREAD"] = Field( + default="PACK", description="Placement group strategy" + ) + + @model_validator(mode="before") + @classmethod + def validate_bundles_exist(cls, values): + if isinstance(values, dict) and "bundles" not in values: + raise ValueError("placement_group_config must contain 'bundles'") + return values + + +class VLLMEngineConfig(BaseModelExtended): + model_config = ConfigDict( + use_enum_values=True, + ) + + model_id: str = Field( + description="The identifier for the model. This is the id that will be used to query the model.", + ) + hf_model_id: Optional[str] = Field( + None, description="The Hugging Face model identifier." + ) + mirror_config: Optional[CloudMirrorConfig] = Field( + None, + description="Configuration for cloud storage mirror. This is for where the weights are downloaded from.", + ) + accelerator_type: Optional[GPUType] = Field( + None, + description="The type of accelerator to use. This is used to determine the placement group strategy.", + ) + placement_group_config: Optional[Dict[str, Any]] = Field( + default=None, + description=( + "Ray placement group configuration for scheduling vLLM engine workers. " + "Defines resource bundles and placement strategy for multi-node deployments. " + "Defaults to PACK strategy with automatic bundle generation based on TP/PP sizes." + ), + ) + + @field_validator("placement_group_config") + @classmethod + def validate_placement_group_config(cls, value): + if value is None: + return None + # Validate through PlacementGroupConfig, then dump back to dict + validated = PlacementGroupConfig(**value) + return validated.model_dump() + + runtime_env: Optional[Dict[str, Any]] = None + engine_kwargs: Dict[str, Any] = {} + frontend_kwargs: Dict[str, Any] = {} + + @property + def actual_hf_model_id(self) -> str: + return self.hf_model_id or self.model_id + + @property + def trust_remote_code(self) -> bool: + return self.engine_kwargs.get("trust_remote_code", False) + + def get_initialization_kwargs(self) -> dict: + """ + Get kwargs that will be actually passed to the LLMInitializer + constructor. + """ + engine_kwargs = self.engine_kwargs.copy() + + if "model" in engine_kwargs or "served_model_name" in engine_kwargs: + raise ValueError( + "model or served_model_name is not allowed in engine_kwargs when using Ray Serve LLM. Please use `model_loading_config` in LLMConfig instead." + ) + + engine_kwargs["model"] = self.actual_hf_model_id + engine_kwargs["served_model_name"] = [self.model_id] + + if ( + "distributed_executor_backend" in engine_kwargs + and engine_kwargs["distributed_executor_backend"] != "ray" + ): + raise ValueError( + "distributed_executor_backend != 'ray' is not allowed in engine_kwargs when using Ray Serve LLM Configs." + ) + else: + engine_kwargs["distributed_executor_backend"] = "ray" + + # TODO (Nikhil): Remove this once vLLM fully deprecates disable_log_requests. + if "disable_log_requests" in engine_kwargs: + logger.warning( + "disable_log_requests is set in engine_kwargs, but vLLM " + "does not support it. Converting to enable_log_requests." + ) + engine_kwargs["enable_log_requests"] = not engine_kwargs.pop( + "disable_log_requests" + ) + elif "enable_log_requests" not in engine_kwargs: + engine_kwargs["enable_log_requests"] = False + + return engine_kwargs + + def get_runtime_env_with_local_env_vars(self) -> dict: + runtime_env = self.runtime_env or {} + runtime_env.setdefault("env_vars", {}) + + # Propagate env vars to the runtime env + for env_var in ENV_VARS_TO_PROPAGATE: + if env_var in os.environ: + runtime_env["env_vars"][env_var] = os.getenv(env_var) + return runtime_env + + @classmethod + def from_llm_config(cls, llm_config: LLMConfig) -> "VLLMEngineConfig": + """Converts the LLMConfig to a VLLMEngineConfig.""" + # Set up the model downloading configuration. + hf_model_id, mirror_config = None, None + if llm_config.model_loading_config.model_source is None: + hf_model_id = llm_config.model_id + elif isinstance(llm_config.model_loading_config.model_source, str): + hf_model_id = llm_config.model_loading_config.model_source + else: + # If it's a CloudMirrorConfig (or subtype) + mirror_config = llm_config.model_loading_config.model_source + + all_engine_kwargs = llm_config.engine_kwargs.copy() + engine_kwargs = {} + frontend_kwargs = {} + + # Get field names from dataclasses + frontend_field_names = { + field.name for field in dataclasses.fields(FrontendArgs) + } + async_engine_field_names = { + field.name for field in dataclasses.fields(AsyncEngineArgs) + } + + for key, value in all_engine_kwargs.items(): + if key in frontend_field_names: + frontend_kwargs[key] = value + elif key in async_engine_field_names: + engine_kwargs[key] = value + else: + raise ValueError(f"Unknown engine argument: {key}") + + # placement_group_config is already validated and stored as dict in LLMConfig + placement_group_config = llm_config.placement_group_config + + return VLLMEngineConfig( + model_id=llm_config.model_id, + hf_model_id=hf_model_id, + mirror_config=mirror_config, + accelerator_type=llm_config.accelerator_type, + engine_kwargs=engine_kwargs, + frontend_kwargs=frontend_kwargs, + runtime_env=llm_config.runtime_env, + placement_group_config=placement_group_config, + ) + + def ray_accelerator_type(self) -> str: + """Converts the accelerator type to the Ray Core format.""" + return f"accelerator_type:{self.accelerator_type}" + + @property + def tensor_parallel_degree(self) -> int: + return self.engine_kwargs.get("tensor_parallel_size", 1) + + @property + def pipeline_parallel_degree(self) -> int: + return self.engine_kwargs.get("pipeline_parallel_size", 1) + + @property + def num_devices(self) -> int: + return self.tensor_parallel_degree * self.pipeline_parallel_degree + + @property + def placement_strategy(self) -> str: + # Use custom strategy if placement_group_config is provided + if self.placement_group_config: + return self.placement_group_config.get("strategy", "PACK") + # Default to PACK (cross-node best-effort placement) + # DP deployments overridden to STRICT_PACK in Serve config + return "PACK" + + @property + def placement_bundles(self) -> List[Dict[str, float]]: + if self.placement_group_config: + # placement_group_config is validated dict; extract bundles + bundles = [] + for bundle_dict in self.placement_group_config["bundles"]: + bundle = bundle_dict.copy() + if self.accelerator_type: + # Use setdefault to add accelerator hint WITHOUT overriding explicit user values + bundle.setdefault(self.ray_accelerator_type(), 0.001) + bundles.append(bundle) + return bundles + + # Default bundles: GPU-only; replica actor contributes CPU to first bundle via merge + bundle = {"GPU": 1} + + if self.accelerator_type: + bundle[self.ray_accelerator_type()] = 0.001 + bundles = [copy.deepcopy(bundle) for _ in range(self.num_devices)] + + return bundles + + @property + def use_gpu(self) -> bool: + """Returns True if vLLM is configured to use GPU resources.""" + # Check placement_group_config bundles for explicit GPU specification + if self.placement_group_config: + bundles = self.placement_group_config.get("bundles", []) + if bundles: + # If any bundle has GPU > 0, we use GPU + return any(bundle.get("GPU", 0) > 0 for bundle in bundles) + + # Default behavior based on accelerator_type + if not self.accelerator_type: + # By default, GPU resources are used + return True + + return self.accelerator_type in ( + GPUType.NVIDIA_TESLA_V100.value, + GPUType.NVIDIA_TESLA_P100.value, + GPUType.NVIDIA_TESLA_T4.value, + GPUType.NVIDIA_TESLA_P4.value, + GPUType.NVIDIA_TESLA_K80.value, + GPUType.NVIDIA_TESLA_A10G.value, + GPUType.NVIDIA_L4.value, + GPUType.NVIDIA_L40S.value, + GPUType.NVIDIA_A100.value, + GPUType.NVIDIA_H100.value, + GPUType.NVIDIA_H200.value, + GPUType.NVIDIA_H20.value, + GPUType.NVIDIA_A100_40G.value, + GPUType.NVIDIA_A100_80G.value, + ) + + def get_or_create_pg(self) -> PlacementGroup: + """Gets or a creates a placement group. + + If we are already in a placement group, return the existing placement group. + Else, create a new placement group based on the scaling config. + """ + dp_rank = self.engine_kwargs.get("data_parallel_rank", None) + pg = get_current_placement_group() + if pg: + logger.debug( + "Using existing placement group %s, details: %s", + pg.id, + placement_group_table(pg), + ) + else: + if not ALLOW_NEW_PLACEMENT_GROUPS_IN_DEPLOYMENT: + raise RuntimeError( + "Creating new placement groups is not allowed. " + "Change RAYLLM_ALLOW_NEW_PLACEMENT_GROUPS_IN_DEPLOYMENT " + "if this is not intended." + ) + name = "" if dp_rank is None else f"dp_{dp_rank}" + + # Use placement_bundles and placement_strategy properties which handle + # both custom and default placement group configurations + pg = placement_group( + bundles=self.placement_bundles, + strategy=self.placement_strategy, + name=name, + ) + + logger.info(f"Using new placement group {pg}. {placement_group_table(pg)}") + return pg diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..22da68f1f549f3dc51a8621171d838a1ae6f3823 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__init__.py @@ -0,0 +1,23 @@ +from ray.llm._internal.common.observability.logging_utils import ( + disable_datasets_logger, + disable_vllm_custom_ops_logger_on_cpu_nodes, +) +from ray.llm._internal.common.observability.telemetry_utils import Once +from ray.llm._internal.serve.observability.logging.setup import ( + setup_logging, +) + +_setup_observability_once = Once() + + +def _setup_observability(): + setup_logging() + disable_datasets_logger() + disable_vllm_custom_ops_logger_on_cpu_nodes() + + +def setup_observability(): + _setup_observability_once.do_once(_setup_observability) + + +__all__ = ["setup_observability"] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b813efb6f447b33864ba22c32fafe87dcc5d95fc Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..914e2a8dce9fccec2e4c4449cb485bd838a24ec0 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__init__.py @@ -0,0 +1,39 @@ +import logging +from typing import Optional + +from ray._common.filters import CoreContextFilter +from ray.serve._private.logging_utils import ServeContextFilter + + +def _setup_logger(logger_name: str): + """Setup logger given the logger name. + + This function is idempotent and won't set up the same logger multiple times. It will + Also skip the setup if Serve logger is already setup and has handlers. + """ + logger = logging.getLogger(logger_name) + serve_logger = logging.getLogger("ray.serve") + + # Skip setup if the logger already has handlers setup or if the parent (Serve + # logger) has handlers. + if logger.handlers or serve_logger.handlers: + return + + # Set up stream handler, which logs to console as plaintext. + stream_handler = logging.StreamHandler() + stream_handler.addFilter(CoreContextFilter()) + stream_handler.addFilter(ServeContextFilter()) + logger.addHandler(stream_handler) + logger.setLevel(logging.INFO) + logger.propagate = False + + +def get_logger(name: Optional[str] = None): + """Get a structured logger inherited from the Ray Serve logger. + + Loggers by default are logging to stdout, and are expected to be scraped by an + external process. + """ + logger_name = f"ray.serve.{name}" + _setup_logger(logger_name) + return logging.getLogger(logger_name) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5eecfff58760387bba085547636b9a92a174f55b Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/setup.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/setup.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba1b0db974de5907d0731a2028679c42dbd474db Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/__pycache__/setup.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/setup.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..3b1915fd2ac62017d00c04d20a323ab191b9234b --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/logging/setup.py @@ -0,0 +1,28 @@ +import logging + +from ray._common.filters import CoreContextFilter +from ray._common.formatters import JSONFormatter +from ray.serve._private.logging_utils import ServeContextFilter + + +def _configure_stdlib_logging(): + """Configures stdlib root logger to make sure stdlib loggers (created as + `logging.getLogger(...)`) are using Ray's `JSONFormatter` with Core and Serve + context filters. + """ + + handler = logging.StreamHandler() + handler.addFilter(CoreContextFilter()) + handler.addFilter(ServeContextFilter()) + handler.setFormatter(JSONFormatter()) + + root_logger = logging.getLogger() + # NOTE: It's crucial we reset all the handlers of the root logger, + # to make sure that logs aren't emitted twice + root_logger.handlers = [] + root_logger.addHandler(handler) + root_logger.setLevel(logging.INFO) + + +def setup_logging(): + _configure_stdlib_logging() diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a8ab2bb39395b9be6e1f3123304133f5758b85d Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/event_loop_monitoring.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/event_loop_monitoring.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4962e43d922bcf39c4fc1d48aac1bc94ad3af8ce Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/event_loop_monitoring.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fast_api_metrics.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fast_api_metrics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6e3d251409c96c8509cf5f23ab86f4259525d9a Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fast_api_metrics.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fastapi_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fastapi_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bf09d64780c47eac75464ba6a70848e7adec3b8 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/fastapi_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/middleware.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/middleware.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abc85f51faa8f36b7d0d54254097fba3482851a5 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/middleware.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d18a1c9091d21fe821ba82fe8a1e5233dc7cb4b3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/event_loop_monitoring.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/event_loop_monitoring.py new file mode 100644 index 0000000000000000000000000000000000000000..ea139c6ab8f7e175dfe2ced23b0213c60c5327ac --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/event_loop_monitoring.py @@ -0,0 +1,77 @@ +import asyncio +import time +from typing import Dict + +from ray.llm._internal.serve.observability.logging import get_logger +from ray.util import metrics + +logger = get_logger(__name__) + +_METRICS_LOOP_INTERVAL = 5 # 5 seconds +EVENT_LOOP_LATENCY_HISTOGRAM_BOUNDARIES = [ + 0.05, + 0.1, + 0.15, + 0.20, + 0.25, + 0.5, + 0.75, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + 15.0, + 20.0, + 30.0, + 45.0, + 60.0, + 90.0, + 120.0, + 150.0, + 180.0, + 300.0, + 600.0, +] + + +def setup_event_loop_monitoring( + loop: asyncio.AbstractEventLoop, + scheduling_latency: metrics.Histogram, + iterations: metrics.Counter, + tasks: metrics.Gauge, + tags: Dict[str, str], +) -> asyncio.Task: + return asyncio.create_task( + _run_monitoring_loop( + loop, + schedule_latency=scheduling_latency, + iterations=iterations, + task_gauge=tasks, + tags=tags, + ) + ) + + +async def _run_monitoring_loop( + loop: asyncio.AbstractEventLoop, + schedule_latency: metrics.Histogram, + iterations: metrics.Counter, + task_gauge: metrics.Gauge, + tags: Dict[str, str], +) -> None: + while loop.is_running(): + iterations.inc(1, tags) + num_tasks = len(asyncio.all_tasks()) + task_gauge.set(num_tasks, tags) + yield_time = time.monotonic() + await asyncio.sleep(_METRICS_LOOP_INTERVAL) + elapsed_time = time.monotonic() - yield_time + + # Historically, Ray's implementation of histograms are extremely finicky + # with non-positive values (https://github.com/ray-project/ray/issues/26698). + # Technically it shouldn't be possible for this to be negative, add the + # max just to be safe. + latency = max(0.0, elapsed_time - _METRICS_LOOP_INTERVAL) + schedule_latency.observe(latency, tags) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fast_api_metrics.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fast_api_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..50dc6bb22584109b974f1a8faec3d3256c14acb4 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fast_api_metrics.py @@ -0,0 +1,133 @@ +import asyncio +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from ray.llm._internal.serve.constants import ENABLE_VERBOSE_TELEMETRY +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.observability.metrics.event_loop_monitoring import ( + EVENT_LOOP_LATENCY_HISTOGRAM_BOUNDARIES, + setup_event_loop_monitoring, +) +from ray.llm._internal.serve.observability.metrics.fastapi_utils import ( + FASTAPI_API_SERVER_TAG_KEY, + FASTAPI_BASE_HTTP_METRIC_TAG_KEYS, + get_app_name, +) +from ray.llm._internal.serve.observability.metrics.middleware import ( + MeasureHTTPRequestMetricsMiddleware, +) +from ray.util import metrics + +logger = get_logger(__name__) + +ray_llm_build_info_gauge = metrics.Gauge( + "ray_llm_build_info", + description="Metadata about the ray-llm build.", + tag_keys=("git_commit",), +) + + +_HTTP_HANDLER_LATENCY_S_HISTOGRAM_BUCKETS = [ + 0.01, + 0.05, + 0.1, + 0.25, + 0.5, + 0.75, + 1, + 1.5, + 2, + 5, + 10, + 30, + 60, + 120, + 300, +] + + +async def add_fastapi_event_loop_monitoring(app: FastAPI): + tags = {FASTAPI_API_SERVER_TAG_KEY: get_app_name(app)} + tag_keys = tuple(tags.keys()) + + # Store the task handle to prevent it from being garbage collected + app.state.fastapi_event_loop_schedule_latency = metrics.Histogram( + "fastapi_event_loop_schedule_latency", + description="Latency of getting yielded control on the FastAPI event loop in seconds", + boundaries=EVENT_LOOP_LATENCY_HISTOGRAM_BOUNDARIES, + tag_keys=tag_keys, + ) + app.state.fastapi_event_loop_monitoring_iterations = metrics.Counter( + "fastapi_event_loop_monitoring_iterations", + description="Number of times the FastAPI event loop has iterated to get anyscale_fastapi_event_loop_schedule_latency.", + tag_keys=tag_keys, + ) + app.state.fastapi_event_loop_monitoring_tasks = metrics.Gauge( + "fastapi_event_loop_monitoring_tasks", + description="Number of outstanding tasks on the FastAPI event loop.", + tag_keys=tag_keys, + ) + + app.state.fastapi_event_loop_schedule_latency_metrics_task = ( + setup_event_loop_monitoring( + asyncio.get_running_loop(), + app.state.fastapi_event_loop_schedule_latency, + app.state.fastapi_event_loop_monitoring_iterations, + app.state.fastapi_event_loop_monitoring_tasks, + tags, + ) + ) + + +def add_http_metrics_middleware(app: FastAPI): + if not ENABLE_VERBOSE_TELEMETRY: + logger.debug( + "ENABLE_VERBOSE_TELEMETRY is false, not setting up FastAPI telemetry" + ) + return + + logger.info("ENABLE_VERBOSE_TELEMETRY is true, setting up FastAPI telemetry") + base_tag_keys = FASTAPI_BASE_HTTP_METRIC_TAG_KEYS + + logger.debug("Setting up FastAPI telemetry") + + app.state.http_requests_metrics = metrics.Counter( + "http_requests", + description=( + "Total number of HTTP requests by status code, handler and method." + ), + tag_keys=base_tag_keys, + ) + # NOTE: Custom decorators are not applied to histogram-based metrics + # to make sure we can keep cardinality of those in check + app.state.http_requests_latency_metrics = metrics.Histogram( + "http_request_duration_seconds", + description="Duration in seconds of HTTP requests.", + boundaries=_HTTP_HANDLER_LATENCY_S_HISTOGRAM_BUCKETS, + tag_keys=base_tag_keys, + ) + + app.add_middleware(MeasureHTTPRequestMetricsMiddleware) + + logger.debug("Setting up FastAPI telemetry completed") + + +async def set_ray_llm_build_info(): + git_commit = os.environ.get("GIT_COMMIT") + if git_commit: + tags = {"git_commit": git_commit} + ray_llm_build_info_gauge.set(1, tags) + + +@asynccontextmanager +async def metrics_lifespan(app: FastAPI): + """Lifespan for a FastAPI app that sets up metrics observability.""" + + if ENABLE_VERBOSE_TELEMETRY: + await add_fastapi_event_loop_monitoring(app) + + await set_ray_llm_build_info() + + yield diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fastapi_utils.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fastapi_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ce15928fa42101871b7059e914489df755a4b97a --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/fastapi_utils.py @@ -0,0 +1,26 @@ +"""This file contains constants and utility functions for FastAPI.""" + +from fastapi import FastAPI + +# These tag keys are used in metrics for the FastAPI app. +FASTAPI_HTTP_RESPONSE_CODE_TAG_KEY = "code" +FASTAPI_HTTP_HANDLER_TAG_KEY = "handler" +FASTAPI_HTTP_METHOD_TAG_KEY = "method" +FASTAPI_HTTP_PATH_TAG_KEY = "path" +FASTAPI_HTTP_USER_ID_TAG_KEY = "user_id" +FASTAPI_API_SERVER_TAG_KEY = "api_server" + +FASTAPI_BASE_HTTP_METRIC_TAG_KEYS = ( + FASTAPI_HTTP_RESPONSE_CODE_TAG_KEY, + FASTAPI_HTTP_HANDLER_TAG_KEY, + FASTAPI_HTTP_METHOD_TAG_KEY, + FASTAPI_HTTP_PATH_TAG_KEY, + FASTAPI_HTTP_USER_ID_TAG_KEY, + FASTAPI_API_SERVER_TAG_KEY, +) + + +def get_app_name(app: FastAPI) -> str: + """Gets the FastAPI app name.""" + + return getattr(app.state, "name", "unknown") diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/middleware.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..c12ba682e2071b8827dc3a75aa5d8a0b56af4535 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/middleware.py @@ -0,0 +1,156 @@ +import time +from asyncio import CancelledError +from typing import Dict + +from fastapi import FastAPI +from starlette.requests import Request +from starlette.routing import Match +from starlette.types import Message + +from ray.llm._internal.serve.core.ingress.middleware import ( + get_request_id, + get_user_id, +) +from ray.llm._internal.serve.observability.logging import get_logger +from ray.llm._internal.serve.observability.metrics.fastapi_utils import ( + FASTAPI_API_SERVER_TAG_KEY, + FASTAPI_HTTP_HANDLER_TAG_KEY, + FASTAPI_HTTP_METHOD_TAG_KEY, + FASTAPI_HTTP_PATH_TAG_KEY, + FASTAPI_HTTP_RESPONSE_CODE_TAG_KEY, + FASTAPI_HTTP_USER_ID_TAG_KEY, + get_app_name, +) + +logger = get_logger("ray.serve") + + +class MeasureHTTPRequestMetricsMiddleware: + """Measures and stores HTTP request metrics.""" + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] not in ("http", "websocket"): + await self.app(scope, receive, send) + + # If the status_code isn't set by send_wrapper, + # we should consider that an error. + status_code = 500 + send_wrapper_failed_exc_info = "Status code was never set by send_wrapper." + exception_info = send_wrapper_failed_exc_info + + async def send_wrapper(message: Message) -> None: + """Wraps the send message. + + Enables this middleware to access the response headers. + """ + + nonlocal status_code, exception_info + if message["type"] == "http.response.start": + status_code = message.get("status", 500) + + # Clear the send_wrapper_failed_exc_info. + if exception_info == send_wrapper_failed_exc_info: + exception_info = None + + await send(message) + + request = Request(scope) + req_id = get_request_id(request) + now = time.monotonic() + try: + logger.info(f"Starting handling of the request {req_id}") + await self.app(scope, receive, send_wrapper) + + except CancelledError as ce: + status_code = -1 + exception_info = ce + raise + + except BaseException as e: + status_code = 500 + exception_info = e + raise + + finally: + duration_s = time.monotonic() - now + + tags = _get_tags(request, status_code, request.app) + # NOTE: Custom decorators are not applied to histogram-based metrics + # to make sure we can keep cardinality of those in check + truncated_tags = { + **tags, + FASTAPI_HTTP_USER_ID_TAG_KEY: "truncated", + } + + request.app.state.http_requests_metrics.inc(1, tags) + request.app.state.http_requests_latency_metrics.observe( + duration_s, truncated_tags + ) + + extra_context = { + "status_code": status_code, + "duration_ms": duration_s * 1000, + } + + if status_code >= 400: + log = logger.error if status_code >= 500 else logger.warning + log( + f"Handling of the request {req_id} failed", + exc_info=exception_info, + extra={"ray_serve_extra_fields": extra_context}, + ) + elif status_code == -1: + logger.info( + f"Handling of the request {req_id} have been cancelled", + extra={"ray_serve_extra_fields": extra_context}, + ) + else: + logger.info( + f"Handling of the request {req_id} successfully completed", + extra={"ray_serve_extra_fields": extra_context}, + ) + + +def _get_route_details(scope): + """ + Function to retrieve Starlette route from scope. + TODO: there is currently no way to retrieve http.route from + a starlette application from scope. + See: https://github.com/encode/starlette/pull/804 + Args: + scope: A Starlette scope + Returns: + A string containing the route or None + """ + app = scope["app"] + route = None + + for starlette_route in app.routes: + match, _ = starlette_route.matches(scope) + if match == Match.FULL: + route = starlette_route.path + break + if match == Match.PARTIAL: + route = starlette_route.path + return route + + +def _get_tags(request: Request, status_code: int, app: FastAPI) -> Dict[str, str]: + """Generates tags for the request's metrics.""" + + route = str(_get_route_details(request.scope)) or "unknown" + path = str(request.url.path) or "unknown" + method = str(request.method) or "unknown" + user_id = str(get_user_id(request) or "unknown") + + return { + FASTAPI_API_SERVER_TAG_KEY: get_app_name(app), + FASTAPI_HTTP_RESPONSE_CODE_TAG_KEY: str(status_code), + FASTAPI_HTTP_PATH_TAG_KEY: path, + FASTAPI_HTTP_HANDLER_TAG_KEY: route, + FASTAPI_HTTP_METHOD_TAG_KEY: method, + FASTAPI_HTTP_USER_ID_TAG_KEY: user_id, + } diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/utils.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9c49cbcf4d0408637ce8bde474ea7b239dc66d43 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/metrics/utils.py @@ -0,0 +1,144 @@ +import time +from enum import Enum +from typing import ( + AsyncGenerator, + Callable, + List, + Set, + TypeVar, +) + +from ray.util import metrics + +# Histogram buckets for short-range latencies measurements: +# Min=1ms, Max=30s +# +# NOTE: Number of buckets have to be bounded (and not exceed 30) +# to avoid overloading metrics sub-system +SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS: List[float] = [ + 1, + 5, + 10, + 25, + 50, + 100, + 150, + 250, + 500, + 1000, + 1500, + 2500, + 5000, + 7500, + 10000, + 20000, + 30000, +] + +# Histogram buckets for long-range latencies measurements: +# Min=10ms, Max=300s +LONG_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS = [ + x * 10 for x in SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS +] + + +class ClockUnit(int, Enum): + ms = 1000 + s = 1 + + +class MsClock: + """A clock that tracks intervals in milliseconds""" + + def __init__(self, unit: ClockUnit = ClockUnit.ms): + self.reset() + self.unit = unit.value + self.start_time = time.perf_counter() + + def reset(self): + self.start_time = time.perf_counter() + + def interval(self): + return (time.perf_counter() - self.start_time) * self.unit + + def reset_interval(self): + interval = self.interval() + self.reset() + return interval + + +T = TypeVar("T") + + +class InstrumentTokenAsyncGenerator: + """This class instruments an asynchronous generator. + + It gathers 3 metrics: + 1. Time to first time + 2. Time between tokens + 3. Total completion time + + Usage: + + @InstrumentTokenAsyncGenerator("my_special_fn") + async def to_instrument(): + yield ... + """ + + all_instrument_names: Set[str] = set() + + def __init__( + self, generator_name: str, latency_histogram_buckets: List[float] = None + ): + self.generator_name = f"rayllm_{generator_name}" + + target_latency_histogram_buckets = ( + latency_histogram_buckets or SHORT_RANGE_LATENCY_HISTOGRAM_BUCKETS_MS + ) + + assert ( + self.generator_name not in self.all_instrument_names + ), "This generator name was already used elsewhere. Please specify another one." + self.all_instrument_names.add(self.generator_name) + + self.token_latency_histogram = metrics.Histogram( + f"{self.generator_name}_per_token_latency_ms", + f"Generator metrics for {self.generator_name}", + boundaries=target_latency_histogram_buckets, + ) + + self.first_token_latency_histogram = metrics.Histogram( + f"{self.generator_name}_first_token_latency_ms", + f"Generator metrics for {self.generator_name}", + boundaries=target_latency_histogram_buckets, + ) + self.total_latency_histogram = metrics.Histogram( + f"{self.generator_name}_total_latency_ms", + f"Generator metrics for {self.generator_name}", + boundaries=target_latency_histogram_buckets, + ) + + def __call__( + self, async_generator_fn: Callable[..., AsyncGenerator[T, None]] + ) -> Callable[..., AsyncGenerator[T, None]]: + async def new_gen(*args, **kwargs): + interval_clock = MsClock() + total_clock = MsClock() + is_first_token = True + try: + async for x in async_generator_fn(*args, **kwargs): + if is_first_token: + self.first_token_latency_histogram.observe( + total_clock.interval() + ) + interval_clock.reset() + is_first_token = False + else: + self.token_latency_histogram.observe( + interval_clock.reset_interval() + ) + yield x + finally: + self.total_latency_histogram.observe(total_clock.interval()) + + return new_gen diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89ad1f85dac165be7919a52cdf1a52f3d0487d09 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07c5a6482053fe9d31371188155dfaaf6214bd55 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/__pycache__/usage.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/usage.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/usage.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ce141cfc41267c63075f3a37f09455a4aabd99 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/observability/usage_telemetry/usage.py @@ -0,0 +1,322 @@ +import random +import time +from enum import Enum +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence + +import ray +from ray import serve +from ray._common.usage.usage_lib import ( + get_hardware_usages_to_report, + record_extra_usage_tag, +) +from ray.llm._internal.common.base_pydantic import BaseModelExtended +from ray.llm._internal.common.observability.telemetry_utils import DEFAULT_GPU_TYPE +from ray.llm._internal.common.utils.lora_utils import get_lora_model_ids +from ray.llm._internal.serve.observability.logging import get_logger + +if TYPE_CHECKING: + from ray.llm._internal.serve.core.configs.llm_config import LLMConfig + +LLM_SERVE_TELEMETRY_NAMESPACE = "llm_serve_telemetry" +LLM_SERVE_TELEMETRY_ACTOR_NAME = "llm_serve_telemetry" + +logger = get_logger(__name__) + + +class TelemetryTags(str, Enum): + """Telemetry tags for LLM SERVE.""" + + LLM_SERVE_SERVE_MULTIPLE_MODELS = "LLM_SERVE_SERVE_MULTIPLE_MODELS" + LLM_SERVE_SERVE_MULTIPLE_APPS = "LLM_SERVE_SERVE_MULTIPLE_APPS" + LLM_SERVE_JSON_MODE_MODELS = "LLM_SERVE_JSON_MODE_MODELS" + LLM_SERVE_JSON_MODE_NUM_REPLICAS = "LLM_SERVE_JSON_MODE_NUM_REPLICAS" + LLM_SERVE_LORA_BASE_MODELS = "LLM_SERVE_LORA_BASE_MODELS" + LLM_SERVE_INITIAL_NUM_LORA_ADAPTERS = "LLM_SERVE_INITIAL_NUM_LORA_ADAPTERS" + LLM_SERVE_AUTOSCALING_ENABLED_MODELS = "LLM_SERVE_AUTOSCALING_ENABLED_MODELS" + LLM_SERVE_AUTOSCALING_MIN_REPLICAS = "LLM_SERVE_AUTOSCALING_MIN_REPLICAS" + LLM_SERVE_AUTOSCALING_MAX_REPLICAS = "LLM_SERVE_AUTOSCALING_MAX_REPLICAS" + LLM_SERVE_TENSOR_PARALLEL_DEGREE = "LLM_SERVE_TENSOR_PARALLEL_DEGREE" + LLM_SERVE_NUM_REPLICAS = "LLM_SERVE_NUM_REPLICAS" + LLM_SERVE_MODELS = "LLM_SERVE_MODELS" + LLM_SERVE_GPU_TYPE = "LLM_SERVE_GPU_TYPE" + LLM_SERVE_NUM_GPUS = "LLM_SERVE_NUM_GPUS" + + +class TelemetryModel(BaseModelExtended): + """Telemetry model for LLM Serve.""" + + model_architecture: str + num_replicas: int + use_json_mode: bool + use_lora: bool + initial_num_lora_adapters: int + use_autoscaling: bool + min_replicas: int + max_replicas: int + tensor_parallel_degree: int + gpu_type: str + num_gpus: int + + +@ray.remote( + name=LLM_SERVE_TELEMETRY_ACTOR_NAME, + namespace=LLM_SERVE_TELEMETRY_NAMESPACE, + num_cpus=0, + lifetime="detached", +) +class TelemetryAgent: + """Named Actor to keep the state of all deployed models and record telemetry.""" + + def __init__(self): + self.models: List[TelemetryModel] = [] + self.record_tag_func = record_extra_usage_tag + + def _update_record_tag_func(self, record_tag_func: Callable) -> None: + """This method is only used in tests to record the telemetry tags to a different + object than Ray's default `record_extra_usage_tag` function.""" + self.record_tag_func = record_tag_func + + def _reset_models(self): + """This method is only used in tests to clean up the models list.""" + self.models = [] + + def _multiple_models(self) -> str: + unique_models = {model.model_architecture for model in self.models} + return "1" if len(unique_models) > 1 else "0" + + @staticmethod + def _multiple_apps() -> str: + try: + serve_status = serve.status() + except ray.exceptions.ActorDiedError: + # This only happens in a workspace and when multiple Serve sessions are + # used. Since telemetry agent is long running, it might be connecting to the + # previous Serve session, which is already dead. In this case, we need to + # call `serve.shutdown()` before the telemetry agent can re-connect to the + # existing Serve session. + serve.shutdown() + serve_status = serve.status() + return "1" if len(serve_status.applications) > 1 else "0" + + def _json_mode_models(self) -> str: + return ",".join( + [model.model_architecture for model in self.models if model.use_json_mode] + ) + + def _json_mode_num_deployments(self) -> str: + return ",".join( + [str(model.num_replicas) for model in self.models if model.use_json_mode] + ) + + def _lora_base_nodes(self) -> str: + return ",".join( + [model.model_architecture for model in self.models if model.use_lora] + ) + + def _lora_initial_num_adaptors(self) -> str: + return ",".join( + [ + str(model.initial_num_lora_adapters) + for model in self.models + if model.use_lora + ] + ) + + def _autoscaling_enabled_models(self) -> str: + return ",".join( + [model.model_architecture for model in self.models if model.use_autoscaling] + ) + + def _autoscaling_min_replicas(self) -> str: + return ",".join( + [str(model.min_replicas) for model in self.models if model.use_autoscaling] + ) + + def _autoscaling_max_replicas(self) -> str: + return ",".join( + [str(model.max_replicas) for model in self.models if model.use_autoscaling] + ) + + def _model_architectures(self) -> str: + return ",".join([model.model_architecture for model in self.models]) + + def _tensor_parallel_degree(self) -> str: + return ",".join([str(model.tensor_parallel_degree) for model in self.models]) + + def _num_replicas(self) -> str: + return ",".join([str(model.num_replicas) for model in self.models]) + + def _gpu_type(self) -> str: + return ",".join([model.gpu_type for model in self.models]) + + def _num_gpus(self) -> str: + return ",".join([str(model.num_gpus) for model in self.models]) + + def generate_report(self) -> Dict[str, str]: + return { + TelemetryTags.LLM_SERVE_SERVE_MULTIPLE_MODELS: self._multiple_models(), + TelemetryTags.LLM_SERVE_SERVE_MULTIPLE_APPS: self._multiple_apps(), + TelemetryTags.LLM_SERVE_JSON_MODE_MODELS: self._json_mode_models(), + TelemetryTags.LLM_SERVE_JSON_MODE_NUM_REPLICAS: self._json_mode_num_deployments(), + TelemetryTags.LLM_SERVE_LORA_BASE_MODELS: self._lora_base_nodes(), + TelemetryTags.LLM_SERVE_INITIAL_NUM_LORA_ADAPTERS: self._lora_initial_num_adaptors(), + TelemetryTags.LLM_SERVE_AUTOSCALING_ENABLED_MODELS: self._autoscaling_enabled_models(), + TelemetryTags.LLM_SERVE_AUTOSCALING_MIN_REPLICAS: self._autoscaling_min_replicas(), + TelemetryTags.LLM_SERVE_AUTOSCALING_MAX_REPLICAS: self._autoscaling_max_replicas(), + TelemetryTags.LLM_SERVE_MODELS: self._model_architectures(), + TelemetryTags.LLM_SERVE_TENSOR_PARALLEL_DEGREE: self._tensor_parallel_degree(), + TelemetryTags.LLM_SERVE_NUM_REPLICAS: self._num_replicas(), + TelemetryTags.LLM_SERVE_GPU_TYPE: self._gpu_type(), + TelemetryTags.LLM_SERVE_NUM_GPUS: self._num_gpus(), + } + + def record(self, model: Optional[TelemetryModel] = None) -> None: + """Record telemetry model.""" + from ray._common.usage.usage_lib import TagKey + + if model: + self.models.append(model) + + for key, value in self.generate_report().items(): + try: + self.record_tag_func(TagKey.Value(key), value) + except ValueError: + # If the key doesn't exist in the TagKey enum, skip it. + continue + + +def _get_or_create_telemetry_agent() -> TelemetryAgent: + """Helper to get or create the telemetry agent.""" + try: + telemetry_agent = ray.get_actor( + LLM_SERVE_TELEMETRY_ACTOR_NAME, namespace=LLM_SERVE_TELEMETRY_NAMESPACE + ) + except ValueError: + from ray._common.constants import HEAD_NODE_RESOURCE_NAME + from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy + + telemetry_agent = TelemetryAgent.options( + # Ensure the actor is created on the head node. + resources={HEAD_NODE_RESOURCE_NAME: 0.001}, + # Ensure the actor is not scheduled with the existing placement group. + scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=None), + ).remote() + + return telemetry_agent + + +def _retry_get_telemetry_agent( + max_retries: int = 5, base_delay: float = 0.1 +) -> TelemetryAgent: + max_retries = 5 + base_delay = 0.1 + + telemetry_agent = None + for attempt in range(max_retries): + try: + telemetry_agent = _get_or_create_telemetry_agent() + return telemetry_agent + except ValueError as e: + # Due to race conditions among multiple replicas, we may get: + # ValueError: Actor with name 'llm_serve_telemetry' already + # exists in the namespace llm_serve_telemetry + logger.info( + "Attempt %s/%s to get telemetry agent failed", attempt + 1, max_retries + ) + if attempt == max_retries - 1: + raise e + + # Exponential backoff with jitter + exponential_delay = base_delay * (2**attempt) + jitter = random.uniform(0, 0.5) + delay = exponential_delay + jitter + # Max total wait time is ~3.5 seconds for 5 attempts. + time.sleep(delay) + + +def _push_telemetry_report(model: Optional[TelemetryModel] = None) -> None: + """Push telemetry report for a model.""" + telemetry_agent = _retry_get_telemetry_agent() + assert telemetry_agent is not None + ray.get(telemetry_agent.record.remote(model)) + + +class HardwareUsage: + """Hardware usage class to report telemetry.""" + + def __init__(self, get_hardware_fn: Callable = get_hardware_usages_to_report): + self._get_hardware_fn = get_hardware_fn + + def infer_gpu_from_hardware(self) -> str: + """Infer the GPU type from the hardware when the accelerator type on llm config is + not specified. + + Iterate through all the hardware recorded on the cluster and return the first + ray-compatible accelerator as the GPU type used for the deployment. If not, return + `UNSPECIFIED` as the default GPU type. + """ + from ray.llm._internal.serve.core.configs.llm_config import GPUType + + all_accelerator_types = [t.value for t in GPUType] + gcs_client = ray.experimental.internal_kv.internal_kv_get_gcs_client() + hardwares = self._get_hardware_fn(gcs_client) + for hardware in hardwares: + if hardware in all_accelerator_types: + return hardware + + return DEFAULT_GPU_TYPE + + +def push_telemetry_report_for_all_models( + all_models: Optional[Sequence["LLMConfig"]] = None, + get_lora_model_func: Callable = get_lora_model_ids, + get_hardware_fn: Callable = get_hardware_usages_to_report, +): + """Push telemetry report for all models.""" + if not all_models: + return + + for model in all_models: + use_lora = ( + model.lora_config is not None + and model.lora_config.dynamic_lora_loading_path is not None + ) + initial_num_lora_adapters = 0 + if use_lora: + lora_model_ids = get_lora_model_func( + dynamic_lora_loading_path=model.lora_config.dynamic_lora_loading_path, + base_model_id=model.model_id, + ) + initial_num_lora_adapters = len(lora_model_ids) + + use_autoscaling = model.deployment_config.get("autoscaling_config") is not None + num_replicas, min_replicas, max_replicas = 1, 1, 1 + if use_autoscaling: + from ray.serve.config import AutoscalingConfig + + autoscaling_config = AutoscalingConfig( + **model.deployment_config["autoscaling_config"] + ) + num_replicas = ( + autoscaling_config.initial_replicas or autoscaling_config.min_replicas + ) + min_replicas = autoscaling_config.min_replicas + max_replicas = autoscaling_config.max_replicas + + engine_config = model.get_engine_config() + hardware_usage = HardwareUsage(get_hardware_fn) + + telemetry_model = TelemetryModel( + model_architecture=model.model_architecture, + num_replicas=num_replicas, + use_json_mode=True, + use_lora=use_lora, + initial_num_lora_adapters=initial_num_lora_adapters, + use_autoscaling=use_autoscaling, + min_replicas=min_replicas, + max_replicas=max_replicas, + tensor_parallel_degree=engine_config.tensor_parallel_degree, + gpu_type=model.accelerator_type or hardware_usage.infer_gpu_from_hardware(), + num_gpus=engine_config.num_devices, + ) + _push_telemetry_report(telemetry_model) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__init__.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd1125c6ab9920f94014135528d55fa5d49b01d3 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/batcher.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/batcher.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa158859cd2bdc54a7f044ce327644a941556796 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/batcher.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/lora_serve_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/lora_serve_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61c5f98017545b7937d8ee9b178014976e9a5a5b Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/lora_serve_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/node_initialization_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/node_initialization_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2904d9b95b2ddabdec2af643cd24137276717e7a Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/node_initialization_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/registry.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/registry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2b0d3b8e48e1e49ae042df13f52bce2e6d41028 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/registry.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/server_utils.cpython-312.pyc b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/server_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18d4aebc4558ea83211b006116cb4d94b94bd9a1 Binary files /dev/null and b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/__pycache__/server_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/batcher.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/batcher.py new file mode 100644 index 0000000000000000000000000000000000000000..1b2a941cb178e59165f190a9216ad660e4e9f319 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/batcher.py @@ -0,0 +1,111 @@ +import asyncio +from typing import AsyncGenerator, Generic, Iterable, List, Optional, TypeVar + +from ray.llm._internal.serve.constants import ( + MODEL_RESPONSE_BATCH_TIMEOUT_MS, +) +from ray.llm._internal.serve.observability.logging import get_logger + +logger = get_logger(__name__) + +T = TypeVar("T") + + +class Batcher(Generic[T]): + """This class batches multiple responses from a generator into a list of + single responses, at some time interval. + + Args: + generator: the async generator that this class pulls responses + from. + interval_ms: the interval at which this class yields the current batch. + If None, this class will batch all responses from the generator + together and yield the entire batch once. + """ + + def __init__( + self, + generator: AsyncGenerator[T, None], + interval_ms: Optional[float] = MODEL_RESPONSE_BATCH_TIMEOUT_MS, + ): + self.generator = generator + self.queue: asyncio.Queue = asyncio.Queue() + + if interval_ms is None: + self.interval_s = None + else: + self.interval_s = interval_ms / 1000 + + if interval_ms == 0: + return + + self.done_event: asyncio.Event = asyncio.Event() + + # We are okay with this task getting cancelled (to propagate cancellations) + self.read_task = asyncio.create_task(self.read()) + + def _merge_results(self, results: List[T]) -> Iterable[T]: + return results + + async def stream(self) -> AsyncGenerator[Iterable[T], None]: + """Drain from the queue every interval_ms and yield the merged results""" + + if self.interval_s == 0: + async for item in self.generator: + yield [item] + + return + + try: + while True: + # Wait for the interval or until we finish, whichever is faster. + # We use an event to avoid asyncio.wait_for cancelling the real task on timeout. + try: + if self.interval_s is None: + await self.done_event.wait() + else: + await asyncio.wait_for( + self.done_event.wait(), timeout=self.interval_s + ) + except asyncio.TimeoutError: + pass + + # Get all elements from the queue + results, is_done = self.check_done_and_drain() + + # If there are results, merge and yield them + if results: + output = self._merge_results(results) + yield output + + # If the read task is done, exit the stream task + if is_done: + # Raise exception, if any + self.read_task.result() + break + finally: + # If the stream task is done, make sure to exit the read task + if not self.read_task.done(): + self.read_task.cancel() + + def check_done_and_drain(self): + results = self.drain_queue() + return results, self.read_task.done() + + async def read(self): + """Read from the generator and put into the queue in a tight loop""" + try: + async for x in self.generator: + self.queue.put_nowait(x) + finally: + self.done_event.set() + + def drain_queue(self): + """Drain all results currently in the queue""" + results = [] + try: + while True: + results.append(self.queue.get_nowait()) + except asyncio.QueueEmpty: + pass + return results diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/lora_serve_utils.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/lora_serve_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..eb15658a39ba32d3ef0bb80c20ce193e1ab02d91 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/lora_serve_utils.py @@ -0,0 +1,233 @@ +""" +Serve-specific LoRA utilities that use generic abstractions from lora_utils.py. + +This module provides serve-specific functionality while using the generic +LoRA abstractions from common/lora_utils.py. This ensures clean separation +between generic and serve-specific concerns. +""" + +import asyncio +import json +import os +from typing import Any, Dict, Optional + +from fastapi import HTTPException + +from ray.llm._internal.common.constants import LORA_ADAPTER_CONFIG_NAME +from ray.llm._internal.common.models import global_id_manager, make_async +from ray.llm._internal.common.utils.cloud_utils import ( + LoraMirrorConfig, +) +from ray.llm._internal.common.utils.lora_utils import ( + CLOUD_OBJECT_MISSING, + clean_model_id, + clear_directory, + get_base_model_id, + get_lora_id, + get_object_from_cloud, + retry_with_exponential_backoff, + sync_files_with_lock, +) +from ray.llm._internal.serve.core.configs.llm_config import ( + DiskMultiplexConfig, + LLMConfig, +) +from ray.llm._internal.serve.observability.logging import get_logger + +logger = get_logger(__name__) + + +async def get_lora_finetuned_context_length(bucket_uri: str) -> Optional[int]: + """Gets the sequence length used to tune the LoRA adapter. + + Return: Returns the max sequence length for the adapter, if it exists. + + Raises: HTTPException if the LoRA adapter config file isn't available + in the cloud storage repository. + """ + if bucket_uri.endswith("/"): + bucket_uri = bucket_uri.rstrip("/") + object_uri = f"{bucket_uri}/{LORA_ADAPTER_CONFIG_NAME}" + + object_str_or_missing_message = await get_object_from_cloud(object_uri) + + if object_str_or_missing_message is CLOUD_OBJECT_MISSING: + raise HTTPException( + 404, + f"Unable to find LoRA adapter config file " + f'"{LORA_ADAPTER_CONFIG_NAME}" in folder {bucket_uri}. ' + "Check that the file exists and that you have read permissions.", + ) + else: + adapter_config_str = object_str_or_missing_message + adapter_config = json.loads(adapter_config_str) + return adapter_config.get("max_length") + + +async def download_multiplex_config_info( + model_id: str, base_path: str +) -> tuple[str, Optional[int]]: + """Downloads info needed to create a multiplex config. + + Downloads objects using cloud storage provider APIs. + + Returns: 2-tuple containing + 1. A bucket_uri for the bucket containing LoRA weights and config. + 2. The maximum LoRA sequence length. + + Raises: HTTPException if the LoRA adapter config file isn't available + in the cloud storage repository. + """ + bucket_uri = f"{base_path}/{model_id}" + ft_context_length = await get_lora_finetuned_context_length(bucket_uri) + return bucket_uri, ft_context_length + + +async def get_lora_model_metadata( + model_id: str, llm_config: LLMConfig +) -> Dict[str, Any]: + """Get the lora model metadata for a given model id and llm config. + + This is used to get the metadata for the model with the given model id. + """ + # Note (genesu): `model_id` passed is a lora model id where it's in a form of + # base_model_id:suffix:id + base_model_id = get_base_model_id(model_id) + lora_id = get_lora_id(model_id) + base_path = llm_config.lora_config.dynamic_lora_loading_path + + # Examples of the variables: + # model_id: "meta-llama/Meta-Llama-3.1-8B-Instruct:my_suffix:aBc1234" + # base_path: "s3://ray-llama-weights" + # bucket_uri: "s3://ray-llama-weights/my_suffix:aBc1234" + ( + bucket_uri, + ft_context_length, + ) = await download_multiplex_config_info(lora_id, base_path) + + return { + "model_id": model_id, + "base_model_id": base_model_id, + "max_request_context_length": ft_context_length, + # Note (genesu): `bucket_uri` affects where the lora weights are downloaded + # from remote location. + "bucket_uri": bucket_uri, + } + + +async def get_lora_mirror_config( + model_id: str, + llm_config: LLMConfig, +) -> LoraMirrorConfig: + """Get LoRA mirror configuration for serve-specific LLM config.""" + metadata = await get_lora_model_metadata(model_id, llm_config) + + return LoraMirrorConfig( + lora_model_id=model_id, + bucket_uri=metadata["bucket_uri"], + max_total_tokens=metadata["max_request_context_length"], + sync_args=None, + ) + + +class LoraModelLoader: + """Download LoRA weights from remote storage and manage disk cache. + + This class is serve-specific as it depends on DiskMultiplexConfig and + other serve-specific concepts. + """ + + def __init__( + self, + lora_root: Optional[str] = None, + download_timeout_s: Optional[float] = None, + max_tries: int = 1, + ): + self.lora_root = lora_root or "/tmp/ray/llm/lora/cache" + self.disk_cache: Dict[str, DiskMultiplexConfig] = {} + self.active_syncing_tasks: Dict[str, asyncio.Task[DiskMultiplexConfig]] = {} + if download_timeout_s is not None and download_timeout_s <= 0: + raise ValueError( + f"download_timeout_s must be None or >0, got {download_timeout_s}" + ) + self.download_timeout_s = download_timeout_s + if max_tries < 1: + raise ValueError(f"max_tries must be >=1, got {max_tries}") + self.max_tries = max_tries + + async def load_model_from_config( + self, lora_model_id: str, llm_config + ) -> DiskMultiplexConfig: + """Load a LoRA model by first fetching its mirror config from S3.""" + lora_mirror_config = await get_lora_mirror_config(lora_model_id, llm_config) + return await self.load_model(lora_model_id, lora_mirror_config) + + async def load_model( + self, lora_model_id: str, lora_mirror_config: LoraMirrorConfig + ) -> DiskMultiplexConfig: + """Load a LoRA model.""" + if lora_model_id in self.disk_cache: + return self.disk_cache[lora_model_id] + + if lora_model_id not in self.active_syncing_tasks: + task = asyncio.create_task(self._load_model_async(lora_mirror_config)) + task.add_done_callback( + lambda result: self.active_syncing_tasks.pop(lora_model_id, None) + ) + self.active_syncing_tasks[lora_model_id] = task + else: + task = self.active_syncing_tasks[lora_model_id] + + disk_config = await asyncio.shield(task) + self.disk_cache[lora_model_id] = disk_config + return disk_config + + async def _load_model_async( + self, lora_mirror_config: LoraMirrorConfig + ) -> DiskMultiplexConfig: + return await self._load_model(lora_mirror_config) + + @make_async + def _load_model(self, lora_mirror_config: LoraMirrorConfig) -> DiskMultiplexConfig: + return self._load_model_sync(lora_mirror_config) + + @make_async + def clear_cache(self): + """Clear the disk cache.""" + clear_directory(self.lora_root) + + def _model_dir_path(self, model_id: str) -> str: + """Construct the path for the lora weight.""" + lora_id = get_lora_id(clean_model_id(model_id)) + path = os.path.join(self.lora_root, lora_id) + os.makedirs(path, exist_ok=True) + return path + + def _download_lora(self, lora_mirror_config: LoraMirrorConfig) -> str: + """Download LoRA weights using generic download primitives.""" + model_local_path = self._model_dir_path(lora_mirror_config.lora_model_id) + sync_files_with_lock( + lora_mirror_config.bucket_uri, + model_local_path, + timeout=self.download_timeout_s, + ) + return model_local_path + + def _load_model_sync( + self, lora_mirror_config: LoraMirrorConfig + ) -> DiskMultiplexConfig: + """Load a model from the given mirror configuration.""" + download_with_retries = retry_with_exponential_backoff( + max_tries=self.max_tries, + exception_to_check=Exception, + )(lambda config: self._download_lora(config)) + + local_path = download_with_retries(lora_mirror_config) + return DiskMultiplexConfig.model_validate( + { + "model_id": lora_mirror_config.lora_model_id, + "max_total_tokens": lora_mirror_config.max_total_tokens, + "local_path": local_path, + "lora_assigned_int_id": global_id_manager.next(), + } + ) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/node_initialization_utils.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/node_initialization_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ce1d04a5fe73a4179a2a97c28a3c69b3ce932e34 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/node_initialization_utils.py @@ -0,0 +1,75 @@ +import asyncio +from typing import Optional + +import ray +from ray.llm._internal.common.utils.download_utils import ( + download_model_files, +) +from ray.llm._internal.common.utils.import_utils import try_import +from ray.llm._internal.serve.core.configs.llm_config import LLMConfig +from ray.llm._internal.serve.observability.logging import get_logger + +torch = try_import("torch") +transformers = try_import("transformers") + +logger = get_logger(__name__) + + +def initialize_remote_node(llm_config: LLMConfig) -> Optional[str]: + + callback = llm_config.get_or_create_callback() + engine_config = llm_config.get_engine_config() + + local_path = download_model_files( + model_id=engine_config.actual_hf_model_id, + mirror_config=engine_config.mirror_config, + download_model=callback.ctx.worker_node_download_model, + download_extra_files=True, + callback=callback, + ) + + # Validate that the binary exists + if local_path and local_path != engine_config.actual_hf_model_id: + engine_config.hf_model_id = local_path + + return local_path + + +async def initialize_node(llm_config: LLMConfig): + """Implements node initialization for LLM engines. + + Downloads model, tokenizer, and extra files as necessary. + """ + # Get callback instance (if configured) with context information + callback = llm_config.get_or_create_callback() + ctx = callback.ctx + pg_table = ray.util.placement_group_table(ctx.placement_group) + + node_set = set(pg_table["bundles_to_node_id"].values()) + download_tasks = [] + for node_id in node_set: + node_affinity_strategy = ( + ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy( + node_id=node_id, + soft=False, + ) + ) + download_tasks.append( + ray.remote(initialize_remote_node).options( + num_cpus=1, + scheduling_strategy=node_affinity_strategy, + runtime_env=ctx.runtime_env, + ) + ) + + logger.info("Running tasks to download model files on worker nodes") + paths = await asyncio.gather( + *[download_task.remote(llm_config) for download_task in download_tasks] + ) + + # assume that all paths are the same + assert paths, "No paths returned from download_model_files" + assert ( + len(set(paths)) == 1 + ), "Paths returned from download_model_files are not the same" + llm_config.get_engine_config().hf_model_id = paths[0] diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/registry.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..e20bf5fd5d57cc47aff731386d81737e2c4da311 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/registry.py @@ -0,0 +1,328 @@ +"""Generic registry for LLM serving components using Ray's internal KV store. + +This module provides a reusable registry mechanism that enables components to be +registered in the driver process and accessed across all Ray processes in the cluster, +including Ray Serve child processes. + +Similar to RLlib/Tune's registry but with a fixed global prefix for cross-job access. +""" + +import importlib +from typing import Any, Callable + +import ray._private.worker as worker +import ray.cloudpickle as pickle +from ray.experimental.internal_kv import ( + _internal_kv_del, + _internal_kv_exists, + _internal_kv_get, + _internal_kv_initialized, + _internal_kv_put, +) +from ray.llm._internal.serve.observability.logging import get_logger + +logger = get_logger(__name__) + + +# Fixed prefix for cross-job accessibility (Serve deployments run in different jobs) +_SERVE_REGISTRY_PREFIX = "serve_global" + + +def _make_key(category: str, name: str) -> bytes: + """Generate a binary key for the KV store. + + Args: + category: The component category (e.g., "kv_connector_backend") + name: The component name + + Returns: + The key to use for storing the value + """ + return ( + b"LLMServeRegistry:" + + _SERVE_REGISTRY_PREFIX.encode("ascii") + + b":" + + category.encode("ascii") + + b"/" + + name.encode("ascii") + ) + + +def _create_loader(value: Any) -> Callable[[], Any]: + """Create a loader callable for a value. + + Handles both direct objects/classes and string paths for lazy loading. + + Args: + value: Either: + - A class, object, or callable (returns lambda: value) + - A string in format "module_path:class_name" (creates import loader) + + Returns: + A callable that returns the value when called + + Raises: + ValueError: If value is a string but doesn't have the correct format + """ + if isinstance(value, str): + if ":" not in value: + raise ValueError( + f"Invalid format for string value: '{value}'. " + f"Expected format: 'module_path:class_name' or a class/object." + ) + module_path, class_name = value.rsplit(":", 1) + # Create a loader callable that imports on demand + def loader(): + module = importlib.import_module(module_path) + return getattr(module, class_name) + + return loader + else: + # For direct objects/classes, create a simple loader + return lambda: value + + +class ComponentRegistry: + """Generic registry for LLM serving components using Ray's internal KV store. + + This registry enables components to be registered in the driver process and + accessed across all Ray processes in the cluster, including Ray Serve child processes. + + Similar to RLlib/Tune's registry but with a fixed global prefix for cross-job access. + + **Usage Pattern:** + This registry is designed for a "register once, read many" pattern: + - Components are typically registered in the driver process before deployment + - Ray Serve replicas read from the KV store during initialization + - Once a component is resolved and cached in a process, subsequent `get()` calls return the cached value without checking the KV store for updates + + Example: + # Create a registry for a component category + registry = ComponentRegistry("my_component") + + # Register a component + registry.register("my_component", MyComponentClass) + + # Get a registered component + component = registry.get("my_component") + + # Check if registered + if registry.contains("my_component"): + ... + """ + + def __init__(self, category: str): + """Initialize a registry for a specific component category. + + Args: + category: The category name (e.g., "kv_connector_backend") + """ + self.category = category + self._loader_cache: dict[str, Callable[[], Any]] = {} + self._resolved_cache: dict[str, Any] = {} + self._pending: dict[str, bytes] = {} + + def register(self, name: str, value: Any) -> None: + """Register a component. + + Args: + name: The name to register under + value: The component to register. Can be: + - A class, object, or callable (serialized directly) + - A string in format "module_path:class_name" (lazy-loaded via import) + + Raises: + ValueError: If the component is already registered. Use unregister() first if you need to change the registration. + + Examples: + # Register a class directly + registry.register("MyClass", MyClass) + + # Register via module path (lazy loading) + registry.register("MyClass", "my.module:MyClass") + """ + # Prevent double registration to avoid cache inconsistencies + if self.contains(name): + raise ValueError( + f"{self.category} '{name}' is already registered. " + f"Use unregister() first if you need to change the registration." + ) + + # Create a loader callable (handles both direct values and string paths) + loader = _create_loader(value) + + # Serialize the loader callable + serialized = pickle.dumps(loader) + + # Store loader in cache + self._loader_cache[name] = loader + + # Store in KV store if Ray is initialized, otherwise queue for later + if _internal_kv_initialized(): + try: + key = _make_key(self.category, name) + _internal_kv_put(key, serialized, overwrite=True) + logger.debug(f"Registered {self.category} '{name}' in KV store") + except Exception as e: + logger.warning( + f"Failed to register {self.category} '{name}' in KV store: {e}", + exc_info=True, + ) + self._pending[name] = serialized + else: + self._pending[name] = serialized + + def get(self, name: str) -> Any: + """Get a registered component. + + Args: + name: The name of the component + + Returns: + The registered component. If registered with a string path, + returns the imported class/object. If registered directly, + returns the original value. + + Raises: + ValueError: If the component is not registered + """ + # Check resolved cache first. + if name in self._resolved_cache: + return self._resolved_cache[name] + + loader = self._loader_cache.get(name) + # If not in local loader cache, try fetching from KV store. + if loader is None and _internal_kv_initialized(): + try: + key = _make_key(self.category, name) + serialized = _internal_kv_get(key) + if serialized is not None: + loader = pickle.loads(serialized) + # Cache the loader for future gets. + self._loader_cache[name] = loader + logger.debug(f"Loaded {self.category} '{name}' from KV store") + except Exception as e: + logger.warning( + f"Failed to load {self.category} '{name}' from KV store: {e}", + exc_info=True, + ) + + if loader is not None: + value = loader() + self._resolved_cache[name] = value + return value + + # Not found + raise ValueError( + f"{self.category} '{name}' not found. " + f"Registered: {list(self._loader_cache.keys())}" + ) + + def contains(self, name: str) -> bool: + """Check if a component is registered. + + Args: + name: The name to check + + Returns: + True if registered, False otherwise + """ + if name in self._loader_cache: + return True + + if _internal_kv_initialized(): + try: + key = _make_key(self.category, name) + return _internal_kv_exists(key) + except Exception as e: + logger.warning( + f"Failed to check if {self.category} '{name}' exists in KV store: {e}", + exc_info=True, + ) + return False + + return False + + def unregister(self, name: str) -> None: + """Unregister a component. + + Removes the component from local cache, pending registrations, and KV store. + + Args: + name: The name of the component to unregister + """ + # Remove from local caches + if name in self._loader_cache: + del self._loader_cache[name] + if name in self._resolved_cache: + del self._resolved_cache[name] + + # Remove from pending if present + if name in self._pending: + del self._pending[name] + + # Remove from KV store if Ray is initialized + if _internal_kv_initialized(): + try: + key = _make_key(self.category, name) + _internal_kv_del(key) + logger.debug(f"Unregistered {self.category} '{name}' from KV store") + except Exception as e: + logger.warning( + f"Failed to unregister {self.category} '{name}' from KV store: {e}", + exc_info=True, + ) + + def flush_pending(self) -> None: + """Flush pending registrations to KV store. + + This is called automatically when Ray initializes via _post_init_hooks. + """ + if not _internal_kv_initialized() or not self._pending: + return + + for name, serialized in self._pending.items(): + try: + key = _make_key(self.category, name) + _internal_kv_put(key, serialized, overwrite=True) + logger.debug( + f"Flushed pending registration for {self.category} '{name}'" + ) + except Exception as e: + logger.warning( + f"Failed to flush {self.category} '{name}': {e}", exc_info=True + ) + + self._pending.clear() + + +# Global registry instances for different component categories +_registries: dict[str, ComponentRegistry] = {} + + +def get_registry(category: str) -> ComponentRegistry: + """Get or create a registry for a component category. + + Args: + category: The component category name + + Returns: + The ComponentRegistry instance for this category + """ + if category not in _registries: + _registries[category] = ComponentRegistry(category) + return _registries[category] + + +def _flush_all_registries(): + """Flush all pending registrations to KV store. + + This is registered as a Ray post-init hook to ensure registrations + made before Ray initialization are available across processes. + """ + for registry in _registries.values(): + registry.flush_pending() + + +if _flush_all_registries not in worker._post_init_hooks: + worker._post_init_hooks.append(_flush_all_registries) diff --git a/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/server_utils.py b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/server_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..dfeaad8e94e34c44f8de3c591c03f5ea5d25bb41 --- /dev/null +++ b/lib/python3.12/site-packages/ray/llm/_internal/serve/utils/server_utils.py @@ -0,0 +1,137 @@ +import asyncio +import traceback +from functools import partial +from typing import Awaitable, Callable, TypeVar + +from fastapi import HTTPException, status +from httpx import HTTPStatusError as HTTPXHTTPStatusError +from pydantic import ValidationError as PydanticValidationError + +from ray import serve +from ray.llm._internal.serve.core.configs.openai_api_models import ( + ErrorInfo, + ErrorResponse, + OpenAIHTTPException, +) +from ray.llm._internal.serve.observability.logging import get_logger + +logger = get_logger(__name__) + +T = TypeVar("T") + + +def make_async(_func: Callable[..., T]) -> Callable[..., Awaitable[T]]: + """Take a blocking function, and run it on in an executor thread. + + This function prevents the blocking function from blocking the asyncio event loop. + The code in this function needs to be thread safe. + """ + + def _async_wrapper(*args, **kwargs) -> asyncio.Future: + loop = asyncio.get_event_loop() + func = partial(_func, *args, **kwargs) + return loop.run_in_executor(executor=None, func=func) + + return _async_wrapper + + +def extract_message_from_exception(e: Exception) -> str: + # If the exception is a Ray exception, we need to dig through the text to get just + # the exception message without the stack trace + # This also works for normal exceptions (we will just return everything from + # format_exception_only in that case) + message_lines = traceback.format_exception_only(type(e), e)[-1].strip().split("\n") + message = "" + # The stack trace lines will be prefixed with spaces, so we need to start from the bottom + # and stop at the last line before a line with a space + found_last_line_before_stack_trace = False + for line in reversed(message_lines): + if not line.startswith(" "): + found_last_line_before_stack_trace = True + if found_last_line_before_stack_trace and line.startswith(" "): + break + message = line + "\n" + message + message = message.strip() + return message + + +def _extract_message(e): + if isinstance(e, OpenAIHTTPException) and e.internal_message is not None: + internal_message = e.internal_message + else: + internal_message = extract_message_from_exception(e) + + if isinstance(e, HTTPException): + message = e.detail + elif isinstance(e, OpenAIHTTPException): + message = e.message + else: + message = internal_message + + return internal_message, message + + +def get_response_for_error( + e: Exception, + request_id: str, +) -> ErrorResponse: + if isinstance(e, HTTPException): + status_code = e.status_code + elif isinstance(e, OpenAIHTTPException): + status_code = e.status_code + elif isinstance(e, PydanticValidationError): + status_code = 400 + elif isinstance(e, HTTPXHTTPStatusError): + status_code = e.response.status_code + else: + # Try to get the status code attribute from exception, + # if not present, fallback to generic 500 + status_code = int( + getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR) + ) + + log = logger.error if status_code >= 500 else logger.warning + log( + f"Encountered failure while handling request {request_id}", + exc_info=e, + extra={"ray_serve_extra_fields": {"status_code": status_code}}, + ) + + if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR: + internal_message = message = "Internal Server Error" + exc_type = "InternalServerError" + else: + internal_message, message = _extract_message(e) + exc_type = e.__class__.__name__ + + # TODO make this more robust + if "(Request ID: " not in message: + message += f" (Request ID: {request_id})" + + if "(Request ID: " not in internal_message: + internal_message += f" (Request ID: {request_id})" + + error_info = ErrorInfo( + message=f"Message: {message}, Internal exception: {internal_message}, original exception: {str(e)}", + code=status_code, + type=exc_type, + ) + error_response = ErrorResponse(error=error_info) + return error_response + + +def get_serve_request_id() -> str: + """Get request id from serve request context.""" + context = serve.context._serve_request_context.get() + if context is not None: + return context.request_id + return "" + + +def get_model_request_id(model: str): + return model + "-" + get_serve_request_id() + + +def replace_prefix(model: str) -> str: + """Replace -- with / in model name to handle slashes within the URL path segment""" + return model.replace("--", "/")