diff --git a/.github/workflows/cicd-main.yml b/.github/workflows/cicd-main.yml index 049a181c09e..a2fb932135c 100644 --- a/.github/workflows/cicd-main.yml +++ b/.github/workflows/cicd-main.yml @@ -224,7 +224,11 @@ jobs: h100_functional_test_scripts=$(jq -c \ '[.functional[] | select((ascii_downcase | contains("gb200")) | not)]' \ <<< "$test_plan") - gb200_functional_test_scripts=$(jq -c '.functional' <<< "$test_plan") + # The managed Dynamo runtime is currently amd64-only and is not + # installed in the arm64 GB200 image. + gb200_functional_test_scripts=$(jq -c \ + '[.functional[] | select(. != "L1_Functional_Tests_Dynamo")]' \ + <<< "$test_plan") unit_test_count=$(jq 'length' <<< "$unit_test_scripts") h100_functional_test_count=$(jq 'length' <<< "$h100_functional_test_scripts") @@ -553,6 +557,7 @@ jobs: megatron-lock-artifact: ${{ needs.prepare-megatron-lock.outputs.artifact_name }} build-args: | MAX_JOBS=4 + BUILD_DYNAMO=1 TRTLLM_BUILD_JOBS=24 NEMO_RL_COMMIT=${{ needs.pre-flight.outputs.test_sha }} diff --git a/.github/workflows/lockfile-check.yml b/.github/workflows/lockfile-check.yml index 7d54bf94049..7ecaf0d2684 100644 --- a/.github/workflows/lockfile-check.yml +++ b/.github/workflows/lockfile-check.yml @@ -26,6 +26,8 @@ on: - "pyproject.toml" - "uv.lock" - "3rdparty/**" + - "docker/dynamo/pyproject.toml" + - "docker/dynamo/uv.lock" - ".github/workflows/lockfile-check.yml" jobs: @@ -48,3 +50,6 @@ jobs: # uv.lock is slow (~5 min); cached runs are fast. - name: Check lockfile is up to date run: uv lock --check + + - name: Check Dynamo lockfile is up to date + run: uv lock --check --directory docker/dynamo diff --git a/docker/Dockerfile b/docker/Dockerfile index 30eb4e71577..5dc01a5233c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,6 +22,8 @@ # --build-arg SKIP_VLLM_BUILD=1 # Skip vLLM dependencies # --build-arg SKIP_SGLANG_BUILD=1 # Skip SGLang dependencies # --build-arg SKIP_TRTLLM_BUILD=1 # Skip TRT-LLM dependencies +# Optional isolated managed Dynamo runtime (Dynamo + vLLM only): +# --build-arg BUILD_DYNAMO=1 ARG BASE_IMAGE=nvcr.io/nvidia/cuda-dl-base:26.05-cuda13.2-devel-ubuntu24.04 FROM scratch AS nemo-rl @@ -334,6 +336,10 @@ FROM hermetic AS release ARG SKIP_VLLM_BUILD ARG SKIP_SGLANG_BUILD ARG SKIP_TRTLLM_BUILD +ARG BUILD_DYNAMO +ARG DYNAMO_PYTHON_VERSION=3.12.11 +ARG ETCD_VERSION=v3.5.21 +ARG NATS_VERSION=v2.11.6 # Space-separated config paths whose NeMo Gym venvs are prefetched into the image # (empty default = skip). Consumed by the gym prefetch RUN below. ARG NEMO_GYM_PREFETCH_CONFIGS= @@ -349,6 +355,7 @@ LABEL com.nvidia.build.id="${NVIDIA_BUILD_ID}" LABEL com.nvidia.build.ref="${NVIDIA_BUILD_REF}" ENV NEMO_RL_VENV_DIR=/opt/ray_venvs +ENV NEMO_RL_DYNAMO_VENV_DIR=/opt/dynamo_venv # AWS EFA OFI plugin discovery (p4d / p5 / p5en multi-node NCCL via SRD). # The host's aws-efa-installer places libnccl-net-ofi.so under @@ -364,6 +371,16 @@ ENV LD_LIBRARY_PATH="/opt/amazon/ofi-nccl/lib:/opt/amazon/efa/lib:${LD_LIBRARY_P # Copy in source from build context (defaults to cloned repo, can be overridden) # Exclude pyproject.toml and uv.lock since those may be altered by build-custom-vllm.sh COPY --from=nemo-rl --exclude=pyproject.toml --exclude=uv.lock . /opt/nemo-rl + +# Keep Dynamo's Python 3.12 dependency graph isolated from NeMo-RL's normal +# Ray/vLLM environments. Default image builds do not create this environment. +RUN <<"EOF" bash -exu -o pipefail +if [[ "${BUILD_DYNAMO:-0}" != "1" ]]; then + echo "BUILD_DYNAMO is not 1; skipping the managed Dynamo runtime" + exit 0 +fi +bash docker/dynamo/install.sh +EOF # Unshallow the repo to get the full history (in the case it was from the scratch layer). # Potentially not necessary if the repo is passed in as a complete repository (w/ full git history), # so do a quick check before trying to unshallow. diff --git a/docker/dynamo/install.sh b/docker/dynamo/install.sh new file mode 100644 index 00000000000..758ec1d193b --- /dev/null +++ b/docker/dynamo/install.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +project_dir=${NEMO_RL_DYNAMO_PROJECT_DIR:-${script_dir}} +repo_root=$(realpath "${script_dir}/../..") +dynamo_venv_dir=${NEMO_RL_DYNAMO_VENV_DIR:-${repo_root}/venvs/dynamo} +dynamo_python_version=${DYNAMO_PYTHON_VERSION:-3.12.11} +etcd_version=${ETCD_VERSION:-v3.5.21} +nats_version=${NATS_VERSION:-v2.11.6} + +target_arch=${TARGETARCH:-} +if [[ -z "${target_arch}" ]]; then + case "$(uname -m)" in + x86_64) target_arch=amd64 ;; + aarch64) target_arch=arm64 ;; + *) + echo "Unsupported host architecture: $(uname -m)" >&2 + exit 2 + ;; + esac +fi +case "${target_arch}" in + amd64|arm64) ;; + *) + echo "Unsupported TARGETARCH: ${target_arch}" >&2 + exit 2 + ;; +esac + +uv python install "${dynamo_python_version}" +uv venv --python "${dynamo_python_version}" "${dynamo_venv_dir}" +UV_PROJECT_ENVIRONMENT="${dynamo_venv_dir}" uv sync \ + --directory "${project_dir}" \ + --locked \ + --no-dev \ + --no-install-project \ + --link-mode copy + +dynamo_python=${dynamo_venv_dir}/bin/python +vllm_version=$("${dynamo_python}" -c \ + 'from importlib.metadata import version; print(version("vllm"))') +if [[ "${vllm_version}" != "0.23.0" ]]; then + echo "Expected vllm==0.23.0 from ai-dynamo[vllm]==1.3.0.post1; got ${vllm_version}" >&2 + exit 1 +fi + +vllm_root=$("${dynamo_python}" -c \ + 'from pathlib import Path; import vllm; print(Path(vllm.__file__).resolve().parent.parent)') +patch_file=${project_dir}/patches/vllm-0.23.0-layerwise-reload-composed-loader.patch + +# Dynamo 1.3.0 pins vLLM 0.23.0, which predates vLLM PR #44814. +# Without that fix, composed weight loaders can make layerwise reload finalize +# a layer early, leaving trailing NemotronH/Mamba2 parameters such as mixer.D +# unloaded and corrupting logits after a weight refit. +# Remove this backport only after Dynamo pins a vLLM release containing #44814. +if git -C "${vllm_root}" apply --check "${patch_file}"; then + git -C "${vllm_root}" apply "${patch_file}" +elif [[ -f "${dynamo_venv_dir}/VLLM_BACKPORTS" ]] \ + && git -C "${vllm_root}" apply --reverse --check "${patch_file}"; then + echo "vLLM PR #44814 backport is already applied" +else + echo "vLLM PR #44814 backport does not apply cleanly to vLLM ${vllm_version}" >&2 + exit 1 +fi +printf '%s\n' \ + 'vllm PR #44814 merge commit c9e5bf813530fb9ce06024e075da0f520b0718c8' \ + > "${dynamo_venv_dir}/VLLM_BACKPORTS" + +download_dir=$(mktemp -d "${TMPDIR:-/tmp}/nemorl-dynamo-install.XXXXXX") +trap 'rm -rf "${download_dir}"' EXIT + +curl --fail --location --retry 3 \ + "https://github.com/etcd-io/etcd/releases/download/${etcd_version}/etcd-${etcd_version}-linux-${target_arch}.tar.gz" \ + --output "${download_dir}/etcd.tgz" +tar -xzf "${download_dir}/etcd.tgz" -C "${download_dir}" +install -m 0755 \ + "${download_dir}/etcd-${etcd_version}-linux-${target_arch}/etcd" \ + "${dynamo_venv_dir}/bin/etcd" + +curl --fail --location --retry 3 \ + "https://github.com/nats-io/nats-server/releases/download/${nats_version}/nats-server-${nats_version}-linux-${target_arch}.tar.gz" \ + --output "${download_dir}/nats.tgz" +tar -xzf "${download_dir}/nats.tgz" -C "${download_dir}" +install -m 0755 \ + "${download_dir}/nats-server-${nats_version}-linux-${target_arch}/nats-server" \ + "${dynamo_venv_dir}/bin/nats-server" + +"${dynamo_python}" -c \ + 'import importlib.metadata as m; assert m.version("ai-dynamo") == "1.3.0.post1"; assert m.version("vllm") == "0.23.0"; assert m.version("nvidia-nccl-cu13") == "2.30.7"' +test -s "${dynamo_venv_dir}/VLLM_BACKPORTS" +grep -Fqx \ + 'vllm PR #44814 merge commit c9e5bf813530fb9ce06024e075da0f520b0718c8' \ + "${dynamo_venv_dir}/VLLM_BACKPORTS" +"${dynamo_venv_dir}/bin/etcd" --version +"${dynamo_venv_dir}/bin/nats-server" --version diff --git a/docker/dynamo/patches/vllm-0.23.0-layerwise-reload-composed-loader.patch b/docker/dynamo/patches/vllm-0.23.0-layerwise-reload-composed-loader.patch new file mode 100644 index 00000000000..179b1dc6c51 --- /dev/null +++ b/docker/dynamo/patches/vllm-0.23.0-layerwise-reload-composed-loader.patch @@ -0,0 +1,24 @@ +diff --git a/vllm/model_executor/model_loader/reload/meta.py b/vllm/model_executor/model_loader/reload/meta.py +index 397a458cb..ad6eceb8a 100644 +--- a/vllm/model_executor/model_loader/reload/meta.py ++++ b/vllm/model_executor/model_loader/reload/meta.py +@@ -185,4 +185,18 @@ def get_numel_loaded( + """ + with CopyCounter() as counter: + return_value = weight_loader(*args.args, **args.kwargs) +- return counter.copied_numel, return_value ++ ++ # A weight loader fills a single destination parameter, so the number of ++ # loaded elements is at most that parameter's size. Some loaders copy into ++ # the parameter more than once -- e.g. ``composed_weight_loader`` runs an ++ # in-place post-load transform (``param.copy_(fn(param))``) on top of the ++ # initial copy -- which would make CopyCounter report twice the parameter ++ # size. Over-counting inflates the layer's loaded-element total and can ++ # finalize the layer before every parameter is loaded, silently dropping ++ # the trailing parameter(s) (e.g. Mamba ``mixer.D``). Cap the count at the ++ # destination size to keep the per-layer accounting correct. ++ numel = counter.copied_numel ++ param = args.arguments.get("param", None) ++ if isinstance(param, torch.Tensor): ++ numel = min(numel, param.numel()) ++ return numel, return_value diff --git a/docker/dynamo/pyproject.toml b/docker/dynamo/pyproject.toml new file mode 100644 index 00000000000..303ea7dc5d1 --- /dev/null +++ b/docker/dynamo/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "nemo-rl-dynamo-runtime" +version = "0.0.0" +requires-python = "==3.12.*" +dependencies = ["ai-dynamo[vllm]==1.3.0.post1"] + +[tool.uv] +package = false +override-dependencies = ["nvidia-nccl-cu13==2.30.7; sys_platform == 'linux'"] diff --git a/docker/dynamo/uv.lock b/docker/dynamo/uv.lock new file mode 100644 index 00000000000..718c866415e --- /dev/null +++ b/docker/dynamo/uv.lock @@ -0,0 +1,3832 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" +resolution-markers = [ + "sys_platform == 'darwin'", + "sys_platform != 'darwin'", +] + +[manifest] +overrides = [{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'", specifier = "==2.30.7" }] + +[[package]] +name = "ai-dynamo" +version = "1.3.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ai-dynamo-runtime" }, + { name = "aiohttp" }, + { name = "kubernetes" }, + { name = "msgspec" }, + { name = "prometheus-client" }, + { name = "pyzmq" }, + { name = "transformers" }, + { name = "typing-extensions" }, + { name = "zstandard" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/ef/8c0d3bef0f555f375a18a968d607be4af40dcf51144866db5cb5f70c51fd/ai_dynamo-1.3.0.post1-py3-none-any.whl", hash = "sha256:cd855d8e567752ebac92a8a6746963efcdc90bce02d944580ef8d0f6f9739847", size = 2681107, upload-time = "2026-07-22T04:11:48.476Z" }, +] + +[package.optional-dependencies] +vllm = [ + { name = "blake3" }, + { name = "librosa" }, + { name = "nixl", extra = ["cu13"] }, + { name = "ray" }, + { name = "soundfile" }, + { name = "uvloop" }, + { name = "vllm", extra = ["otel", "runai"] }, +] + +[[package]] +name = "ai-dynamo-runtime" +version = "1.3.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "uvloop" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/f1/6b468ac39afdb0292d69c9c3b99ca165c9ed97493b9f0fd66b325e1255d4/ai_dynamo_runtime-1.3.0.post1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:45ca602fe4afca166c66150f2eccf2161dc39c1d0055914bef2a66c9bc0844dc", size = 46554625, upload-time = "2026-07-22T04:36:11.066Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/a3f4247b4b36ba0414ca8a06bcecddc5913d267bfb9c2a79464bf195fcfe/ai_dynamo_runtime-1.3.0.post1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c6da59d250da41f2bde33641649edbca800186e2cec0aede8763c6c6de35b877", size = 47106907, upload-time = "2026-07-22T04:35:26.366Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anthropic" +version = "0.120.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/10/4ca013cb166f226bd89e0aeb0fcaff94f45ddf716d4925ce89475d3c587b/anthropic-0.120.2.tar.gz", hash = "sha256:9722efc10c27a30a69f5338ddacdb35bc6a64297a4e4ba729bf83af873d5fb3a", size = 1008421, upload-time = "2026-07-28T17:38:26.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/af/0f5db57b9397a0f3b7fc204cbef143401a7cadaf982330f97f1ce3d39f34/anthropic-0.120.2-py3-none-any.whl", hash = "sha256:0f0bc2b381dc0eb41c8d886b815d79c2041cd2374f83aed36f574b6dc9c579c1", size = 1022851, upload-time = "2026-07-28T17:38:25.466Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "apache-tvm-ffi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/f2/b8c4b151169f6d7ba8773c8af68b2e0c1013d7fb3f1bdf87573f47157ce9/apache_tvm_ffi-0.1.9-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:49e52350b0470654847de752e65603b604a4d3323e7e9f5e8a982f44acc4c143", size = 2041756, upload-time = "2026-02-27T19:27:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/1b7dc5f0807f83098183a57db6ee85b2c93b646d74a6e03781c9208aaeb0/apache_tvm_ffi-0.1.9-cp312-abi3-win_amd64.whl", hash = "sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b", size = 1973200, upload-time = "2026-02-27T19:27:32.367Z" }, +] + +[[package]] +name = "astor" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "azure-core" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, +] + +[[package]] +name = "azure-storage-blob" +version = "12.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/48/84a820d898267f662b5c06f7cd76fdb8a9e272b44aa9376cef3ec0f6a294/azure_storage_blob-12.30.0.tar.gz", hash = "sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be", size = 618229, upload-time = "2026-06-08T11:45:35.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0b/e106f0fd7fa785867d9ffcc47dc9e6237c0e58f51058473b777487a98edc/azure_storage_blob-12.30.0-py3-none-any.whl", hash = "sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423", size = 435610, upload-time = "2026-06-08T11:45:37.213Z" }, +] + +[[package]] +name = "blake3" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/6a/4cc5a9dd40fd8a6d283fd3761e5f59c490109571ef8e3c73245417e5a305/blake3-1.0.9.tar.gz", hash = "sha256:5fa374fa5070ca084368776c19b420157eb0f2d3f091343d6bc59189929d62e2", size = 116872, upload-time = "2026-06-22T18:02:25.366Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/d2/9bdf8345c70993aaef635398f52edfb915d6e8ad2c000c801204e387c456/blake3-1.0.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a70c20542d5e7960983a0ff32999049a2b0e5ef1f22dbbbdfb51cf04828a4156", size = 344587, upload-time = "2026-06-22T18:00:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/be8b1f7f85b12bb45a0fade6ca7bdbf83a507d23d0b6141ba29fe69c8cea/blake3-1.0.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:72cdecf088a9d25e6ec79948a578995649b0dbee407e7a46c543a9ecc0f6f281", size = 328864, upload-time = "2026-06-22T18:00:35.59Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/66580635d744c826671fd219938caffb16281a26f62c4f856695d4233677/blake3-1.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42fa57bf462285ef16400601b0fd32214c248ba92505bbb94b1221ab9af5a092", size = 373795, upload-time = "2026-06-22T18:00:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/b5b17d3004bb81a5732c0b176c812703d200ed8c652b3b7713b9633bbe10/blake3-1.0.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b25ccde5a64be070f20e5c7a81da70292db40b164b6c77588cbd6230856badbb", size = 374183, upload-time = "2026-06-22T18:00:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/3c/63/0d209c44b2041bbe130ced12a23c92dd995fbfe5bce7ee77fffea16f5cb0/blake3-1.0.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a800b87433955f37691b5f361ad29c7dd3ee089c9cd109adc5aea8e24bc4c1f", size = 446783, upload-time = "2026-06-22T18:00:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/c5/51/efd1f9b8a9d3e9a0e235f3ced99a738529a1019fe78b3988e29d9c2fbba6/blake3-1.0.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6879739e7904b9c42afbedbcc2e8c36cebe140fb3fc3f5c492993579cf5cd516", size = 487369, upload-time = "2026-06-22T18:00:40.875Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/a8dcaea9e0b26e419a540ca0cd6203c9fbb505e85b02b03c5a59bf9e6a45/blake3-1.0.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6edeb3d49a24c307995899b70dd47aa901d0e9ad51d2f8a79aba4f074f32d8c5", size = 383845, upload-time = "2026-06-22T18:00:42.251Z" }, + { url = "https://files.pythonhosted.org/packages/f6/10/e9907f5b86410d5071982aaf05d149ca4d4fd8acab7e77eebbc9a333c7b4/blake3-1.0.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcd56a7a972c4185070f7042ccc20166927eec3c0f98b8405f375d007b604a0b", size = 383851, upload-time = "2026-06-22T18:00:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/c7863a185550706a9624f6aa7b6d46470aaed0bb46a827c5cda2a7d03151/blake3-1.0.9-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:a288664d08dee154cc496e06e62517fc9e655ecec12b0d7db538d244ac79edf1", size = 380067, upload-time = "2026-06-22T18:00:45.249Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/e7af679c719368b400c9ba9c3460072aac2ba077ddbd4bc806fef28cda03/blake3-1.0.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:91db52a809b68b5bebe7c413ddcd230e1f759398e7fa7a873104595a4fa648b6", size = 549471, upload-time = "2026-06-22T18:00:46.793Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/37c1dd3539b7bd9b6d2eef019802aacdb4a3d48ab484b140603bbf9c5b5a/blake3-1.0.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfaa671b07eb73883162ca940442193868358b0b904cfa266e4b74131ce966da", size = 591396, upload-time = "2026-06-22T18:00:48.122Z" }, + { url = "https://files.pythonhosted.org/packages/ae/55/4f0a23b72795292e74084834130900ea778c0583004519c86698dfffe1a5/blake3-1.0.9-cp312-cp312-win32.whl", hash = "sha256:ae47c3d5729ff89baa6ddf6de47fcfcc915985d39eb1bfcd6db653331f3c6fcc", size = 229271, upload-time = "2026-06-22T18:00:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/12/91/7db93e4689f0f145bcb954dc62936e5f5090548a9fa20c6bbebfaeaa648a/blake3-1.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:15566065ff90ab3da46ec0be1417406f00507af902b6fb0fbc6563e77f02fc42", size = 218220, upload-time = "2026-06-22T18:00:50.659Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.58" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/95/bd6276870084c9c1a94e17d1dc73b2de275e59bd7a0ad8bfff0a1598cec4/boto3-1.43.58.tar.gz", hash = "sha256:12871fb50c383f1b9aa4ed6dd386ba689062baef730552e79d5a9cd782b53058", size = 112685, upload-time = "2026-07-28T19:35:09.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/85/b0709066efb4ce7b86aba4092c739ad0e458be9d62cf32550fc4c130c93f/boto3-1.43.58-py3-none-any.whl", hash = "sha256:ce1a20cbcfaa1d0b3c8f568e2b6c7fbd34b842ea00e729d49a4b4de522828db9", size = 140026, upload-time = "2026-07-28T19:35:06.993Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.58" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/64/5cd46a7e72b0647e6d78fc8da016259ad66b9ae0818f4c5d629c75e7ca49/botocore-1.43.58.tar.gz", hash = "sha256:e110ca53f65c128fe98df4d6d36a459b1db17c6114671c8a18becf151bf20909", size = 15742412, upload-time = "2026-07-28T19:34:57.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/82/6f8fbbea47b773734ba0199643d2d851e5c2f75bc3699fe99db8af344d96/botocore-1.43.58-py3-none-any.whl", hash = "sha256:f516159f0732da8249206163ccea3bd1f82ad2a9d184fe6ed447e1abdba4330e", size = 15426503, upload-time = "2026-07-28T19:34:53.508Z" }, +] + +[[package]] +name = "cachetools" +version = "7.1.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/af/861ebc2e318a5c3300e3eb63bc4d30f3d70a46d13b360093728ac0705eed/cachetools-7.1.6.tar.gz", hash = "sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1", size = 40572, upload-time = "2026-07-23T22:47:53.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/f2/2086ba18a925a73586c4d4e61d25f4a6058e56fd00d77ce8f1d361ab4c9b/cachetools-7.1.6-py3-none-any.whl", hash = "sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096", size = 16954, upload-time = "2026-07-23T22:47:52.397Z" }, +] + +[[package]] +name = "cbor2" +version = "6.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/6f/07b4af8da8bd27f640362b1ac8271d80895407f2ede0c2bcc9433c06e1ca/cbor2-6.1.3.tar.gz", hash = "sha256:8d70680acb55c04ea5b5ad86da094f9612b53d5a8a65d0f5b3aafc3ce917ecbb", size = 89503, upload-time = "2026-07-04T10:36:48.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/16/cff14259c3d19a7f0ae88b6996fe4c85f6ff1764dad889ac8a39e843e39c/cbor2-6.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d939f55097c21e032f5a2d67592fcc57298986281f219356e2f519e4466f4ea", size = 412779, upload-time = "2026-07-04T10:36:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/50/6c/f3641d19b7b85a63cb2756c10164131489c2cb46b379ec51ae22283fefb9/cbor2-6.1.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b025009478d644dab407164fd60e3ef4381af284f5af6966df94c663756d949e", size = 457781, upload-time = "2026-07-04T10:36:06.349Z" }, + { url = "https://files.pythonhosted.org/packages/55/85/0c55a66f3037056bfb8e1c7184168085fdea67ae5830404498bcf466233b/cbor2-6.1.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2226d32e102e375737656ad5d141ad8c6ae3e705e04e263f24756f0eb379c6c1", size = 468373, upload-time = "2026-07-04T10:36:07.769Z" }, + { url = "https://files.pythonhosted.org/packages/46/74/40f7db3e0d880560193916a5c9b744fcf299558bed7113f77c28237c7c29/cbor2-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e61d465244d66ffed36492eef3b44d43795d76a2bba0663a2f15c186af7f7513", size = 523844, upload-time = "2026-07-04T10:36:09.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a1/b5e07d6a08441c3a552fe2ae48ccb7e9dfc5065b9f6a3bae9879b4f0fbc0/cbor2-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87fe7be8fab6ec4796aa127c1a52e09e79dbafd2aa31caf809cf04b8080a5975", size = 536238, upload-time = "2026-07-04T10:36:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/e166be0fd74bf3a91f5a0d103e34883efbc438d970f72cc8200e274787e5/cbor2-6.1.3-cp312-cp312-win32.whl", hash = "sha256:da25d345f01e6a40b2e5c57ef96b4dcff7be69394fb62f0f70e07f437f2376a9", size = 279858, upload-time = "2026-07-04T10:36:12.247Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/90b4a121e40aba189c55a5822dd3c698eaf487e1d4a780ab18c804a5ef1c/cbor2-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:d5514f693db6fa6f433b4096e9b604e6a7bf151c9ef1d2db86d0858e4c5e768f", size = 300929, upload-time = "2026-07-04T10:36:13.564Z" }, + { url = "https://files.pythonhosted.org/packages/19/db/52c58a8d33464927389dde8103997b3fa51b081ce29b347ac2cc4fd0dfbf/cbor2-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:3d43183d7beb3d3cd198d69b31bd2ee487ed704a1150c75cb0a66d6ad63d8c1a", size = 290908, upload-time = "2026-07-04T10:36:14.857Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "compressed-tensors" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "loguru" }, + { name = "pydantic" }, + { name = "torch" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/9e/d7f18bd9a0354088abc11a0c1f2c7698f7c49e5a709faedf6a46e388f693/compressed_tensors-0.17.0.tar.gz", hash = "sha256:15c20d06bdbcf35b51fc99fd125e7b9be1e1855567c33b7a46dfac26ad6fb126", size = 257091, upload-time = "2026-06-03T16:49:17.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/63/6edf0415b072fff0bf8b546074dea3f0f9b148e49b601ac98bdc60a76c68/compressed_tensors-0.17.0-py3-none-any.whl", hash = "sha256:4a1b89b508f7efb8ffb4eee8a6e69e0452d9b080cae130146025c64fbe9fa9aa", size = 211714, upload-time = "2026-06-03T16:49:15.672Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/7c/95/872a0392122f1fb43fcb06869790ef3171f37beee9f7db8f441739113570/cuda_bindings-13.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:b134dd8c5c66ae4c4ad814f7aee88fd215353c077010cbc47e3b55ed35ec9eff", size = 5875099, upload-time = "2026-05-29T23:11:54.635Z" }, +] + +[[package]] +name = "cuda-core" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/a0/1daeae599cadd612689dbbf70d7da1c01883964fc2fbc7386f3c630a68cf/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6816dc020aee6103d8071bc02d8e4e1d91f2b49596f666896d608d92224d79d1", size = 4789856, upload-time = "2026-05-12T20:11:30.862Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4d/603557ab3cb171cc2a61d3678a39cb4dae3fd21275078bfbd1c0b0b5230b/cuda_core-1.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7b65311bf78964b7905adbf3c0f8f717d432f2854dc45169277729bf60f1e2", size = 5106023, upload-time = "2026-05-12T20:11:33.509Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/ae079963c9df7f4274227eb63cf8f6083a532a6443adb340d951fd21c626/cuda_core-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a5c1aa3b738a7599ea289498d038fe625d259fd7ab795394541eee58a8e29bc", size = 4663076, upload-time = "2026-05-12T20:11:35.784Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" }, +] + +[[package]] +name = "cuda-python" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings" }, + { name = "cuda-core" }, + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/31/7ff3f7768eded7535c621abc2fecb9d181a34ea4cae3afe682feb796f242/cuda_python-13.3.1-py3-none-any.whl", hash = "sha256:280b014139ab447b6dd70a377db1596f310d6e887d9d342e6651b919ec145fb3", size = 8295, upload-time = "2026-05-29T23:28:47.012Z" }, +] + +[[package]] +name = "cuda-tile" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +dependencies = [ + { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/49/4592bc94ca05a07c7947ea114fd12734c8497f2daffee9faa79a03e39fb5/cuda_tile-1.3.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:375316b64c51ee7cfadb2f170a30c1547bc41eb39f1e233a6556713857d2e81f", size = 245744, upload-time = "2026-04-20T15:52:09.621Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/84cb68be463c827bf79da9fa0aa5140838de6455ef6f438bbe0ffa75d378/cuda_tile-1.3.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:e4865acbff1172aaee304bf9c550586088d8b4545a384423597a590899386709", size = 247301, upload-time = "2026-04-20T15:51:04.042Z" }, + { url = "https://files.pythonhosted.org/packages/db/6f/d2fd16c2b0d878021dc703eea5f8fe09599d6b04bdc2531a36fc617751fd/cuda_tile-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:93e20ed31e46e5bf704fb31d13e1c08338d2177838798876f7ee9ec4384b75ba", size = 240923, upload-time = "2026-04-20T15:52:14.939Z" }, +] + +[package.optional-dependencies] +tileiras = [ + { name = "nvidia-cuda-nvcc", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cuda-tileiras", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvvm", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, +] + +[[package]] +name = "cuda-tile" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, +] + +[package.optional-dependencies] +tileiras = [ + { name = "cuda-toolkit", version = "13.3.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", version = "13.0.96", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", version = "13.0.88", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/a1/54c1e9498ba0df91ca15a46f41af6320cb9faed6ec2dbb30b6cbff8887c4/cuda_toolkit-13.3.1-py2.py3-none-any.whl", hash = "sha256:2ceda460a540323d52469bcfde48b48c1861f6482e4b5ea3cb5bdac00a1b11bd", size = 2656, upload-time = "2026-06-29T17:23:23.848Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "depyf" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astor" }, + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/65/4df6936130b56e1429114e663e7c1576cf845f3aef1b2dd200c0a5d19dba/depyf-0.20.0-py3-none-any.whl", hash = "sha256:d31effad4261cebecb58955d832e448ace88f432328f95f82fd99c30fd9308d4", size = 39381, upload-time = "2025-10-13T12:33:33.647Z" }, +] + +[[package]] +name = "detect-installer" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "fastar" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "detect-installer" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dc/63aaf9913f455e39a7027c27140edd887a87d47d65ac43532d77a51718e5/fastapi_cloud_cli-0.23.0.tar.gz", hash = "sha256:840895bb8d14309aeffc905e0dcd1334d18c6f5da54b735413a8f1cb385e581e", size = 95295, upload-time = "2026-07-28T14:03:33.463Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/96/7e9aba6fabce3cb05f320abeef5b81efd5134823ae85d1a517872cb83cbc/fastapi_cloud_cli-0.23.0-py3-none-any.whl", hash = "sha256:1cd2ffa56e92e92c1fc63acc426c214dd928cbeed2a4c7c6a9a5fc85ea73de16", size = 78058, upload-time = "2026-07-28T14:03:34.386Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, +] + +[[package]] +name = "fastsafetensors" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/4c/f17bd54c933fd23648ce1e4272adf27d8d7e99b788c8859544bbd39f02e7/fastsafetensors-0.3.3.tar.gz", hash = "sha256:ba4fb59be8a6adbc91723848c3c6f57a9a9a5d2247d9768f84511380d01e554c", size = 77817, upload-time = "2026-07-07T07:21:47.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/3a/f95f7fc099ac1fc4c22aa46257d159eac88e29dd0765d21c4fc91caedb01/fastsafetensors-0.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c2f788a936ffd17938484360339645812e14cf1b2bdf4c18c035c713218e5a9", size = 1887326, upload-time = "2026-07-07T07:21:34.624Z" }, + { url = "https://files.pythonhosted.org/packages/92/8c/e3347b2a44a8ab9aced94fa450df4f309baa21f7f2981a8a7bd6a977f4d3/fastsafetensors-0.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3587bc66b8dec560ad903becf9540889013d4d47f0e10cf45f19bedc7b7bffa7", size = 1915478, upload-time = "2026-07-07T07:21:35.901Z" }, + { url = "https://files.pythonhosted.org/packages/80/65/388a55e6b2b3023fb732843335de14803f16a6d92f0cd47f1125d28b77ac/fastsafetensors-0.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:39c252a5528fa8653366f979d2ab8fa81159db4456b7c72cb5c288adb7699079", size = 424934, upload-time = "2026-07-07T07:21:37.216Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "flashinfer-cubin" +version = "0.6.12" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/c6/63b1bb7b1a7ae612ecf53c0e568312c3d004f9f7558b0ab5edcf7900c360/flashinfer_cubin-0.6.12-py3-none-any.whl", hash = "sha256:01de132c493bb21d5df42ebe6890966cf83b40aa970dae06b2a3c0bed85f13ec", size = 447533460, upload-time = "2026-05-29T23:45:27.579Z" }, +] + +[[package]] +name = "flashinfer-python" +version = "0.6.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "click" }, + { name = "cuda-tile", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["tileiras"], marker = "sys_platform != 'darwin'" }, + { name = "cuda-tile", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, extra = ["tileiras"], marker = "sys_platform == 'darwin'" }, + { name = "einops" }, + { name = "ninja" }, + { name = "numpy" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl" }, + { name = "nvidia-ml-py" }, + { name = "packaging" }, + { name = "requests" }, + { name = "tabulate" }, + { name = "torch" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/d0/114a64319f5a804def2f307d5ed8f95e6d94a2acdacac4ed5f57525cbf46/flashinfer_python-0.6.12.tar.gz", hash = "sha256:bed67f9c46d81dd22611dfef2787998fc412b2fe2648d9e7d336861dda912694", size = 9453326, upload-time = "2026-05-29T23:45:16.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/26/3ca33edbf64906603633cb91904798e427c0ac1c55a13707f8081708f3ae/flashinfer_python-0.6.12-py3-none-any.whl", hash = "sha256:0c7a01e586b4796810d974cbf13a9c0eb2ade6a94d12e3220cf7782a1c09b8d3", size = 13985243, upload-time = "2026-05-29T23:45:13.477Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "gguf" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ae/17f1308ae45cd7b08ebb521747d5b23f4efc4d172038a4e228dd5106c3ff/gguf-0.19.0.tar.gz", hash = "sha256:dbadcd6cc7ccd44256f2229fe7c2dff5e8aa5cf0612ab987fd2b1a57e428923f", size = 111220, upload-time = "2026-05-06T13:04:03.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/bb/d71d6da82763528c2c2ed6b59a9d6142c6595545a4c448e2085d155e88c2/gguf-0.19.0-py3-none-any.whl", hash = "sha256:70bcd10edfe697fb2dad6e40af2234b9d8ece9a41a99761405121ebda1c3c1cd", size = 118475, upload-time = "2026-05-06T13:04:02.588Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/62/8fb1fb647d2788c950d69d6a769cd9d55c918ac1fc57be2f90b7e4029787/google_api_core-2.33.0.tar.gz", hash = "sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb", size = 181607, upload-time = "2026-07-22T16:28:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/31/5056a347bb934ea04583c8b27916ef1501729c72638629545bce26ff4223/google_api_core-2.33.0-py3-none-any.whl", hash = "sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc", size = 176462, upload-time = "2026-07-22T16:28:22.447Z" }, +] + +[[package]] +name = "google-auth" +version = "2.56.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/33/dbc946a407401b975f0719658f18e664ece2109f79ffd1ff3bf226c205f4/google_auth-2.56.2.tar.gz", hash = "sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051", size = 365820, upload-time = "2026-07-21T21:53:28.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/63/50636aae68c9bf17c891c7eb18b49baa9bd6b31d2a97b8de4813a9fc8d1c/google_auth-2.56.2-py3-none-any.whl", hash = "sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6", size = 258588, upload-time = "2026-07-21T21:53:26.399Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/25/355ed97c1723c787dfaa888808d55db18371f82c38ff862357b1e902cd19/google_cloud_storage-3.13.0.tar.gz", hash = "sha256:d11d8706ea1520fba0f21043bcb7897caf7015d76ce1ad9a4f60237e4d7a9f6c", size = 17340960, upload-time = "2026-07-13T19:10:07.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e8/b3678a0931ee7d4b3fdaf0813e6206d66e0922b2c26d912f308728b5b95a/google_cloud_storage-3.13.0-py3-none-any.whl", hash = "sha256:648af3ef8a6acc674e1359d3c920c67eb89a7a5ab66b336bd3ac43fed6b5ab84", size = 341428, upload-time = "2026-07-13T19:09:52.39Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/f8/1ca5781d6be9cb9f73f7d40f4958c4bd1226a60598e3e39e1d6aaf838c4b/google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee", size = 2164570, upload-time = "2026-06-03T16:14:26.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c", size = 81533, upload-time = "2026-06-03T16:13:12.51Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "grpcio" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpcore2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/db3771a6e4fad4bd28fb055d4363b51cb0ae98c1aa504b79d41fdcab5483/huggingface_hub-1.25.1.tar.gz", hash = "sha256:21129595ca7a753be479b319913e22cc8808361ac118bd76cc413db831b28a99", size = 928426, upload-time = "2026-07-27T09:24:10.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/21e816831c6d16f88a6c784974413fa0421ce8a5d04380c2666ed5b503e5/huggingface_hub-1.25.1-py3-none-any.whl", hash = "sha256:004d4e70350517e24c68a7dbb7dc5e40b2b6aefef8f94bf7a85f6f9835102ea5", size = 774909, upload-time = "2026-07-27T09:24:08.079Z" }, +] + +[[package]] +name = "humanize" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" }, +] + +[[package]] +name = "humming-kernels" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings" }, + { name = "jinja2" }, + { name = "numpy" }, + { name = "nvidia-ml-py" }, + { name = "pyelftools" }, + { name = "safetensors" }, + { name = "tabulate" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "triton", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/05e95b66cca48def9db0d6c40374fe285c7d9c913fe126030bcfb7cb3088/humming_kernels-0.1.4.tar.gz", hash = "sha256:fdaf4f23cc6b03bb1be3fd24aa11dc7798881e5448826e2404b4f12d8096f0d0", size = 117555, upload-time = "2026-06-04T03:24:03.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/16/d9318061a560305034e14cb7bf6483ffc8735eff6b30f260907dbbd4e85d/humming_kernels-0.1.4-py3-none-any.whl", hash = "sha256:c85094cd7cf8cdd959c5e2f7f239a7d72a7640ec1f948787434bc06e24e9ed00", size = 161312, upload-time = "2026-06-04T03:24:01.897Z" }, +] + +[package.optional-dependencies] +cu13 = [ + { name = "nvidia-cuda-cccl" }, + { name = "nvidia-cuda-nvcc", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cuda-nvcc", version = "13.3.73", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "nvidia-cuda-nvrtc", version = "13.0.88", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cuda-nvrtc", version = "13.3.33", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "nvidia-cuda-runtime", version = "13.0.96", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cuda-runtime", version = "13.3.29", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "ijson" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/b31f040a8764336a11152e474a7abcb3782fedb0d1cdf78f442b82878c56/ijson-3.5.1.tar.gz", hash = "sha256:af40bd1a85f55db0b8b30715c858761306bd92d5590148636f75c3309e6e76bd", size = 69913, upload-time = "2026-07-06T17:37:42.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/6e/f3ded1ebb85ccc89a30f7b10a0076f30db70ae1d1e0b6423ff93c57b7539/ijson-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee60c7741012671867678eae71c51872cac938b76f3d4ca40a778e6c361774d2", size = 88643, upload-time = "2026-07-06T17:36:28.529Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f2/18f14a1d79ef4898e746b4f50dcdbe60abab317cc2bd8390f043b9553c4e/ijson-3.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:11c1d7d36a13054b5872ecd5d745dc4009d9abdbcba2312de69e66c2f92a46d2", size = 60611, upload-time = "2026-07-06T17:36:29.597Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/6e3e591324fd4c7a7a9e1bc23548bacbd84c0d91766b71f09f13e945e7e9/ijson-3.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9517efbe6604bce16f3e50d49b0cd1bdc58917f98cf2eab026599c5c0422991", size = 60447, upload-time = "2026-07-06T17:36:30.747Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a5/9af7be670381ddac26dd55107ed0110b50f5161673b053311db67f510dcc/ijson-3.5.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea4fd7bec203a600b1cc88a492dfe6b75ce4b1b87488a66adcd5406022213f64", size = 139092, upload-time = "2026-07-06T17:36:31.749Z" }, + { url = "https://files.pythonhosted.org/packages/41/fb/f9c1664d75467453e6bd4e5f9cd2211b730b09e049445ab64cbac68cc6a3/ijson-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350caea815e53151994b597abc80cf669454276b5ac6aadcec69ef6d48f7e90b", size = 149921, upload-time = "2026-07-06T17:36:32.912Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/d20b1c49c4aa7cc6644131e2e57192b45346ef4816566ed1cd9fd05bae38/ijson-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4fcebfe1685bb7ba06a8255a5d428ea6b4b895d7acf979cb637d8bbc9db2f47", size = 149848, upload-time = "2026-07-06T17:36:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fc/5baa710869f5ab939e6233583ced1546889b55c35f35b844c518ac10abc3/ijson-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d78f362f51c8691798758a9e6ac3c9d385ee1228cb82987c91562a2fae235cd3", size = 150810, upload-time = "2026-07-06T17:36:35.19Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/a12b3d987a5c1677b04557c6f9b9feb7e04b7d4171e9a344856cb9136e9b/ijson-3.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0b184180d45f85fd4479659582749b109e49f4a29c21ac700ccc9c2280fe015e", size = 142989, upload-time = "2026-07-06T17:36:36.23Z" }, + { url = "https://files.pythonhosted.org/packages/ed/63/1026c535671fc334fc85aeb78f0945c825e7a338575edc753c0f455459ae/ijson-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e353891d33a2e6aa5caf72c2a5fbadd7a46f5f9b32dcfd0c84113b2444c255b8", size = 151702, upload-time = "2026-07-06T17:36:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/cb/af/b58aa3a2bf4d31c388ea78b49826605f60932891ce97e404d196766b4ea3/ijson-3.5.1-cp312-cp312-win32.whl", hash = "sha256:936f28671f018f8ac4d3f003ae9fa01d0467ab4ef4cfd0c97f23beda485b61c6", size = 52613, upload-time = "2026-07-06T17:36:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/04/66/ce70a92949c2a753dad91fdd5761dc14f3a44517e80cfc3c26612982ed61/ijson-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:322c783f3ee0c6b383bbd4db88370b10172168808cc2a0bf811f1253f7435602", size = 54729, upload-time = "2026-07-06T17:36:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/e17784240c9cf1d58de2f2853ebaf9cc54f6bce117a1f12a6150bbb4a5aa/ijson-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:e2ac204b59f09e38e16d277f906240e9fd38780e42076599419265af183dc4b4", size = 53714, upload-time = "2026-07-06T17:36:40.308Z" }, +] + +[[package]] +name = "interegular" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9d/8b6dde58a028a3962ce17e84d5fe73758df61378e00ef8ac3d85da34b0ff/interegular-0.3.3.tar.gz", hash = "sha256:d9b697b21b34884711399ba0f0376914b81899ce670032486d0d048344a76600", size = 24705, upload-time = "2024-01-06T23:01:22.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/01/72d6472f80651673716d1deda2a5bbb633e563ecf94f4479da5519d69d25/interegular-0.3.3-py37-none-any.whl", hash = "sha256:b0c07007d48c89d6d19f7204972d369b2a77222722e126b6aa63aa721dc3b19c", size = 23635, upload-time = "2024-01-06T23:01:20.829Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kubernetes" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/0598f0e8b4af37cd9b10d8b87386cf3173cb8045d834ab5f6ec347a758b3/kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28", size = 946691, upload-time = "2025-02-18T21:06:34.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070, upload-time = "2025-02-18T21:06:31.391Z" }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pooch" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "llguidance" +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/91/6bc8bb503dc259e46d253b5424385a54fe06c38a4c7a12befe69a3c2455a/llguidance-1.7.6.tar.gz", hash = "sha256:db7febbe412ed2015501904646750071d7e00e6df7f85c4b956ad4f206fd2df7", size = 1156574, upload-time = "2026-06-03T20:13:25.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/1d/5a9a13421b1f3f1c1acf82beb63ed72fa4d302e65099b72f4a4fe5a098ab/llguidance-1.7.6-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:eabf4572c8731734c0444c353b9ea06bc5c156986d2ff0a4ec0499159271381f", size = 3227892, upload-time = "2026-06-03T20:13:09.533Z" }, + { url = "https://files.pythonhosted.org/packages/46/fe/bb185f11bad82f2637e3cd8cbf6b200cbb6ed56ac395de47ea05a60d4649/llguidance-1.7.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:9c54c899db8cb4b4fba128a7d844730066576c70d806c95ada92b2bd2d6ab498", size = 3138127, upload-time = "2026-06-03T20:13:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/dc76d7716e04dc7b3427cae52eaa32bd20771382d4d1dd9f4538a9dd2086/llguidance-1.7.6-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:e70fa25ed550c2b50c2fd70baa9e2808b4ecb859d01e453bd5459aff62ba38c3", size = 2899993, upload-time = "2026-06-03T20:13:13.563Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/d74336f22242ef94356a456057d4ff1be7c1bc9c7dbc867171c6982a5512/llguidance-1.7.6-cp39-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:ceec951d29a74309984e3be0fe7f5f56c1362434cd937abd517b259a60908b1e", size = 3074809, upload-time = "2026-06-03T20:13:15.498Z" }, + { url = "https://files.pythonhosted.org/packages/49/37/99d700f0e2c83acf25a8d8946b2bee9f5eac47bc530bfbd53ba3126c667f/llguidance-1.7.6-cp39-abi3-win_amd64.whl", hash = "sha256:ace7e81cd31950a87186356ab24bd7f75fbc10a05ca9d9f7f8748f931963f763", size = 2879207, upload-time = "2026-06-03T20:13:23.341Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, +] + +[[package]] +name = "lm-format-enforcer" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "interegular" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/ef/11292bb0b85cf4c93447cab5a29f64576ed14d3ab4280e35ddd23486594a/lm_format_enforcer-0.11.3-py3-none-any.whl", hash = "sha256:cf586350875def1ae7a8fba84fcbbfc8371424b6c9d05c1fcba70aa233fbf06f", size = 45418, upload-time = "2025-08-24T19:37:46.325Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mcp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx2" }, + { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistral-common" +version = "1.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydantic-extra-types", extra = ["pycountry"] }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/d0/61b2c24be62a8e2f0e46a1c16de23de386c8644408da249bc66768a6681b/mistral_common-1.11.7.tar.gz", hash = "sha256:d3b79583595cf6d96a2ab33e42cb8449768383147b8c56cac5a4f193be19d20d", size = 6387178, upload-time = "2026-07-23T09:21:17.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a4/bc2850eb33cc2d633a21f51530756350dca325ee69b07c9202552f2bbadb/mistral_common-1.11.7-py3-none-any.whl", hash = "sha256:a9511b88eacacbe7dacddd9d3498c1739f56847b7fdddbd5a22e7844fd9def95", size = 6553583, upload-time = "2026-07-23T09:21:19.818Z" }, +] + +[package.optional-dependencies] +image = [ + { name = "opencv-python-headless" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, +] + +[[package]] +name = "model-hosting-container-standards" +version = "0.1.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "jmespath" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "starlette" }, + { name = "supervisor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/5f/bc0d0fce1bd0a35378696aa13b21feffa18d9cda837f4e1be124e45ee090/model_hosting_container_standards-0.1.16.tar.gz", hash = "sha256:d34589633900e53c3ee5f7c78280a7cf7e4f6532c35e763341a262fc85cbe84a", size = 94130, upload-time = "2026-06-15T21:29:34.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ef/6eabeb251d2a0598cb5f9a274159e05ae07a1e3fe6a1473bf6035793252a/model_hosting_container_standards-0.1.16-py3-none-any.whl", hash = "sha256:47f4f65713120bc3a69feb022981a38db9e557aedf88dbd72077f20588caa12b", size = 125666, upload-time = "2026-06-15T21:29:33.415Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msal" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/99/d840198ecf6e8057bbc937f129ae940404485d736cda73253bbff9537f01/msal-1.37.0.tar.gz", hash = "sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec", size = 182444, upload-time = "2026-05-29T19:49:05.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl", hash = "sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d", size = 123725, upload-time = "2026-05-29T19:49:04.335Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, +] + +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "nemo-rl-dynamo-runtime" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "ai-dynamo", extra = ["vllm"] }, +] + +[package.metadata] +requires-dist = [{ name = "ai-dynamo", extras = ["vllm"], specifier = "==1.3.0.post1" }] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nixl" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nixl-cu12" }, + { name = "nixl-cu13" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/07/d7498fc2be7768fcf172bd86226aa20fea9ae44b887b8adc527304c19824/nixl-1.1.0-py3-none-any.whl", hash = "sha256:f46f65768770fa508eb52921c41b5dc52b754478b0ebb606fff6d80f41375d8b", size = 8727, upload-time = "2026-05-12T03:27:00.729Z" }, +] + +[package.optional-dependencies] +cu13 = [ + { name = "nixl-cu13" }, +] + +[[package]] +name = "nixl-cu12" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/3a/4f91e30271522e17ee6c36b1f8505b671fc318adcc9e81876cbb8b51606d/nixl_cu12-1.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c9d4ef0b057c06e3fd7492b02605fb6cfb3d88c02d46ea7cdd3373b3050d2f79", size = 37378486, upload-time = "2026-05-12T03:19:51.086Z" }, + { url = "https://files.pythonhosted.org/packages/87/80/c6ff885789ecd15bc12d20ffc62f209b29351a120202d99de50a95389386/nixl_cu12-1.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cf270a5319f4feb80cedc7ae19029ef48eb4428f587bbcdcd3b787933430890b", size = 38469377, upload-time = "2026-05-12T03:17:12.123Z" }, +] + +[[package]] +name = "nixl-cu13" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/de/2a56dba6b9d1e5bc5f7ef8a1cbe3969054548bb7e0cdb10b5a8f56e31790/nixl_cu13-1.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4e6031798b0a123d1821db698b1f9b3a1534c821af860ee0ef23601638c50d8f", size = 34975342, upload-time = "2026-05-12T03:25:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/13/ad/a3ee9b2cad49e42b2b215d07f55afe0ad38d671d72b6cbd573c98d5a75ba/nixl_cu13-1.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:60cc00b12871d8c7d78c2385ad9380070424d5b07d3fe01680f222d6c4f1f428", size = 36046966, upload-time = "2026-05-12T03:22:40.952Z" }, +] + +[[package]] +name = "numba" +version = "0.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/61/7299643b9c18d669e04be7c5bcb64d985070d07553274817b45b049e7bfe/numba-0.65.0.tar.gz", hash = "sha256:edad0d9f6682e93624c00125a471ae4df186175d71fd604c983c377cdc03e68b", size = 2764131, upload-time = "2026-04-01T03:52:01.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/2f/8bd31a1ea43c01ac215283d83aa5f8d5acbe7a36c85b82f1757bfe9ccb31/numba-0.65.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b27ee4847e1bfb17e9604d100417ee7c1d10f15a6711c6213404b3da13a0b2aa", size = 2680705, upload-time = "2026-04-01T03:51:32.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/88406bd58600cc696417b8e5dd6a056478da808f3eaf48d18e2421e0c2d9/numba-0.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a52d92ffd297c10364bce60cd1fcb88f99284ab5df085f2c6bcd1cb33b529a6f", size = 3801411, upload-time = "2026-04-01T03:51:34.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/61/ce753a1d7646dd477e16d15e89473703faebb8995d2f71d7ad69a540b565/numba-0.65.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da8e371e328c06d0010c3d8b44b21858652831b85bcfba78cb22c042e22dbd8e", size = 3501622, upload-time = "2026-04-01T03:51:36.348Z" }, + { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cccl" +version = "13.3.3.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/bd/572971ffc14bd36676c821fc15d991b08fe6179cb09368250147475f954d/nvidia_cuda_cccl-13.3.3.4.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:067d19b4b3c9d0f2ebec9f29a311b2863db96bf98e058bbc331597d51ce818cf", size = 3454030, upload-time = "2026-06-29T16:41:49.092Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ab/049726d90147865a3ea53bae6cb7c35b98bf1fdf96cdb967101329625f83/nvidia_cuda_cccl-13.3.3.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc0adc188d570b09f4d606c7dc05a42aa3d8aa082e0d60f7bbfc5b6435f627c6", size = 3454034, upload-time = "2026-06-29T16:42:07.435Z" }, + { url = "https://files.pythonhosted.org/packages/24/d3/b1afcd9c40ceca72022579215fcaf5318cd747fd896cb928d4a1de924ff8/nvidia_cuda_cccl-13.3.3.4.1-py3-none-win_amd64.whl", hash = "sha256:d7c92cc03047031fa7af30866636d35ce4af409c28fc7dd8f69cb17053741399", size = 3454014, upload-time = "2026-06-29T17:09:09.012Z" }, +] + +[[package]] +name = "nvidia-cuda-crt" +version = "13.3.73" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/2089e411507d66458d67208bdd1bc562d492bb6458c3d2aea4603072a219/nvidia_cuda_crt-13.3.73-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:60aacc0b5e1e8b40c62abe4d1ab16440add91b99bd2f17f62dd091586b73d166", size = 157353, upload-time = "2026-06-29T16:42:38.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ce/16d76f4b5b3f7460f5ebd17516685495c149c66651ffdd381f90e4d4e65c/nvidia_cuda_crt-13.3.73-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df14a17ae1c5c3171265411212246654d780f89344ea85344466c6b955247543", size = 157352, upload-time = "2026-06-29T16:43:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/6791ffba6f4b8e0d3ed875285aad8078ee407afa464ecd934ae298c205b1/nvidia_cuda_crt-13.3.73-py3-none-win_amd64.whl", hash = "sha256:af04e75148db1f0eea30958f33a9ec5a5a2dc2afa99ca4323f9a93b840602ca5", size = 158286, upload-time = "2026-06-29T17:09:28.621Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvcc" +version = "13.2.86" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +dependencies = [ + { name = "nvidia-cuda-crt", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cuda-runtime", version = "13.0.96", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvvm", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a3/403638f80960e677ae8ecc78059d8c85dc2519b85ed8c0229840dc6f3b54/nvidia_cuda_nvcc-13.2.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:909140a1f942b943982b2eff120e618c94e29d75d9e33f5cd074f0e64eb411e8", size = 38716270, upload-time = "2026-07-16T09:37:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/ad9b5f84e8820af24afa8db66b5b2785b9bdc3b8724dffcd5e6ef2d40e53/nvidia_cuda_nvcc-13.2.86-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3f56c8d705bad35bbba69eb0470d90856fd5d4dc48c6a6173aaf1e9f887cf5", size = 44042846, upload-time = "2026-07-16T09:37:47.98Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/01dad23300993e52933773751d0dd8e795175daa830b798ce324db377e3a/nvidia_cuda_nvcc-13.2.86-py3-none-win_amd64.whl", hash = "sha256:4171face482ef8ca35b5b2d59cfd25d2d0e2a2e2534fe34d2ce20695437868d9", size = 31816369, upload-time = "2026-07-16T10:01:34.327Z" }, +] + +[[package]] +name = "nvidia-cuda-nvcc" +version = "13.3.73" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] +dependencies = [ + { name = "nvidia-cuda-crt", marker = "sys_platform == 'darwin'" }, + { name = "nvidia-cuda-runtime", version = "13.3.29", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "nvidia-nvvm", version = "13.3.73", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/4a/af/345fedb9f4c76c84ab4fa445b36bd4048a4d9db60e6bc76b4f913ff4b852/nvidia_cuda_nvrtc-13.0.88-py3-none-win_amd64.whl", hash = "sha256:6bcd4e7f8e205cbe644f5a98f2f799bef9556fefc89dd786e79a16312ce49872", size = 76807835, upload-time = "2025-09-04T08:39:15.274Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/b7/94/6b867483bec07da24ffa32736c79fabb94ef3a7af4d787a9d4a974868576/nvidia_cuda_runtime-13.0.96-py3-none-win_amd64.whl", hash = "sha256:f79298c8a098cec150a597c8eba58ecdab96e3bdc4b9bc4f9983635031740492", size = 2927037, upload-time = "2025-10-09T09:04:23.782Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.3.29" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] + +[[package]] +name = "nvidia-cuda-tileiras" +version = "13.2.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvcc", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvvm", version = "13.2.86", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/a2/ade26f7fb55a5bb87815f7650660463d6601db411d5a9228f414b3ed5dfe/nvidia_cuda_tileiras-13.2.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f79c6a9bf8583dae105cd67b985555f39f4576416664a86383ba892e8c346c9", size = 36418795, upload-time = "2026-07-16T09:43:35.006Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ec/e01bcfe4f48fabd8fd1af139061a74f92f3b71fe3cac8a64e943157c2585/nvidia_cuda_tileiras-13.2.86-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:832f360aa8ce478ff878c4e6630cd767349a597ea54382e7c07a835b0daead96", size = 36970477, upload-time = "2026-07-16T09:44:08.421Z" }, + { url = "https://files.pythonhosted.org/packages/86/ba/3d8fefbaa2cf17545448064bbec72d32b1146165eb7b56be730af7a65a56/nvidia_cuda_tileiras-13.2.86-py3-none-win_amd64.whl", hash = "sha256:f1434762898ac914585aaa651ad0fe170c57bcdf8d878ba38343772998bb7ff3", size = 29384842, upload-time = "2026-07-16T10:04:45.813Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cudnn-frontend" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/c4/3f587b73ac2eb6e391aebffb7a7a9ac9ed70e1e0e6a1d90ec0fdcb1a516f/nvidia_cudnn_frontend-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee50df3468f672aa31402fde4c911ad545d08e1d5e133e15bc55c3679d9fd9ca", size = 3471242, upload-time = "2026-07-07T20:55:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/64818fbaa117456349241b42ddfaef3ae6b050f5e99bb0e9d78eb831279b/nvidia_cudnn_frontend-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a1223c4e2e8bbe6f620148d6848f4eb773dd94bef534d5b748e91232b5be618", size = 3627033, upload-time = "2026-07-07T20:55:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/f9/99/04a37f34b271ed157c6108c3191bd1993ab3fad8d5d66516970bd1e7c12d/nvidia_cudnn_frontend-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f104a40cb6f25cf01b7d7e84cb99af7dedd32b1cb8264384c2f363b7a3f7fb6", size = 2998730, upload-time = "2026-07-07T20:56:09.848Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cutlass-dsl-libs-base" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/15/575d7df4fe2f3406f1cfc68be72aeff2834f8a696daf1cd5bee8017e4507/nvidia_cutlass_dsl-4.5.2-py3-none-any.whl", hash = "sha256:68ed1b63ca74aae87955012da9dfd7fdaae471329d0028b229b841c7192ccf52", size = 10179, upload-time = "2026-05-25T03:38:56.364Z" }, +] + +[package.optional-dependencies] +cu13 = [ + { name = "nvidia-cutlass-dsl-libs-cu13" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/ef/e827e3c67d72adbf4e8f680bdf03b1b67723d9e1ae7c3d0a1751f39f69ce/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d2a3c412287e356fbe48fe9f845d6d33cd35dea5e20d7e4f628c20957967cacd", size = 75643473, upload-time = "2026-05-25T03:49:15.857Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/c1247ab848f26c4ab56e562eea0e3f31fc14c9aaf0d883afaa92d8f05592/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15ef6a59193667e663934ef4873f8ccad37455e9b7c3c419c3072113b8aedf61", size = 74513226, upload-time = "2026-05-25T03:51:32.496Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-cu13" +version = "4.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy" }, + { name = "nvidia-cutlass-dsl-libs-base" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/e5/aeb570713a7bd6c2cb08102c2ebe6de234ef1bbc276d1af4643266cd71a8/nvidia_cutlass_dsl_libs_cu13-4.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3032405dff28892340f96b467e744a822079cae454dce534fc17b77e85190e42", size = 79084280, upload-time = "2026-05-25T03:40:57.547Z" }, + { url = "https://files.pythonhosted.org/packages/03/60/443e559139da15ab544761ac14f4206dffb981af48cc9856cd5b5b7cf0e7/nvidia_cutlass_dsl_libs_cu13-4.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:80f0cd402e0f1d1571e5aed33bfa17dbc9cb90cc5b1352f0f806b4788558e80e", size = 78759198, upload-time = "2026-05-25T03:45:59.297Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.610.43" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b5/a8fbc356f768fa5c9cfd646668fd7d34bf55bdd1c6e20754642a64d930d4/nvidia_ml_py-13.610.43.tar.gz", hash = "sha256:65437eb73d68d0c62c931ca4d45038472faff03bd0b8729abba4b899f70d60f2", size = 52109, upload-time = "2026-06-01T18:54:08.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl", hash = "sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8", size = 53163, upload-time = "2026-06-01T18:54:07.704Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, + { url = "https://files.pythonhosted.org/packages/e4/01/07530b0e37546231052e30234540289c42eaffa486f1a34a87fed340157b/nvidia_nvjitlink-13.0.88-py3-none-win_amd64.whl", hash = "sha256:634e96e3da9ef845ae744097a1f289238ecf946ce0b82e93cdce14b9782e682f", size = 36035115, upload-time = "2025-09-04T08:43:03.001Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "nvidia-nvvm" +version = "13.2.86" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/1d/b50325516e259d4de9fca78b069840d39b6a172c5ba012b88d9985e01fa3/nvidia_nvvm-13.2.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a9244f3209922d655612c11ebb4117ea1f80983cb2b215c9cccded127282cebc", size = 64280112, upload-time = "2026-07-16T09:58:27.625Z" }, + { url = "https://files.pythonhosted.org/packages/e6/64/039ad70d68355634581d3b66f99b7aa8f75ca6078e1a6a2c9223677bbee3/nvidia_nvvm-13.2.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a502dcc2859f17a925adba495c222d44a221b9eb10e7d111a7046dc2cc883b69", size = 61886756, upload-time = "2026-07-16T09:57:51.818Z" }, + { url = "https://files.pythonhosted.org/packages/c8/1c/a1e27ca53d767f5043eab9332564581e976953a437e1bc8c3642679a622f/nvidia_nvvm-13.2.86-py3-none-win_amd64.whl", hash = "sha256:7d140c2dd2d177af71240a31bbca20f40b4aee9a4e331156083e387855222b49", size = 56750837, upload-time = "2026-07-16T10:11:39.216Z" }, +] + +[[package]] +name = "nvidia-nvvm" +version = "13.3.73" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "openai" +version = "2.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/9a/275bee8349b766906741223b98c668fcd4e17e1982a87597778743b4156f/openai-2.49.0.tar.gz", hash = "sha256:80f934333b5b83cef2fde9af7151dacaa72e150f43f92b7675f7647ca6157f48", size = 1080973, upload-time = "2026-07-27T22:51:40.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/ca/53357e460a1172e831ecbe43dd0c37342b7211a1eb09f4cf21a412adbbdf/openai-2.49.0-py3-none-any.whl", hash = "sha256:b694201eaa42a1ccf2aa125fe29458150108fb22df1abfb55d7188599da81d8c", size = 1648589, upload-time = "2026-07-27T22:51:38Z" }, +] + +[[package]] +name = "openai-harmony" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/c6/2502f416d46be3ec08bb66d696cccffb57781a499e3ff2e4d7c174af4e8f/openai_harmony-0.0.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:029ec25ca74abe48fdb58eb9fdd2a8c1618581fc33ce8e5653f8a1ffbfbd9326", size = 2627806, upload-time = "2025-11-05T19:06:57.063Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d2/ce6953ca87db9cae3e775024184da7d1c5cb88cead19a2d75b42f00a959c/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4f709815924ec325b9a890e6ab2bbb0ceec8e319a4e257328eb752cf36b2efc", size = 2948463, upload-time = "2025-11-05T19:06:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4c/b553c9651662d6ce102ca7f3629d268b23df1abe5841e24bed81e8a8e949/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5cfcfd963b50a41fc656c84d3440ca6eecdccd6c552158ce790b8f2e33dfb5a9", size = 2704083, upload-time = "2025-11-05T19:06:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/9b/af/4eec8f9ab9c27bcdb444460c72cf43011d176fc44c79d6e113094ca1e152/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a3a16972aa1cee38ea958470cd04ac9a2d5ac38fdcf77ab686611246220c158", size = 2959765, upload-time = "2025-11-05T19:06:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/11/3c/33f3374e4624e0e776f6b13b73c45a7ead7f9c4529f8369ed5bfcaa30cac/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4d5cfa168e74d08f8ba6d58a7e49bc7daef4d58951ec69b66b0d56f4927a68d", size = 3427031, upload-time = "2025-11-05T19:06:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/25/3f/1a192b93bb47c6b44cd98ba8cc1d3d2a9308f1bb700c3017e6352da11bda/openai_harmony-0.0.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c007d277218a50db8839e599ed78e0fffe5130f614c3f6d93ae257f282071a29", size = 2953260, upload-time = "2025-11-05T19:06:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/93b582cad3531797c3db7c2db5400fd841538ccddfd9f5e3df61be99a630/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8565d4f5a0638da1bffde29832ed63c9e695c558611053add3b2dc0b56c92dbc", size = 3127044, upload-time = "2025-11-05T19:06:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/4327dbf87f75ae813405fd9a9b4a5cde63d506ffed0a096a440a4cabd89c/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cbaa3bda75ef0d8836e1f8cc84af62f971b1d756d740efc95c38c3e04c0bfde2", size = 2932931, upload-time = "2025-11-05T19:07:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c8/1774eec4f6f360ef57618fb8f52e3d3af245b2491bd0297513aa09eec04b/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:772922a9bd24e133950fad71eb1550836f415a88e8c77870e12d0c3bd688ddc2", size = 2996140, upload-time = "2025-11-05T19:07:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/3d1e01e2dba517a91760e4a03e4f20ffc75039a6fe584d0e6f9b5c78fd15/openai_harmony-0.0.8-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:007b0476a1f331f8130783f901f1da6f5a7057af1a4891f1b6a31dec364189b5", size = 3205080, upload-time = "2025-11-05T19:07:05.078Z" }, + { url = "https://files.pythonhosted.org/packages/14/63/119de431572d7c70a7bf1037034a9be6ed0a7502a7498ba7302bca5b3242/openai_harmony-0.0.8-cp38-abi3-win32.whl", hash = "sha256:a9b5f893326b28d9e935ade14b4f655f5a840942473bc89b201c25f7a15af9cf", size = 2082457, upload-time = "2025-11-05T19:07:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/c83cf5a206c263ee70448a5ae4264682555f4d0b5bed0d2cc6ca1108103d/openai_harmony-0.0.8-cp38-abi3-win_amd64.whl", hash = "sha256:39d44f0d8f466bd56698e7ead708bead3141e27b9b87e3ab7d5a6d0e4a869ee5", size = 2438369, upload-time = "2025-11-05T19:07:08.1Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/99/76b7c80252aa83c1af16393454aafd125a0287101afe8deb0a6821af0e30/opencv_python_headless-5.0.0.93.tar.gz", hash = "sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c", size = 81817738, upload-time = "2026-07-02T07:01:06.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7c/8c8097891c509d98cd128493835c95631c80be6a8f37ed9d25716c2e16f1/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f", size = 48322581, upload-time = "2026-07-02T05:50:34.207Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/eab2ad388c3cbab2a350c10c2ef19ce6bd099240afc31789032c996bab52/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00", size = 34782894, upload-time = "2026-07-02T05:51:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/ec/78/afca939f40ffe2b2380bfa86f812b2f7d4acc5a27b27dc41b49cad7ce7b4/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f", size = 36521085, upload-time = "2026-07-02T06:55:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/2b/97/8170e9819764c47e436c130d3ff6cfb73b58f923eae9d3a03d8982b04aec/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4", size = 56563598, upload-time = "2026-07-02T06:55:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/1a28a7101e31801042b3098871a74b76c61581d328ef40774ff4edb53a56/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4", size = 39648433, upload-time = "2026-07-02T06:56:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/9b/21/f6ef335f6e65724aa78b8d792b48d40a48c381715f1e62f5a5049e09d07e/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37", size = 61204038, upload-time = "2026-07-02T06:56:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8f/b8756467ea991449a293797f6b3fa80fcfdd29598a0a60d1cd5715b96e61/opencv_python_headless-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9", size = 35411237, upload-time = "2026-07-02T05:50:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/b8/88/763b967f7efd7226b82c9fae16d560cba049b1f0c036647e65c610fd636e/opencv_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e", size = 43825962, upload-time = "2026-07-02T05:50:09.627Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/45/7af37fe54e5d3e66e7dcd7ba8b8aeee73f202bfac909cc94b8c4e428f9ac/opentelemetry_exporter_otlp-1.44.0.tar.gz", hash = "sha256:af1cde7c33ea8ed624bf04ac49a885730fe44c1f1ad698656e592c38f70ce106", size = 6090, upload-time = "2026-07-16T15:25:34.585Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c3/7b466a9463944e70b37b744072a0c1b88a425dade3fff0631adec66c9bcc/opentelemetry_exporter_otlp-1.44.0-py3-none-any.whl", hash = "sha256:4a498fa8d8fd8be9e8e2d175fe5524a3fe581ccffadd8509db86526a5fb97051", size = 6727, upload-time = "2026-07-16T15:25:14.445Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463", size = 27225, upload-time = "2026-07-16T15:25:38.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e", size = 19624, upload-time = "2026-07-16T15:25:19.096Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, +] + +[[package]] +name = "outlines-core" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/04/4a0812eb27c086cfd2e66e7ec9150f33e105912a9b7f8b335e3479f03a06/outlines_core-0.2.14.tar.gz", hash = "sha256:64808deed1591ca3029ff64346ceb974cd5d780c916ea82504951fe83523039e", size = 191539, upload-time = "2026-01-09T15:59:10.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/93/30b9188648a479b32be429a24166db47a7bfdb0f9a8aac4c6dcf569e0a52/outlines_core-0.2.14-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:95e6476d9702d2fcc4e85370dbbfb6933a46c816e9c90107f6ce36eb68b5d64a", size = 2049651, upload-time = "2026-01-09T15:58:28.549Z" }, + { url = "https://files.pythonhosted.org/packages/0d/06/f3557daa8e87d5b95f64de269a301d73ec3c2202ab897c3e1f1cb93eb1db/outlines_core-0.2.14-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f04731a5e29a190e2cc9f692a1f3fb2414a645355ca7d01b83df43439c38bea8", size = 2201046, upload-time = "2026-01-09T15:58:29.958Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/d8acf778990964c951080d568284e858d466f27dfd6f2674781927faba1c/outlines_core-0.2.14-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:0e4c69f0a8565edb56464c4c9b6c291a10805f3a96dff84182980e90ae1a5e2f", size = 2049558, upload-time = "2026-01-09T15:58:31.003Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/0320b14b49b8379ced1ab195ecf5875dbd2267b90148847541f43bfde6c1/outlines_core-0.2.14-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:63f53cfd9614e754499ae86dd699f3abcecf42d6a4e58d80fd80347881d85960", size = 2197854, upload-time = "2026-01-09T15:58:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/29/29/3a04944407207a5d214879ca5ca33c2bd3e65199a4e927051c1bdaaa4d50/outlines_core-0.2.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bb2060c240c4507f334965a8948dbeeb22007560d797f6debd92346c0b620cb", size = 2341426, upload-time = "2026-01-09T15:58:33.553Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/a77f746272504bac3f628047d56ea1731b61549a3e1d9bbfd226f2968246/outlines_core-0.2.14-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1de34681c7e0e7e1551fc9036e4fa3c57986336c905a10536591ceb6d869c258", size = 2236941, upload-time = "2026-01-09T15:58:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/9f599d938923ab8ceeff26fdf2f9ea53bea3c962085c4927a08338a32349/outlines_core-0.2.14-cp312-cp312-win32.whl", hash = "sha256:870e8e038853818cb202ccc8cde92251f300f96805bfcc3be1c883adda7b5297", size = 1842940, upload-time = "2026-01-09T15:58:36.544Z" }, + { url = "https://files.pythonhosted.org/packages/f8/df/0f145c52ebd156d80273e2f5278227ea57e0275b2aa863bed33f44f77923/outlines_core-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:87b42440478764cce1353a87d8560ef82f3b39b9d753bfe93195ea3584f369e3", size = 2137266, upload-time = "2026-01-09T15:58:37.831Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "partial-json-parser" +version = "0.2.1.1.post7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/f4/cdcebf7094b03b99fba71ac8f56bd6f227973642662f49d272332d8419b3/prometheus_fastapi_instrumentator-8.1.0.tar.gz", hash = "sha256:b77f3043665e8d28e2bbd21017506195a43d9adf1d402d01bf95b494b7e560e1", size = 20492, upload-time = "2026-07-26T11:12:44.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b9/91a2246e6cf01b7ccb14479803c8a50f9c258ae5c6a0f16ba3294820632b/prometheus_fastapi_instrumentator-8.1.0-py3-none-any.whl", hash = "sha256:b9f40b2cff3f7891ca0610b3ae4fc6ec723fd326b04bb659819aaeb821a0fc7d", size = 19649, upload-time = "2026-07-26T11:12:45.168Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/3e/29e0d6a2c5adde6ab5772253fd16ab346324026b89a66e354689c86d0584/proto_plus-1.28.2.tar.gz", hash = "sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501", size = 58063, upload-time = "2026-07-22T16:28:29.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/84/4e9a53a062d4073c74897a6bd20fff74d55307341b3e85c081002462b3ef/proto_plus-1.28.2-py3-none-any.whl", hash = "sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52", size = 50693, upload-time = "2026-07-22T16:28:24.059Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, +] + +[[package]] +name = "pycountry" +version = "26.2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[package.optional-dependencies] +pycountry = [ + { name = "pycountry" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/11/767522582afab1b884d277de0e6e011640cb9d7292a38694b4b1a1df1ae8/pyelftools-0.33.tar.gz", hash = "sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f", size = 15068655, upload-time = "2026-05-29T12:56:22.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2a/f9697576603dae937727827505a6126a066affb227034e77e6f9068910da/pyelftools-0.33-py3-none-any.whl", hash = "sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036", size = 201178, upload-time = "2026-05-29T12:56:20.587Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, +] + +[[package]] +name = "quack-kernels" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "einops" }, + { name = "nvidia-cutlass-dsl" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/94/ee76e3a3dc74d986b7b24c5928f1d14b01bd5152375688c2ede369f6d19b/quack_kernels-0.5.0.tar.gz", hash = "sha256:c7c7338b67243397b6ca166e648bba161076e99f3858b532e1c877dcc6eaa03d", size = 366426, upload-time = "2026-05-29T05:00:25.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/2b/a8f171d5e172880885571bf89e93204aaf231a0e92c4c84714eaf18c271a/quack_kernels-0.5.0-py3-none-any.whl", hash = "sha256:08821ebfb8e638cc20308d5c59410c6dbb3b637ccc7b07bd57c7a9261a06af74", size = 327709, upload-time = "2026-05-29T05:00:24.679Z" }, +] + +[[package]] +name = "ray" +version = "2.56.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d9/a17feef16a123f5d32d4c5fa7de853c59ba702f8404cf452a7ce20faca13/ray-2.56.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:44bc0000c5bfad85b2ff6e0ef91e95f901d1a2d2fdd72f94f08a046eb494cd61", size = 66346989, upload-time = "2026-07-17T21:28:39.4Z" }, + { url = "https://files.pythonhosted.org/packages/05/19/c3b1bcccd09decaf2a2e3370041ae67070bb1a8638f2665d36edfbb0261d/ray-2.56.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:8fdd6b096215906cf1f9acdc7898c9d6140606f2d27245778b8385a9f19e6cb0", size = 73319289, upload-time = "2026-07-17T21:28:44.502Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7f/577a61bf2c8eff26e942afe53a6b03f4dbf5b4f233b832c228417d0c954e/ray-2.56.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:e5d3173696831134c76bd09451dfe95c32d72c271253b0d6b09d2df9994aa660", size = 74194147, upload-time = "2026-07-17T21:28:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/00/e9/0fd1223597f9ca98ec496ec726043918a5301547789b1be95a635ec82649/ray-2.56.1-cp312-cp312-win_amd64.whl", hash = "sha256:8052573ee5ef8c4fdd7aeb6a257c80542e69c48f3f6d117101f95c970ffdc7e2", size = 28373294, upload-time = "2026-07-17T21:28:55.122Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/3a/a258c2fbc6c6bdf428611388f5698ba5d57ffdf0755e1cab474d9cc47813/rich_toolkit-0.20.3.tar.gz", hash = "sha256:223dd2cfba325ed55e94933b9e53f3aca13e9fdf76622bd564c18109a2273c1b", size = 205355, upload-time = "2026-07-13T14:38:06.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/ce/639d0d0ce3d25c5edbd1afecd308bb35dc04883a45ac9f0855c8aee4e919/rich_toolkit-0.20.3-py3-none-any.whl", hash = "sha256:419aa87516d5f3849cca553c6dcf707c02a36d508fcf996946606725d34a3002", size = 36195, upload-time = "2026-07-13T14:38:05.687Z" }, +] + +[[package]] +name = "rignore" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/77/6ba90ab4a538d3ec244329c57c4d26a78c8313ea6fa72c8768d46f11c1c9/rignore-0.8.0.tar.gz", hash = "sha256:2e5ad6b19834f04a877d26fe863fd77ed851ed4019fdca097fb1b744311e3562", size = 55358, upload-time = "2026-07-17T19:01:21.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7e/0d270c1ed723b82bea8ccd504185acb5d71830975260e8de02af7acff728/rignore-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a5e285ec58b3b66284a7f48805e4db0ea948370deb484a4935b147187ecf1e25", size = 847184, upload-time = "2026-07-17T18:58:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/ff/dc/841941f8b0883a8038f9d540607238c456df20e98243a61142fd15699806/rignore-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:367d3cf401b477a5ba7eb05b5b94c491b0d704507d9eaf80378a8d843fe00674", size = 816590, upload-time = "2026-07-17T18:58:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/57/53/9e047a6cd95b553519703350f7a3530ddd31d309c9ade1ec913db5882f74/rignore-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d49870c032abcc28db2210ad92b3bd5706ec792ab25c9295a6c536a4b92226ce", size = 884130, upload-time = "2026-07-17T18:58:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/ae444f30dfa47716ccadf9ed9512736494c0bf5222f330845a637f1c569c/rignore-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50632823c273ee19212fd939b04e9b55d5a6e0c9b998fadef6dc9a0c2f6aecc", size = 857230, upload-time = "2026-07-17T18:58:35.32Z" }, + { url = "https://files.pythonhosted.org/packages/ba/67/b2ddfbf5a42a8ea89e6a0330189f7f76f9113e0d8c6a4c68b68ff68f139c/rignore-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3337e69856ba18c9d079e0ce8b342dc256f48585ae3e1c7aac67682b672e83a", size = 1133331, upload-time = "2026-07-17T18:58:36.895Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/291be16f7260ac87dedab41394f7738834bb1cd05d6e1b878eea8d598520/rignore-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1264c48aaff09c6431b428144e9e9fbd0d7864673f7bd752b51d2409971275e", size = 912520, upload-time = "2026-07-17T18:58:38.178Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/cd0df2def95ecfa93c781431ce7c58ea52f90742550ff327ed0f1f715f14/rignore-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8912e3cb4034b972ba75c9ecabb79bc41a1076cdf726e53019e45b71b82da56", size = 927109, upload-time = "2026-07-17T18:58:40Z" }, + { url = "https://files.pythonhosted.org/packages/e4/58/ce8af7214b903a2f86c3223dabd23e03a0c499df6559be5c9e2b15b4344c/rignore-0.8.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:43b20be0f1c8ae3f3a2575dbee4a38bd406608855f6e79cde8fda1c6bff9e203", size = 892314, upload-time = "2026-07-17T18:58:41.391Z" }, + { url = "https://files.pythonhosted.org/packages/47/3f/e8f4ec6cdf04d0cb9915590679771b55a6f579a7478704d1e6be45a2f792/rignore-0.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02cf38d96b6f54b69719b726877e308b34ac2c9c9f636dc8872ede2ff6825579", size = 962528, upload-time = "2026-07-17T18:58:42.736Z" }, + { url = "https://files.pythonhosted.org/packages/58/57/d10a43221644177f776d658ed9a03204e49d9bfd6805fbece841ea6720ac/rignore-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5c820d3fa597ee419f3643f8c098a51249a868ca8515296a56a1505c01a8083d", size = 1060799, upload-time = "2026-07-17T18:58:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0e/d769dfa933861dd469c9158ffc9bc2cb3bc6d46f88000fb6f41cf2905a3a/rignore-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e3e863b2cb1db481384bd43a25730ef00f7eccdd49c82a0e1d481a6412dc2653", size = 1132462, upload-time = "2026-07-17T18:58:45.583Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3d/57fc6264ebf8d9b95850589899e6192d9f92b67ea48e56fc32969b75474a/rignore-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90cc363aedc7a93b4b15933f2104bfcae1c6c1635a9bc479665cae83044577d9", size = 1139679, upload-time = "2026-07-17T18:58:46.928Z" }, + { url = "https://files.pythonhosted.org/packages/fa/be/24b12a8464e19d348aeb388267f897ede0fae6618052c3148b747acae214/rignore-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:303a9fd02d3612d15e6dc3474ae7d3eae1d07b668b7b4943fd4df305be4d2f76", size = 1138125, upload-time = "2026-07-17T18:58:48.42Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8d/89f7cba3491164c04f8a64056ca973494536ffdf48bfba41565f54dfa122/rignore-0.8.0-cp312-cp312-win32.whl", hash = "sha256:e17a0914378fa15d1e29effce4b39fd2031f253e52d307ff4b71c443d1d5d30e", size = 637663, upload-time = "2026-07-17T18:58:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ff/823a5ead8bba0a2054cbf2dbef99989204557904ad8578f514bec1b9b4e7/rignore-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:d5c8f1dff84c4d4114f1553564b5d909a0c4c09dbe1b5916a0506d719f1fc3b5", size = 728265, upload-time = "2026-07-17T18:58:51.028Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ce/73c919505f9f270ee3e34205ff2dbe0b0d73e945f8c569922acff148bcf4/rignore-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cdea3f85de8286a38ae75a0f9092cd3afc4d33ce6ee2e3f6005f97f7da9d249", size = 665216, upload-time = "2026-07-17T18:58:52.308Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, +] + +[[package]] +name = "runai-model-streamer" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanize" }, + { name = "numpy" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/55/ebf29e73c7c4a66d093e0018b212ef8ccc828231f37c3362edda2535ecb3/runai_model_streamer-0.16.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:035cc282935d7d55032ce2da97507b3d94201e647d622bd44476a6ef9f04f824", size = 649145, upload-time = "2026-07-13T07:51:59.982Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3d/32656788027d72d6bb54804a1479429ad1ccb36d212ff408c4f513bdd4a5/runai_model_streamer-0.16.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9958ecdef6786347b855f345d3076a039201f6f2c0e22dd52c74eef8941430", size = 650434, upload-time = "2026-07-13T07:52:01.341Z" }, +] + +[package.optional-dependencies] +azure = [ + { name = "runai-model-streamer-azure" }, +] +gcs = [ + { name = "runai-model-streamer-gcs" }, +] +s3 = [ + { name = "runai-model-streamer-s3" }, +] + +[[package]] +name = "runai-model-streamer-azure" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/13/16cf17e039ce7f7bc93a32c87876917a6cd946d668a3195990ce9f1494d3/runai_model_streamer_azure-0.16.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:77e00e797852167d9036c36a952a6cb46cabbb7d78ddf30a159ef424803f1d20", size = 5905985, upload-time = "2026-07-13T07:52:17.805Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/39c97a540199a0f48a522bf44e5cb8bcc4e9f5a6bd1f051b14677c8c8fec/runai_model_streamer_azure-0.16.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:3c66172e3c22405d68cb47bccf022d7cab299f1571400ade684771b860420483", size = 5627580, upload-time = "2026-07-13T07:52:19.411Z" }, +] + +[[package]] +name = "runai-model-streamer-gcs" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "google-cloud-storage" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f5/347ae314c8a76c76976c7edfc82764e1fefc9c75b393cc16c492e96dcfb0/runai_model_streamer_gcs-0.16.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4a536d2058c025a4452afa2cf869514ccd77d8b1437edd7b5edb9f881fc8ca25", size = 23302179, upload-time = "2026-07-13T07:52:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ca/b34853e30bab59285d7b742af3d5e840213ebea816187b14d0b142c23b4f/runai_model_streamer_gcs-0.16.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:295f1af31452f47c32ae02d07787ab0922393971988070c0b9c618528d8ac8ec", size = 23249177, upload-time = "2026-07-13T07:52:13.385Z" }, +] + +[[package]] +name = "runai-model-streamer-s3" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/c9/36d39711463f48ce753e8fb7e21facff97a9789773a8f7d67f25897bed7d/runai_model_streamer_s3-0.16.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b7ac88beda87b8f2864e8666c4b8974a0c1e886f9d307ae9de6fbfe18e3b9253", size = 6189831, upload-time = "2026-07-13T07:52:04.955Z" }, + { url = "https://files.pythonhosted.org/packages/58/40/275cf91d178abfc6e1feda301b7bc4ac85d8dbafeebbd77ffdb370938cf6/runai_model_streamer_s3-0.16.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:fb225b5dfbecf8087fe9073f5eb6916b56d19b7f7b49625269053f7205d7c0fd", size = 5931724, upload-time = "2026-07-13T07:52:06.589Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/33/ea3cb3839607eb175da835244a798f797f478c5ddf0e8ecdf57ea85a4c70/sentencepiece-0.2.2.tar.gz", hash = "sha256:3d2b5e824b5622038dc7b490897efe05ebbbb9e7350fc142f3ecc8789ef9bdf6", size = 8218435, upload-time = "2026-07-12T08:39:34.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/13/7a562289c8d5b49ebdf3f9c1e8ab67cf14a8743b1d90c8f406bfdec36b72/sentencepiece-0.2.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1edb10e520e4bddf74d85b0f5ae74cc2d60c2b448885080bfb618bc2b3a49f6b", size = 2188384, upload-time = "2026-07-12T08:38:28.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/d1/912f14fd5eae168aba726ffb6a9a2dc1c71fe7676c53da6f5c442b886d4a/sentencepiece-0.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7c06c751c19d923435a54bff4f7e66e728fad160e8da28254f133abc9725820", size = 1441553, upload-time = "2026-07-12T08:38:30.552Z" }, + { url = "https://files.pythonhosted.org/packages/bd/44/caa9cab5f261a019e2808bc5046152775dc57352ba9cbae7525e9e7a1ed4/sentencepiece-0.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38111ed1f79268f399c505028023d5eaaf0ab4e5eafceb709468b0d3323e7838", size = 1347176, upload-time = "2026-07-12T08:38:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/cd798935668cff71d309d8ff10385844ecf216b1fe454f1993ed8bf2cb91/sentencepiece-0.2.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbce24284f51f71d10a42b7b9c964dcb9048b28f1c8e5db40bcbcb6f428cba6a", size = 1325200, upload-time = "2026-07-12T08:38:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/37e3da037318a70066ded0d51bc2a7f35491ae6338dd993d5eb1503fc3b5/sentencepiece-0.2.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8a168b040bc61681293f79a949b5d911c8e25086f4260285b8d97ab5f1195da", size = 1397736, upload-time = "2026-07-12T08:38:35.771Z" }, + { url = "https://files.pythonhosted.org/packages/8d/11/753fca2e6b109be3ab7867abf357dfe48677fe726ae5a5363d0b54ca9450/sentencepiece-0.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:7c6e7bf684dc12145bfa685d3060beaea55139134ba848289bee514ed42e7383", size = 1248030, upload-time = "2026-07-12T08:38:37.604Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0a/70efbe861ca182d7d4b6e1a20f58e043400848fa9f2915229f082e221648/sentencepiece-0.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:76ff5814db72e7462dece042d7593cdf102b8ec82c2b1cc201a2add34ee3050d", size = 1187325, upload-time = "2026-07-12T08:38:39.348Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.66.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/f0/2dc88e842077719d7384d86cc47403e5102810492b33680e7dadcee64cd8/setproctitle-1.3.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2dc99aec591ab6126e636b11035a70991bc1ab7a261da428491a40b84376654e", size = 18049, upload-time = "2025-09-05T12:49:36.241Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b4/50940504466689cda65680c9e9a1e518e5750c10490639fa687489ac7013/setproctitle-1.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdd8aa571b7aa39840fdbea620e308a19691ff595c3a10231e9ee830339dd798", size = 13079, upload-time = "2025-09-05T12:49:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/71630546b9395b095f4082be41165d1078204d1696c2d9baade3de3202d0/setproctitle-1.3.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2906b6c7959cdb75f46159bf0acd8cc9906cf1361c9e1ded0d065fe8f9039629", size = 32932, upload-time = "2025-09-05T12:49:39.271Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/cee06af4ffcfb0e8aba047bd44f5262e644199ae7527ae2c1f672b86495c/setproctitle-1.3.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6915964a6dda07920a1159321dcd6d94fc7fc526f815ca08a8063aeca3c204f1", size = 33736, upload-time = "2025-09-05T12:49:40.565Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/a5949a8bb06ef5e7df214fc393bb2fb6aedf0479b17214e57750dfdd0f24/setproctitle-1.3.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cff72899861c765bd4021d1ff1c68d60edc129711a2fdba77f9cb69ef726a8b6", size = 35605, upload-time = "2025-09-05T12:49:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/50caca532a9343828e3bf5778c7a84d6c737a249b1796d50dd680290594d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7cb05bd446687ff816a3aaaf831047fc4c364feff7ada94a66024f1367b448c", size = 33143, upload-time = "2025-09-05T12:49:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ca/14/b843a251296ce55e2e17c017d6b9f11ce0d3d070e9265de4ecad948b913d/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3a57b9a00de8cae7e2a1f7b9f0c2ac7b69372159e16a7708aa2f38f9e5cc987a", size = 34434, upload-time = "2025-09-05T12:49:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b7/06145c238c0a6d2c4bc881f8be230bb9f36d2bf51aff7bddcb796d5eed67/setproctitle-1.3.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d8828b356114f6b308b04afe398ed93803d7fca4a955dd3abe84430e28d33739", size = 32795, upload-time = "2025-09-05T12:49:46.419Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/ef76a81fac9bf27b84ed23df19c1f67391a753eed6e3c2254ebcb5133f56/setproctitle-1.3.7-cp312-cp312-win32.whl", hash = "sha256:b0304f905efc845829ac2bc791ddebb976db2885f6171f4a3de678d7ee3f7c9f", size = 12552, upload-time = "2025-09-05T12:49:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5b/a9fe517912cd6e28cf43a212b80cb679ff179a91b623138a99796d7d18a0/setproctitle-1.3.7-cp312-cp312-win_amd64.whl", hash = "sha256:9888ceb4faea3116cf02a920ff00bfbc8cc899743e4b4ac914b03625bdc3c300", size = 13247, upload-time = "2025-09-05T12:49:49.16Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soundfile" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" }, +] + +[[package]] +name = "soxr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/11/27cebce4a108f77afea7c80545115536b45e3f11ebfb914f638fdd9ba847/soxr-1.1.0.tar.gz", hash = "sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44", size = 173067, upload-time = "2026-05-03T00:15:18.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/f3da7973b5f1b05d2d7e94d5376b881dcbc05297900cae6c3d33d95b209b/soxr-1.1.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da", size = 204124, upload-time = "2026-05-03T00:14:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/200013a74641f8774664bbcd2346c695c05c2e300ea792adcb40a293eed0/soxr-1.1.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4", size = 165457, upload-time = "2026-05-03T00:14:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/2e5eba817a762a2ec589ff165b8bc5955b25a0ad140045f7cd8e45410543/soxr-1.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06", size = 206529, upload-time = "2026-05-03T00:14:57.646Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/0e55195893228609c9a08c3b13b7a83a46c3a992cd00d3304f0f320cfb07/soxr-1.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1", size = 240413, upload-time = "2026-05-03T00:14:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/621e4150e4815246ad552d215a8a294a90143fedd19ee442cf82d3b3abc8/soxr-1.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4", size = 174357, upload-time = "2026-05-03T00:15:00.724Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "supervisor" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/b5/37e7a3706de436a8a2d75334711dad1afb4ddffab09f25e31d89e467542f/supervisor-4.3.0.tar.gz", hash = "sha256:4a2bf149adf42997e1bb44b70c43b613275ec9852c3edacca86a9166b27e945e", size = 468912, upload-time = "2025-08-23T18:25:02.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, +] + +[[package]] +name = "tilelang" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "cloudpickle" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "setuptools", marker = "sys_platform == 'darwin'" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "z3-solver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/70/5051f65821baa30a3d61fc48f8ba10c776490315e8c90f82559b92089756/tilelang-0.1.9.tar.gz", hash = "sha256:287f727c913bb648fcf6c1968809ba3390e55eeed257a5c6bb9a80bc05966af4", size = 93395292, upload-time = "2026-04-22T09:19:11.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/db/4dd76da8c8585c605639a21bc098d504e317fe324a72f01ce3c7370250b4/tilelang-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:00ed594fdeb229c5505b9ffa895c3c5daeb28641c78f783fa1f724cf1e08cecd", size = 36599020, upload-time = "2026-04-22T09:14:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/f7/8a/1cbeee79d62abaa02441c2d00621554e41aa62dbf3b94a4feb3867184b01/tilelang-0.1.9-cp38-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bbccfe9035aed775ffafb6dc25a5994504b24e2c5d95d0f39643edfafa7bf12", size = 45419374, upload-time = "2026-04-22T09:15:56.014Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a7/f4bfb86f87e107703146e703204cec2c0eae2492b633e0052b0ace3febb6/tilelang-0.1.9-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:77ab0ee2f40f66ea015b6b21426d482751e28cbc635ef9d1198cbd6502454a7c", size = 42110365, upload-time = "2026-04-22T09:17:18.292Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tokenspeed-mla" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "nvidia-cutlass-dsl" }, + { name = "tokenspeed-triton" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/20/4110d624d81d63f0bee2f19dba7ea0e1d8a31ea50147e6c1db82223c88a4/tokenspeed_mla-0.1.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:592590f36d85e624ecdc5e357ff35e29e761e6d879900dce8b67a6785c8ce75c", size = 743769, upload-time = "2026-05-13T03:30:54.486Z" }, + { url = "https://files.pythonhosted.org/packages/84/01/4bf8b74ead3e8e7c1c809435396254c067a33fde48acc20f602aae622d97/tokenspeed_mla-0.1.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c9466a351fe039792e56cf49f3e79744c1dc28c7af10306a02e62b8e92fa5985", size = 748681, upload-time = "2026-05-13T03:30:56.718Z" }, +] + +[[package]] +name = "tokenspeed-triton" +version = "3.8.10.post20260721" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/44/89740db8951918c9acd8731243eef8b44d0eb92ea423552639265c46018e/tokenspeed_triton-3.8.10.post20260721-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d793ad0eaebb1d08272c97a2b8f2c31304231748b03de9a08e70a362de92a6e0", size = 82966664, upload-time = "2026-07-21T17:14:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/91/53/f46b401e8ec8998f5b9c39cff0614b796bf49113a09f588cfdfa342789a3/tokenspeed_triton-3.8.10.post20260721-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66cba8d32a1539afd0ff3eec1782b082d01b4db6824d68017d1d789a03d0be37", size = 87210295, upload-time = "2026-07-21T17:14:42.173Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", version = "13.0.2", source = { registry = "https://pypi.org/simple" }, extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/67/10d236698525d7b7db4d74ec0a4b01f5b2db33968995fdd9ac6b4635e327/torch_c_dlpack_ext-0.1.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c0f2bd51fcd99c0e5b50314e1985f2728c4941bfa821f065e6c30951d1f995ca", size = 5291237, upload-time = "2026-01-12T11:24:44.011Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e6/7d7a97a3953208d6d6ce749180c34d1dab48464ded9a76cecabe9d021ce6/torch_c_dlpack_ext-0.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:670fbbab70123cc228bed41693a3720757af57a0ad22669063c9db25321e8f55", size = 1482855, upload-time = "2026-01-12T11:24:49.581Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/b1/77658817acacd01a72b714440c62f419efc4d90170e704e8e7a2c0918988/torchaudio-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cf1acc883bee9cb906a933572fed6a8a933f86ef34e9ea7d803f72317e8c1b", size = 684226, upload-time = "2026-03-23T18:13:40.023Z" }, + { url = "https://files.pythonhosted.org/packages/78/28/c7adc053039f286c2aca0038b766cbe3294e66fec6b29a820e95128f9ede/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bc653defca1c16154398517a1adc98d0fb7f1dd08e58ced217558d213c2c6e29", size = 1626670, upload-time = "2026-03-23T18:13:42.162Z" }, + { url = "https://files.pythonhosted.org/packages/88/d8/d6d0f896e064aa67377484efef4911cdcc07bce2929474e1417cc0af18c2/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6503c0bdb29daf2e6281bb70ea2dfe2c3553b782b619eb5d73bdadd8a3f7cecf", size = 1771992, upload-time = "2026-03-23T18:13:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/941277ecc39f7a0a169d554302a1f1afd87c1d94a8aec828891916cea59a/torchaudio-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:478110f981e5d40a8d82221732c57a56c85a1d5895fb8fe646e86ee15eded3bd", size = 328663, upload-time = "2026-03-23T18:13:19.218Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/2a2ba88f325e68a921d8b69ff63b477830b2e73ade9a3c8c8cab2f06d741/transformers-5.14.1.tar.gz", hash = "sha256:60d196c27781eacf8637e2b533f517582907ad6f9ae142046d6b69431a5b2173", size = 9295927, upload-time = "2026-07-16T09:41:57.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/67/8d85ca2323233ae3c0365a659c4e52ee1f587b440e4bc577e7d8e4416d0f/transformers-5.14.1-py3-none-any.whl", hash = "sha256:9db974c4079ede2d1a3ea7ca5a240df33f2cc26fc2b36ba64c5f2a4f43b6e725", size = 11625234, upload-time = "2026-07-16T09:41:54.143Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform == 'darwin'", +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, +] + +[[package]] +name = "vllm" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anthropic" }, + { name = "apache-tvm-ffi" }, + { name = "blake3" }, + { name = "cachetools" }, + { name = "cbor2" }, + { name = "cloudpickle" }, + { name = "compressed-tensors" }, + { name = "depyf" }, + { name = "diskcache" }, + { name = "einops" }, + { name = "fastapi", extra = ["standard"] }, + { name = "fastsafetensors" }, + { name = "filelock" }, + { name = "flashinfer-cubin" }, + { name = "flashinfer-python" }, + { name = "gguf" }, + { name = "humming-kernels", extra = ["cu13"] }, + { name = "ijson" }, + { name = "lark" }, + { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 'x86_64'" }, + { name = "lm-format-enforcer" }, + { name = "mcp" }, + { name = "mistral-common", extra = ["image"] }, + { name = "model-hosting-container-standards" }, + { name = "msgspec" }, + { name = "ninja" }, + { name = "numba" }, + { name = "numpy" }, + { name = "nvidia-cudnn-frontend" }, + { name = "nvidia-cutlass-dsl", extra = ["cu13"] }, + { name = "openai" }, + { name = "openai-harmony" }, + { name = "opencv-python-headless" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, + { name = "outlines-core" }, + { name = "partial-json-parser" }, + { name = "pillow" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "pyzmq" }, + { name = "quack-kernels" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "sentencepiece" }, + { name = "setproctitle" }, + { name = "setuptools" }, + { name = "six" }, + { name = "tiktoken" }, + { name = "tilelang" }, + { name = "tokenizers" }, + { name = "tokenspeed-mla" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, + { name = "xgrammar", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c6/c4dc766b09e93de278693502612de0beba822983d4f609830406ead65cc9/vllm-0.23.0.tar.gz", hash = "sha256:760269db3d9611e12e524681df1bca0977d5d2f5fcb4481cc34d33efc4ae7ff5", size = 36624042, upload-time = "2026-06-13T09:27:24.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/c4/f3b912276de88ccffba1210f0d3ef55a2d3f7fb1b2c88e0a1953568d174c/vllm-0.23.0-2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b597c1c71e8732d751233942675c4d74f623743ac9262a505b256ce2ec97fe05", size = 265954666, upload-time = "2026-06-15T05:11:49.394Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/c66d588cc14f91d020294ecddaefe4ce698abdcb140612feec36a4c0aecd/vllm-0.23.0-2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:872aeb7c36a1ea942216af067ce870ffdf960804e829367e1b4eb36d4831c03c", size = 274070565, upload-time = "2026-06-15T05:12:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5a/93830f6509aef185ddac04e9ce78fa4382d3037ec76ecd18b6455a5a4f4b/vllm-0.23.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a1a534f81f0b62f53d73faa68c73dfae540292ace7f97baf30dbac94fe90f2c", size = 265953967, upload-time = "2026-06-13T09:27:50.84Z" }, + { url = "https://files.pythonhosted.org/packages/72/bc/652f889cde1a20585a0ee0b1b6d36109cd8177bb60020dcb8ff477448440/vllm-0.23.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:71eae985c79ddaa999328cc56d206a1e9b785e079fc6da9e2359ec56ef1c842a", size = 274070208, upload-time = "2026-06-13T09:28:16.037Z" }, +] + +[package.optional-dependencies] +otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] +runai = [ + { name = "runai-model-streamer", extra = ["azure", "gcs", "s3"] }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "xgrammar" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "torch" }, + { name = "transformers" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/f4/e71693d8cec60b7e36dab660784ecc5a6aa51e478a83b556011645c58c87/xgrammar-0.2.3.tar.gz", hash = "sha256:f76423630ae3ac4e090cb38ce1e30e7bcc69b3dee4d22d94353944386a4c6f18", size = 2447704, upload-time = "2026-06-27T04:45:24.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/1c/0cdb22fc799e6d158b3243eeb895ae2e086825487b57767838c98d4864ee/xgrammar-0.2.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:173e167d43a5cf4171eee2be86097decff8803b0a0853d7baaf446c732a7d3a9", size = 23284489, upload-time = "2026-06-27T04:44:23.927Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/994dc6f222189174840c29a1f5b4c175e69dfe13ed2e25b6dbbe9f200a29/xgrammar-0.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aa35f24835a59c822e249ecc80912eea4de03fc8b04afb2f82c8b950a56be6ef", size = 23240027, upload-time = "2026-06-27T04:44:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/e4/79/0bb37937bf847c738c64b64dc50ddc12e7c526b34c5ab82cebe58da5ec8f/xgrammar-0.2.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11255f184971489fc72b948b096e2917f482ba2dca975177f5411562cedb9c6d", size = 44314481, upload-time = "2026-06-27T04:44:28.875Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fd/5ebd5d14b8993cb225151bbb8f2011742fc7a7d94a3bdbc3ec3954b9b62d/xgrammar-0.2.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdf081fab29694302d41d61dcf52fad7d253879a718bc6afc68db0a0dabd7f19", size = 44855110, upload-time = "2026-06-27T04:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/17/66/67239f43b0244f65aec4639f51ab95905db42eb66e532ee2a4e5cdce32de/xgrammar-0.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:e7787dd8321a04f86116b756aa3dadd622e3607a3559b1e986cc5f77da00d68e", size = 15780277, upload-time = "2026-06-27T04:44:34.081Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] + +[[package]] +name = "z3-solver" +version = "4.15.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/8e/0c8f17309549d2e5cde9a3ccefa6365437f1e7bafe71878eaf9478e47b18/z3_solver-4.15.4.0.tar.gz", hash = "sha256:928c29b58c4eb62106da51c1914f6a4a55d0441f8f48a81b9da07950434a8946", size = 5018600, upload-time = "2025-10-29T18:12:03.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/33/a3d5d2eaeb0f7b3174d57d405437eabb2075d4d50bd9ea0957696c435c7b/z3_solver-4.15.4.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:407e825cc9211f95ef46bdc8d151bf630e7ab2d62a21d24cd74c09cc5b73f3aa", size = 37052538, upload-time = "2025-10-29T18:11:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/fd7ffac1551cd9f8d44fe41358f738be670fc4c24dfd514fab503f2cf3e7/z3_solver-4.15.4.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:00bd10c5a6a5f6112d3a9a810d0799227e52f76caa860dafa5e00966bb47eb13", size = 39807925, upload-time = "2025-10-29T18:11:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/c9/bb51a96af0091324c81b803f16c49f719f9f6ea0b0bb52200f5c97ec4892/z3_solver-4.15.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e103a6f203f505b8b8b8e5c931cc407c95b61556512d4921c1ddc0b3f41b08e", size = 29268352, upload-time = "2025-10-29T18:11:53.032Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/0b49f7e4e53817cfb09a0f6585012b782dfe0b666e8abefcb4fac0570606/z3_solver-4.15.4.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:62c7e9cbdd711932301f29919ad9158de9b2f58b4d281dd259bbcd0a2f408ba1", size = 27226534, upload-time = "2025-10-29T18:11:55.59Z" }, + { url = "https://files.pythonhosted.org/packages/26/91/33de49538444d4aafbe47415c450c2f9abab1733e1226f276b496672f46c/z3_solver-4.15.4.0-py3-none-win32.whl", hash = "sha256:be3bc916545c96ffbf89e00d07104ff14f78336e55db069177a1bfbcc01b269d", size = 13191672, upload-time = "2025-10-29T18:11:58.424Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/a0b135e4419df475177ae78fc93c422430b0fd8875649486f9a5989772e6/z3_solver-4.15.4.0-py3-none-win_amd64.whl", hash = "sha256:00e35b02632ed085ea8199fb230f6015e6fc40554a6680c097bd5f060e827431", size = 16259597, upload-time = "2025-10-29T18:12:01.14Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, +] diff --git a/docs/design-docs/dynamo-integration.md b/docs/design-docs/dynamo-integration.md new file mode 100644 index 00000000000..bbe174a1253 --- /dev/null +++ b/docs/design-docs/dynamo-integration.md @@ -0,0 +1,64 @@ +# Managed Dynamo generation design + +The managed Dynamo backend owns a fixed vLLM fleet inside the Ray allocation. +It is deliberately narrower than Dynamo itself: there is no external-runtime, +Kubernetes, DGD, multi-node engine-group, or non-vLLM mode. + +## Ownership and placement + +Constructing `ManagedDynamoRuntime` is inert. Its explicit `start()` method +allocates ports, launches etcd and NATS JetStream, creates one Ray-managed +`dynamo.vllm` process per model-parallel group, and starts the frontend. A +worker group must fit on one node. Its world size is derived from vLLM tensor +parallelism times pipeline parallelism; expert parallelism must be either one +or equal to tensor parallelism. + +Startup completes only after the frontend sees the same fixed membership at +the generation and RL endpoints and advertises the configured model. Worker +handles are recorded before readiness checks so partial startup failures can +be torn down. Shutdown is idempotent and guards the frontend, worker pool, +NATS, etcd, and temporary state independently. + +## Generation state + +Both GRPO trainers use `DynamoGeneration.generate_async()` against the managed +frontend. NeMo-Gym traffic passes through a process-local token wrapper. It +uses the policy tokenizer, preserves caller `nvext.extra_fields`, adds Dynamo +engine metadata, and translates rendered multi-turn prefixes back to the exact +caller token IDs. + +Serialized rollout copies contain only frontend URLs and immutable worker +admin endpoints. They cannot own or stop services. Those endpoints are enough +for AREAL-style post-refit cache invalidation; Magistral keeps its existing +driver-side invalidation lifecycle. + +## Weight refit + +Dynamo uses `CollectiveWeightSynchronizer`. If each engine has world size `E`, +worker `i` starts at rank `training_world_size + i * E`. The policy sender uses +vLLM's peer initialization and its fixed packed-transfer geometry: two 1-GiB +buffers. The isolated vLLM environment validates the same constants before a +worker starts. + +Generation is drained before refit. The worker then runs vLLM's native +`start_weight_update`, `update_weights`, and `finish_weight_update` transaction. +KV-cache invalidation stays outside the generic synchronizer because GRPO's +cache mode determines where it runs. + +## Dependency isolation + +`BUILD_DYNAMO=1` adds a Python 3.12 `/opt/dynamo_venv` to the standard image. +It contains only `ai-dynamo[vllm]==1.3.0.post1`, its pinned vLLM 0.23.0, etcd, +and NATS. NeMo-RL's normal Ray and engine environments are unchanged; the +standard NeMo-RL vLLM environment currently uses vLLM 0.25.1. + +vLLM 0.23.0 predates PR #44814, which fixes layerwise reload accounting for +composed loaders. The installer asserts the exact vLLM version, checks and +applies the backport, and records upstream merge commit +`c9e5bf813530fb9ce06024e075da0f520b0718c8` in +`/opt/dynamo_venv/VLLM_BACKPORTS`. Remove the backport only after Dynamo pins a +vLLM release containing that fix. At that point delete the patch, application +logic, marker assertion, and backport text rather than rebasing the patch. + +See [Managed Dynamo generation on Slurm](../guides/dynamo-generation.md) for +build, configuration, and launch instructions. diff --git a/docs/guides/dynamo-generation.md b/docs/guides/dynamo-generation.md new file mode 100644 index 00000000000..d4c76408729 --- /dev/null +++ b/docs/guides/dynamo-generation.md @@ -0,0 +1,200 @@ +# Managed Dynamo generation on Slurm + +NeMo RL can launch and own Dynamo's control plane, frontend, and a fixed vLLM +worker fleet inside a Slurm-backed Ray allocation. This mode supports direct +GRPO and NeMo-Gym rollouts, NCCL weight refits, cache invalidation, and +`generation_metrics/*` telemetry sent to enabled loggers such as W&B. +See the [Dynamo integration design](../design-docs/dynamo-integration.md) for +the service ownership, startup, and weight-refit architecture. + +This integration is managed and vLLM-only. It does not connect to an external +Dynamo deployment and does not support Kubernetes, DGD, SGLang, TensorRT-LLM, +speculative decoding, quantized generation, or model-parallel engine groups +that span nodes. + +## Build the image + +The normal image is unchanged unless `BUILD_DYNAMO` is set: + +```bash +docker buildx build \ + --build-context nemo-rl=. \ + --build-arg BUILD_DYNAMO=1 \ + --target release \ + --file docker/Dockerfile \ + --tag registry.example.com/nemo-rl:dynamo \ + . +``` + +The opt-in layer installs `ai-dynamo[vllm]==1.3.0.post1` in isolated Python +3.12 under `/opt/dynamo_venv`, along with etcd v3.5.21 and NATS Server v2.11.6. +It does not replace NeMo RL's normal Ray or vLLM dependencies: the standard +NeMo RL vLLM environment currently uses vLLM 0.25.1, while this isolated +Dynamo environment uses Dynamo's vLLM 0.23.0 pin. Both environments pin +`nvidia-nccl-cu13==2.30.7` so their NCCL communicators use the same release. +For a local source checkout, the same environment can be installed under +`venvs/dynamo`: + +```bash +bash docker/dynamo/install.sh +``` + +Set `NEMO_RL_DYNAMO_VENV_DIR` to choose another location. The installer checks +that Dynamo resolved vLLM 0.23.0 and NCCL 2.30.7, applies the vLLM PR #44814 +backport only after `git apply --check`, and writes the upstream marker to +`VLLM_BACKPORTS`. + +Treat the isolated dependency pin and backport as one update. A Dynamo upgrade +must update and reverify these coupled locations: + +- `docker/dynamo/pyproject.toml` and `docker/dynamo/uv.lock`: the Dynamo pin + and resolved dependency set +- root `pyproject.toml` and `uv.lock`, plus the isolated Dynamo project and + lockfile: the `nvidia-nccl-cu13` pins must remain identical +- `docker/dynamo/install.sh` and `tests/functional/grpo_dynamo.sh`: the vLLM + version, backport marker, and runtime assertions +- `docker/dynamo/patches/vllm-0.23.0-layerwise-reload-composed-loader.patch`: + the version-specific #44814 backport +- `tests/unit/distributed/test_stateless_process_group.py`: vLLM's + `broadcast_from/0/0` weight-transfer wire key +- `nemo_rl/models/generation/dynamo/token_wrapper.py`: the real Dynamo response + keys `nvext.engine_data.{prompt_token_ids,completion_token_ids,completion_logprobs}`; + reverify them against real Dynamo output because unit tests validate only the + expected local response shape +- `nemo_rl/models/generation/dynamo/managed_runtime.py`: the managed + `DYN_ENABLE_EXPERIMENTAL_PARSERS_V2=1` setting. Dynamo 1.3.0's legacy tool + jail removes `nvext.engine_data`; remove this setting only after an upgraded + Dynamo preserves the token metadata for `tool_choice=auto` +- this guide and the Dynamo design document: the stated versions and backport + behavior + +If the new Dynamo vLLM pin contains PR #44814, delete the patch file, +patch-application block, marker assertion, and explanatory backport text. Do +not rebase the patch onto the newer vLLM release. + +## Configure Dynamo + +Start with [`examples/configs/grpo_math_1B_dynamo.yaml`](../../examples/configs/grpo_math_1B_dynamo.yaml). +The important boundary is: + +```yaml +policy: + generation: + backend: dynamo + dynamo_cfg: + engine: vllm + frontend_args: + router_mode: kv + vllm_cfg: + tensor_parallel_size: 1 + pipeline_parallel_size: 1 + expert_parallel_size: 1 + colocated: + enabled: false + resources: + gpus_per_node: 1 + num_nodes: 1 +``` + +NeMo RL derives each engine's world size from TP times PP. EP must be one or +equal to TP. Parser settings belong under `dynamo_cfg.worker_args`; inherited +vLLM HTTP-parser settings are rejected with the corresponding Dynamo field. +Service ports and the namespace are runtime-owned rather than public config. + +`vllm_cfg` settings are handled in five explicit classes: + +| Class | Behavior | Examples | +| --- | --- | --- | +| Translated | Forwarded to `dynamo.vllm` | TP, PP, EP, dtype, model length | +| Moved | Startup error naming the Dynamo replacement | tool and reasoning parsers, HTTP serving chat kwargs | +| Unsupported | Warning when active, or an error when it requests unsupported low precision | tokenizer skipping, MX and mixed BF16/FP8 helpers | +| Managed runtime | Consumed or enforced by NeMo RL rather than forwarded | HTTP-wrapper enablement, metrics sampling, processed rollout logprobs | +| Inapplicable | Ignored because the managed path owns that behavior | async mode, progress bars, NeMo RL HTTP/ZMQ refit ports | + +The shared GRPO base config also supplies `mcore_generation_config` and +`refit_cfg`. Dynamo accepts these inherited sections but does not use them; +worker arguments come from `vllm_cfg`, and refit uses the collective weight +synchronizer. + +Enable and filter worker telemetry with both managed configuration sections: + +```yaml +policy: + generation: + vllm_cfg: + enable_vllm_metrics_logger: true + vllm_metrics_logger_interval: 1.0 + dynamo_cfg: + metrics_include_prefixes: null # null selects the curated defaults + metrics_exclude_prefixes: null # null excludes python_ and process_ +``` + +The NCCL sender also selects vLLM's peer protocol: the policy publishes both +the raw NeMo RL unique ID and vLLM's pickled `ncclUniqueId`, then uses the +all-reduce warmup expected by `PyNcclCommunicator`. This protocol choice and +the packed 1-GiB/two-buffer geometry come from the generation backend rather +than GRPO-specific branches. + +The fixed port layout is: + +- `1313-1399`: driver-local etcd and NATS control plane +- `3000-3999`: frontend and token-wrapper HTTP endpoints +- `4000-4099`: node-local `DYN_SYSTEM_PORT` +- `7000 + slot * 100`: node-local vLLM rendezvous ports + +## Run the two-GPU smoke + +Convert the image to the format required by the Slurm site, then submit from +the repository root: + +```bash +export CONTAINER=/shared/images/nemo-rl-dynamo.sqsh +export MOUNTS="$PWD:$PWD" +export GPUS_PER_NODE=2 +export BASE_LOG_DIR="$PWD/results/dynamo-smoke/logs" +printf -v COMMAND '%q ' \ + /opt/nemo_rl_venv/bin/python -u "$PWD/examples/run_grpo.py" \ + --config "$PWD/examples/configs/grpo_math_1B_dynamo.yaml" +export COMMAND + +sbatch \ + --nodes=1 \ + --gres=gpu:2 \ + --exclusive \ + --account= \ + --partition= \ + ray.sub +``` + +The recipe assigns one GPU to training and one to a TP1 Dynamo worker. Its two +steps exercise generation, refit, post-refit cache invalidation, telemetry, +and cleanup. For a matched control, run the same seed/model/batch settings with +the standard non-colocated vLLM backend and compare post-refit output validity. + +## Run SWE1 with W&B + +The three-node nightly recipe targets +`nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16`. It uses two 8-GPU training +nodes and one 8-GPU inference node. The inference node runs two TP4/EP4 Dynamo +engines. Download the standard SWE1 split under +`${HF_HOME}/superv3_data/swe1`, then run the registered test-suite driver: + +```bash +HF_HOME=/shared/huggingface \ +WANDB_API_KEY= \ +bash tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh +``` + +A successful acceptance run completes four training steps, produces valid +generations after refit, and records worker timelines under +`generation_metrics/*` in TensorBoard and W&B. + +## Operational notes + +- The driver owns all services. Do not start a separate etcd, NATS, frontend, + or worker fleet for this mode. +- Startup validates fixed worker membership; a dead or replaced worker fails + refit instead of serving mixed model versions. +- Shutdown is idempotent and terminates whole subprocess groups, including + partial-startup failures. +- Fault tolerance and a multi-controller architecture remain follow-up work. diff --git a/docs/index.md b/docs/index.md index 8f673f02f24..7c38f0f6612 100644 --- a/docs/index.md +++ b/docs/index.md @@ -79,6 +79,13 @@ Learn how to evaluate your models using built-in evaluation datasets and custom Configure and launch NeMo RL on multi-node Slurm or Kubernetes clusters for distributed computing. ::: +:::{grid-item-card} {octicon}`workflow` Managed Dynamo Generation +:link: guides/dynamo-generation +:link-type: doc + +Run a fixed Dynamo vLLM fleet with NCCL refit and W&B telemetry inside a Slurm Ray allocation. +::: + :::: ## Guides and Examples @@ -326,6 +333,7 @@ guides/yarn-long-context.md guides/xtoken-off-policy-distillation.md guides/refit.md guides/checkpoint-engine-refit.md +guides/dynamo-generation.md guides/router-replay.md guides/muon-optimizer.md guides/dtensor-tp-accuracy.md @@ -360,6 +368,7 @@ design-docs/uv.md design-docs/dependency-management.md design-docs/chat-datasets.md design-docs/generation.md +design-docs/dynamo-integration.md design-docs/sparse-delta-refit.md design-docs/checkpoint-engines.md design-docs/checkpointing.md diff --git a/examples/configs/grpo_math_1B_dynamo.yaml b/examples/configs/grpo_math_1B_dynamo.yaml new file mode 100644 index 00000000000..c238ad74c00 --- /dev/null +++ b/examples/configs/grpo_math_1B_dynamo.yaml @@ -0,0 +1,99 @@ +# Two-step, two-GPU smoke for the Ray-managed Dynamo vLLM backend. +defaults: grpo_math_1B.yaml + +grpo: + num_prompts_per_step: 2 + num_generations_per_prompt: 2 + max_num_steps: 2 + val_period: 0 + val_at_start: false + val_at_end: false + async_grpo: + enabled: true + in_flight_weight_updates: false + recompute_kv_cache_after_weight_updates: true + +loss_fn: + use_importance_sampling_correction: true + truncated_importance_sampling_type: tis + truncated_importance_sampling_ratio: 2.0 + +checkpointing: + enabled: false + +policy: + train_global_batch_size: 4 + train_micro_batch_size: 1 + logprob_batch_size: 1 + max_total_sequence_length: 512 + sequence_packing: + enabled: false + dtensor_cfg: + _v2: false + enabled: true + cpu_offload: false + activation_checkpointing: false + megatron_cfg: + enabled: false + generation: + backend: dynamo + max_new_tokens: 128 + temperature: 0.7 + top_p: 0.95 + top_k: 50 + dynamo_cfg: + engine: vllm + startup_timeout_s: 600 + request_timeout_s: 900 + control_timeout_s: 600 + metrics_include_prefixes: null + metrics_exclude_prefixes: null + worker_args: + tool_call_parser: null + reasoning_parser: null + exclude_tools_when_tool_choice_none: true + enable_structural_tag: false + structural_tag_scope: auto + structural_tag_schema: auto + custom_jinja_template: null + endpoint_types: [chat, completions] + extra_cli_args: [] + frontend_args: + tokenizer: default + tokenizer_cache: false + tokenizer_cache_bytes: 52428800 + router_mode: kv + router_reset_states: true + extra_cli_args: [] + vllm_cfg: + async_engine: true + tensor_parallel_size: 1 + pipeline_parallel_size: 1 + expert_parallel_size: 1 + precision: bfloat16 + gpu_memory_utilization: 0.6 + max_model_len: ${policy.max_total_sequence_length} + kv_cache_dtype: auto + enforce_eager: true + expose_http_server: false + enable_vllm_metrics_logger: true + vllm_metrics_logger_interval: 1.0 + env_vars: + NCCL_MNNVL_ENABLE: "0" + vllm_kwargs: + max_num_seqs: 16 + colocated: + enabled: false + resources: + gpus_per_node: 1 + num_nodes: 1 + +logger: + log_dir: results/grpo-math-1b-dynamo + wandb_enabled: false + tensorboard_enabled: true + monitor_gpus: false + +cluster: + gpus_per_node: 2 + num_nodes: 1 diff --git a/examples/configs/recipes/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.yaml b/examples/configs/recipes/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.yaml new file mode 100644 index 00000000000..c7f02e8d3e8 --- /dev/null +++ b/examples/configs/recipes/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.yaml @@ -0,0 +1,184 @@ +defaults: ../../../nemo_gym/grpo_nanov3.yaml +cluster: + num_nodes: 3 + segment_size: 1 +checkpointing: + enabled: false + checkpoint_dir: results/nemotron-3-nano-swe-dynamo + save_period: 3 + keep_top_k: 1 + checkpoint_must_save_by: 00:03:30:00 + model_save_format: safetensors + save_consolidated: false +grpo: + num_prompts_per_step: 2 + num_generations_per_prompt: 2 + val_num_generations_per_prompt: 2 + max_num_epochs: 4 + max_num_steps: 4 + advantage_clip_low: -100 + advantage_clip_high: 100 + val_period: 10000 + overlong_filtering: false + invalid_tool_call_advantage: -5.0 + malformed_thinking_advantage: -5.0 + reward_shaping: + overlong_buffer_length: 128 + overlong_buffer_penalty: 1 + max_response_length: ${policy.max_total_sequence_length} + stop_properly_penalty_coef: null + reward_scaling: + source_min: 0.0 + source_max: 1.0 + target_min: 0.0 + target_max: 1.0 + async_grpo: + enabled: true + in_flight_weight_updates: false + recompute_kv_cache_after_weight_updates: true +loss_fn: + truncated_importance_sampling_ratio: 5 + truncated_importance_sampling_ratio_min: 0.2 + truncated_importance_sampling_type: tis + force_on_policy_ratio: true + use_kl_in_reward: false +policy: + model_name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + tokenizer: + name: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 + chat_template_kwargs: + enable_thinking: true + hf_config_overrides: {} + train_global_batch_size: 4 + generation_batch_size: 2 + max_total_sequence_length: 196608 + logprob_chunk_size: 1024 + megatron_cfg: + empty_unused_memory_level: 2 + tensor_model_parallel_size: 4 + pipeline_model_parallel_size: 1 + context_parallel_size: 2 + moe_flex_dispatcher_backend: hybridep + moe_hybridep_num_sms: 32 + use_gloo_process_groups: false + use_fused_weighted_squared_relu: true + do_not_average_loss: true + cp_normalize: true + calculate_per_token_loss: true + scale_loss_by_dp_cp_size: false + mtp_loss_scaling_factor: 0.0 + mtp_use_repeated_layer: true + mtp_num_layers: 0 + mtp_detach_heads: true + scheduler: + override_opt_param_scheduler: true + distributed_data_parallel_config: + overlap_grad_reduce: false + fp8_cfg: + enabled: false + fp8: e4m3 + fp8_recipe: mxfp8 + fp8_param: false + checkpoint: + ckpt_assume_constant_structure: true + sequence_packing: + fuse_loss: true + generation: + backend: dynamo + dynamo_cfg: + engine: vllm + startup_timeout_s: 1800 + request_timeout_s: 7200 + control_timeout_s: 600 + metrics_include_prefixes: null + metrics_exclude_prefixes: null + worker_args: + tool_call_parser: qwen3_coder + reasoning_parser: nemotron_nano + exclude_tools_when_tool_choice_none: true + enable_structural_tag: false + structural_tag_scope: auto + structural_tag_schema: auto + custom_jinja_template: null + endpoint_types: + - chat + - completions + extra_cli_args: [] + frontend_args: + tokenizer: fastokens + tokenizer_cache: true + tokenizer_cache_bytes: 4294967296 + router_mode: kv + router_reset_states: true + extra_cli_args: [] + vllm_cfg: + expert_parallel_size: 4 + precision: bfloat16 + gpu_memory_utilization: 0.85 + enable_vllm_metrics_logger: true + vllm_metrics_logger_interval: 0.5 + env_vars: null + reasoning_parser_plugin: null + http_server_serving_chat_kwargs: null + vllm_kwargs: + attention_backend: FLASH_ATTN + moe_backend: triton + compilation_config: + cudagraph_capture_sizes: + - 1 + - 2 + - 4 + - 8 + - 16 + - 32 + - 64 + pass_config: + fuse_allreduce_rms: false + colocated: + enabled: false + resources: + gpus_per_node: 8 + num_nodes: 1 +data: + train: + data_path: ${oc.env:HF_HOME}/superv3_data/swe1/train-split.jsonl + validation: + data_path: ${oc.env:HF_HOME}/superv3_data/swe1/val-split.jsonl +env: + nemo_gym: + skip_venv_if_present: true + num_gpu_nodes: 0 + invalid_tool_call_patterns: + - + - + - + - + thinking_tags: + - + - + config_paths: + - responses_api_models/vllm_model/configs/vllm_model_for_training.yaml + - resources_servers/single_step_tool_use_with_argument_comparison/configs/swe_pivot_single_step_tool_use_with_argument_comparison.yaml + policy_model: + responses_api_models: + vllm_model: + chat_template_kwargs: + force_nonempty_content: true + single_step_tool_use_with_argument_comparison_swe: + responses_api_agents: + tool_simulation_agent: + entrypoint: app.py + resources_server: + type: resources_servers + name: swe_pivot_single_step_tool_use_with_argument_comparison_resources_server + model_server: + type: responses_api_models + name: policy_model + use_absolute_ip: true +logger: + log_dir: results/nemotron-3-nano-swe-dynamo + wandb_enabled: true + tensorboard_enabled: true + wandb: + project: nemo-rl + name: grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1 diff --git a/examples/nemo_gym/run_grpo_nemo_gym.py b/examples/nemo_gym/run_grpo_nemo_gym.py index cfa6fa464ba..a9c86a2be48 100644 --- a/examples/nemo_gym/run_grpo_nemo_gym.py +++ b/examples/nemo_gym/run_grpo_nemo_gym.py @@ -37,6 +37,7 @@ grpo_train, refit_policy_generation, setup, + shutdown_environments, ) from nemo_rl.algorithms.utils import get_tokenizer from nemo_rl.data.utils import setup_response_data @@ -266,76 +267,87 @@ def main() -> None: task_to_env = {"nemo_gym": nemo_gym} val_task_to_env = task_to_env - if is_trajectory_collection: - collect_trajectories( - policy=policy, - policy_generation=policy_generation, - val_dataloader=val_dataloader, - tokenizer=tokenizer, - val_task_to_env=val_task_to_env, - logger=logger, - master_config=master_config, - ) - # Check if async mode is enabled - elif config.grpo.async_grpo.enabled: - # Async GRPO does not support dynamic sampling, reward scaling, or reward shaping (DAPO features) - if config.grpo.use_dynamic_sampling: - raise NotImplementedError( - "use_dynamic_sampling is not supported with async GRPO" + try: + if is_trajectory_collection: + collect_trajectories( + policy=policy, + policy_generation=policy_generation, + val_dataloader=val_dataloader, + tokenizer=tokenizer, + val_task_to_env=val_task_to_env, + logger=logger, + master_config=master_config, ) - if config.grpo.reward_scaling.enabled: - raise NotImplementedError("reward_scaling is not supported with async GRPO") - if config.grpo.reward_shaping.enabled: - raise NotImplementedError("reward_shaping is not supported with async GRPO") - - # Async GRPO does not support multiple dataloaders - if config.data["use_multiple_dataloader"]: - raise NotImplementedError( - "use_multiple_dataloader is not supported with async GRPO" + # Check if async mode is enabled + elif config.grpo.async_grpo.enabled: + # Async GRPO does not support dynamic sampling, reward scaling, or reward shaping (DAPO features) + if config.grpo.use_dynamic_sampling: + raise NotImplementedError( + "use_dynamic_sampling is not supported with async GRPO" + ) + if config.grpo.reward_scaling.enabled: + raise NotImplementedError( + "reward_scaling is not supported with async GRPO" + ) + if config.grpo.reward_shaping.enabled: + raise NotImplementedError( + "reward_shaping is not supported with async GRPO" + ) + + # Async GRPO does not support multiple dataloaders + if config.data["use_multiple_dataloader"]: + raise NotImplementedError( + "use_multiple_dataloader is not supported with async GRPO" + ) + + from nemo_rl.algorithms.grpo import async_grpo_train + + print("🚀 Running async GRPO training") + + # Run async GRPO training + async_grpo_train( + policy=policy, + policy_generation=policy_generation, + dataloader=dataloader, + val_dataloader=val_dataloader, + tokenizer=tokenizer, + loss_fn=loss_fn, + task_to_env=task_to_env, + val_task_to_env=val_task_to_env, + logger=logger, + checkpointer=checkpointer, + grpo_save_state=grpo_state, + master_config=master_config, + max_trajectory_age_steps=config.grpo.async_grpo.max_trajectory_age_steps, + teacher_worker_groups=teacher_worker_groups, + alias_to_group_alias=alias_to_group_alias, + processor=processor, ) - - from nemo_rl.algorithms.grpo import async_grpo_train - - print("🚀 Running async GRPO training") - - # Run async GRPO training - async_grpo_train( - policy=policy, - policy_generation=policy_generation, - dataloader=dataloader, - val_dataloader=val_dataloader, - tokenizer=tokenizer, - loss_fn=loss_fn, - task_to_env=task_to_env, - val_task_to_env=val_task_to_env, - logger=logger, - checkpointer=checkpointer, - grpo_save_state=grpo_state, - master_config=master_config, - max_trajectory_age_steps=config.grpo.async_grpo.max_trajectory_age_steps, - teacher_worker_groups=teacher_worker_groups, - alias_to_group_alias=alias_to_group_alias, - processor=processor, - ) - else: - print("🚀 Running synchronous GRPO training") - - # Run standard GRPO training - grpo_train( - policy, - policy_generation, - dataloader, - val_dataloader, - tokenizer, - loss_fn, - task_to_env, - val_task_to_env, - logger, - checkpointer, - grpo_state, - master_config, - processor=processor, - ) + else: + print("🚀 Running synchronous GRPO training") + + # Run standard GRPO training + grpo_train( + policy, + policy_generation, + dataloader, + val_dataloader, + tokenizer, + loss_fn, + task_to_env, + val_task_to_env, + logger, + checkpointer, + grpo_state, + master_config, + processor=processor, + ) + finally: + shutdown_environments(task_to_env, val_task_to_env) + try: + policy_generation.shutdown() + except Exception as error: + print(f"Error shutting down generation: {error}", flush=True) if __name__ == "__main__": diff --git a/examples/run_grpo.py b/examples/run_grpo.py index 6732ae0115c..51b1f08698e 100644 --- a/examples/run_grpo.py +++ b/examples/run_grpo.py @@ -19,7 +19,12 @@ from omegaconf import OmegaConf -from nemo_rl.algorithms.grpo import MasterConfig, grpo_train, setup +from nemo_rl.algorithms.grpo import ( + MasterConfig, + grpo_train, + setup, + shutdown_environments, +) from nemo_rl.algorithms.utils import get_tokenizer from nemo_rl.data.utils import setup_response_data from nemo_rl.distributed.virtual_cluster import init_ray @@ -174,68 +179,79 @@ def _make_policy(**kwargs): print(f" {label}: {value:.1f}s") print("=" * 60 + "\n", flush=True) - # Check if async mode is enabled - if config.grpo.async_grpo.enabled: - # Async GRPO does not support dynamic sampling, reward scaling, or reward shaping (DAPO features) - if config.grpo.use_dynamic_sampling: - raise NotImplementedError( - "use_dynamic_sampling is not supported with async GRPO" - ) - if config.grpo.reward_scaling.enabled: - raise NotImplementedError("reward_scaling is not supported with async GRPO") - if config.grpo.reward_shaping.enabled: - raise NotImplementedError("reward_shaping is not supported with async GRPO") - - # Async GRPO does not support multiple dataloaders - if config.data["use_multiple_dataloader"]: - raise NotImplementedError( - "use_multiple_dataloader is not supported with async GRPO" - ) - - from nemo_rl.algorithms.grpo import async_grpo_train - - print("🚀 Running async GRPO training") - - # Run async GRPO training - async_grpo_train( - policy=policy, - policy_generation=policy_generation, - dataloader=dataloader, - val_dataloader=val_dataloader, - tokenizer=tokenizer, - loss_fn=loss_fn, - task_to_env=task_to_env, - val_task_to_env=val_task_to_env, - logger=logger, - checkpointer=checkpointer, - grpo_save_state=grpo_state, - master_config=master_config, - max_trajectory_age_steps=config.grpo.async_grpo.max_trajectory_age_steps, - teacher_worker_groups=teacher_worker_groups, - alias_to_group_alias=alias_to_group_alias, - ) - else: - # Two parallel synchronous trainers (verl-style — main_ppo.py vs - # main_ppo_sync.py). data_plane.enabled selects which one runs. - trainer = _select_trainer(master_config) - # grpo_train_sync defers checkpoint finalization to the checkpointer's - # background threads; the context manager guarantees they are flushed on - # exit. (grpo_train also flushes internally; shutdown() is idempotent.) - with checkpointer: - trainer( - policy, - policy_generation, - dataloader, - val_dataloader, - tokenizer, - loss_fn, - task_to_env, - val_task_to_env, - logger, - checkpointer, - grpo_state, - master_config, + try: + # Check if async mode is enabled + if config.grpo.async_grpo.enabled: + # Async GRPO does not support dynamic sampling, reward scaling, or reward shaping (DAPO features) + if config.grpo.use_dynamic_sampling: + raise NotImplementedError( + "use_dynamic_sampling is not supported with async GRPO" + ) + if config.grpo.reward_scaling.enabled: + raise NotImplementedError( + "reward_scaling is not supported with async GRPO" + ) + if config.grpo.reward_shaping.enabled: + raise NotImplementedError( + "reward_shaping is not supported with async GRPO" + ) + + # Async GRPO does not support multiple dataloaders + if config.data["use_multiple_dataloader"]: + raise NotImplementedError( + "use_multiple_dataloader is not supported with async GRPO" + ) + + from nemo_rl.algorithms.grpo import async_grpo_train + + print("🚀 Running async GRPO training") + + # Run async GRPO training + async_grpo_train( + policy=policy, + policy_generation=policy_generation, + dataloader=dataloader, + val_dataloader=val_dataloader, + tokenizer=tokenizer, + loss_fn=loss_fn, + task_to_env=task_to_env, + val_task_to_env=val_task_to_env, + logger=logger, + checkpointer=checkpointer, + grpo_save_state=grpo_state, + master_config=master_config, + max_trajectory_age_steps=config.grpo.async_grpo.max_trajectory_age_steps, + teacher_worker_groups=teacher_worker_groups, + alias_to_group_alias=alias_to_group_alias, ) + else: + # Two parallel synchronous trainers (verl-style — main_ppo.py vs + # main_ppo_sync.py). data_plane.enabled selects which one runs. + trainer = _select_trainer(master_config) + # grpo_train_sync defers checkpoint finalization to the checkpointer's + # background threads; the context manager guarantees they are flushed on + # exit. (grpo_train also flushes internally; shutdown() is idempotent.) + with checkpointer: + trainer( + policy, + policy_generation, + dataloader, + val_dataloader, + tokenizer, + loss_fn, + task_to_env, + val_task_to_env, + logger, + checkpointer, + grpo_state, + master_config, + ) + finally: + shutdown_environments(task_to_env, val_task_to_env) + try: + policy_generation.shutdown() + except Exception as error: + print(f"Error shutting down generation: {error}", flush=True) if __name__ == "__main__": diff --git a/nemo_rl/algorithms/async_utils/trajectory_collector.py b/nemo_rl/algorithms/async_utils/trajectory_collector.py index e69c169f450..652a86fe62c 100644 --- a/nemo_rl/algorithms/async_utils/trajectory_collector.py +++ b/nemo_rl/algorithms/async_utils/trajectory_collector.py @@ -562,6 +562,14 @@ def prepare_for_refit(self) -> None: "synchronous engine path (async_engine=false) is no longer supported." ) is_async_engine = True + elif backend == "dynamo": + # Dynamo's native layerwise reload temporarily materializes model + # parameters while the NCCL update is in progress. It is not safe + # to execute an already-issued vLLM request concurrently with that + # reload (in particular for NemotronH/Mamba parameters), even when + # the update route accepts allow_unpaused=True. Stop new trajectory + # starts above and drain every active trajectory before refitting. + is_async_engine = False else: is_async_engine = False async_grpo_config = self.master_config.grpo.async_grpo @@ -611,8 +619,17 @@ def resume_after_refit(self) -> None: ) except Exception as e: print(f"⚠️ Failed to invalidate generation backend KV caches: {e}") - - self._refit_pause_cleared.set() + if ( + "generation" in self.master_config.policy + and self.master_config.policy["generation"]["backend"] == "dynamo" + ): + raise RuntimeError( + "Managed Dynamo KV cache invalidation failed after refit" + ) from e + finally: + self._refit_pause_cleared.set() + else: + self._refit_pause_cleared.set() def wait_for_pending_generations(self) -> None: """Wait for all in-flight generation threads to complete.""" diff --git a/nemo_rl/algorithms/grpo.py b/nemo_rl/algorithms/grpo.py index 6676b81af05..48365b69d75 100644 --- a/nemo_rl/algorithms/grpo.py +++ b/nemo_rl/algorithms/grpo.py @@ -59,7 +59,7 @@ from nemo_rl.algorithms.utils import ( calculate_baseline_and_std_per_prompt, get_gdpo_reward_component_keys, - log_generation_metrics_to_wandb, + log_generation_metrics, print_efficiency_summary, print_performance_metrics, set_seed, @@ -99,6 +99,7 @@ run_nemo_gym_rollout_sync, should_mask_flagged_samples, ) +from nemo_rl.models.generation.dynamo import DynamoConfig, DynamoGeneration from nemo_rl.models.generation.interfaces import ( GenerationConfig, GenerationInterface, @@ -400,6 +401,35 @@ def _needs_hf_refit_handshake( return not (nccl_reshard_refit_enabled and not colocated_inference) +def shutdown_environments( + task_to_env: dict[str, EnvironmentInterface] | None, + val_task_to_env: dict[str, EnvironmentInterface] | None, +) -> None: + """Shut down each unique environment actor before generation stops.""" + seen_environment_handles: set[int] = set() + for environment_map in (task_to_env, val_task_to_env): + if environment_map is None: + continue + for task_name, environment in environment_map.items(): + handle_id = id(environment) + if handle_id in seen_environment_handles: + continue + seen_environment_handles.add(handle_id) + + print(f"🛑 Shutting down environment {task_name}...") + try: + ray.get(environment.shutdown.remote(), timeout=10) + except Exception as shutdown_error: + print( + f"Environment {task_name} graceful shutdown failed: " + f"{shutdown_error}" + ) + try: + ray.kill(environment) + except Exception as kill_error: + print(f"Error stopping environment {task_name}: {kill_error}") + + def setup( master_config: MasterConfig, tokenizer: TokenizerType, @@ -454,6 +484,20 @@ def setup( ) if generation_config["backend"] == "vllm": normalize_vllm_refit_config(cast(VllmConfig, generation_config)) + elif generation_config["backend"] == "dynamo": + # Validate the complete managed-Dynamo boundary before allocating Ray + # placement groups or starting any external services. + if grpo_config.async_grpo.in_flight_weight_updates: + raise ValueError( + "grpo.async_grpo.in_flight_weight_updates must be false when " + "policy.generation.backend='dynamo'; managed Dynamo drains " + "rollouts before layerwise weight refit" + ) + generation_config.setdefault("vllm_kwargs", {})["hf_overrides"] = ( + policy_config.get("hf_config_overrides") or {} + ) + generation_config = DynamoConfig.model_validate(generation_config).model_dump() + policy_config["generation"] = generation_config _validate_multimodal_dedup_capability(master_config) # Validation-only sampling is honored only on the NeMo-Gym vLLM rollout @@ -939,6 +983,10 @@ def _spinup_nemo_gym(base_urls, model_name): gpus_per_instance = trtllm_cfg[ "tensor_parallel_size" ] * trtllm_cfg.get("pipeline_parallel_size", 1) + elif generation_config["backend"] == "dynamo": + gpus_per_instance = DynamoConfig.model_validate( + generation_config + ).engine_world_size else: sglang_cfg = generation_config.get("sglang_cfg", {}) gpus_per_instance = sglang_cfg.get("gpus_per_server", 1) @@ -1012,6 +1060,10 @@ def _spinup_nemo_gym(base_urls, model_name): MegatronGeneration.init_cluster_placement_groups( inference_cluster, policy_config ) + elif generation_config["backend"] == "dynamo": + # Managed Dynamo creates one single-node engine per placement + # group and does not need a backend-specific PG strategy. + inference_cluster.get_placement_groups() else: { "vllm": VllmGeneration, @@ -1420,6 +1472,37 @@ def init_trtllm(): ) setup_timing_metrics.nemo_gym_init_time_s = nemo_gym_time + elif backend == "dynamo": + # Managed Dynamo owns a fixed worker fleet on the inference virtual cluster. + + def init_dynamo(): + t0 = time.perf_counter() + generation = DynamoGeneration( + cluster=inference_cluster, + config=generation_config, + tokenizer=tokenizer, + tokenizer_config=policy_config["tokenizer"], + ) + return generation, time.perf_counter() - t0 + + policy_generation, policy = initialize_generation_with_policy( + init_generation_fn=init_dynamo, + colocated_inference=False, + setup_timing_metrics=setup_timing_metrics, + ) + + if enable_nemo_gym: + nemo_gym_actor, nemo_gym_time = _spinup_nemo_gym( + policy_generation.dp_openai_server_base_urls, + generation_config["model_name"], + ) + setup_timing_metrics.nemo_gym_init_time_s = nemo_gym_time + + print( + f" ✓ Using Dynamo backend (frontend: {policy_generation.frontend_url})", + flush=True, + ) + # Record when worker initialization completes (for calculating other setup time) worker_init_complete_time = time.perf_counter() - setup_start_time @@ -1469,15 +1552,8 @@ def init_trtllm(): and checkpoint_engine_config is None ): t0 = time.perf_counter() - ip, port = train_cluster.get_master_address_and_port() - print(f"Using ip: {ip}, port: {port} for collective communication", flush=True) - # world includes all training workers and all inference workers - train_world_size = train_cluster.world_size() - inference_world_size = inference_nodes * inference_gpus_per_node - world_size = train_world_size + inference_world_size - # init collective - if nccl_reshard_refit_enabled: + if nccl_reshard_refit_enabled or backend == "dynamo": policy_generation.weight_synchronizer = create_weight_synchronizer( policy=policy, generation=policy_generation, @@ -1488,6 +1564,14 @@ def init_trtllm(): ) policy_generation.weight_synchronizer.init_communicator() else: + ip, port = train_cluster.get_master_address_and_port() + print( + f"Using ip: {ip}, port: {port} for collective communication", + flush=True, + ) + train_world_size = train_cluster.world_size() + inference_world_size = inference_nodes * inference_gpus_per_node + world_size = train_world_size + inference_world_size futures_train = policy.init_collective( ip, port, world_size, train_world_size=train_world_size ) @@ -1537,7 +1621,9 @@ def init_trtllm(): flush=True, ) else: - if _needs_hf_refit_handshake( + if getattr( + policy_generation, "weight_synchronizer", None + ) is None and _needs_hf_refit_handshake( backend, nccl_reshard_refit_enabled, colocated_inference ): state_dict_info = policy.prepare_refit_info() @@ -2027,6 +2113,7 @@ def _apply_configured_message_level_advantage_penalties( def _should_use_async_rollouts(master_config: MasterConfig) -> bool: """Determine if async rollouts should be used based on the configuration. + Dynamo is intrinsically async because all rollouts use its HTTP frontend. SGLang only uses async rollouts when configured with ``policy.generation.use_async_rollouts``. vLLM uses async rollouts when ``vllm_cfg.async_engine`` is enabled. TRT-LLM always requires ``trtllm_cfg.async_engine=true``. @@ -2037,6 +2124,9 @@ def _should_use_async_rollouts(master_config: MasterConfig) -> bool: return False backend = generation_config.get("backend", "") + if backend == "dynamo": + return True + if backend == "sglang": return bool(generation_config.get("use_async_rollouts", False)) @@ -2145,6 +2235,8 @@ def _should_use_nemo_gym(master_config: MasterConfig) -> bool: should_expose_http_server = generation_config["trtllm_cfg"].get( "expose_http_server" ) + elif generation_config["backend"] == "dynamo": + should_expose_http_server = generation_config["vllm_cfg"]["expose_http_server"] else: should_expose_http_server = False assert should_expose_http_server, ( @@ -2377,7 +2469,9 @@ def refit_policy_generation( raise NotImplementedError( "SGLang haven't implemented non-colocated inference mode. " ) - futures_train = policy.broadcast_weights_for_collective(kv_scales=kv_scales) + futures_train = policy.broadcast_weights_for_collective( + kv_scales=kv_scales, + ) futures_inference = policy_generation.update_weights_from_collective() # wait for all futures to complete ray.get(futures_train) @@ -3658,9 +3752,8 @@ def grpo_train( master_config.policy["generation"] .get("vllm_cfg", {}) .get("enable_vllm_metrics_logger", False) - and master_config.logger["wandb_enabled"] ): - log_generation_metrics_to_wandb( + log_generation_metrics( generation_logger_metrics, total_steps + 1, master_config.policy["generation"]["vllm_cfg"][ @@ -4082,18 +4175,20 @@ def async_grpo_train( media to NeMo Gym prompt rows. """ # Ensure we are running with a compatible async generation backend. - # Async GRPO (with in-flight weight updates) supports vLLM, Megatron, and TRT-LLM; + # Async GRPO supports vLLM, Megatron, TRT-LLM, and Dynamo; # SGLang async rollouts do not support the async GRPO replay path. generation_config = master_config.policy["generation"] backend = generation_config.get("backend", "") if generation_config else "" - assert backend in ("vllm", "megatron", "trtllm"), ( - "Async GRPO supports the vLLM, Megatron, and TRT-LLM generation backends; " + assert backend in ("vllm", "megatron", "trtllm", "dynamo"), ( + "Async GRPO supports the vLLM, Megatron, TRT-LLM, and Dynamo generation backends; " f"got policy.generation.backend={backend!r}." ) assert _should_use_async_rollouts(master_config), ( - "Async GRPO requires an async generation engine. Set " - "policy.generation.vllm_cfg.async_engine=true (vLLM) or " - "policy.generation.trtllm_cfg.async_engine=true (TRT-LLM)." + "Async GRPO requires Dynamo, Megatron, or an async vLLM or TRT-LLM " + "generation engine. Set policy.generation.backend=dynamo, " + "policy.generation.vllm_cfg.async_engine=true (vLLM), or " + "policy.generation.trtllm_cfg.async_engine=true (TRT-LLM). " + "Megatron Inference always uses its async engine." ) assert master_config.loss_fn.use_importance_sampling_correction, ( "Importance sampling correction must be enabled for async GRPO for good convergence due to off-policy samples!" @@ -4280,14 +4375,6 @@ def async_grpo_train( processor=processor, ) - # Start trajectory collection in background - collection_task = trajectory_collector.start_collection.remote(dataloader) - - # Ensure collector knows initial weight version - trajectory_collector.set_weight_version.remote(weight_version) - - print("📦 Started continuous background trajectory collection") - print( f"🚀 Starting async GRPO training with buffer_size={optimal_buffer_size}, " f"max_age={max_trajectory_age_steps} steps, " @@ -4323,6 +4410,13 @@ def async_grpo_train( traceback.print_exc() return + # Generation must hold the policy's real weights before any backend starts + # collecting. In particular, vLLM and Dynamo start with dummy weights when + # the first refit supplies model parameters. + ray.get(trajectory_collector.set_weight_version.remote(weight_version)) + trajectory_collector.start_collection.remote(dataloader) + print("📦 Started continuous background trajectory collection") + print("✅ Policy generation setup complete, proceeding to validation...") # Run validation at start if configured @@ -4890,8 +4984,12 @@ def async_grpo_train( # Update weight version before resuming trajectory collection so that all trajectories are updated with the new correct weight version weight_version += 1 - trajectory_collector.set_weight_version.remote(weight_version) - trajectory_collector.resume_after_refit.remote() + ray.get( + trajectory_collector.set_weight_version.remote( + weight_version + ) + ) + ray.get(trajectory_collector.resume_after_refit.remote()) timer.stop("idle/refit_bubble") @@ -5211,9 +5309,8 @@ def async_grpo_train( master_config.policy["generation"] .get("vllm_cfg", {}) .get("enable_vllm_metrics_logger", False) - and master_config.logger["wandb_enabled"] ): - log_generation_metrics_to_wandb( + log_generation_metrics( generation_logger_metrics, step + 1, master_config.policy["generation"]["vllm_cfg"][ @@ -5342,7 +5439,6 @@ def async_grpo_train( except Exception as e: print(f"Error finalizing pending checkpoint: {e}") - # Clean up print("🛑 Stopping trajectory collection...") try: ray.kill(trajectory_collector) @@ -5354,21 +5450,8 @@ def async_grpo_train( except Exception as e: print(f"Error stopping replay buffer: {e}") - # Environments must be shut down before generation workers because - # they may have in-flight HTTP requests to vLLM HTTP endpoints. - # Killing generation first leaves environments retrying dead connections. - for env_dict in (task_to_env, val_task_to_env): - if env_dict is None: - continue - for task_name, env in env_dict.items(): - print(f"🛑 Shutting down environment {task_name}...") - try: - ray.get(env.shutdown.remote(), timeout=10) - except Exception: - try: - ray.kill(env) - except Exception as e: - print(f"Error shutting down environment {task_name}: {e}") + # Environments can have in-flight HTTP requests to generation workers. + shutdown_environments(task_to_env, val_task_to_env) print("🛑 Shutting down generation workers...") try: diff --git a/nemo_rl/algorithms/grpo_sync.py b/nemo_rl/algorithms/grpo_sync.py index 97e536cfb02..3d843e36b05 100644 --- a/nemo_rl/algorithms/grpo_sync.py +++ b/nemo_rl/algorithms/grpo_sync.py @@ -69,7 +69,7 @@ from nemo_rl.algorithms.utils import ( calculate_baseline_and_std_per_prompt, get_gdpo_reward_component_keys, - log_generation_metrics_to_wandb, + log_generation_metrics, print_performance_metrics, ) from nemo_rl.data.interfaces import DatumSpec @@ -1279,10 +1279,12 @@ def grpo_train_sync( total_steps + 1, name="train/token_mult_prob_error_plot_sample", ) - if master_config.policy["generation"].get("vllm_cfg", {}).get( - "enable_vllm_metrics_logger", False - ) and master_config.logger.get("wandb_enabled", False): - log_generation_metrics_to_wandb( + if ( + master_config.policy["generation"] + .get("vllm_cfg", {}) + .get("enable_vllm_metrics_logger", False) + ): + log_generation_metrics( generation_logger_metrics, total_steps + 1, master_config.policy["generation"]["vllm_cfg"][ diff --git a/nemo_rl/algorithms/utils.py b/nemo_rl/algorithms/utils.py index 35b31a855d2..28179b8a126 100644 --- a/nemo_rl/algorithms/utils.py +++ b/nemo_rl/algorithms/utils.py @@ -920,13 +920,13 @@ def visualize_per_worker_timeline( return performance_metrics -def log_generation_metrics_to_wandb( +def log_generation_metrics( generation_logger_metrics: dict[str, dict[int, list[Any]]], step: int, timeline_interval: float, logger: Logger, ) -> None: - """Log generation metrics to wandb. + """Log generation metric timelines to every configured logger backend. Args: generation_logger_metrics: Dictionary of generation logger metrics diff --git a/nemo_rl/distributed/ray_actor_environment_registry.py b/nemo_rl/distributed/ray_actor_environment_registry.py index 0479994fd59..59a90cf2d45 100644 --- a/nemo_rl/distributed/ray_actor_environment_registry.py +++ b/nemo_rl/distributed/ray_actor_environment_registry.py @@ -29,11 +29,11 @@ TRTLLM_EXECUTABLE = ( PY_EXECUTABLES.SYSTEM if USE_SYSTEM_EXECUTABLE else PY_EXECUTABLES.TRTLLM ) - ACTOR_ENVIRONMENT_REGISTRY: dict[str, str] = { "nemo_rl.models.generation.vllm.vllm_worker.VllmGenerationWorker": VLLM_EXECUTABLE, "nemo_rl.models.generation.vllm.vllm_worker_async.VllmAsyncGenerationWorker": VLLM_EXECUTABLE, "nemo_rl.models.generation.sglang.sglang_worker.SGLangGenerationWorker": SGLANG_EXECUTABLE, + "nemo_rl.models.generation.dynamo.dynamo_worker.DynamoVllmWorker": PY_EXECUTABLES.SYSTEM, "nemo_rl.models.policy.workers.dtensor_policy_worker.DTensorPolicyWorker": PY_EXECUTABLES.FSDP, "nemo_rl.models.policy.workers.dtensor_policy_worker_v2.DTensorPolicyWorkerV2": PY_EXECUTABLES.AUTOMODEL, "nemo_rl.models.value.workers.dtensor_value_worker_v2.DTensorValueWorkerV2": PY_EXECUTABLES.AUTOMODEL, diff --git a/nemo_rl/distributed/stateless_process_group.py b/nemo_rl/distributed/stateless_process_group.py index b7fd5012854..5d5d12ccfc5 100644 --- a/nemo_rl/distributed/stateless_process_group.py +++ b/nemo_rl/distributed/stateless_process_group.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,12 +12,79 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ctypes +import pickle +import sys +import threading +import types from typing import Optional import torch +from nccl.core import SUM from nccl.core.communicator import Communicator from nccl.core.utils import UniqueId, get_unique_id +_NEMO_UNIQUE_ID_KEY = "nccl_unique_id" +_VLLM_UNIQUE_ID_KEY = "broadcast_from/0/0" +_VLLM_NCCL_MODULE = "vllm.distributed.device_communicators.pynccl_wrapper" +_VLLM_PICKLE_LOCK = threading.Lock() + + +class _VllmNcclUniqueId(ctypes.Structure): + _fields_ = [("internal", ctypes.c_byte * 128)] + + +_VllmNcclUniqueId.__module__ = _VLLM_NCCL_MODULE +_VllmNcclUniqueId.__name__ = "ncclUniqueId" +_VllmNcclUniqueId.__qualname__ = "ncclUniqueId" + + +def _pickle_vllm_unique_id(unique_id_bytes: bytes) -> bytes: + """Serialize an NCCL unique ID in vLLM's metadata wire format. + + vLLM's stateless process group pickles its ``ncclUniqueId`` ctypes + structure. Training workers do not install vLLM, so construct the same + ctypes type under its canonical module name only while serializing. + """ + if len(unique_id_bytes) != 128: + raise ValueError( + f"Expected a 128-byte NCCL unique ID, got {len(unique_id_bytes)} bytes." + ) + + module_names = [ + "vllm", + "vllm.distributed", + "vllm.distributed.device_communicators", + _VLLM_NCCL_MODULE, + ] + with _VLLM_PICKLE_LOCK: + previous_modules = {name: sys.modules.get(name) for name in module_names} + modules = {name: types.ModuleType(name) for name in module_names} + for name in module_names[:-1]: + modules[name].__path__ = [] + + modules["vllm"].distributed = modules["vllm.distributed"] + modules["vllm.distributed"].device_communicators = modules[ + "vllm.distributed.device_communicators" + ] + modules["vllm.distributed.device_communicators"].pynccl_wrapper = modules[ + _VLLM_NCCL_MODULE + ] + + modules[_VLLM_NCCL_MODULE].ncclUniqueId = _VllmNcclUniqueId + + try: + sys.modules.update(modules) + unique_id = _VllmNcclUniqueId.from_buffer_copy(unique_id_bytes) + payload = pickle.dumps(unique_id) + finally: + for name, previous_module in previous_modules.items(): + if previous_module is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous_module + return payload + class StatelessProcessGroup: def __init__(self, master_address: str, port: int, rank: int, world_size: int): @@ -32,18 +99,31 @@ def __init__(self, master_address: str, port: int, rank: int, world_size: int): is_master=(self.rank == 0), ) - def init_nccl_communicator(self, device: int): - UNIQUE_ID_KEY = "nccl_unique_id" + def init_nccl_communicator(self, device: int, *, peer: str = "nemo") -> None: + """Initialize NCCL using the metadata and warmup protocol of the peer. + + ``peer="nemo"`` publishes the raw 128-byte unique ID under + ``nccl_unique_id`` and warms up with a rank-zero broadcast. + ``peer="vllm"`` additionally publishes vLLM's pickled ``ncclUniqueId`` + under ``broadcast_from/0/0`` and warms up with an all-reduce, matching + ``PyNcclCommunicator``. The receiver protocol is not negotiable, so a + generation backend must select the peer it implements. + """ + if peer not in ("nemo", "vllm"): + raise ValueError(f"Unsupported NCCL peer protocol: {peer!r}.") if self.rank == 0: unique_id = get_unique_id() unique_id_bytes = unique_id.as_bytes - # Rank 0: store unique_id to TCPStore - self.tcp_store.set(UNIQUE_ID_KEY, unique_id_bytes) + self.tcp_store.set(_NEMO_UNIQUE_ID_KEY, unique_id_bytes) + if peer == "vllm": + self.tcp_store.set( + _VLLM_UNIQUE_ID_KEY, + _pickle_vllm_unique_id(unique_id_bytes), + ) else: - # Other ranks: get unique_id from TCPStore - self.tcp_store.wait([UNIQUE_ID_KEY]) - unique_id_bytes = self.tcp_store.get(UNIQUE_ID_KEY) + self.tcp_store.wait([_NEMO_UNIQUE_ID_KEY]) + unique_id_bytes = self.tcp_store.get(_NEMO_UNIQUE_ID_KEY) unique_id = UniqueId.from_bytes(unique_id_bytes) with torch.cuda.device(device): @@ -52,15 +132,25 @@ def init_nccl_communicator(self, device: int): rank=self.rank, unique_id=unique_id, ) - # warmup and check if broadcast is working stream = torch.cuda.current_stream() - if self.rank == 0: - data = torch.ones(1, device=device) - else: + if peer == "vllm": + # Match PyNcclCommunicator's first collective exactly. data = torch.zeros(1, device=device) - self.broadcast(data, 0, stream=stream) - torch.cuda.current_stream().synchronize() - assert torch.allclose(data, torch.ones(1, device=device)) + self.nccl_communicator.allreduce( + sendbuf=data, + recvbuf=data, + op=SUM, + stream=int(stream.cuda_stream), + ) + else: + if self.rank == 0: + data = torch.ones(1, device=device) + else: + data = torch.zeros(1, device=device) + self.broadcast(data, 0, stream=stream) + stream.synchronize() + if peer == "nemo": + assert torch.allclose(data, torch.ones(1, device=device)) def broadcast( self, tensor: torch.Tensor, src: int, stream: Optional[torch.cuda.Stream] = None diff --git a/nemo_rl/distributed/virtual_cluster.py b/nemo_rl/distributed/virtual_cluster.py index c3c901cb5c1..0604c17359b 100644 --- a/nemo_rl/distributed/virtual_cluster.py +++ b/nemo_rl/distributed/virtual_cluster.py @@ -85,9 +85,13 @@ class PY_EXECUTABLES: # service port is pinned below 9000 to avoid TOCTOU collisions. See ray.sub for # the full layout including Ray's own GCS / worker gRPC ports. # +# Python port-range bounds below are half-open: [low, high). +# +# 1313-1399 Dynamo etcd/NATS control plane (driver-local allocation) # 1400-1999 Master address / TCPStore (cluster.master_port_range_low/high) -# 3000-4999 NeMo RL generation HTTP servers + SGLang engine NCCL/dist_init -# (policy.generation.port_range_low/high) +# [3000, 4999) Shared NeMo RL generation range (policy.generation.port_range_low/high) +# [3000, 4000) Dynamo frontend/token-wrapper HTTP endpoints +# [4000, 4100) Dynamo worker system endpoints (node-local free-port selection) # 5000-5999 NeMo Gym HTTP servers (env.nemo_gym.port_range_low/high) # 6000-6099 SingleController gen. router (async_rl.generation_router.port_range_low/high; # one fixed port per run — NeMo-Gym holds the @@ -99,6 +103,12 @@ class PY_EXECUTABLES: # 8800-8999 SGLang Prometheus metrics (DEFAULT_SGLANG_PROMETHEUS_PORT_RANGE_*, hard-coded) DEFAULT_GENERATION_PORT_RANGE_LOW = 3000 DEFAULT_GENERATION_PORT_RANGE_HIGH = 4999 +DEFAULT_DYNAMO_CONTROL_PORT_RANGE_LOW = 1313 +DEFAULT_DYNAMO_CONTROL_PORT_RANGE_HIGH = 1400 +DEFAULT_DYNAMO_HTTP_PORT_RANGE_LOW = 3000 +DEFAULT_DYNAMO_HTTP_PORT_RANGE_HIGH = 4000 +DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_LOW = 4000 +DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_HIGH = 4100 DEFAULT_GYM_PORT_RANGE_LOW = 5000 DEFAULT_GYM_PORT_RANGE_HIGH = 5999 # vLLM TP/DP rendezvous ports. Each engine gets PORTS_PER_ENGINE ports starting @@ -172,33 +182,64 @@ def _bind_socket_in_range( sock: socket.socket, port_range_low: int, port_range_high: int, - max_retries: int = 50, + max_retries: int | None = 50, + excluded_ports: set[int] | None = None, ) -> int: """Try to bind *sock* to a random port in [port_range_low, port_range_high). - Raises ``RuntimeError`` after *max_retries* failed attempts. + When *max_retries* is ``None``, try every non-excluded port once. Otherwise, + preserve the existing bounded random-retry behavior. """ import random - for _ in range(max_retries): - port = random.randint(port_range_low, port_range_high - 1) - try: - sock.bind(("", port)) - return port - except OSError: - continue + excluded = excluded_ports or set() + if max_retries is None: + candidates = [ + port + for port in range(port_range_low, port_range_high) + if port not in excluded + ] + random.shuffle(candidates) + for port in candidates: + try: + sock.bind(("", port)) + return port + except OSError: + continue + retry_description = f"all {len(candidates)} available ports" + else: + for _ in range(max_retries): + port = random.randint(port_range_low, port_range_high - 1) + if port in excluded: + continue + try: + sock.bind(("", port)) + return port + except OSError: + continue + retry_description = f"{max_retries} attempts" + raise RuntimeError( f"Could not find a free port in range [{port_range_low}, {port_range_high}) " - f"after {max_retries} attempts." + f"after {retry_description}." ) def _get_free_port_local( port_range_low: int = DEFAULT_MASTER_PORT_RANGE_LOW, port_range_high: int = DEFAULT_MASTER_PORT_RANGE_HIGH, + *, + max_retries: int | None = 50, + excluded_ports: set[int] | None = None, ) -> int: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - port = _bind_socket_in_range(s, port_range_low, port_range_high) + port = _bind_socket_in_range( + s, + port_range_low, + port_range_high, + max_retries=max_retries, + excluded_ports=excluded_ports, + ) s.listen(1) return port diff --git a/nemo_rl/distributed/worker_groups.py b/nemo_rl/distributed/worker_groups.py index 81bba1d79b7..905854e0ea6 100644 --- a/nemo_rl/distributed/worker_groups.py +++ b/nemo_rl/distributed/worker_groups.py @@ -14,6 +14,7 @@ import importlib import math import os +import sys import time from copy import deepcopy from dataclasses import dataclass @@ -506,10 +507,11 @@ def _create_workers_from_bundle_indices( for key in ("HF_HOME", "HF_MODULES_CACHE", "PYTHONPATH") if key in env_vars } - initializer_runtime_env = { - "py_executable": py_executable, - "env_vars": initializer_env_vars, - } + initializer_runtime_env = {} + if py_executable != sys.executable: + initializer_runtime_env["py_executable"] = py_executable + if initializer_env_vars: + initializer_runtime_env["env_vars"] = initializer_env_vars self._initializer_pool: dict[int, ray.actor.ActorHandle] = {} for pg_idx in unique_pg_indices: # num_cpus=0 so the initializer doesn't consume a CPU slot — it diff --git a/nemo_rl/environments/nemo_gym.py b/nemo_rl/environments/nemo_gym.py index a341c4acb23..1ddd9921d8e 100644 --- a/nemo_rl/environments/nemo_gym.py +++ b/nemo_rl/environments/nemo_gym.py @@ -914,7 +914,9 @@ def shutdown(self) -> None: # into a confusing AttributeError from a never-spun-up (e.g. restarted) actor. if self.rh is None: return - self.rh.shutdown() + run_helper = self.rh + self.rh = None + run_helper.shutdown() def step(self, message_log_batch, metadata): # This is not used since NeMo-Gym will handle the rollouts entirely. diff --git a/nemo_rl/experience/rollouts.py b/nemo_rl/experience/rollouts.py index 72f718bcf9f..72d78b53c4d 100644 --- a/nemo_rl/experience/rollouts.py +++ b/nemo_rl/experience/rollouts.py @@ -535,6 +535,7 @@ async def generate_responses_async( # SGLang exposes ``sglang_cfg`` and gates on ``use_async_rollouts``; # vLLM exposes ``cfg`` and gates on ``vllm_cfg.async_engine``; # TRT-LLM requires its flag; the Megatron backend is always async. + # Managed Dynamo always exposes its rollout frontend asynchronously. vllm_cfg = getattr(policy_generation, "cfg", None) sglang_cfg = getattr(policy_generation, "sglang_cfg", None) generation_config = vllm_cfg or sglang_cfg or {} @@ -546,6 +547,8 @@ async def generate_responses_async( use_async_generation = bool( generation_config.get("vllm_cfg", {}).get("async_engine", False) ) + elif backend == "dynamo": + use_async_generation = True elif backend == "trtllm": assert generation_config.get("trtllm_cfg", {}).get("async_engine", False), ( "TRT-LLM backend requires trtllm_cfg.async_engine=true; the " diff --git a/nemo_rl/models/generation/__init__.py b/nemo_rl/models/generation/__init__.py index 91139aacd40..52b80e6d317 100644 --- a/nemo_rl/models/generation/__init__.py +++ b/nemo_rl/models/generation/__init__.py @@ -43,7 +43,11 @@ def configure_generation_config( if config["stop_token_ids"] is None: config["stop_token_ids"] = [tokenizer.eos_token_id] - # vllm setting + # vLLM setting shared by the standard and managed Dynamo backends. + if config["backend"] in ("vllm", "dynamo"): + vllm_backed_config = cast(VllmConfig, config) + vllm_backed_config["vllm_cfg"]["load_format"] = "auto" if is_eval else "dummy" + if config["backend"] == "vllm": config = cast(VllmConfig, config) if config.get("real_quant"): @@ -64,11 +68,8 @@ def configure_generation_config( ) # set load_format - config["vllm_cfg"]["load_format"] = ( - "auto" - if is_eval or config.get("refit_transport") in VLLM_SPARSE_REFIT_TRANSPORTS - else "dummy" - ) + if config.get("refit_transport") in VLLM_SPARSE_REFIT_TRANSPORTS: + config["vllm_cfg"]["load_format"] = "auto" speculative_config = config.get("vllm_kwargs", {}).get("speculative_config") if speculative_config and not is_eval and not has_refit_draft_weights: # Speculative decoding needs real draft weights at startup, since the diff --git a/nemo_rl/models/generation/constants.py b/nemo_rl/models/generation/constants.py index b6d747d13a4..973ea9d0ae7 100644 --- a/nemo_rl/models/generation/constants.py +++ b/nemo_rl/models/generation/constants.py @@ -21,3 +21,4 @@ VLLM_BACKEND = "vllm" SGLANG_BACKEND = "sglang" MEGATRON_BACKEND = "megatron" +DYNAMO_BACKEND = "dynamo" diff --git a/nemo_rl/models/generation/dynamo/__init__.py b/nemo_rl/models/generation/dynamo/__init__.py new file mode 100644 index 00000000000..082e6260c19 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from nemo_rl.models.generation.dynamo.config import ( + DynamoCfg, + DynamoConfig, + DynamoFrontendArgs, + DynamoWorkerArgs, +) +from nemo_rl.models.generation.dynamo.dynamo_generation import DynamoGeneration + +__all__ = [ + "DynamoCfg", + "DynamoConfig", + "DynamoFrontendArgs", + "DynamoGeneration", + "DynamoWorkerArgs", +] diff --git a/nemo_rl/models/generation/dynamo/arguments.py b/nemo_rl/models/generation/dynamo/arguments.py new file mode 100644 index 00000000000..7a3783a4cdf --- /dev/null +++ b/nemo_rl/models/generation/dynamo/arguments.py @@ -0,0 +1,302 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Deterministic argument and environment construction for managed Dynamo.""" + +import json +from collections.abc import Mapping, Sequence +from typing import Any + +from nemo_rl.models.generation.dynamo.config import ( + DYNAMO_VLLM_FLAGS, + DynamoCfg, +) + +_MANAGED_FLAGS = { + "--component", + "--model-name", + "--model-path", + "--endpoint", +} + +_CREDENTIAL_FLAGS = { + "--access-token", + "--api-key", + "--apikey", + "--auth-token", + "--hf-token", + "--password", + "--secret", + "--token", +} + + +def _normalise_flag(flag: str) -> str: + flag = flag.split("=", 1)[0].replace("_", "-") + if flag.startswith("--no-"): + return "--" + flag[5:] + return flag + + +def _flag_for_key(key: str) -> str: + return "--" + key.replace("_", "-") + + +def _serialise_value(value: Any) -> str: + if isinstance(value, (dict, list, tuple)): + return json.dumps(value, separators=(",", ":"), sort_keys=True) + return str(value) + + +class _ArgvBuilder: + def __init__(self) -> None: + self.argv: list[str] = [] + self.sources: dict[str, str] = {} + + def add(self, flag: str, value: Any = None, *, source: str) -> None: + normalised = _normalise_flag(flag) + prior = self.sources.get(normalised) + if prior is not None: + raise ValueError( + f"Dynamo worker option {normalised} is set by both {prior} and {source}." + ) + self.sources[normalised] = source + if isinstance(value, bool): + self.argv.append(flag if value else f"--no-{flag.removeprefix('--')}") + else: + self.argv.append(flag) + if value is not None: + self.argv.append(_serialise_value(value)) + + def add_raw(self, args: Sequence[str], *, source: str) -> None: + if not args: + return + current_flag: str | None = None + current_has_value = False + for token in args: + if token.startswith("--"): + current_flag = token + current_has_value = "=" in token + normalised = _normalise_flag(token) + if normalised in _MANAGED_FLAGS: + raise ValueError( + f"{source} may not override managed option {normalised}." + ) + if normalised in self.sources: + raise ValueError( + f"Dynamo worker option {normalised} is set by both " + f"{self.sources[normalised]} and {source}." + ) + self.sources[normalised] = source + elif current_flag is None or current_has_value: + raise ValueError( + f"{source} contains invalid positional argument {token!r}; " + "values must immediately follow one --option." + ) + else: + current_has_value = True + self.argv.extend(args) + + +def build_dynamo_vllm_argv( + *, + model_name: str, + namespace: str, + seed: int, + vllm_cfg: Mapping[str, Any], + vllm_kwargs: Mapping[str, Any], + dynamo_cfg: DynamoCfg, +) -> list[str]: + """Build a conflict-free ``python -m dynamo.vllm`` argument list.""" + builder = _ArgvBuilder() + builder.add("--model", model_name, source="managed runtime") + builder.add("--served-model-name", model_name, source="managed runtime") + builder.add("--namespace", namespace, source="managed runtime") + builder.add("--discovery-backend", "etcd", source="managed runtime") + builder.add("--request-plane", "tcp", source="managed runtime") + builder.add("--event-plane", "nats", source="managed runtime") + builder.add("--enable-rl", source="managed runtime") + builder.add( + "--weight-transfer-config", + {"backend": "nccl"}, + source="managed runtime", + ) + builder.add("--trust-remote-code", source="managed runtime") + builder.add("--seed", seed, source="managed runtime") + + for key, flag in DYNAMO_VLLM_FLAGS.items(): + value = vllm_cfg.get(key) + if value is not None: + builder.add(flag, value, source=f"vllm_cfg.{key}") + + if int(vllm_cfg["expert_parallel_size"]) > 1: + builder.add( + "--enable-expert-parallel", + source="vllm_cfg.expert_parallel_size", + ) + + worker_args = dynamo_cfg.worker_args + if worker_args.tool_call_parser is not None: + builder.add( + "--dyn-tool-call-parser", + worker_args.tool_call_parser, + source="dynamo_cfg.worker_args.tool_call_parser", + ) + if worker_args.reasoning_parser is not None: + builder.add( + "--dyn-reasoning-parser", + worker_args.reasoning_parser, + source="dynamo_cfg.worker_args.reasoning_parser", + ) + builder.add( + "--exclude-tools-when-tool-choice-none", + worker_args.exclude_tools_when_tool_choice_none, + source="dynamo_cfg.worker_args.exclude_tools_when_tool_choice_none", + ) + builder.add( + "--dyn-enable-structural-tag", + worker_args.enable_structural_tag, + source="dynamo_cfg.worker_args.enable_structural_tag", + ) + builder.add( + "--dyn-structural-tag-scope", + worker_args.structural_tag_scope, + source="dynamo_cfg.worker_args.structural_tag_scope", + ) + builder.add( + "--dyn-structural-tag-schema", + worker_args.structural_tag_schema, + source="dynamo_cfg.worker_args.structural_tag_schema", + ) + if worker_args.custom_jinja_template is not None: + builder.add( + "--custom-jinja-template", + worker_args.custom_jinja_template, + source="dynamo_cfg.worker_args.custom_jinja_template", + ) + builder.add( + "--endpoint-types", + ",".join(worker_args.endpoint_types), + source="dynamo_cfg.worker_args.endpoint_types", + ) + + for key, value in vllm_kwargs.items(): + if value is None: + continue + if key == "speculative_config": + raise ValueError( + "policy.generation.vllm_kwargs.speculative_config is not " + "supported by backend='dynamo' because draft weights are not " + "refit after step 0" + ) + flag = _flag_for_key(key) + normalised = _normalise_flag(flag) + source = f"vllm_kwargs.{key}" + if normalised in _MANAGED_FLAGS: + raise ValueError(f"{source} may not override managed option {normalised}.") + builder.add(flag, value, source=source) + + builder.add_raw( + worker_args.extra_cli_args, + source="dynamo_cfg.worker_args.extra_cli_args", + ) + return builder.argv + + +def build_dynamo_frontend_argv( + *, + host: str, + port: int, + namespace: str, + dynamo_cfg: DynamoCfg, +) -> list[str]: + """Build the managed ``dynamo.frontend`` argument list.""" + builder = _ArgvBuilder() + builder.add("--http-host", host, source="managed runtime") + builder.add("--http-port", port, source="managed runtime") + builder.add("--namespace-prefix", namespace, source="managed runtime") + builder.add("--discovery-backend", "etcd", source="managed runtime") + builder.add("--request-plane", "tcp", source="managed runtime") + builder.add("--event-plane", "nats", source="managed runtime") + builder.add( + "--router-mode", + dynamo_cfg.frontend_args.router_mode, + source="dynamo_cfg.frontend_args.router_mode", + ) + builder.add( + "--router-reset-states", + dynamo_cfg.frontend_args.router_reset_states, + source="dynamo_cfg.frontend_args.router_reset_states", + ) + builder.add_raw( + dynamo_cfg.frontend_args.extra_cli_args, + source="dynamo_cfg.frontend_args.extra_cli_args", + ) + return builder.argv + + +def build_managed_worker_env( + *, + base_env: Mapping[str, str], + configured_env: Mapping[str, str], + manager_env: Mapping[str, str], +) -> dict[str, str]: + """Return a reproducible worker environment with semantic overrides blocked.""" + reserved_env_keys = set(manager_env) + forbidden = sorted( + key + for key in configured_env + if key.startswith("DYN_") or key in reserved_env_keys + ) + if forbidden: + raise ValueError( + "vllm_cfg.env_vars may not override managed Dynamo settings: " + + ", ".join(forbidden) + ) + + env = { + key: value + for key, value in base_env.items() + if not key.startswith(("DYN_", "ETCD_", "NATS_")) + and key not in reserved_env_keys + } + env.update(configured_env) + env.update(manager_env) + return env + + +def redact_argv(argv: Sequence[str]) -> list[str]: + """Redact values following explicit credential options for safe logging.""" + redacted = list(argv) + for idx, token in enumerate(redacted): + is_sensitive = token.startswith("--") and ( + _normalise_flag(token.lower()) in _CREDENTIAL_FLAGS + ) + if is_sensitive and "=" in token: + redacted[idx] = token.split("=", 1)[0] + "=" + elif is_sensitive and idx + 1 < len(redacted): + if not redacted[idx + 1].startswith("--"): + redacted[idx + 1] = "" + return redacted + + +def redact_environment(env: Mapping[str, str]) -> dict[str, str]: + """Redact credential-like environment values before logging.""" + sensitive_fragments = ("token", "password", "secret", "api_key", "apikey") + return { + key: "" + if any(part in key.lower() for part in sensitive_fragments) + else value + for key, value in env.items() + } diff --git a/nemo_rl/models/generation/dynamo/config.py b/nemo_rl/models/generation/dynamo/config.py new file mode 100644 index 00000000000..09ee2c326eb --- /dev/null +++ b/nemo_rl/models/generation/dynamo/config.py @@ -0,0 +1,331 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Validated configuration for the managed Dynamo generation backend.""" + +import warnings +from typing import Annotated, Any, Literal + +from pydantic import ( + BaseModel, + BeforeValidator, + PositiveFloat, + PositiveInt, + model_validator, +) + +# Must match vLLM's packed weight-transfer defaults exactly. The Dynamo +# producer and consumer recompute chunk boundaries without negotiation. Verify +# these values whenever Dynamo's pinned vLLM changes. +VLLM_PACKED_BUFFER_SIZE_BYTES = 1024**3 +VLLM_PACKED_NUM_BUFFERS = 2 + +DYNAMO_VLLM_FLAGS: dict[str, str] = { + "tensor_parallel_size": "--tensor-parallel-size", + "pipeline_parallel_size": "--pipeline-parallel-size", + "gpu_memory_utilization": "--gpu-memory-utilization", + "max_model_len": "--max-model-len", + "kv_cache_dtype": "--kv-cache-dtype", + "load_format": "--load-format", + "precision": "--dtype", + "enforce_eager": "--enforce-eager", +} + +_VLLM_CFG_STRUCTURAL = { + "env_vars", + "expert_parallel_size", +} + +_VLLM_CFG_MOVED = { + "http_server_serving_chat_kwargs": ( + "dynamo_cfg.worker_args.custom_jinja_template and tool_call_parser" + ), + "reasoning_parser_plugin": "dynamo_cfg.worker_args.reasoning_parser", + "tool_parser_plugin": "dynamo_cfg.worker_args.tool_call_parser", +} + +_VLLM_CFG_UNSUPPORTED = { + # Applied inside NeMo RL's in-process vLLM worker; it cannot configure the + # managed ``dynamo.vllm`` subprocess. + "cap_max_tokens_to_context", + "is_mx", + "num_first_layers_in_bf16", + "num_last_layers_in_bf16", + "skip_tokenizer_init", + "use_deep_gemm", +} + +_VLLM_CFG_MANAGED_RUNTIME = { + "enable_vllm_metrics_logger", + "expose_http_server", + "logprobs_mode", + "vllm_metrics_logger_interval", +} + +_VLLM_CFG_INAPPLICABLE = { + "async_engine", + "enable_return_routed_experts", + "http_refit_api_key_env_var", + "http_refit_server_port", + "use_tqdm", + "zmq_refit_server_port", +} + +_VLLM_SINGLE_RANK_ONLY_FIELDS = { + "data_parallel_size", + "decode_context_parallel_size", + "prefill_context_parallel_size", +} + + +class DynamoWorkerArgs(BaseModel, extra="forbid"): + """Structured arguments passed to every managed ``dynamo.vllm`` worker.""" + + tool_call_parser: str | None + reasoning_parser: str | None + exclude_tools_when_tool_choice_none: bool + enable_structural_tag: bool + structural_tag_scope: Literal["auto", "always"] + structural_tag_schema: Literal["auto", "strict"] + custom_jinja_template: str | None + endpoint_types: list[Literal["chat", "completions"]] + extra_cli_args: list[str] + + @model_validator(mode="after") + def _validate_endpoint_types(self) -> "DynamoWorkerArgs": + if not self.endpoint_types: + raise ValueError("endpoint_types must contain at least one endpoint") + if len(self.endpoint_types) != len(set(self.endpoint_types)): + raise ValueError("endpoint_types must not contain duplicates") + return self + + +class DynamoFrontendArgs(BaseModel, extra="forbid"): + """Structured arguments passed to the managed Dynamo frontend.""" + + tokenizer: Literal["default", "fastokens"] + tokenizer_cache: bool + tokenizer_cache_bytes: PositiveInt + router_mode: Literal[ + "round-robin", + "random", + "power-of-two", + "kv", + "direct", + "least-loaded", + "device-aware-weighted", + ] + router_reset_states: bool + extra_cli_args: list[str] + + +class DynamoCfg(BaseModel, extra="forbid"): + """Driver-owned Dynamo service and worker-fleet configuration.""" + + engine: Literal["vllm"] + startup_timeout_s: PositiveFloat + request_timeout_s: PositiveFloat + control_timeout_s: PositiveFloat + worker_args: DynamoWorkerArgs + frontend_args: DynamoFrontendArgs + metrics_include_prefixes: list[str] | None + metrics_exclude_prefixes: list[str] | None + + +class DynamoVllmConfig(BaseModel, extra="allow"): + """Known vLLM settings consumed by ``dynamo.vllm``. + + Additional fields remain visible in ``model_extra`` so argument construction + can warn rather than silently dropping an inherited vLLM setting. + """ + + async_engine: bool + tensor_parallel_size: PositiveInt + pipeline_parallel_size: PositiveInt + expert_parallel_size: PositiveInt + gpu_memory_utilization: float + max_model_len: PositiveInt + kv_cache_dtype: str + load_format: str + precision: str + enforce_eager: bool + expose_http_server: bool + enable_vllm_metrics_logger: bool + vllm_metrics_logger_interval: PositiveFloat + env_vars: dict[str, str] | None + + @model_validator(mode="after") + def _validate_parallelism_and_precision(self) -> "DynamoVllmConfig": + if self.expert_parallel_size not in (1, self.tensor_parallel_size): + raise ValueError( + "backend='dynamo' requires expert_parallel_size to be 1 or " + "equal tensor_parallel_size" + ) + if self.precision.lower() not in { + "bf16", + "bfloat16", + }: + raise ValueError( + f"policy.generation.vllm_cfg.precision={self.precision!r} is not " + "supported by backend='dynamo'; managed weight refit currently " + "supports BF16 generation only" + ) + if self.kv_cache_dtype != "auto": + raise ValueError( + f"policy.generation.vllm_cfg.kv_cache_dtype={self.kv_cache_dtype!r} " + "is not supported by backend='dynamo'; use 'auto'" + ) + extra = self.model_extra or {} + if extra.get("is_mx"): + raise ValueError( + "policy.generation.vllm_cfg.is_mx is not supported by " + "backend='dynamo'; use backend='vllm' for MXFP8 generation" + ) + if ( + int(extra.get("num_first_layers_in_bf16") or 0) != 0 + or int(extra.get("num_last_layers_in_bf16") or 0) != 0 + ): + raise ValueError( + "mixed BF16/FP8 generation is not supported by backend='dynamo'; " + "use backend='vllm'" + ) + logprobs_mode = extra.get("logprobs_mode") + if logprobs_mode not in (None, "processed_logprobs"): + raise ValueError( + "policy.generation.vllm_cfg.logprobs_mode must be " + "'processed_logprobs' when backend='dynamo'; the managed " + "--enable-rl option selects processed rollout log probabilities" + ) + + configured_fields = self.model_fields_set | set(extra) + for key, replacement in _VLLM_CFG_MOVED.items(): + if extra.get(key) is not None: + raise ValueError( + f"policy.generation.vllm_cfg.{key} is not read by the " + f"Dynamo backend; set {replacement} instead" + ) + for key in sorted(_VLLM_CFG_UNSUPPORTED & configured_fields): + if not extra.get(key): + continue + warnings.warn( + f"policy.generation.vllm_cfg.{key} is ignored by backend='dynamo'", + stacklevel=2, + ) + classified = ( + set(DYNAMO_VLLM_FLAGS) + | _VLLM_CFG_STRUCTURAL + | set(_VLLM_CFG_MOVED) + | _VLLM_CFG_UNSUPPORTED + | _VLLM_CFG_MANAGED_RUNTIME + | _VLLM_CFG_INAPPLICABLE + | _VLLM_SINGLE_RANK_ONLY_FIELDS + ) + unclassified = { + key for key in configured_fields if getattr(self, key, None) is not None + } - classified + if unclassified: + warnings.warn( + "vllm_cfg keys ignored by backend='dynamo': " + f"{sorted(unclassified)}. Add them to DYNAMO_VLLM_FLAGS or an " + "explicit not-forwarded classification.", + stacklevel=2, + ) + return self + + +def _require_nonempty_vllm_config(value: Any) -> Any: + if not isinstance(value, dict) or not value: + raise ValueError( + "policy.generation.vllm_cfg must be a nonempty mapping when " + "backend='dynamo'" + ) + return value + + +class DynamoConfig(BaseModel, extra="allow"): + """Validated boundary for ``policy.generation.backend=dynamo``.""" + + backend: Literal["dynamo"] + dynamo_cfg: DynamoCfg + vllm_cfg: Annotated[ + DynamoVllmConfig, BeforeValidator(_require_nonempty_vllm_config) + ] + vllm_kwargs: dict[str, Any] + + @property + def engine_world_size(self) -> int: + """Return the derived ranks in each single-node vLLM engine.""" + return self.vllm_cfg.tensor_parallel_size * self.vllm_cfg.pipeline_parallel_size + + @model_validator(mode="after") + def _validate_backend_boundary(self) -> "DynamoConfig": + extra = self.model_extra or {} + # Shared GRPO YAML inheritance supplies mcore_generation_config and + # refit_cfg. Managed Dynamo uses vLLM arguments and the collective + # synchronizer instead, so these two sections are intentionally ignored. + for backend_cfg in ("sglang_cfg", "trtllm_cfg"): + if extra.get(backend_cfg): + raise ValueError( + f"policy.generation.{backend_cfg} is not valid when " + "backend='dynamo'; Dynamo manages vLLM only" + ) + colocated = extra.get("colocated") + if isinstance(colocated, dict) and colocated.get("enabled"): + raise ValueError( + "policy.generation.colocated.enabled must be false when " + "backend='dynamo'" + ) + if extra.get("refit_transport") is not None: + raise ValueError( + "policy.generation.refit_transport must be null when " + "backend='dynamo'; managed Dynamo supports NCCL collective refit only" + ) + for quantization_field in ("quant_cfg", "real_quant"): + if extra.get(quantization_field): + raise ValueError( + f"policy.generation.{quantization_field} is not supported " + "when backend='dynamo'" + ) + speculative_config = self.vllm_kwargs.get("speculative_config") or ( + self.vllm_cfg.model_extra or {} + ).get("speculative_config") + if speculative_config: + raise ValueError( + "policy.generation.vllm_kwargs.speculative_config is not " + "supported by backend='dynamo' because draft weights are not " + "refit after step 0" + ) + if self.vllm_kwargs.get("quantization") is not None: + raise ValueError( + "policy.generation.vllm_kwargs.quantization is not supported " + "when backend='dynamo'" + ) + vllm_extra = self.vllm_cfg.model_extra or {} + for field in sorted(_VLLM_SINGLE_RANK_ONLY_FIELDS): + for source, value in ( + ("vllm_cfg", vllm_extra.get(field)), + ("vllm_kwargs", self.vllm_kwargs.get(field)), + ): + if value is not None and int(value) != 1: + raise ValueError( + f"policy.generation.{source}.{field} must be 1 when " + "backend='dynamo'; managed refit rank geometry is TP × PP" + ) + stop_strings = extra.get("stop_strings") + if stop_strings is not None and len(stop_strings) > 32: + raise ValueError( + "policy.generation.stop_strings supports at most 32 values when " + "backend='dynamo'" + ) + return self diff --git a/nemo_rl/models/generation/dynamo/dynamo_generation.py b/nemo_rl/models/generation/dynamo/dynamo_generation.py new file mode 100644 index 00000000000..1c488855c7d --- /dev/null +++ b/nemo_rl/models/generation/dynamo/dynamo_generation.py @@ -0,0 +1,648 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Generation and NCCL refit through a driver-owned Dynamo vLLM fleet.""" + +import asyncio +import logging +from typing import Any, AsyncGenerator, Optional + +import ray +import torch + +from nemo_rl.distributed.batched_data_dict import BatchedDataDict +from nemo_rl.distributed.virtual_cluster import RayVirtualCluster +from nemo_rl.models.generation.dynamo.config import DynamoConfig +from nemo_rl.models.generation.dynamo.http_client import ( + async_http_post_json, + format_dynamo_error, +) +from nemo_rl.models.generation.dynamo.managed_runtime import ManagedDynamoRuntime +from nemo_rl.models.generation.dynamo.metrics import DynamoMetricsSampler +from nemo_rl.models.generation.dynamo.refit import DynamoRefitChannel +from nemo_rl.models.generation.dynamo.token_wrapper import DynamoTokenWrapperServer +from nemo_rl.models.generation.interfaces import ( + CollectiveSenderSpec, + GenerationDatumSpec, + GenerationInterface, + GenerationOutputSpec, + verify_right_padding, +) + +LOGGER = logging.getLogger(__name__) + +_HTTP_MAX_ATTEMPTS = 3 +_HTTP_RETRY_DELAY_S = 1.0 +_RETRYABLE_HTTP_STATUS_CODES = {408, 429} + + +def _is_retryable_http_response(response: Any) -> bool: + """Return whether an internal HTTP error shape represents a transient error.""" + if not isinstance(response, dict): + return False + if "transport_error" in response or response.get("json_decode_error") is True: + return True + status = response.get("http_status") + return isinstance(status, int) and ( + status in _RETRYABLE_HTTP_STATUS_CODES or 500 <= status < 600 + ) + + +def _parse_dynamo_completion_response( + response: dict[str, Any], *, request_url: str +) -> tuple[list[int], list[float], bool]: + """Parse the Dynamo OpenAI completion response for direct generation.""" + if not isinstance(response, dict): + raise RuntimeError( + f"Dynamo completion response from {request_url} was not a JSON object." + ) + if response.get("status") == "error": + raise RuntimeError( + f"Dynamo completion request to {request_url} failed: " + f"{format_dynamo_error(response)}" + ) + + choices = response.get("choices") + if not isinstance(choices, list) or not choices: + raise RuntimeError( + f"Dynamo completion response from {request_url} did not include choices." + ) + choice = choices[0] + if not isinstance(choice, dict): + raise RuntimeError( + f"Dynamo completion response from {request_url} has invalid choice shape." + ) + + nvext = response.get("nvext") + if not isinstance(nvext, dict): + raise RuntimeError( + f"Dynamo completion response from {request_url} did not include nvext." + ) + completion_token_ids = nvext.get("completion_token_ids") + if not isinstance(completion_token_ids, list): + raise RuntimeError( + "Dynamo completion response did not include " + "nvext.completion_token_ids. Ensure the Dynamo frontend is " + "configured to return completion token IDs." + ) + generated_token_ids = [int(token_id) for token_id in completion_token_ids] + + if not generated_token_ids: + return ( + generated_token_ids, + [], + choice.get("finish_reason") == "length", + ) + + logprobs = choice.get("logprobs") + if not isinstance(logprobs, dict): + raise RuntimeError( + f"Dynamo completion response from {request_url} did not include " + "choice.logprobs." + ) + token_logprobs = logprobs.get("token_logprobs") + if not isinstance(token_logprobs, list): + raise RuntimeError( + f"Dynamo completion response from {request_url} did not include " + "choice.logprobs.token_logprobs." + ) + if len(token_logprobs) != len(generated_token_ids): + raise RuntimeError( + f"Dynamo completion response from {request_url} returned " + f"{len(token_logprobs)} token logprobs for " + f"{len(generated_token_ids)} generated tokens." + ) + + generated_logprobs = [] + for idx, logprob in enumerate(token_logprobs): + if not isinstance(logprob, (int, float)) or isinstance(logprob, bool): + raise RuntimeError( + f"Dynamo completion response from {request_url} returned invalid " + f"logprob {logprob!r} for generated token {idx}." + ) + generated_logprobs.append(float(logprob)) + + return ( + generated_token_ids, + generated_logprobs, + choice.get("finish_reason") == "length", + ) + + +class DynamoGeneration(GenerationInterface): + """Own a fixed Dynamo service fleet and expose it for NeMo-RL rollouts.""" + + def __init__( + self, + cluster: Optional[RayVirtualCluster], + config: dict[str, Any], + tokenizer: Any | None = None, + tokenizer_config: Optional[dict[str, Any]] = None, + ): + validated_config = DynamoConfig.model_validate(config) + self.cfg = validated_config.model_dump() + self._dynamo_cfg = validated_config.dynamo_cfg + dynamo_cfg = self._dynamo_cfg + vllm_cfg = validated_config.vllm_cfg + expose_http_server = vllm_cfg.expose_http_server + tokenizer_chat_template_kwargs: Optional[dict[str, Any]] = None + if expose_http_server: + if tokenizer is None: + raise RuntimeError( + "DynamoGeneration requires a tokenizer when exposing an " + "OpenAI-compatible rollout server." + ) + if ( + tokenizer_config is not None + and "chat_template_kwargs" in tokenizer_config + and tokenizer_config["chat_template_kwargs"] is not None + ): + chat_template_kwargs = tokenizer_config["chat_template_kwargs"] + if not isinstance(chat_template_kwargs, dict): + raise RuntimeError( + "policy.tokenizer.chat_template_kwargs must be a dictionary." + ) + tokenizer_chat_template_kwargs = dict(chat_template_kwargs) + if cluster is None: + raise RuntimeError( + "Managed Dynamo requires a non-colocated inference RayVirtualCluster." + ) + self._managed_runtime: Optional[ManagedDynamoRuntime] = ManagedDynamoRuntime( + cluster=cluster, + config=self.cfg, + ) + self._token_wrapper_server: Optional[DynamoTokenWrapperServer] = None + self._dynamo_frontend_base_url = "" + self.dp_openai_server_base_urls: list[Optional[str]] = [] + self._refit_channel: DynamoRefitChannel | None = None + self._metrics_sampler: DynamoMetricsSampler | None = None + try: + self._managed_runtime.start() + url = self._managed_runtime.frontend_url + self._dynamo_frontend_base_url = url + workers = self._managed_runtime.refit_workers() + self._refit_channel = DynamoRefitChannel( + workers, + engine_world_size=validated_config.engine_world_size, + control_timeout_s=dynamo_cfg.control_timeout_s, + validate_workers=self._managed_runtime.validate_workers, + ) + + if expose_http_server: + self._token_wrapper_server = DynamoTokenWrapperServer( + dynamo_frontend_base_url=url, + tokenizer=tokenizer, + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=( + dynamo_cfg.worker_args.exclude_tools_when_tool_choice_none + ), + request_timeout_s=dynamo_cfg.request_timeout_s, + ) + wrapper_url = self._token_wrapper_server.start() + self.dp_openai_server_base_urls = [wrapper_url] + print( + " [Dynamo] Forwarding rollout chat requests through token " + f"wrapper {wrapper_url} -> {url}", + flush=True, + ) + else: + self.dp_openai_server_base_urls = [None] + print(f" [Dynamo] Forwarding rollouts to {url}", flush=True) + + if vllm_cfg.enable_vllm_metrics_logger: + self._metrics_sampler = DynamoMetricsSampler( + workers, + interval_s=vllm_cfg.vllm_metrics_logger_interval, + include_prefixes=dynamo_cfg.metrics_include_prefixes, + exclude_prefixes=dynamo_cfg.metrics_exclude_prefixes, + ) + self._metrics_sampler.start() + except Exception: + self.shutdown() + raise + + # ------------------------------------------------------------------ + # GenerationInterface — lifecycle + # ------------------------------------------------------------------ + + def prepare_for_generation(self, *args: Any, **kwargs: Any) -> bool: + return True + + @property + def frontend_url(self) -> str: + """Return the internal managed Dynamo OpenAI frontend URL.""" + if not self._dynamo_frontend_base_url: + raise RuntimeError("DynamoGeneration does not have a frontend URL.") + return self._dynamo_frontend_base_url + + def finish_generation(self, *args: Any, **kwargs: Any) -> bool: + """Invalidate cached rollout state after synchronous generation.""" + return self.invalidate_kv_cache() + + def get_logger_metrics(self) -> dict[str, Any]: + """Return per-worker Dynamo metric timelines for generation logging.""" + sampler = self._metrics_sampler + return {} if sampler is None else sampler.snapshot() + + def clear_logger_metrics(self) -> None: + """Clear the Dynamo metric timelines for the next logging window.""" + sampler = self._metrics_sampler + if sampler is not None: + sampler.clear() + + def get_inference_world_size(self) -> int: + """Return the number of vLLM ranks across all discovered workers.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + return channel.inference_world_size + + def get_collective_sender_spec(self) -> CollectiveSenderSpec: + """Return vLLM's NCCL protocol and packed-transfer geometry.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + return channel.sender_spec + + def shutdown(self) -> bool: + """Stop process-local helpers and any driver-owned managed runtime.""" + sampler = self._metrics_sampler + self._metrics_sampler = None + if sampler is not None: + try: + sampler.shutdown() + except Exception: + LOGGER.exception("Failed to stop the Dynamo metrics sampler") + token_wrapper_server = self._token_wrapper_server + self._token_wrapper_server = None + if token_wrapper_server is not None: + try: + token_wrapper_server.shutdown() + except Exception: + LOGGER.exception("Failed to stop the Dynamo token wrapper") + managed_runtime = self._managed_runtime + self._managed_runtime = None + if managed_runtime is not None: + try: + managed_runtime.shutdown() + except Exception: + LOGGER.exception("Failed to stop the managed Dynamo runtime") + self._refit_channel = None + return True + + # ------------------------------------------------------------------ + # Pickling — async rollouts ship the GenerationInterface across Ray actors + # ------------------------------------------------------------------ + + def __getstate__(self) -> dict[str, Any]: + """Serialize only HTTP clients needed by Ray rollout actors. + + Driver-owned subprocesses, threads, and Ray worker handles are excluded. + The endpoint-only refit channel is retained so AREAL-style cache + invalidation still reaches every managed worker after deserialization. + """ + refit_channel = self._refit_channel + return { + "cfg": self.cfg, + "dp_openai_server_base_urls": self.dp_openai_server_base_urls, + "_dynamo_frontend_base_url": self._dynamo_frontend_base_url, + "_refit_channel": ( + None if refit_channel is None else refit_channel.client_copy() + ), + } + + def __setstate__(self, state: dict[str, Any]) -> None: + """Restore a client-only rollout copy with no service ownership.""" + self.cfg = state["cfg"] + validated_config = DynamoConfig.model_validate(self.cfg) + self._dynamo_cfg = validated_config.dynamo_cfg + self.dp_openai_server_base_urls = state["dp_openai_server_base_urls"] + frontend_url = state["_dynamo_frontend_base_url"] + if not isinstance(frontend_url, str) or not frontend_url: + raise RuntimeError("Pickled DynamoGeneration has no frontend URL.") + self._dynamo_frontend_base_url = frontend_url + self._token_wrapper_server = None + self._managed_runtime = None + self._metrics_sampler = None + self._refit_channel = state["_refit_channel"] + + def _completion_url(self) -> str: + base_url = self._dynamo_frontend_base_url + if not base_url: + raise RuntimeError("DynamoGeneration does not have a frontend URL.") + return f"{base_url.rstrip('/')}/completions" + + def _request_timeout_s(self) -> float: + return self._dynamo_cfg.request_timeout_s + + def _merge_stop_strings(self, batch_stop_strings: Any) -> Optional[list[str]]: + stop_set: set[str] = set() + + configured_stop_strings = self.cfg.get("stop_strings") + if configured_stop_strings is not None: + stop_set.update(configured_stop_strings) + + if batch_stop_strings is not None: + for sample_stop_strings in batch_stop_strings: + if not sample_stop_strings: + continue + if isinstance(sample_stop_strings, str): + stop_set.add(sample_stop_strings) + else: + stop_set.update(sample_stop_strings) + + if len(stop_set) > 32: + raise ValueError( + "Dynamo supports at most 32 stop strings after merging configured " + "and per-sample values" + ) + return list(stop_set) if stop_set else None + + def _prompt_token_ids( + self, + data: BatchedDataDict["GenerationDatumSpec"], + sample_idx: int, + ) -> list[int]: + if "vllm_content" in data: + raise NotImplementedError( + "DynamoGeneration direct generate() supports token-ID LLM " + "prompts only; multimodal vllm_content is not supported." + ) + + input_length = int(data["input_lengths"][sample_idx].item()) + return data["input_ids"][sample_idx, :input_length].tolist() + + def _build_completion_request( + self, + *, + prompt_token_ids: list[int], + greedy: bool, + stop_strings: Optional[list[str]], + max_new_tokens: int, + ) -> dict[str, Any]: + top_k_cfg = self.cfg["top_k"] + top_k_val = 1 if greedy else (top_k_cfg if top_k_cfg is not None else -1) + + payload: dict[str, Any] = { + "model": self.cfg["model_name"], + "prompt": prompt_token_ids, + "max_tokens": int(max_new_tokens), + "temperature": 0.0 if greedy else self.cfg["temperature"], + "top_p": self.cfg["top_p"], + "top_k": top_k_val, + "n": 1, + "logprobs": 0, + "include_stop_str_in_output": True, + "nvext": {"extra_fields": ["completion_token_ids"]}, + } + + if self.cfg["stop_token_ids"] is not None: + payload["stop_token_ids"] = self.cfg["stop_token_ids"] + if stop_strings is not None: + payload["stop"] = stop_strings + + return payload + + def _allowed_new_tokens(self, input_length: int) -> int: + """Return the generation budget for a prompt.""" + remaining_ctx = int(self.cfg["vllm_cfg"]["max_model_len"]) - input_length + if remaining_ctx <= 0: + raise ValueError( + f"Dynamo prompt length {input_length} must be less than " + f"vllm_cfg.max_model_len={self.cfg['vllm_cfg']['max_model_len']}" + ) + return min(self.cfg["max_new_tokens"], remaining_ctx) + + def _assert_response_within_context( + self, *, input_length: int, generated_length: int + ) -> None: + response_length = input_length + generated_length + max_model_len = int(self.cfg["vllm_cfg"]["max_model_len"]) + if response_length > max_model_len: + raise AssertionError( + "Dynamo response length exceeded " + f"vllm_cfg.max_model_len: {response_length} > {max_model_len}" + ) + + async def _post_completion_request( + self, + *, + prompt_token_ids: list[int], + greedy: bool, + stop_strings: Optional[list[str]], + max_new_tokens: int, + ) -> tuple[list[int], list[float], bool]: + request_url = self._completion_url() + payload = self._build_completion_request( + prompt_token_ids=prompt_token_ids, + greedy=greedy, + stop_strings=stop_strings, + max_new_tokens=max_new_tokens, + ) + response: dict[str, Any] = {} + for attempt in range(1, _HTTP_MAX_ATTEMPTS + 1): + response = await async_http_post_json( + request_url, + payload, + self._request_timeout_s(), + ) + if not _is_retryable_http_response(response): + break + if attempt == _HTTP_MAX_ATTEMPTS: + break + LOGGER.warning( + "Dynamo completion attempt %d/%d failed; retrying in %.1fs: %s", + attempt, + _HTTP_MAX_ATTEMPTS, + _HTTP_RETRY_DELAY_S, + format_dynamo_error(response), + ) + await asyncio.sleep(_HTTP_RETRY_DELAY_S) + return _parse_dynamo_completion_response(response, request_url=request_url) + + def _single_sample_output( + self, + *, + input_ids: torch.Tensor, + input_length: int, + generated_token_ids: list[int], + generated_logprobs: list[float], + truncated: bool, + ) -> BatchedDataDict["GenerationOutputSpec"]: + output_length = input_length + len(generated_token_ids) + self._assert_response_within_context( + input_length=input_length, + generated_length=len(generated_token_ids), + ) + output_ids = torch.full( + (output_length,), + self.cfg["_pad_token_id"], + dtype=input_ids.dtype, + device=input_ids.device, + ) + output_ids[:input_length] = input_ids[:input_length] + if generated_token_ids: + output_ids[input_length:output_length] = torch.tensor( + generated_token_ids, + dtype=input_ids.dtype, + device=input_ids.device, + ) + + logprobs = torch.zeros( + (1, output_length), + dtype=torch.float32, + device=input_ids.device, + ) + for idx, logprob in enumerate(generated_logprobs[: len(generated_token_ids)]): + logprobs[0, input_length + idx] = logprob + + return BatchedDataDict[GenerationOutputSpec]( + { + "output_ids": output_ids.unsqueeze(0), + "logprobs": logprobs, + "generation_lengths": torch.tensor( + [len(generated_token_ids)], + dtype=torch.long, + device=input_ids.device, + ), + "unpadded_sequence_lengths": torch.tensor( + [output_length], + dtype=torch.long, + device=input_ids.device, + ), + "truncated": torch.tensor( + [truncated], + dtype=torch.bool, + device=input_ids.device, + ), + } + ) + + def generate( + self, + data: BatchedDataDict["GenerationDatumSpec"], + greedy: bool = False, + ) -> BatchedDataDict["GenerationOutputSpec"]: + """Reject the unused blocking interface. + + Both synchronous and asynchronous GRPO trainers use ``generate_async`` + for the managed HTTP frontend. + """ + raise NotImplementedError( + "Dynamo generation uses generate_async() for both synchronous and " + "asynchronous GRPO trainers" + ) + + async def generate_async( + self, + data: BatchedDataDict["GenerationDatumSpec"], + greedy: bool = False, + ) -> AsyncGenerator[tuple[int, BatchedDataDict["GenerationOutputSpec"]], None]: + """Generate one token-ID prompt asynchronously through the managed frontend.""" + assert isinstance(data, BatchedDataDict), ( + f"data must be a BatchedDataDict, got type: {type(data)}" + ) + assert "input_ids" in data and "input_lengths" in data, ( + "input_ids and input_lengths are required in data for Dynamo generation" + ) + if len(data["input_ids"]) == 0: + return + + verify_right_padding(data, pad_value=self.cfg["_pad_token_id"]) + + input_ids_batch = data["input_ids"] + input_lengths_batch = data["input_lengths"] + batch_size = input_ids_batch.shape[0] + assert batch_size == 1, ( + "generate_async is restricted to handle only single samples, " + f"but received batch_size={batch_size}. Please handle batching " + "outside this method." + ) + sample_idx = 0 + input_length = int(input_lengths_batch[sample_idx].item()) + batch_stop_strings = data.get("stop_strings", [[] for _ in range(batch_size)]) + per_sample_stop_strings = None + if batch_stop_strings and sample_idx < len(batch_stop_strings): + per_sample_stop_strings = batch_stop_strings[sample_idx] + final_stop_strings = self._merge_stop_strings( + [per_sample_stop_strings] if per_sample_stop_strings else None + ) + + allowed_new_tokens = self._allowed_new_tokens(input_length) + input_ids = input_ids_batch[sample_idx] + ( + generated_token_ids, + generated_logprobs, + truncated, + ) = await self._post_completion_request( + prompt_token_ids=self._prompt_token_ids(data, sample_idx), + greedy=greedy, + stop_strings=final_stop_strings, + max_new_tokens=allowed_new_tokens, + ) + + yield ( + sample_idx, + self._single_sample_output( + input_ids=input_ids, + input_length=input_length, + generated_token_ids=generated_token_ids, + generated_logprobs=generated_logprobs, + truncated=truncated, + ), + ) + + def init_collective( + self, + ip: str, + port: int, + world_size: int, + *, + train_world_size: int, + ) -> list[ray.ObjectRef]: + """Initialize native vLLM NCCL transfer on every managed worker.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + return channel.init_collective( + ip, + port, + world_size, + train_world_size=train_world_size, + ) + + def prepare_refit_info(self, state_dict_info: dict[str, Any]) -> None: + """Serialize checkpoint-format tensor metadata for native vLLM refit.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + channel.prepare(state_dict_info) + + def update_weights_via_ipc_zmq(self) -> list[ray.ObjectRef]: + raise NotImplementedError( + "DynamoGeneration only supports NCCL weight transfer." + ) + + def update_weights_from_collective(self) -> list[ray.ObjectRef]: + """Receive packed checkpoint-format weights on every Dynamo worker.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + return channel.update_weights() + + def invalidate_kv_cache(self) -> bool: + """Flush every fixed Dynamo worker's prefix/KV cache.""" + channel = self._refit_channel + if channel is None: + raise RuntimeError("Dynamo refit channel is unavailable") + return channel.flush_cache() diff --git a/nemo_rl/models/generation/dynamo/dynamo_worker.py b/nemo_rl/models/generation/dynamo/dynamo_worker.py new file mode 100644 index 00000000000..624d58ca09a --- /dev/null +++ b/nemo_rl/models/generation/dynamo/dynamo_worker.py @@ -0,0 +1,304 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ray actors used by the fixed, managed Dynamo vLLM fleet.""" + +import json +import os +import signal +import socket +import subprocess +import time +from pathlib import Path +from typing import Any + +import ray + +from nemo_rl.distributed.virtual_cluster import _get_free_port_local, _get_node_ip_local +from nemo_rl.models.generation.dynamo.arguments import ( + build_dynamo_vllm_argv, + build_managed_worker_env, + redact_argv, + redact_environment, +) +from nemo_rl.models.generation.dynamo.config import ( + VLLM_PACKED_BUFFER_SIZE_BYTES, + VLLM_PACKED_NUM_BUFFERS, + DynamoConfig, +) +from nemo_rl.models.generation.dynamo.venv import ( + get_dynamo_python, + get_dynamo_venv_dir, +) + + +@ray.remote(num_cpus=0) +class DynamoGpuReservation: # pragma: no cover + """Hold one placement-group GPU while a sibling actor owns the engine.""" + + def __init__(self) -> None: + self._process_pid: int | None = None + + def metadata(self) -> dict[str, Any]: + gpu_ids = [int(float(gpu_id)) for gpu_id in ray.get_gpu_ids()] + if len(gpu_ids) != 1: + raise RuntimeError( + f"Expected one GPU for Dynamo reservation, got {gpu_ids}." + ) + return {"node_ip": _get_node_ip_local(), "gpu_id": gpu_ids[0]} + + def select_free_port( + self, + *, + port_range_low: int, + port_range_high: int, + excluded_ports: list[int], + ) -> int: + """Select an unused node-local port from a half-open range.""" + return _get_free_port_local( + port_range_low, + port_range_high, + max_retries=None, + excluded_ports=set(excluded_ports), + ) + + def register_process_group(self, pid: int) -> bool: + """Record the colocated worker process group for failure cleanup.""" + if self._process_pid not in (None, pid): + raise RuntimeError( + "Dynamo GPU reservation already owns process group " + f"{self._process_pid}; cannot register {pid}." + ) + self._process_pid = pid + return True + + def cleanup_process_group(self) -> bool: + """Best-effort cleanup if the subprocess-owning actor died first.""" + pid = self._process_pid + if pid is None: + return True + try: + os.killpg(pid, signal.SIGTERM) + except ProcessLookupError: + self._process_pid = None + return True + time.sleep(2) + try: + os.killpg(pid, signal.SIGKILL) + except ProcessLookupError: + pass + self._process_pid = None + return True + + +@ray.remote(num_cpus=0) +class DynamoVllmWorker: # pragma: no cover + """Own one ``dynamo.vllm`` subprocess for a model-parallel GPU group.""" + + def __init__( + self, + config: dict[str, Any], + *, + namespace: str, + group_name: str, + cuda_devices: list[int], + system_port: int, + vllm_port: int, + manager_env: dict[str, str], + startup_timeout_s: float, + seed: int, + cleanup_reservation: ray.actor.ActorHandle, + ) -> None: + self._group_name = group_name + self._node_ip = _get_node_ip_local() + self._system_port = system_port + self._vllm_port = vllm_port + self._process: subprocess.Popen | None = None + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.bind(("0.0.0.0", system_port)) + except OSError as exc: + raise RuntimeError( + f"DYN_SYSTEM_PORT {system_port} is unavailable for {group_name}." + ) from exc + + validated_config = DynamoConfig.model_validate(config) + dynamo_cfg = validated_config.dynamo_cfg + dynamo_python = get_dynamo_python() + dynamo_venv = str(get_dynamo_venv_dir()) + vllm_cfg = validated_config.vllm_cfg.model_dump() + vllm_kwargs = dict(validated_config.vllm_kwargs) + configured_env = dict(vllm_cfg.get("env_vars") or {}) + worker_env = build_managed_worker_env( + base_env=os.environ, + configured_env=configured_env, + manager_env={ + **manager_env, + "CUDA_VISIBLE_DEVICES": ",".join(str(gpu) for gpu in cuda_devices), + "DYN_SYSTEM_PORT": str(system_port), + "PYTHONHASHSEED": "0", + "VLLM_PORT": str(vllm_port), + "VLLM_SKIP_P2P_CHECK": "1", + "VIRTUAL_ENV": dynamo_venv, + "UV_PROJECT_ENVIRONMENT": dynamo_venv, + }, + ) + argv = build_dynamo_vllm_argv( + model_name=config["model_name"], + namespace=namespace, + seed=seed, + vllm_cfg=vllm_cfg, + vllm_kwargs=vllm_kwargs, + dynamo_cfg=dynamo_cfg, + ) + self._validate_argv( + dynamo_python, + argv, + worker_env, + timeout_s=startup_timeout_s, + ) + + command = [dynamo_python, "-m", "dynamo.vllm", *argv] + relevant_env = { + key: value + for key, value in worker_env.items() + if key.startswith(("CUDA_", "DYN_", "ETCD_", "NATS_", "NCCL_", "VLLM_")) + } + print( + f" [Dynamo:{group_name}] launching argv={redact_argv(command)!r} " + f"env={redact_environment(relevant_env)!r} " + f"system_url={self.system_url}", + flush=True, + ) + try: + self._process = subprocess.Popen( + command, env=worker_env, start_new_session=True + ) + ray.get( + cleanup_reservation.register_process_group.remote(self._process.pid) + ) + self._wait_for_system_port(startup_timeout_s) + except Exception: + self._stop_process() + raise + + @property + def system_url(self) -> str: + host = f"[{self._node_ip}]" if ":" in self._node_ip else self._node_ip + return f"http://{host}:{self._system_port}" + + @staticmethod + def _validate_argv( + dynamo_python: str, + argv: list[str], + env: dict[str, str], + *, + timeout_s: float, + ) -> None: + validator = Path(__file__).with_name("validate_dynamo_vllm_args.py") + command = [ + dynamo_python, + str(validator), + json.dumps(argv), + json.dumps( + { + "buffer_size_bytes": VLLM_PACKED_BUFFER_SIZE_BYTES, + "num_buffers": VLLM_PACKED_NUM_BUFFERS, + } + ), + ] + try: + result = subprocess.run( + command, + env=env, + capture_output=True, + text=True, + check=False, + timeout=timeout_s, + ) + except subprocess.TimeoutExpired as error: + raise RuntimeError( + "Resolved dynamo.vllm argument validation exceeded " + f"startup_timeout_s={timeout_s}." + ) from error + if result.returncode != 0: + raise RuntimeError( + "Resolved dynamo.vllm arguments failed validation: " + f"stdout={result.stdout!r}, stderr={result.stderr!r}" + ) + + def _wait_for_system_port(self, timeout_s: float) -> None: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + process = self._process + if process is not None and process.poll() is not None: + raise RuntimeError( + f"dynamo.vllm for {self._group_name} exited with " + f"code {process.returncode} before its system endpoint was ready." + ) + try: + with socket.create_connection( + ("127.0.0.1", self._system_port), timeout=1 + ): + return + except OSError: + time.sleep(0.5) + raise RuntimeError( + f"dynamo.vllm for {self._group_name} did not open DYN_SYSTEM_PORT " + f"{self._system_port} within {timeout_s}s." + ) + + def metadata(self) -> dict[str, Any]: + process = self._process + if process is None: + raise RuntimeError(f"dynamo.vllm for {self._group_name} is not running.") + return { + "instance_id": self._group_name, + "system_url": self.system_url, + "process_pid": process.pid, + "vllm_port": self._vllm_port, + } + + def is_alive(self) -> bool: + return self._process is not None and self._process.poll() is None + + def _stop_process(self) -> None: + process = self._process + if process is None: + return + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + if process.poll() is None: + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=5) + self._process = None + + def shutdown(self) -> bool: + process = self._process + if process is None: + return True + self._stop_process() + print( + f" [Dynamo:{self._group_name}] stopped pid={process.pid}", + flush=True, + ) + return True diff --git a/nemo_rl/models/generation/dynamo/http_client.py b/nemo_rl/models/generation/dynamo/http_client.py new file mode 100644 index 00000000000..8f69bee6b4d --- /dev/null +++ b/nemo_rl/models/generation/dynamo/http_client.py @@ -0,0 +1,96 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Small HTTP client helpers shared by managed Dynamo components.""" + +import json +import urllib.error +import urllib.request +from typing import Any + +import aiohttp + + +def _decode_json_object(body: bytes) -> dict[str, Any]: + """Decode an HTTP response body using the shared Dynamo error contract.""" + try: + decoded = json.loads(body) + except json.JSONDecodeError: + return { + "status": "error", + "json_decode_error": True, + "raw": body.decode("utf-8", "replace"), + } + if not isinstance(decoded, dict): + return {"status": "error", "raw": repr(decoded)} + return decoded + + +def http_post_json( + url: str, payload: dict[str, Any], timeout_s: float +) -> dict[str, Any]: + """POST JSON and return either the decoded object or an error object.""" + data = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + url, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout_s) as response: + body = response.read() + except urllib.error.HTTPError as error: + raw = error.read().decode("utf-8", "replace") if error.fp else "" + return {"status": "error", "http_status": error.code, "raw": raw} + except (urllib.error.URLError, TimeoutError) as error: + return { + "status": "error", + "transport_error": f"{type(error).__name__}: {error}", + } + return _decode_json_object(body) + + +async def async_http_post_json( + url: str, payload: dict[str, Any], timeout_s: float +) -> dict[str, Any]: + """POST JSON without blocking the rollout actor event loop.""" + timeout = aiohttp.ClientTimeout(total=timeout_s) + try: + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post(url, json=payload) as response: + body = await response.read() + if response.status >= 400: + return { + "status": "error", + "http_status": response.status, + "raw": body.decode("utf-8", "replace"), + } + except (aiohttp.ClientError, TimeoutError) as error: + return { + "status": "error", + "transport_error": f"{type(error).__name__}: {error}", + } + return _decode_json_object(body) + + +def format_dynamo_error(response: dict[str, Any]) -> str: + """Format an error object returned by :func:`http_post_json`.""" + if "http_status" in response: + return f"HTTP {response['http_status']}: {response.get('raw', '')}" + if "transport_error" in response: + return str(response["transport_error"]) + if "raw" in response: + return str(response["raw"]) + return str(response) diff --git a/nemo_rl/models/generation/dynamo/managed_runtime.py b/nemo_rl/models/generation/dynamo/managed_runtime.py new file mode 100644 index 00000000000..34e0eb77696 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/managed_runtime.py @@ -0,0 +1,464 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Driver-owned lifecycle for a fixed Ray-managed Dynamo deployment.""" + +import atexit +import json +import logging +import os +import re +import shutil +import signal +import socket +import subprocess +import tempfile +import time +import urllib.error +import urllib.request +from typing import Any + +from nemo_rl.distributed.virtual_cluster import ( + DEFAULT_DYNAMO_CONTROL_PORT_RANGE_HIGH, + DEFAULT_DYNAMO_CONTROL_PORT_RANGE_LOW, + DEFAULT_DYNAMO_HTTP_PORT_RANGE_HIGH, + DEFAULT_DYNAMO_HTTP_PORT_RANGE_LOW, + RayVirtualCluster, + _get_free_port_local, + _get_node_ip_local, +) +from nemo_rl.models.generation.dynamo.arguments import ( + build_dynamo_frontend_argv, + redact_argv, + redact_environment, +) +from nemo_rl.models.generation.dynamo.config import DynamoConfig +from nemo_rl.models.generation.dynamo.venv import ( + get_dynamo_executable, + get_dynamo_python, +) +from nemo_rl.models.generation.dynamo.worker_pool import FixedDynamoWorkerPool + +LOGGER = logging.getLogger(__name__) + + +def _managed_namespace() -> str: + raw = f"nemo-rl-{os.environ.get('SLURM_JOB_ID', os.getpid())}" + namespace = re.sub(r"[^a-zA-Z0-9_-]+", "-", str(raw)).strip("-_").lower() + if not namespace: + raise ValueError(f"Could not derive a valid Dynamo namespace from {raw!r}.") + return namespace + + +class ManagedDynamoRuntime: + """Own etcd, NATS, frontend, and a fixed Ray actor worker fleet.""" + + def __init__( + self, + *, + cluster: RayVirtualCluster, + config: dict[str, Any], + ) -> None: + validated_config = DynamoConfig.model_validate(config) + self._cluster = cluster + self._config = validated_config.model_dump() + self._dynamo_cfg = validated_config.dynamo_cfg + self._engine_world_size = validated_config.engine_world_size + if self._engine_world_size > cluster.num_gpus_per_node: + raise ValueError( + "Managed Dynamo requires each TP/PP engine group to fit on one " + f"node: tp*pp={self._engine_world_size} exceeds " + f"cluster.num_gpus_per_node={cluster.num_gpus_per_node}" + ) + self._namespace = _managed_namespace() + self._host = "" + self._etcd_port = 0 + self._etcd_peer_port = 0 + self._nats_port = 0 + self._frontend_port = 0 + self._manager_env: dict[str, str] = {} + self._started = False + self._atexit_registered = False + self._etcd_process: subprocess.Popen | None = None + self._nats_process: subprocess.Popen | None = None + self._frontend_process: subprocess.Popen | None = None + self._etcd_data_dir: str | None = None + self._nats_data_dir: str | None = None + self._pool: FixedDynamoWorkerPool | None = None + + @property + def frontend_url(self) -> str: + if not self._started: + raise RuntimeError("Managed Dynamo runtime has not been started") + host = f"[{self._host}]" if ":" in self._host else self._host + return f"http://{host}:{self._frontend_port}/v1" + + def start(self) -> None: + """Start the complete managed service fleet.""" + if self._started: + raise RuntimeError("Managed Dynamo runtime is already started") + # Managed services use new process groups and would survive an + # exception that escapes GRPO setup. Keep an interpreter-exit fallback + # in addition to the normal explicit shutdown path. + atexit.register(self.shutdown) + self._atexit_registered = True + self._host = _get_node_ip_local() + used_ports: set[int] = set() + + def allocate_port(*, low: int, high: int) -> int: + port = _get_free_port_local( + low, + high, + max_retries=None, + excluded_ports=used_ports, + ) + used_ports.add(port) + return port + + self._etcd_port = allocate_port( + low=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_LOW, + high=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_HIGH, + ) + self._etcd_peer_port = allocate_port( + low=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_LOW, + high=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_HIGH, + ) + self._nats_port = allocate_port( + low=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_LOW, + high=DEFAULT_DYNAMO_CONTROL_PORT_RANGE_HIGH, + ) + self._frontend_port = allocate_port( + low=DEFAULT_DYNAMO_HTTP_PORT_RANGE_LOW, + high=DEFAULT_DYNAMO_HTTP_PORT_RANGE_HIGH, + ) + self._manager_env = { + "ETCD_ENDPOINTS": f"http://{self._host}:{self._etcd_port}", + "NATS_SERVER": f"nats://{self._host}:{self._nats_port}", + "DYN_NAMESPACE": self._namespace, + "DYN_DISCOVERY_BACKEND": "etcd", + # Dynamo 1.3.0's legacy tool jail rebuilds response chunks with + # nvext=None. Its qwen3/deepseek v2 path preserves engine_data, + # which NeMo-Gym needs for exact token IDs and log probabilities. + "DYN_ENABLE_EXPERIMENTAL_PARSERS_V2": "1", + "DYN_REQUEST_PLANE": "tcp", + "DYN_EVENT_PLANE": "nats", + "DYN_HEALTH_CHECK_ENABLED": "false", + "DYN_SDK_DISABLE_ANSI_LOGGING": "1", + "DYN_RL_INIT_WEIGHTS_TIMEOUT_S": str(self._dynamo_cfg.control_timeout_s), + } + print( + f" [Dynamo] managed environment={redact_environment(self._manager_env)!r}", + flush=True, + ) + try: + self._start_etcd() + self._start_nats() + self._pool = FixedDynamoWorkerPool( + cluster=self._cluster, + config=self._config, + namespace=self._namespace, + engine_world_size=self._engine_world_size, + manager_env=self._manager_env, + startup_timeout_s=self._dynamo_cfg.startup_timeout_s, + ) + self._pool.start() + self._start_frontend() + self._wait_for_frontend(self._pool.size) + self._started = True + except Exception: + self.shutdown() + raise + + @staticmethod + def _stop_process( + process: subprocess.Popen | None, label: str, timeout_s: float = 15 + ) -> None: + if process is None: + return + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + if process.poll() is None: + try: + process.wait(timeout=timeout_s) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=5) + print(f" [Dynamo] {label} stopped pid={process.pid}", flush=True) + + def _service_env(self) -> dict[str, str]: + env = { + key: value + for key, value in os.environ.items() + if not key.startswith(("DYN_", "ETCD_", "NATS_")) + } + env.update(self._manager_env) + return env + + def _frontend_env(self) -> dict[str, str]: + env = self._service_env() + frontend_args = self._dynamo_cfg.frontend_args + env["DYN_TOKENIZER"] = frontend_args.tokenizer + if frontend_args.tokenizer_cache: + env["DYN_TOKENIZER_CACHE"] = "1" + env["DYN_TOKENIZER_CACHE_BYTES"] = str(frontend_args.tokenizer_cache_bytes) + return env + + def _start_etcd(self) -> None: + self._etcd_data_dir = tempfile.mkdtemp(prefix="nemorl_dynamo_etcd_") + peer_url = f"http://{self._host}:{self._etcd_peer_port}" + command = [ + get_dynamo_executable("etcd"), + "--listen-client-urls", + f"http://0.0.0.0:{self._etcd_port}", + "--advertise-client-urls", + f"http://{self._host}:{self._etcd_port}", + "--listen-peer-urls", + f"http://0.0.0.0:{self._etcd_peer_port}", + "--initial-advertise-peer-urls", + peer_url, + "--initial-cluster", + f"default={peer_url}", + "--data-dir", + self._etcd_data_dir, + ] + self._etcd_process = subprocess.Popen( + command, env=self._service_env(), start_new_session=True + ) + self._wait_for_etcd() + print(f" [Dynamo] etcd ready on {self._host}:{self._etcd_port}", flush=True) + + def _wait_for_etcd(self) -> None: + url = f"http://127.0.0.1:{self._etcd_port}/health" + deadline = time.monotonic() + min(self._dynamo_cfg.startup_timeout_s, 60) + while time.monotonic() < deadline: + if self._etcd_process is not None and self._etcd_process.poll() is not None: + raise RuntimeError( + f"etcd exited with code {self._etcd_process.returncode}." + ) + try: + with urllib.request.urlopen(url, timeout=2) as response: + if response.status == 200: + return + except (urllib.error.URLError, TimeoutError): + time.sleep(0.5) + raise RuntimeError(f"etcd did not become healthy at {url}.") + + def _start_nats(self) -> None: + self._nats_data_dir = tempfile.mkdtemp(prefix="nemorl_dynamo_nats_") + self._nats_process = subprocess.Popen( + [ + get_dynamo_executable("nats-server"), + "-js", + "-sd", + self._nats_data_dir, + "-p", + str(self._nats_port), + ], + env=self._service_env(), + start_new_session=True, + ) + self._wait_for_port(self._nats_port, "NATS", self._nats_process) + print(f" [Dynamo] NATS ready on {self._host}:{self._nats_port}", flush=True) + + def _wait_for_port( + self, port: int, label: str, process: subprocess.Popen | None = None + ) -> None: + deadline = time.monotonic() + min(self._dynamo_cfg.startup_timeout_s, 60) + while time.monotonic() < deadline: + if process is not None and process.poll() is not None: + raise RuntimeError(f"{label} exited with code {process.returncode}.") + try: + with socket.create_connection(("127.0.0.1", port), timeout=1): + return + except OSError: + time.sleep(0.5) + raise RuntimeError(f"{label} did not open port {port}.") + + def _start_frontend(self) -> None: + argv = build_dynamo_frontend_argv( + host="0.0.0.0", + port=self._frontend_port, + namespace=self._namespace, + dynamo_cfg=self._dynamo_cfg, + ) + command = [ + get_dynamo_python(), + "-m", + "dynamo.frontend", + *argv, + ] + print( + f" [Dynamo] launching frontend argv={redact_argv(command)!r}", flush=True + ) + frontend_env = self._frontend_env() + tokenizer_env = { + key: value + for key, value in frontend_env.items() + if key.startswith("DYN_TOKENIZER") + } + print( + f" [Dynamo] frontend tokenizer environment={tokenizer_env!r}", + flush=True, + ) + self._frontend_process = subprocess.Popen( + command, env=frontend_env, start_new_session=True + ) + + def _wait_for_frontend(self, expected_workers: int) -> None: + health_url = f"http://127.0.0.1:{self._frontend_port}/health" + models_url = f"http://127.0.0.1:{self._frontend_port}/v1/models" + expected_model = str(self._config["model_name"]) + deadline = time.monotonic() + self._dynamo_cfg.startup_timeout_s + last_counts = (0, 0) + last_models: set[str] = set() + while time.monotonic() < deadline: + if self._pool is None or not self._pool.is_alive(): + raise RuntimeError( + "A Ray-managed Dynamo vLLM worker exited while the frontend " + "was waiting for model registration." + ) + if ( + self._frontend_process is not None + and self._frontend_process.poll() is not None + ): + raise RuntimeError( + f"Dynamo frontend exited with code {self._frontend_process.returncode}." + ) + try: + with urllib.request.urlopen(health_url, timeout=5) as response: + payload = json.loads(response.read()) + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): + time.sleep(1) + continue + generate_ids: set[str] = set() + rl_ids: set[str] = set() + for instance in payload.get("instances", []): + if not isinstance(instance, dict): + continue + if instance.get("namespace") != self._namespace: + continue + if instance.get("component") != "backend": + continue + instance_id = instance.get("instance_id") + if instance_id is None: + continue + if instance.get("endpoint") == "generate": + generate_ids.add(str(instance_id)) + elif instance.get("endpoint") == "rl": + rl_ids.add(str(instance_id)) + last_counts = (len(generate_ids), len(rl_ids)) + if ( + last_counts == (expected_workers, expected_workers) + and generate_ids == rl_ids + ): + # /health reflects discovery registrations before the frontend's + # model watcher has necessarily installed its OpenAI routes. Do + # not expose frontend_url until the served model is visible; + # otherwise an immediate /v1/completions request can race with + # watcher setup and receive a transient 404. + try: + with urllib.request.urlopen(models_url, timeout=5) as response: + models_payload = json.loads(response.read()) + except ( + urllib.error.URLError, + TimeoutError, + json.JSONDecodeError, + ): + time.sleep(1) + continue + last_models = { + str(model["id"]) + for model in models_payload.get("data", []) + if isinstance(model, dict) and model.get("id") is not None + } + if expected_model not in last_models: + time.sleep(1) + continue + print( + f" [Dynamo] frontend ready with {expected_workers} generation " + "and RL workers", + flush=True, + ) + return + time.sleep(1) + raise RuntimeError( + "Dynamo frontend did not observe the fixed worker fleet within " + f"{self._dynamo_cfg.startup_timeout_s}s: expected={expected_workers}, " + f"last_generate={last_counts[0]}, last_rl={last_counts[1]}, " + f"expected_model={expected_model!r}, last_models={sorted(last_models)!r}." + ) + + def refit_workers(self) -> list[dict[str, Any]]: + self._assert_services_alive() + if self._pool is None: + raise RuntimeError("Managed Dynamo worker pool is not running.") + return self._pool.refit_workers() + + def validate_workers(self, expected: list[dict[str, Any]]) -> list[dict[str, Any]]: + self._assert_services_alive() + if self._pool is None: + raise RuntimeError("Managed Dynamo worker pool is not running.") + return self._pool.validate(expected) + + def _assert_services_alive(self) -> None: + for label, process in ( + ("etcd", self._etcd_process), + ("NATS", self._nats_process), + ("frontend", self._frontend_process), + ): + if process is None or process.poll() is not None: + code = None if process is None else process.returncode + raise RuntimeError( + f"Managed Dynamo {label} is not alive (code={code})." + ) + + def shutdown(self) -> None: + """Best-effort, idempotent teardown of every owned resource.""" + try: + self._stop_process(self._frontend_process, "frontend") + except Exception: + LOGGER.exception("Failed to stop the managed Dynamo frontend") + self._frontend_process = None + pool = self._pool + self._pool = None + if pool is not None: + try: + pool.shutdown() + except Exception: + LOGGER.exception("Failed to stop the managed Dynamo worker pool") + try: + self._stop_process(self._nats_process, "NATS") + except Exception: + LOGGER.exception("Failed to stop managed Dynamo NATS") + self._nats_process = None + try: + self._stop_process(self._etcd_process, "etcd") + except Exception: + LOGGER.exception("Failed to stop managed Dynamo etcd") + self._etcd_process = None + if self._etcd_data_dir is not None: + shutil.rmtree(self._etcd_data_dir, ignore_errors=True) + self._etcd_data_dir = None + if self._nats_data_dir is not None: + shutil.rmtree(self._nats_data_dir, ignore_errors=True) + self._nats_data_dir = None + if self._atexit_registered: + atexit.unregister(self.shutdown) + self._atexit_registered = False + self._started = False diff --git a/nemo_rl/models/generation/dynamo/metrics.py b/nemo_rl/models/generation/dynamo/metrics.py new file mode 100644 index 00000000000..d73cc95f8a7 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/metrics.py @@ -0,0 +1,213 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Prometheus polling lifecycle for managed Dynamo workers.""" + +import logging +import threading +import urllib.error +import urllib.request +from collections.abc import Sequence +from typing import Any + +from nemo_rl.models.generation.dynamo.refit import DynamoWorkerEndpoint + +LOGGER = logging.getLogger(__name__) + +DEFAULT_METRICS_EXCLUDE_PREFIXES = ("python_", "process_") +CURATED_METRICS_INCLUDE_PREFIXES = ( + "dynamo_component_gpu_cache_usage", + "dynamo_component_inflight_requests", + "dynamo_work_handler_queue_depth", + "dynamo_component_requests_total", + "dynamo_work_handler_time_to_first_response", + "vllm:num_requests_running", + "vllm:num_requests_waiting", + "vllm:kv_cache_usage_perc", + "vllm:generation_tokens", + "vllm:prompt_tokens_total", + "vllm:inter_token_latency", +) +CANONICAL_LOGGER_ALIASES = { + "inflight_batch_sizes": [ + "dynamo_component_inflight_requests", + "vllm_num_requests_running", + ], + "num_pending_samples": [ + "dynamo_work_handler_queue_depth", + "vllm_num_requests_waiting", + ], + "kv_cache_usage_perc": [ + "dynamo_component_gpu_cache_usage_percent", + "vllm_kv_cache_usage_perc", + "vllm_gpu_cache_usage_perc", + ], + "generation_tokens": [ + "vllm_generation_tokens_total", + "vllm_generation_tokens", + ], +} + + +def _http_get_text(url: str, timeout_s: float) -> str | None: + try: + with urllib.request.urlopen(url, timeout=timeout_s) as response: + return response.read().decode("utf-8", "replace") + except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError): + return None + + +def parse_prometheus_metrics( + text: str, + include_prefixes: tuple[str, ...] | None = None, + exclude_prefixes: tuple[str, ...] = DEFAULT_METRICS_EXCLUDE_PREFIXES, +) -> dict[str, float]: + """Parse Prometheus text exposition into summed scalar values.""" + metrics: dict[str, float] = {} + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if "{" in line: + name = line[: line.index("{")] + try: + value_text = line[line.rindex("}") + 1 :] + except ValueError: + continue + else: + parts = line.split(None, 1) + if len(parts) != 2: + continue + name, value_text = parts + if name.endswith(("_bucket", "_created")): + continue + if include_prefixes and not name.startswith(include_prefixes): + continue + if exclude_prefixes and name.startswith(exclude_prefixes): + continue + value_parts = value_text.split() + if not value_parts: + continue + try: + value = float(value_parts[0]) + except ValueError: + continue + key = name.replace(":", "_") + metrics[key] = metrics.get(key, 0.0) + value + return metrics + + +class DynamoMetricsSampler: + """Own polling thread, samples, and deterministic sampler shutdown.""" + + def __init__( + self, + workers: Sequence[dict[str, Any] | DynamoWorkerEndpoint], + *, + interval_s: float, + include_prefixes: list[str] | None, + exclude_prefixes: list[str] | None, + ) -> None: + self._workers = tuple( + DynamoWorkerEndpoint.from_metadata(worker) + if isinstance(worker, dict) + else worker + for worker in workers + ) + if not self._workers: + raise ValueError("Managed Dynamo metrics require at least one worker") + self._interval_s = interval_s + self._include_prefixes = ( + tuple(include_prefixes) + if include_prefixes is not None + else CURATED_METRICS_INCLUDE_PREFIXES + ) + self._exclude_prefixes = ( + tuple(exclude_prefixes) + if exclude_prefixes is not None + else DEFAULT_METRICS_EXCLUDE_PREFIXES + ) + self._samples: dict[str, dict[int, list[float]]] = {} + self._lock = threading.Lock() + self._stop = threading.Event() + self._thread: threading.Thread | None = None + + def start(self) -> None: + if self._thread is not None: + raise RuntimeError("Dynamo metrics sampler is already started") + self._thread = threading.Thread( + target=self._run, + name="dynamo-metrics-sampler", + daemon=True, + ) + self._thread.start() + + def _run(self) -> None: + self._stop.wait(min(2.0, self._interval_s)) + while not self._stop.is_set(): + for ordinal, worker in enumerate(self._workers): + text = _http_get_text( + f"{worker.system_url}/metrics", + timeout_s=self._interval_s + 2.0, + ) + if self._stop.is_set(): + break + if not text: + continue + metrics = parse_prometheus_metrics( + text, + self._include_prefixes, + self._exclude_prefixes, + ) + with self._lock: + for name, value in metrics.items(): + self._samples.setdefault(name, {}).setdefault( + ordinal, [] + ).append(value) + self._stop.wait(self._interval_s) + + def snapshot(self) -> dict[str, Any]: + with self._lock: + metrics = { + name: { + worker_id: list(samples) + for worker_id, samples in worker_metrics.items() + } + for name, worker_metrics in self._samples.items() + } + for alias, sources in CANONICAL_LOGGER_ALIASES.items(): + if alias in metrics: + continue + source = next((name for name in sources if name in metrics), None) + metrics[alias] = dict(metrics[source]) if source is not None else {} + if source is not None: + del metrics[source] + return metrics + + def clear(self) -> None: + with self._lock: + self._samples = {} + + def shutdown(self) -> None: + self._stop.set() + thread = self._thread + if ( + thread is not None + and thread is not threading.current_thread() + and thread.is_alive() + ): + thread.join(timeout=max(5.0, self._interval_s + 3.0)) + if thread.is_alive(): + LOGGER.warning("Dynamo metrics sampler did not stop before shutdown") + self._thread = None diff --git a/nemo_rl/models/generation/dynamo/refit.py b/nemo_rl/models/generation/dynamo/refit.py new file mode 100644 index 00000000000..72ea8865706 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/refit.py @@ -0,0 +1,271 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""NCCL refit protocol for a fixed managed Dynamo worker fleet.""" + +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import Any + +import ray + +from nemo_rl.models.generation.dynamo.config import ( + VLLM_PACKED_BUFFER_SIZE_BYTES, + VLLM_PACKED_NUM_BUFFERS, +) +from nemo_rl.models.generation.dynamo.http_client import ( + format_dynamo_error, + http_post_json, +) +from nemo_rl.models.generation.interfaces import CollectiveSenderSpec + + +@dataclass(frozen=True) +class DynamoWorkerEndpoint: + """Serializable identity and admin endpoint for one Dynamo vLLM engine.""" + + instance_id: str + system_url: str + + @classmethod + def from_metadata(cls, metadata: dict[str, Any]) -> "DynamoWorkerEndpoint": + return cls( + instance_id=str(metadata["instance_id"]), + system_url=str(metadata["system_url"]), + ) + + +@ray.remote(num_cpus=0) +def _post_worker_route( # pragma: no cover + *, + system_url: str, + route: str, + payload: dict[str, Any], + timeout_s: float, +) -> bool: + response = http_post_json( + f"{system_url}/engine/{route}", + payload, + timeout_s, + ) + if response.get("status") != "ok": + raise RuntimeError( + f"Dynamo worker {system_url} route {route} failed: " + f"{format_dynamo_error(response)}" + ) + return True + + +@ray.remote(num_cpus=0) +def _update_worker_weights( # pragma: no cover + *, + system_url: str, + update_info: dict[str, Any], + timeout_s: float, +) -> bool: + common = {"allow_unpaused": True, "reset_prefix_cache": False} + steps: tuple[tuple[str, dict[str, Any]], ...] = ( + ("start_weight_update", {"is_checkpoint_format": True}), + ("update_weights", {"update_info": update_info}), + ("finish_weight_update", {}), + ) + for engine_rpc, kwargs in steps: + response = http_post_json( + f"{system_url}/engine/update_weights_from_distributed", + {"engine_rpc": engine_rpc, **common, **kwargs}, + timeout_s, + ) + if response.get("status") != "ok": + raise RuntimeError( + f"Dynamo worker {system_url} RPC {engine_rpc} failed: " + f"{format_dynamo_error(response)}" + ) + return True + + +class DynamoRefitChannel: + """Closed refit protocol shared by driver and serialized rollout copies.""" + + def __init__( + self, + workers: Sequence[dict[str, Any] | DynamoWorkerEndpoint], + *, + engine_world_size: int, + control_timeout_s: float, + validate_workers: Callable[[list[dict[str, Any]]], list[dict[str, Any]]] + | None = None, + ) -> None: + self._worker_metadata = [ + dict(worker) for worker in workers if isinstance(worker, dict) + ] + self._workers = tuple( + DynamoWorkerEndpoint.from_metadata(worker) + if isinstance(worker, dict) + else worker + for worker in workers + ) + if not self._workers: + raise ValueError("Dynamo refit requires at least one worker endpoint") + self._engine_world_size = engine_world_size + self._control_timeout_s = control_timeout_s + self._validate_workers = validate_workers + self._update_info: dict[str, Any] | None = None + + def client_copy(self) -> "DynamoRefitChannel": + """Return a serializable endpoint-only channel for rollout actors.""" + return DynamoRefitChannel( + self._workers, + engine_world_size=self._engine_world_size, + control_timeout_s=self._control_timeout_s, + ) + + def _validated_workers(self) -> tuple[DynamoWorkerEndpoint, ...]: + if self._validate_workers is None: + return self._workers + current = self._validate_workers(self._worker_metadata) + endpoints = tuple(DynamoWorkerEndpoint.from_metadata(item) for item in current) + if endpoints != self._workers: + raise RuntimeError( + "Managed Dynamo worker membership changed after collective setup" + ) + return endpoints + + @property + def inference_world_size(self) -> int: + return len(self._workers) * self._engine_world_size + + @property + def sender_spec(self) -> CollectiveSenderSpec: + """Return vLLM's non-negotiated peer and packing contract.""" + return CollectiveSenderSpec( + nccl_peer="vllm", + buffer_size_bytes=VLLM_PACKED_BUFFER_SIZE_BYTES, + num_buffers=VLLM_PACKED_NUM_BUFFERS, + ) + + def prepare(self, state_dict_info: dict[str, Any] | None) -> None: + if state_dict_info is None: + raise ValueError("state_dict_info must not be None for Dynamo refit") + names: list[str] = [] + dtype_names: list[str] = [] + shapes: list[list[int]] = [] + for name, (shape, dtype) in state_dict_info.items(): + names.append(name) + dtype_names.append(str(dtype).removeprefix("torch.")) + shapes.append(list(shape)) + self._update_info = { + "names": names, + "dtype_names": dtype_names, + "shapes": shapes, + "packed": True, + } + + def init_collective( + self, + ip: str, + port: int, + world_size: int, + *, + train_world_size: int, + ) -> list[ray.ObjectRef]: + expected_world_size = train_world_size + self.inference_world_size + if world_size != expected_world_size: + raise ValueError( + f"NCCL world_size={world_size} does not match expected " + f"{expected_world_size}" + ) + workers = self._validated_workers() + return [ + _post_worker_route.remote( + system_url=worker.system_url, + route="init_weights_update_group", + payload={ + "engine_rpc": "init_weight_transfer_engine", + "init_info": { + "master_address": ip, + "master_port": port, + "rank_offset": train_world_size + + worker_index * self._engine_world_size, + "world_size": world_size, + }, + }, + timeout_s=self._control_timeout_s, + ) + for worker_index, worker in enumerate(workers) + ] + + def update_weights(self) -> list[ray.ObjectRef]: + if self._update_info is None: + raise RuntimeError( + "prepare_refit_info() must be called before Dynamo weight updates" + ) + return [ + _update_worker_weights.remote( + system_url=worker.system_url, + update_info=self._update_info, + timeout_s=self._control_timeout_s, + ) + for worker in self._validated_workers() + ] + + def flush_cache(self) -> bool: + """Drain, clear, and resume every worker using immutable endpoints.""" + pause_futures = [ + _post_worker_route.remote( + system_url=worker.system_url, + route="pause_generation", + payload={"mode": "wait", "clear_cache": True}, + timeout_s=self._control_timeout_s, + ) + for worker in self._workers + ] + paused_workers: list[DynamoWorkerEndpoint] = [] + pause_errors: list[str] = [] + for worker, future in zip(self._workers, pause_futures, strict=True): + try: + ray.get(future) + except Exception as error: + pause_errors.append(f"{worker.system_url}: {error}") + else: + paused_workers.append(worker) + + resume_futures = [ + ( + worker, + _post_worker_route.remote( + system_url=worker.system_url, + route="resume_generation", + payload={}, + timeout_s=self._control_timeout_s, + ), + ) + for worker in paused_workers + ] + resume_errors: list[str] = [] + for worker, future in resume_futures: + try: + ray.get(future) + except Exception as error: + resume_errors.append(f"{worker.system_url}: {error}") + + if pause_errors or resume_errors: + details = [] + if pause_errors: + details.append("pause/clear failed for " + "; ".join(pause_errors)) + if resume_errors: + details.append("resume failed for " + "; ".join(resume_errors)) + raise RuntimeError( + "Dynamo KV cache invalidation failed: " + "; ".join(details) + ) + return True diff --git a/nemo_rl/models/generation/dynamo/token_wrapper.py b/nemo_rl/models/generation/dynamo/token_wrapper.py new file mode 100644 index 00000000000..eab81fafa35 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/token_wrapper.py @@ -0,0 +1,556 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OpenAI-compatible token wrapper for NeMo-Gym traffic to Dynamo.""" + +import asyncio +import json +import threading +from contextlib import asynccontextmanager +from copy import deepcopy +from typing import Any, Optional + +from nemo_rl.distributed.virtual_cluster import ( + DEFAULT_DYNAMO_HTTP_PORT_RANGE_HIGH, + DEFAULT_DYNAMO_HTTP_PORT_RANGE_LOW, + _get_free_port_local, + _get_node_ip_local, +) +from nemo_rl.models.generation.openai_server_utils import replace_prefix_tokens + +_GYM_TOKEN_METADATA_FIELDS = ( + "prompt_token_ids", + "generation_token_ids", + "generation_log_probs", +) +_TOOL_ARGUMENT_MAPPING_ERROR = "Can only get item pairs from a mapping." + + +def _coerce_token_id_list(value: Any, field_name: str) -> list[int]: + if not isinstance(value, list): + raise ValueError(f"{field_name} must be a list of token IDs.") + try: + return [int(token_id) for token_id in value] + except (TypeError, ValueError) as e: + raise ValueError(f"{field_name} must contain only integer token IDs.") from e + + +def _coerce_logprob_list(value: Any, field_name: str) -> list[float]: + if not isinstance(value, list): + raise ValueError(f"{field_name} must be a list of numeric log probabilities.") + logprobs: list[float] = [] + for index, logprob in enumerate(value): + if not isinstance(logprob, (int, float)) or isinstance(logprob, bool): + raise ValueError(f"{field_name}[{index}] must be numeric.") + logprobs.append(float(logprob)) + return logprobs + + +def _strip_gym_token_metadata(messages: list[Any]) -> list[Any]: + stripped_messages = deepcopy(messages) + for message in stripped_messages: + if isinstance(message, dict): + for field in _GYM_TOKEN_METADATA_FIELDS: + message.pop(field, None) + return stripped_messages + + +def _chat_template_kwargs( + request_body: dict[str, Any], + tokenizer_chat_template_kwargs: Optional[dict[str, Any]], +) -> dict[str, Any]: + kwargs: dict[str, Any] = {} + if tokenizer_chat_template_kwargs is not None: + if not isinstance(tokenizer_chat_template_kwargs, dict): + raise ValueError("tokenizer chat_template_kwargs must be a JSON object.") + kwargs.update(tokenizer_chat_template_kwargs) + + request_kwargs = request_body.get("chat_template_kwargs") + if request_kwargs is not None: + if not isinstance(request_kwargs, dict): + raise ValueError("chat_template_kwargs must be a JSON object.") + kwargs.update(request_kwargs) + + if "reasoning_effort" in request_body: + kwargs["reasoning_effort"] = request_body["reasoning_effort"] + return kwargs + + +def _request_add_generation_prompt(request_body: dict[str, Any]) -> bool: + if "add_generation_prompt" in request_body: + return bool(request_body["add_generation_prompt"]) + return not bool(request_body.get("continue_final_message", False)) + + +def _apply_chat_template( + *, + tokenizer: Any, + request_body: dict[str, Any], + messages: list[Any], + tokenizer_chat_template_kwargs: Optional[dict[str, Any]], + exclude_tools_when_tool_choice_none: bool, + add_generation_prompt: bool, + tokenize: bool, +) -> Any: + tools = request_body.get("tools") + if ( + exclude_tools_when_tool_choice_none + and request_body.get("tool_choice") == "none" + ): + tools = None + + apply_chat_template = type(tokenizer).apply_chat_template + return apply_chat_template( + tokenizer, + messages, + tools=tools, + documents=request_body.get("documents"), + chat_template=request_body.get("chat_template"), + add_generation_prompt=add_generation_prompt, + continue_final_message=bool(request_body.get("continue_final_message", False)), + tokenize=tokenize, + return_tensors=None, + return_dict=False, + **_chat_template_kwargs(request_body, tokenizer_chat_template_kwargs), + ) + + +def _render_prompt_token_ids( + *, + tokenizer: Any, + request_body: dict[str, Any], + messages: list[Any], + tokenizer_chat_template_kwargs: Optional[dict[str, Any]], + exclude_tools_when_tool_choice_none: bool, + add_generation_prompt: bool, +) -> list[int]: + token_ids = _apply_chat_template( + tokenizer=tokenizer, + request_body=request_body, + messages=messages, + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none, + add_generation_prompt=add_generation_prompt, + tokenize=True, + ) + + if isinstance(token_ids, list) and ( + not token_ids or not isinstance(token_ids[0], list) + ): + return _coerce_token_id_list(token_ids, "prompt token IDs") + if isinstance(token_ids, list) and len(token_ids) == 1: + return _coerce_token_id_list(token_ids[0], "prompt token IDs") + raise ValueError( + "Dynamo token wrapper expected chat template rendering to return one " + "list of prompt token IDs." + ) + + +def _render_prompt_token_ids_with_optional_prefix( + *, + tokenizer: Any, + request_body: dict[str, Any], + messages: list[Any], + tokenizer_chat_template_kwargs: Optional[dict[str, Any]], + exclude_tools_when_tool_choice_none: bool, + add_generation_prompt: bool, + assistant_index: int | None, +) -> tuple[list[int], list[int] | None]: + full_prompt_token_ids = _render_prompt_token_ids( + tokenizer=tokenizer, + request_body=request_body, + messages=messages, + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none, + add_generation_prompt=add_generation_prompt, + ) + if assistant_index is None: + return full_prompt_token_ids, None + + template_prefix_token_ids = _render_prompt_token_ids( + tokenizer=tokenizer, + request_body=request_body, + messages=messages[: assistant_index + 1], + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none, + add_generation_prompt=False, + ) + return full_prompt_token_ids, template_prefix_token_ids + + +def _latest_tokenized_assistant_index(messages: list[Any]) -> Optional[int]: + for index in reversed(range(len(messages))): + message = messages[index] + if not isinstance(message, dict) or message.get("role") != "assistant": + continue + if ( + message.get("prompt_token_ids") is not None + and message.get("generation_token_ids") is not None + ): + return index + return None + + +def _derive_required_prefix_token_ids(messages: list[Any]) -> list[int] | None: + """Return the exact prompt and generation IDs from the latest model turn.""" + assistant_index = _latest_tokenized_assistant_index(messages) + if assistant_index is None: + return None + message = messages[assistant_index] + if not isinstance(message, dict): + return None + return _coerce_token_id_list( + message["prompt_token_ids"], "prompt_token_ids" + ) + _coerce_token_id_list(message["generation_token_ids"], "generation_token_ids") + + +def _normalize_tool_arguments_for_template( + messages: list[Any], *, before_index: int +) -> None: + """Make OpenAI tool calls renderable by model chat templates. + + OpenAI chat messages carry ``function.arguments`` as a JSON string, while + some model templates iterate those arguments as a mapping. Normalize only + the local template copy; the request forwarded to Dynamo retains its + original OpenAI payload. + """ + for message in messages[:before_index]: + if not isinstance(message, dict) or message.get("role") != "assistant": + continue + tool_calls = message.get("tool_calls") + if not isinstance(tool_calls, list): + continue + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + function = tool_call.get("function", tool_call) + if not isinstance(function, dict): + continue + arguments = function.get("arguments") + if not isinstance(arguments, str): + continue + try: + parsed_arguments = json.loads(arguments) + except json.JSONDecodeError: + parsed_arguments = {} + function["arguments"] = ( + parsed_arguments if isinstance(parsed_arguments, dict) else {} + ) + + +def _validate_engine_data( + response_body: dict[str, Any], +) -> tuple[list[int], list[int], list[float]]: + nvext = response_body.get("nvext") + engine_data = nvext.get("engine_data") if isinstance(nvext, dict) else None + if not isinstance(engine_data, dict): + raise ValueError("Dynamo response did not include nvext.engine_data.") + + prompt_token_ids = _coerce_token_id_list( + engine_data.get("prompt_token_ids"), + "nvext.engine_data.prompt_token_ids", + ) + completion_token_ids = _coerce_token_id_list( + engine_data.get("completion_token_ids"), + "nvext.engine_data.completion_token_ids", + ) + completion_logprobs = _coerce_logprob_list( + engine_data.get("completion_logprobs"), + "nvext.engine_data.completion_logprobs", + ) + if len(completion_logprobs) != len(completion_token_ids): + raise ValueError( + "Dynamo response returned " + f"{len(completion_logprobs)} generation log probabilities for " + f"{len(completion_token_ids)} generation token IDs." + ) + return prompt_token_ids, completion_token_ids, completion_logprobs + + +def _inject_gym_token_metadata(response_body: dict[str, Any]) -> None: + """Expose Dynamo engine metadata on the message fields consumed by Gym.""" + ( + prompt_token_ids, + generation_token_ids, + generation_logprobs, + ) = _validate_engine_data(response_body) + choices = response_body.get("choices") + if not isinstance(choices, list) or not choices: + raise ValueError("Dynamo response did not include choices[0].") + choice = choices[0] + if not isinstance(choice, dict): + raise ValueError("Dynamo response choices[0] must be a JSON object.") + message = choice.get("message") + if not isinstance(message, dict): + raise ValueError("Dynamo response choices[0].message must be a JSON object.") + message["prompt_token_ids"] = prompt_token_ids + message["generation_token_ids"] = generation_token_ids + message["generation_log_probs"] = generation_logprobs + + +def prepare_dynamo_chat_completion_request( + request_body: dict[str, Any], + *, + tokenizer: Any, + tokenizer_chat_template_kwargs: Optional[dict[str, Any]] = None, + exclude_tools_when_tool_choice_none: bool, +) -> dict[str, Any]: + """Prepare a NeMo-Gym chat-completion request for Dynamo token input.""" + if request_body.get("stream"): + raise ValueError("Dynamo native token wrapper does not support stream=True.") + + n = request_body.get("n", 1) + if n is not None and int(n) != 1: + raise ValueError("Dynamo native token wrapper currently supports only n=1.") + + messages = request_body.get("messages") + if not isinstance(messages, list): + raise ValueError("Dynamo token wrapper requires chat-completion messages.") + + prepared_body = deepcopy(request_body) + stripped_messages = _strip_gym_token_metadata(messages) + prepared_body["messages"] = stripped_messages + prepared_body.pop("required_prefix_token_ids", None) + + required_prefix_token_ids = _derive_required_prefix_token_ids(messages) + add_generation_prompt = _request_add_generation_prompt(prepared_body) + template_messages = deepcopy(stripped_messages) + assistant_index: int | None = None + if required_prefix_token_ids is not None: + assistant_index = _latest_tokenized_assistant_index(messages) + if assistant_index is None: + raise ValueError( + "Dynamo prefix token metadata must be attached to an assistant message." + ) + + try: + ( + full_prompt_token_ids, + template_prefix_token_ids, + ) = _render_prompt_token_ids_with_optional_prefix( + tokenizer=tokenizer, + request_body=prepared_body, + messages=template_messages, + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none, + add_generation_prompt=add_generation_prompt, + assistant_index=assistant_index, + ) + except TypeError as e: + if str(e) != _TOOL_ARGUMENT_MAPPING_ERROR: + raise + _normalize_tool_arguments_for_template( + template_messages, before_index=len(template_messages) + ) + ( + full_prompt_token_ids, + template_prefix_token_ids, + ) = _render_prompt_token_ids_with_optional_prefix( + tokenizer=tokenizer, + request_body=prepared_body, + messages=template_messages, + tokenizer_chat_template_kwargs=tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none, + add_generation_prompt=add_generation_prompt, + assistant_index=assistant_index, + ) + + if required_prefix_token_ids is not None: + assert template_prefix_token_ids is not None + full_prompt_token_ids = replace_prefix_tokens( + tokenizer, + model_prefix_token_ids=required_prefix_token_ids, + template_prefix_token_ids=template_prefix_token_ids, + template_token_ids=full_prompt_token_ids, + ) + + nvext = prepared_body.get("nvext") + if nvext is None: + nvext = {} + if not isinstance(nvext, dict): + raise ValueError("nvext must be a JSON object.") + nvext = dict(nvext) + extra_fields = nvext.get("extra_fields", []) + if not isinstance(extra_fields, list): + raise ValueError("nvext.extra_fields must be a JSON list.") + nvext["extra_fields"] = list(dict.fromkeys([*extra_fields, "engine_data"])) + nvext["token_data"] = full_prompt_token_ids + prepared_body["nvext"] = nvext + + return prepared_body + + +class DynamoTokenWrapperServer: + """Small HTTP server that supplies tokenized chat prompts to Dynamo.""" + + def __init__( + self, + *, + dynamo_frontend_base_url: str, + tokenizer: Any, + tokenizer_chat_template_kwargs: Optional[dict[str, Any]], + exclude_tools_when_tool_choice_none: bool, + request_timeout_s: Optional[float], + ) -> None: + self.dynamo_frontend_base_url = dynamo_frontend_base_url + self.tokenizer = tokenizer + self.tokenizer_chat_template_kwargs = tokenizer_chat_template_kwargs + self.exclude_tools_when_tool_choice_none = exclude_tools_when_tool_choice_none + self.request_timeout_s = request_timeout_s + self.base_url: Optional[str] = None + self.server: Any = None + self.thread: Optional[threading.Thread] = None + self._client_session: Any = None + + def start(self) -> str: + """Start the wrapper in a background uvicorn thread.""" + import aiohttp + import uvicorn + from fastapi import FastAPI, HTTPException, Request + from fastapi.responses import JSONResponse + + @asynccontextmanager + async def lifespan(_: FastAPI): + timeout = ( + aiohttp.ClientTimeout(total=self.request_timeout_s) + if self.request_timeout_s is not None + else aiohttp.ClientTimeout(total=None) + ) + async with aiohttp.ClientSession( + timeout=timeout, + # Explicit: the default TCPConnector(limit=100) would cap the whole + # rollout path at 100 concurrent requests to the frontend. + connector=aiohttp.TCPConnector(limit=0), + ) as session: + self._client_session = session + try: + yield + finally: + self._client_session = None + + app = FastAPI(lifespan=lifespan) + + @app.get("/health") + async def health() -> dict[str, str]: + return { + "status": "ok", + "dynamo_frontend_base_url": self.dynamo_frontend_base_url, + } + + @app.post("/v1/chat/completions") + async def chat_completions(request: Request) -> JSONResponse: + try: + request_body = await request.json() + except json.JSONDecodeError as e: + raise HTTPException(status_code=400, detail="Invalid JSON body.") from e + if not isinstance(request_body, dict): + raise HTTPException( + status_code=400, + detail="Chat completion body must be a JSON object.", + ) + + try: + prepared_body = await asyncio.to_thread( + prepare_dynamo_chat_completion_request, + request_body, + tokenizer=self.tokenizer, + tokenizer_chat_template_kwargs=self.tokenizer_chat_template_kwargs, + exclude_tools_when_tool_choice_none=self.exclude_tools_when_tool_choice_none, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + status_code, response_body = await self._forward_chat_completion( + prepared_body, + authorization=request.headers.get("authorization"), + ) + if 200 <= status_code < 300: + try: + _inject_gym_token_metadata(response_body) + except ValueError as e: + return JSONResponse( + content={"error": {"message": str(e)}}, + status_code=502, + ) + return JSONResponse(content=response_body, status_code=status_code) + + node_ip = _get_node_ip_local() + free_port = _get_free_port_local( + DEFAULT_DYNAMO_HTTP_PORT_RANGE_LOW, + DEFAULT_DYNAMO_HTTP_PORT_RANGE_HIGH, + ) + self.base_url = f"http://{node_ip}:{free_port}/v1" + + config = uvicorn.Config( + app, + host="0.0.0.0", + port=free_port, + timeout_keep_alive=120, + ) + self.server = uvicorn.Server(config=config) + self.thread = threading.Thread( + target=self.server.run, + name="dynamo-token-wrapper", + daemon=True, + ) + self.thread.start() + return self.base_url + + async def _forward_chat_completion( + self, + request_body: dict[str, Any], + *, + authorization: Optional[str], + ) -> tuple[int, dict[str, Any]]: + import aiohttp + + url = f"{self.dynamo_frontend_base_url.rstrip('/')}/chat/completions" + headers = {"Content-Type": "application/json"} + if authorization: + headers["Authorization"] = authorization + + session = self._client_session + if session is None: + return 503, {"error": {"message": "Dynamo token wrapper is not ready."}} + try: + async with session.post( + url, + json=request_body, + headers=headers, + ) as response: + response_text = await response.text() + if not response_text: + return response.status, {} + try: + response_body = json.loads(response_text) + except json.JSONDecodeError: + response_body = {"raw": response_text} + if not isinstance(response_body, dict): + response_body = {"response": response_body} + return response.status, response_body + except asyncio.TimeoutError: + return 504, {"error": {"message": f"Timed out forwarding to {url}."}} + except aiohttp.ClientError as e: + return 502, { + "error": { + "message": f"Failed to forward request to {url}: {type(e).__name__}: {e}" + } + } + + def shutdown(self) -> None: + """Stop the background uvicorn server.""" + if self.server is not None: + self.server.should_exit = True + if self.thread is not None: + self.thread.join(timeout=10) diff --git a/nemo_rl/models/generation/dynamo/validate_dynamo_vllm_args.py b/nemo_rl/models/generation/dynamo/validate_dynamo_vllm_args.py new file mode 100644 index 00000000000..ca005011431 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/validate_dynamo_vllm_args.py @@ -0,0 +1,68 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Validate a resolved ``dynamo.vllm`` argv without starting an engine.""" + +import json +import sys + + +def main() -> None: + if len(sys.argv) != 3: + raise SystemExit( + "usage: validate_dynamo_vllm_args.py '' ''" + ) + + # Parser construction evaluates vLLM device defaults. Image builds and + # unit preflights may run on CPU-only Slurm nodes, where a CUDA wheel leaves + # current_platform unspecified even though no engine will be constructed. + # Match Dynamo frontend's parser-only fallback without changing GPU runs. + import vllm.platforms + + if vllm.platforms.current_platform.device_type == "": + from vllm.platforms.cpu import CpuPlatform + + vllm.platforms.current_platform = CpuPlatform() + + from dynamo.vllm.args import parse_args + from vllm.distributed.weight_transfer.packed_tensor import ( + DEFAULT_PACKED_BUFFER_SIZE_BYTES, + DEFAULT_PACKED_NUM_BUFFERS, + ) + + argv = json.loads(sys.argv[1]) + if not isinstance(argv, list) or not all(isinstance(item, str) for item in argv): + raise TypeError("resolved Dynamo argv must be a JSON list of strings") + parse_args(argv) + + expected = json.loads(sys.argv[2]) + actual_geometry = ( + DEFAULT_PACKED_BUFFER_SIZE_BYTES, + DEFAULT_PACKED_NUM_BUFFERS, + ) + expected_geometry = ( + expected["buffer_size_bytes"], + expected["num_buffers"], + ) + if actual_geometry != expected_geometry: + raise SystemExit( + "vLLM packed-transfer geometry changed: " + f"engine has {actual_geometry}, NeMo RL sends {expected_geometry}. " + "Update the Dynamo CollectiveSenderSpec and the Dynamo " + "VLLM_PACKED_* constants." + ) + + +if __name__ == "__main__": + main() diff --git a/nemo_rl/models/generation/dynamo/venv.py b/nemo_rl/models/generation/dynamo/venv.py new file mode 100644 index 00000000000..020c923bb34 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/venv.py @@ -0,0 +1,44 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Resolve executables installed by the optional Dynamo environment.""" + +import os +from pathlib import Path + + +def get_dynamo_venv_dir() -> Path: + """Return the configured Dynamo virtual-environment directory.""" + configured = os.environ.get("NEMO_RL_DYNAMO_VENV_DIR") + if configured: + return Path(configured).expanduser().resolve() + repo_root = Path(__file__).resolve().parents[4] + return repo_root / "venvs" / "dynamo" + + +def get_dynamo_executable(name: str) -> str: + """Return an executable in the Dynamo environment, failing if absent.""" + executable = get_dynamo_venv_dir() / "bin" / name + if not executable.is_file() or not os.access(executable, os.X_OK): + raise FileNotFoundError( + f"Dynamo executable {executable} is unavailable. Build with " + "BUILD_DYNAMO=1 or run docker/dynamo/install.sh with " + "NEMO_RL_DYNAMO_VENV_DIR set." + ) + return str(executable) + + +def get_dynamo_python() -> str: + """Return the validated Python interpreter for ``ai-dynamo``.""" + return get_dynamo_executable("python") diff --git a/nemo_rl/models/generation/dynamo/worker_pool.py b/nemo_rl/models/generation/dynamo/worker_pool.py new file mode 100644 index 00000000000..1042e542f64 --- /dev/null +++ b/nemo_rl/models/generation/dynamo/worker_pool.py @@ -0,0 +1,246 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fixed Ray-managed pool of Dynamo vLLM subprocess owners.""" + +import os +from typing import Any + +import ray +from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy + +from nemo_rl.distributed.ray_actor_environment_registry import get_actor_python_env +from nemo_rl.distributed.virtual_cluster import ( + DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_HIGH, + DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_LOW, + DEFAULT_VLLM_PORT_RANGE_LOW, + DEFAULT_VLLM_PORTS_PER_ENGINE, + RayVirtualCluster, +) +from nemo_rl.models.generation.dynamo.dynamo_worker import ( + DynamoGpuReservation, + DynamoVllmWorker, +) + +_WORKER_FQN = "nemo_rl.models.generation.dynamo.dynamo_worker.DynamoVllmWorker" + + +def _vllm_port_for_node_slot(node_slot: int) -> int: + """Return vLLM's node-local scan base with 100 ports of headroom.""" + return DEFAULT_VLLM_PORT_RANGE_LOW + node_slot * DEFAULT_VLLM_PORTS_PER_ENGINE + + +class FixedDynamoWorkerPool: + """Reserve inference GPUs and launch one worker per model-parallel group.""" + + def __init__( + self, + *, + cluster: RayVirtualCluster, + config: dict[str, Any], + namespace: str, + engine_world_size: int, + manager_env: dict[str, str], + startup_timeout_s: float, + ) -> None: + self._cluster = cluster + self._config = config + self._namespace = namespace + self._engine_world_size = engine_world_size + self._manager_env = manager_env + self._startup_timeout_s = startup_timeout_s + self._workers: list[ray.actor.ActorHandle] = [] + self._reservations: list[ray.actor.ActorHandle] = [] + self._cleanup_reservations: list[ray.actor.ActorHandle] = [] + self._reservation_metadata: list[dict[str, Any]] = [] + self._metadata: list[dict[str, Any]] = [] + + @property + def size(self) -> int: + return len(self._workers) + + def is_alive(self) -> bool: + """Return whether every managed vLLM subprocess is still alive.""" + return bool(self._workers) and all( + ray.get([worker.is_alive.remote() for worker in self._workers]) + ) + + def start(self) -> None: + if self._workers: + raise RuntimeError("Managed Dynamo worker pool is already started.") + placement_groups = self._cluster.get_placement_groups() + python_env = get_actor_python_env(_WORKER_FQN) + runtime_env: dict[str, Any] = { + "py_executable": python_env, + "env_vars": dict(os.environ), + } + + group_index = 0 + engine_slots_by_node: dict[str, int] = {} + system_ports_by_node: dict[str, set[int]] = {} + metadata_refs: list[ray.ObjectRef] = [] + for pg_index, placement_group in enumerate(placement_groups): + bundle_count = placement_group.bundle_count + if bundle_count % self._engine_world_size != 0: + raise ValueError( + f"Inference placement group {pg_index} has {bundle_count} GPU " + f"bundles, which is not divisible by engine_world_size=" + f"{self._engine_world_size}." + ) + for start in range(0, bundle_count, self._engine_world_size): + bundle_indices = list(range(start, start + self._engine_world_size)) + reservation_handles = [] + for bundle_index in bundle_indices: + strategy = PlacementGroupSchedulingStrategy( + placement_group=placement_group, + placement_group_bundle_index=bundle_index, + placement_group_capture_child_tasks=True, + ) + reservation_handles.append( + DynamoGpuReservation.options( + num_gpus=1, + runtime_env=runtime_env, + scheduling_strategy=strategy, + ).remote() + ) + self._reservations.extend(reservation_handles) + reservation_metadata = ray.get( + [handle.metadata.remote() for handle in reservation_handles] + ) + self._reservation_metadata.extend(reservation_metadata) + node_ips = {item["node_ip"] for item in reservation_metadata} + if len(node_ips) != 1: + raise RuntimeError( + "A managed Dynamo engine group spans multiple nodes. " + "Multi-node TP/PP is not supported in the fixed-fleet milestone." + ) + cuda_devices = [item["gpu_id"] for item in reservation_metadata] + node_ip = next(iter(node_ips)) + node_slot = engine_slots_by_node.get(node_ip, 0) + engine_slots_by_node[node_ip] = node_slot + 1 + system_ports = system_ports_by_node.setdefault(node_ip, set()) + system_port = ray.get( + reservation_handles[0].select_free_port.remote( + port_range_low=DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_LOW, + port_range_high=DEFAULT_DYNAMO_SYSTEM_PORT_RANGE_HIGH, + excluded_ports=sorted(system_ports), + ) + ) + system_ports.add(system_port) + vllm_port = _vllm_port_for_node_slot(node_slot) + group_name = f"{self._namespace}-dynamo-vllm-{pg_index}-{group_index}" + leader_strategy = PlacementGroupSchedulingStrategy( + placement_group=placement_group, + placement_group_bundle_index=bundle_indices[0], + placement_group_capture_child_tasks=True, + ) + worker = DynamoVllmWorker.options( + num_gpus=0, + runtime_env={ + **runtime_env, + "env_vars": { + **runtime_env["env_vars"], + "RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES": "1", + }, + }, + scheduling_strategy=leader_strategy, + name=group_name, + ).remote( + self._config, + namespace=self._namespace, + group_name=group_name, + cuda_devices=cuda_devices, + system_port=system_port, + vllm_port=vllm_port, + manager_env=self._manager_env, + startup_timeout_s=self._startup_timeout_s, + seed=pg_index * 1024 + group_index, + cleanup_reservation=reservation_handles[0], + ) + self._workers.append(worker) + self._cleanup_reservations.append(reservation_handles[0]) + self._metadata.append({}) + metadata_refs.append(worker.metadata.remote()) + group_index += 1 + + metadata_error: Exception | None = None + for index, metadata_ref in enumerate(metadata_refs): + try: + self._metadata[index] = dict(ray.get(metadata_ref)) + except Exception as error: + if metadata_error is None: + metadata_error = error + if metadata_error is not None: + raise metadata_error + + def refit_workers(self) -> list[dict[str, Any]]: + return [dict(item) for item in self._metadata] + + def validate(self, expected: list[dict[str, Any]]) -> list[dict[str, Any]]: + if not self._workers or not all( + ray.get([w.is_alive.remote() for w in self._workers]) + ): + raise RuntimeError("A Ray-managed Dynamo vLLM worker exited.") + try: + current_reservations = ray.get( + [reservation.metadata.remote() for reservation in self._reservations] + ) + except Exception as exc: + raise RuntimeError( + "A Ray-managed Dynamo GPU reservation actor exited." + ) from exc + if current_reservations != self._reservation_metadata: + raise RuntimeError( + "Ray-managed Dynamo GPU reservation membership changed: " + f"expected={self._reservation_metadata}, current={current_reservations}." + ) + current = ray.get([worker.metadata.remote() for worker in self._workers]) + if current != expected: + raise RuntimeError( + "Ray-managed Dynamo worker membership changed after NCCL collective " + f"initialization: expected={expected}, current={current}." + ) + return [dict(item) for item in current] + + def shutdown(self) -> None: + for worker, reservation in zip( + self._workers, + self._cleanup_reservations, + strict=True, + ): + try: + ray.get(worker.shutdown.remote(), timeout=30) + except Exception: + try: + ray.get( + reservation.cleanup_process_group.remote(), + timeout=15, + ) + except Exception: + pass + for worker in self._workers: + try: + ray.kill(worker, no_restart=True) + except Exception: + pass + for reservation in self._reservations: + try: + ray.kill(reservation, no_restart=True) + except Exception: + pass + self._workers.clear() + self._reservations.clear() + self._cleanup_reservations.clear() + self._reservation_metadata.clear() + self._metadata.clear() diff --git a/nemo_rl/models/generation/interfaces.py b/nemo_rl/models/generation/interfaces.py index c3b3f74e5dc..f8c7d25ca02 100644 --- a/nemo_rl/models/generation/interfaces.py +++ b/nemo_rl/models/generation/interfaces.py @@ -345,6 +345,15 @@ class GenerationOutputSpec(TypedDict): __extra__: Any +@dataclass(frozen=True) +class CollectiveSenderSpec: + """Policy-side protocol and packing geometry for NCCL weight transfer.""" + + nccl_peer: str = "nemo" + buffer_size_bytes: int | None = None + num_buffers: int | None = None + + class GenerationInterface(ABC): """Abstract base class defining the interface for RL policies.""" @@ -369,6 +378,11 @@ def prepare_for_generation(self, *args: Any, **kwargs: Any) -> bool: def finish_generation(self, *args: Any, **kwargs: Any) -> bool: pass + @abstractmethod + def shutdown(self) -> bool: + """Shut down generation resources; repeated calls must be safe.""" + pass + @property def requires_kv_scale_sync(self) -> bool: """Whether the generation backend requires KV cache scales synchronization.""" @@ -386,6 +400,14 @@ def update_weights_from_collective(self) -> list[ray.ObjectRef]: """Update the model weights from collective communication.""" raise NotImplementedError + def get_collective_sender_spec(self) -> CollectiveSenderSpec: + """Return policy-side NCCL protocol and packed-buffer requirements.""" + return CollectiveSenderSpec() + + def get_inference_world_size(self) -> int | None: + """Return a backend-specific collective world size when required.""" + return None + def prepare_nccl_reshard_refit_info(self, refit_info: dict) -> None: """Prepare per-layer param metadata for nccl_reshard-based refit.""" raise NotImplementedError diff --git a/nemo_rl/models/policy/__init__.py b/nemo_rl/models/policy/__init__.py index 6bb0c4746a0..9506422ad32 100644 --- a/nemo_rl/models/policy/__init__.py +++ b/nemo_rl/models/policy/__init__.py @@ -499,7 +499,8 @@ class DraftConfig(TypedDict): class TokenizerConfig(TypedDict): name: str - chat_template: NotRequired[str] + # None selects NeMo-RL's passthrough prompt/response template. + chat_template: NotRequired[str | None] # Arguments to pass to tokenizer.apply_chat_template(...). This can be used to pass kwargs like enable_thinking=true chat_template_kwargs: NotRequired[dict[str, Any] | None] # Multimodal configs diff --git a/nemo_rl/models/policy/interfaces.py b/nemo_rl/models/policy/interfaces.py index fb5279aee5f..b8e305c24ba 100644 --- a/nemo_rl/models/policy/interfaces.py +++ b/nemo_rl/models/policy/interfaces.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -166,7 +166,13 @@ def shutdown(self) -> bool: class ColocatablePolicyInterface(PolicyInterface): @abstractmethod def init_collective( - self, ip: str, port: int, world_size: int, *, train_world_size: int + self, + ip: str, + port: int, + world_size: int, + *, + train_world_size: int, + nccl_peer: str = "nemo", ) -> list[ray.ObjectRef]: pass @@ -220,7 +226,11 @@ def set_rollout_num_gpus_per_engine(self, num_gpus_per_engine: int) -> None: @abstractmethod def broadcast_weights_for_collective( - self, kv_scales: Optional[dict[str, float]] = None + self, + kv_scales: Optional[dict[str, float]] = None, + *, + buffer_size_bytes: Optional[int] = None, + num_buffers: Optional[int] = None, ) -> list[ray.ObjectRef]: pass diff --git a/nemo_rl/models/policy/lm_policy.py b/nemo_rl/models/policy/lm_policy.py index 2108bb10e92..107efcd1964 100644 --- a/nemo_rl/models/policy/lm_policy.py +++ b/nemo_rl/models/policy/lm_policy.py @@ -401,7 +401,13 @@ def run_all_workers_multiple_data(self, method_name: str, *args, **kwargs) -> An return results def init_collective( - self, ip: str, port: int, world_size: int, *, train_world_size: int + self, + ip: str, + port: int, + world_size: int, + *, + train_world_size: int, + nccl_peer: str = "nemo", ) -> list[ray.ObjectRef]: """Initialize the collective communication.""" futures = self.worker_group.run_all_workers_single_data( @@ -410,6 +416,7 @@ def init_collective( port=port, world_size=world_size, train_world_size=train_world_size, + nccl_peer=nccl_peer, ) # this function should co-work with vllm, so we should wait for all futures to complete outside return futures @@ -1097,12 +1104,18 @@ def set_rollout_num_gpus_per_engine(self, num_gpus_per_engine: int) -> None: ) def broadcast_weights_for_collective( - self, kv_scales: Optional[dict[str, float]] = None + self, + kv_scales: Optional[dict[str, float]] = None, + *, + buffer_size_bytes: Optional[int] = None, + num_buffers: Optional[int] = None, ) -> list[ray.ObjectRef]: """Broadcast the weights for collective communication.""" futures = self.worker_group.run_all_workers_single_data( "broadcast_weights_for_collective", kv_scales=kv_scales, + buffer_size_bytes=buffer_size_bytes, + num_buffers=num_buffers, ) # this function should co-work with vllm, so we should wait for all futures to complete outside return futures diff --git a/nemo_rl/models/policy/workers/base_policy_worker.py b/nemo_rl/models/policy/workers/base_policy_worker.py index 525a1c673ec..e371e4f5cae 100644 --- a/nemo_rl/models/policy/workers/base_policy_worker.py +++ b/nemo_rl/models/policy/workers/base_policy_worker.py @@ -1,4 +1,4 @@ -# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2025-2026, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +26,13 @@ class AbstractPolicyWorker: """Base class for policy workers with shared functionality.""" def init_collective( - self, ip: str, port: int, world_size: int, *, train_world_size: int + self, + ip: str, + port: int, + world_size: int, + *, + train_world_size: int, + nccl_peer: str = "nemo", ) -> None: """Initialize the collective communication. @@ -35,6 +41,7 @@ def init_collective( port: Port for the process group world_size: Total world size (train_world_size + inference_world_size) train_world_size: Number of training workers (used in inference cluster) + nccl_peer: NCCL initialization protocol used by the inference workers """ from nemo_rl.distributed.stateless_process_group import StatelessProcessGroup @@ -45,7 +52,7 @@ def init_collective( # Release unused cached allocator blocks before NCCL communicator # initialization so transport buffers have sufficient device-memory headroom. torch.cuda.empty_cache() - self.model_update_group.init_nccl_communicator(device=device) + self.model_update_group.init_nccl_communicator(device=device, peer=nccl_peer) def init_nccl_reshard_comm_group( self, diff --git a/nemo_rl/models/policy/workers/dtensor_policy_worker.py b/nemo_rl/models/policy/workers/dtensor_policy_worker.py index e7e80f99f52..65a73480753 100644 --- a/nemo_rl/models/policy/workers/dtensor_policy_worker.py +++ b/nemo_rl/models/policy/workers/dtensor_policy_worker.py @@ -1900,7 +1900,11 @@ def _checkpoint_engine_params( @torch.no_grad() def broadcast_weights_for_collective( - self, kv_scales: Optional[dict[str, float]] = None + self, + kv_scales: Optional[dict[str, float]] = None, + *, + buffer_size_bytes: Optional[int] = None, + num_buffers: Optional[int] = None, ) -> None: """Broadcast the weights for collective communication.""" if kv_scales is not None: @@ -1930,6 +1934,8 @@ def _dtensor_post_iter_func(tensor, dtype): group=self.model_update_group, src=0, post_iter_func=dtensor_post_iter_func, + buffer_size_bytes=buffer_size_bytes, + num_buffers=num_buffers, ) # Manually move model to cpu for cpu offload case diff --git a/nemo_rl/models/policy/workers/dtensor_policy_worker_v2.py b/nemo_rl/models/policy/workers/dtensor_policy_worker_v2.py index 8ed9d584fed..32c6ac78514 100644 --- a/nemo_rl/models/policy/workers/dtensor_policy_worker_v2.py +++ b/nemo_rl/models/policy/workers/dtensor_policy_worker_v2.py @@ -1181,7 +1181,11 @@ def _checkpoint_engine_params( @torch.no_grad() def broadcast_weights_for_collective( - self, kv_scales: Optional[dict[str, float]] = None + self, + kv_scales: Optional[dict[str, float]] = None, + *, + buffer_size_bytes: Optional[int] = None, + num_buffers: Optional[int] = None, ) -> None: """Broadcast the weights for collective communication.""" if kv_scales is not None: @@ -1205,6 +1209,8 @@ def broadcast_weights_for_collective( group=self.model_update_group, src=0, post_iter_func=dtensor_post_iter_func, + buffer_size_bytes=buffer_size_bytes, + num_buffers=num_buffers, ) # Manually move model to cpu for cpu offload case diff --git a/nemo_rl/models/policy/workers/megatron_policy_worker.py b/nemo_rl/models/policy/workers/megatron_policy_worker.py index b6ae9a23a95..f65f1d854cf 100644 --- a/nemo_rl/models/policy/workers/megatron_policy_worker.py +++ b/nemo_rl/models/policy/workers/megatron_policy_worker.py @@ -2295,7 +2295,11 @@ def stream_weights_via_ipc_zmq( @torch.no_grad() def broadcast_weights_for_collective( - self, kv_scales: Optional[dict[str, float]] = None + self, + kv_scales: Optional[dict[str, float]] = None, + *, + buffer_size_bytes: Optional[int] = None, + num_buffers: Optional[int] = None, ) -> None: """Broadcast the weights for collective communication.""" # param_iterator will return (name, tensor), we only need tensor. @@ -2304,6 +2308,8 @@ def broadcast_weights_for_collective( group=self.model_update_group, src=0, post_iter_func=lambda x: x[1], + buffer_size_bytes=buffer_size_bytes, + num_buffers=num_buffers, ) def _build_layer_to_pp_stage( diff --git a/nemo_rl/utils/packed_tensor.py b/nemo_rl/utils/packed_tensor.py index a927b0ed862..be8738447ad 100644 --- a/nemo_rl/utils/packed_tensor.py +++ b/nemo_rl/utils/packed_tensor.py @@ -36,7 +36,15 @@ def get_num_buffers(): return int(os.getenv("NRL_REFIT_NUM_BUFFERS", "2")) -def packed_broadcast_producer(iterator, group, src, post_iter_func): +def packed_broadcast_producer( + iterator, + group, + src, + post_iter_func, + *, + buffer_size_bytes: int | None = None, + num_buffers: int | None = None, +): """Broadcast a list of tensors in a packed manner. Args: @@ -44,14 +52,20 @@ def packed_broadcast_producer(iterator, group, src, post_iter_func): group: process group (vllm PyNcclCommunicator) src: source rank (0 in current implementation) post_iter_func: function to apply to each tensor before packing, should return a tensor + buffer_size_bytes: packed-buffer target. Uses the NeMo-RL default when unset. + num_buffers: number of alternating CUDA buffers. Uses the default when unset. Returns: None """ - target_packed_tensor_size = get_target_packed_tensor_size() + target_packed_tensor_size = ( + get_target_packed_tensor_size() + if buffer_size_bytes is None + else buffer_size_bytes + ) - num_buffers = get_num_buffers() + num_buffers = get_num_buffers() if num_buffers is None else num_buffers streams = [torch.cuda.Stream() for _ in range(num_buffers)] buffer_idx = 0 diff --git a/nemo_rl/weight_sync/collective_weight_synchronizer.py b/nemo_rl/weight_sync/collective_weight_synchronizer.py index 84e842dc018..ff730d076e9 100644 --- a/nemo_rl/weight_sync/collective_weight_synchronizer.py +++ b/nemo_rl/weight_sync/collective_weight_synchronizer.py @@ -76,8 +76,11 @@ def sync_weights( else nullcontext() ) with timer_context: + sender_spec = self._generation.get_collective_sender_spec() futures_train = self._policy.broadcast_weights_for_collective( - kv_scales=kv_scales + kv_scales=kv_scales, + buffer_size_bytes=sender_spec.buffer_size_bytes, + num_buffers=sender_spec.num_buffers, ) futures_inference = self._generation.update_weights_from_collective() @@ -107,11 +110,18 @@ def init_communicator(self) -> None: ip, port = self._train_cluster.get_master_address_and_port() train_world_size = self._train_cluster.world_size() - inference_world_size = self._inference_cluster.world_size() + inference_world_size = self._generation.get_inference_world_size() + if inference_world_size is None: + inference_world_size = self._inference_cluster.world_size() world_size = train_world_size + inference_world_size + sender_spec = self._generation.get_collective_sender_spec() futures_train = self._policy.init_collective( - ip, port, world_size, train_world_size=train_world_size + ip, + port, + world_size, + train_world_size=train_world_size, + nccl_peer=sender_spec.nccl_peer, ) futures_inference = self._generation.init_collective( ip, port, world_size, train_world_size=train_world_size diff --git a/nemo_rl/weight_sync/factory.py b/nemo_rl/weight_sync/factory.py index a6f80b4cc91..c48ed19f5d9 100644 --- a/nemo_rl/weight_sync/factory.py +++ b/nemo_rl/weight_sync/factory.py @@ -16,12 +16,14 @@ Selects the appropriate weight synchronizer based on the deployment topology (colocated vs. non-colocated) and the generation backend -(vLLM uses IPC/ZMQ, SGLang uses HTTP, non-colocated uses NCCL). +(vLLM uses IPC/ZMQ, SGLang uses HTTP, and non-colocated vLLM or Dynamo uses +NCCL). """ from typing import Any, Optional from nemo_rl.models.generation.constants import ( + DYNAMO_BACKEND, MEGATRON_BACKEND, SGLANG_BACKEND, VLLM_BACKEND, @@ -46,7 +48,8 @@ def create_weight_synchronizer( Args: policy: Policy object (ColocatablePolicyInterface). generation: Generation object (GenerationInterface). - generation_backend: Name of the generation backend ("vllm", "sglang", "megatron"). + generation_backend: Name of the generation backend ("vllm", "sglang", + "megatron", or "dynamo"). colocated: Whether policy and generation share the same GPUs. train_cluster: RayVirtualCluster for training workers (required for non-colocated). inference_cluster: RayVirtualCluster for inference workers (required for non-colocated). @@ -59,7 +62,12 @@ def create_weight_synchronizer( NotImplementedError: If the requested configuration is not supported. ValueError: If required arguments are missing. """ - _SUPPORTED_BACKENDS = {VLLM_BACKEND, SGLANG_BACKEND, MEGATRON_BACKEND} + _SUPPORTED_BACKENDS = { + VLLM_BACKEND, + SGLANG_BACKEND, + MEGATRON_BACKEND, + DYNAMO_BACKEND, + } if generation_backend not in _SUPPORTED_BACKENDS: raise ValueError( f"Unknown generation backend {generation_backend!r}. " diff --git a/pyrefly.toml b/pyrefly.toml index 7804f10bdbc..3baf62da541 100644 --- a/pyrefly.toml +++ b/pyrefly.toml @@ -8,6 +8,7 @@ replace-imports-with-any = [ "transformers.*", "tensorrt_llm.*", "vllm.*", + "dynamo.*", "math_verify.*", "sympy.*", "torchdata.*", @@ -167,6 +168,19 @@ project-includes = [ "nemo_rl/models/generation/__init__.py", "nemo_rl/models/generation/constants.py", "nemo_rl/models/generation/fleet_health.py", + "nemo_rl/models/generation/dynamo/__init__.py", + "nemo_rl/models/generation/dynamo/arguments.py", + "nemo_rl/models/generation/dynamo/config.py", + "nemo_rl/models/generation/dynamo/dynamo_generation.py", + "nemo_rl/models/generation/dynamo/dynamo_worker.py", + "nemo_rl/models/generation/dynamo/http_client.py", + "nemo_rl/models/generation/dynamo/managed_runtime.py", + "nemo_rl/models/generation/dynamo/metrics.py", + "nemo_rl/models/generation/dynamo/refit.py", + "nemo_rl/models/generation/dynamo/token_wrapper.py", + "nemo_rl/models/generation/dynamo/validate_dynamo_vllm_args.py", + "nemo_rl/models/generation/dynamo/venv.py", + "nemo_rl/models/generation/dynamo/worker_pool.py", "nemo_rl/models/generation/interfaces.py", "nemo_rl/models/generation/generation_router.py", "nemo_rl/models/generation/megatron/__init__.py", diff --git a/ray.sub b/ray.sub index ef88e46f596..4140d0f7f51 100644 --- a/ray.sub +++ b/ray.sub @@ -153,13 +153,16 @@ fi # stock Linux). All service ports are pinned below 9000 to stay clear of even # the lowest observed ephemeral floor. # -# Port layout (all below ephemeral floor at 9000): +# Port layout (all below ephemeral floor at 9000). Python port-range bounds +# are half-open: [low, high). # 1200-1201 Ray GCS + client server (head only) # 1301-1312 Ray management (node-mgr, obj-mgr, etc.; odd=worker, even=head) +# 1313-1399 Dynamo etcd/NATS control plane (driver-local allocation) # 1400-1999 Master address / TCPStore (cluster.master_port_range_low/high) # 2000-2999 Ray worker gRPC (min/max-worker-port) -# 3000-4999 NeMo RL generation HTTP servers + SGLang engine NCCL/dist_init -# (policy.generation.port_range_low/high) +# [3000, 4999) Shared NeMo RL generation range (policy.generation.port_range_low/high) +# [3000, 4000) Dynamo frontend/token-wrapper HTTP endpoints +# [4000, 4100) Dynamo worker system endpoints (node-local free-port selection) # 5000-5999 NeMo Gym HTTP servers (Gym global config port_range_low/high) # 6000 Sandbox Nginx (NEMO_SKILLS_SANDBOX_PORT) # 6001-6999 Sandbox uWSGI workers (SANDBOX_BASE_PORT) diff --git a/tests/functional/L1_Functional_Tests_Dynamo.sh b/tests/functional/L1_Functional_Tests_Dynamo.sh new file mode 100644 index 00000000000..4fe4463c430 --- /dev/null +++ b/tests/functional/L1_Functional_Tests_Dynamo.sh @@ -0,0 +1,41 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/bash +set -xeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +PROJECT_ROOT=$(realpath "${SCRIPT_DIR}/../..") +cd "${PROJECT_ROOT}" + +# run_test [fast] +# - "run_test fast " = always runs (both fast and full modes) +# - "run_test " = only runs in full mode; skipped when FAST=1 +run_test() { + if [[ "$1" == "fast" ]]; then + shift + time "$@" + elif [[ "${FAST:-0}" == "1" ]]; then + echo "FAST: Skipping: $*" + else + time "$@" + fi +} + +run_test fast uv run --no-sync bash ./tests/functional/grpo_dynamo.sh + +cd "${PROJECT_ROOT}/tests" +if compgen -G ".coverage*" > /dev/null; then + coverage combine .coverage* +fi diff --git a/tests/functional/grpo_dynamo.sh b/tests/functional/grpo_dynamo.sh new file mode 100644 index 00000000000..db4e181f54a --- /dev/null +++ b/tests/functional/grpo_dynamo.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +PROJECT_ROOT=$(realpath "${SCRIPT_DIR}/../..") +EXP_NAME=$(basename "$0" .sh) +EXP_DIR=${SCRIPT_DIR}/${EXP_NAME} +LOG_DIR=${EXP_DIR}/logs +RUN_LOG=${EXP_DIR}/run.log +export PYTHONPATH=${PROJECT_ROOT}:${PYTHONPATH:-} + +rm -rf "${EXP_DIR}" +mkdir -p "${LOG_DIR}" +git config --global --add safe.directory "${PROJECT_ROOT}" + +dynamo_python=/opt/dynamo_venv/bin/python +"${dynamo_python}" -c \ + 'import importlib.metadata as m; assert m.version("ai-dynamo") == "1.3.0.post1"; assert m.version("vllm") == "0.23.0"' +grep -Fqx \ + 'vllm PR #44814 merge commit c9e5bf813530fb9ce06024e075da0f520b0718c8' \ + /opt/dynamo_venv/VLLM_BACKPORTS +/opt/dynamo_venv/bin/etcd --version +/opt/dynamo_venv/bin/nats-server --version + +cd "${PROJECT_ROOT}" +uv run --no-sync coverage run -a \ + --data-file="${PROJECT_ROOT}/tests/.coverage" \ + --source="${PROJECT_ROOT}/nemo_rl" \ + "${PROJECT_ROOT}/examples/run_grpo.py" \ + --config "${PROJECT_ROOT}/examples/configs/grpo_math_1B_dynamo.yaml" \ + policy.model_name=Qwen/Qwen3-0.6B \ + policy.tokenizer.name=Qwen/Qwen3-0.6B \ + logger.log_dir="${LOG_DIR}" \ + 2>&1 | tee "${RUN_LOG}" + +grep -F "Performing policy generation refit" "${RUN_LOG}" +grep -F "Invalidated generation backend KV caches after weight update" "${RUN_LOG}" + +refit_count=$(grep -Fc "Performing policy generation refit" "${RUN_LOG}" || true) +cache_success_count=$(grep -Fc \ + "Invalidated generation backend KV caches after weight update" \ + "${RUN_LOG}" || true) +if [[ "${refit_count}" -eq 0 || "${cache_success_count}" -ne "${refit_count}" ]]; then + echo "Expected one successful cache invalidation per refit; refits=${refit_count}, successes=${cache_success_count}" >&2 + exit 1 +fi +if grep -Fq \ + -e "Failed to invalidate generation backend KV caches" \ + -e "KV cache invalidation not supported or only partially applied" \ + "${RUN_LOG}"; then + echo "The Dynamo run reported a cache invalidation failure" >&2 + exit 1 +fi + +metrics_json=${EXP_DIR}/metrics.json +uv run --no-sync tests/json_dump_tb_logs.py \ + "${LOG_DIR}" \ + --output_path "${metrics_json}" \ + --require-tag-prefix "generation_metrics/" +uv run --no-sync tests/check_metrics.py \ + "${metrics_json}" \ + 'max(data["train/token_mult_prob_error"]) < 1.05' + +if pgrep -f '[d]ynamo.frontend|[d]ynamo.vllm|[/]opt/dynamo_venv/bin/etcd|[/]opt/dynamo_venv/bin/nats-server'; then + echo "Managed Dynamo processes remain after GRPO shutdown" >&2 + exit 1 +fi diff --git a/tests/json_dump_tb_logs.py b/tests/json_dump_tb_logs.py index 973e37659b9..de89b0027b7 100644 --- a/tests/json_dump_tb_logs.py +++ b/tests/json_dump_tb_logs.py @@ -38,16 +38,21 @@ error_console = Console(stderr=True) -def merge_tb_logs_to_json(log_dir, output_path, error_on_conflicts=False): +def merge_tb_logs_to_json( + log_dir, output_path, error_on_conflicts=False, required_tag_prefix=None +): """Merge multiple TensorBoard event files into a single JSON file. Arguments: log_dir: Path to directory containing TensorBoard event files (searched recursively) output_path: Path to save the output JSON file error_on_conflicts: If True, raise an error if conflicting values are found for the same step + required_tag_prefix: If set, require at least one TensorBoard tag in any + plugin category to start with this prefix Raises: - ValueError: If conflicting values are found for the same step and error_on_conflicts is True + ValueError: If conflicting values are found for the same step and + error_on_conflicts is True, or if required_tag_prefix is not found """ # Find all event files recursively files = glob.glob(f"{log_dir}/**/events*tfevents*", recursive=True) @@ -76,6 +81,7 @@ def merge_tb_logs_to_json(log_dir, output_path, error_on_conflicts=False): # {metric_name: {step: (value, source_file)}} merged_data = defaultdict(dict) + tensorboard_tags = set() console.print("[bold green]Processing event files...[/bold green]") @@ -85,6 +91,10 @@ def merge_tb_logs_to_json(log_dir, output_path, error_on_conflicts=False): ea = event_accumulator.EventAccumulator(event_file, size_guidance=SIZE_GUIDANCE) ea.Reload() + for tags in ea.Tags().values(): + if isinstance(tags, list): + tensorboard_tags.update(tags) + for metric_name in ea.scalars.Keys(): for scalar in ea.Scalars(metric_name): step, value = scalar.step, scalar.value @@ -107,6 +117,13 @@ def merge_tb_logs_to_json(log_dir, output_path, error_on_conflicts=False): # Add or override the value merged_data[metric_name][step] = (value, event_file) + if required_tag_prefix is not None and not any( + tag.startswith(required_tag_prefix) for tag in tensorboard_tags + ): + raise ValueError( + f"No TensorBoard tag starts with '{required_tag_prefix}' under {log_dir}" + ) + # Convert defaultdict to regular dict and sort the steps output_data = {} for metric_name in sorted(merged_data.keys()): @@ -222,11 +239,21 @@ def merge_tb_logs_to_json(log_dir, output_path, error_on_conflicts=False): action="store_true", help="Error out when conflicting values are found for the same step", ) + parser.add_argument( + "--require-tag-prefix", + default=None, + help="Require a TensorBoard tag in any plugin category with this prefix", + ) args = parser.parse_args() try: - merge_tb_logs_to_json(args.log_dir, args.output_path, args.error_on_conflicts) + merge_tb_logs_to_json( + args.log_dir, + args.output_path, + args.error_on_conflicts, + args.require_tag_prefix, + ) except Exception as e: error_console.print(f"[bold red]Error: {e}[/bold red]") sys.exit(1) diff --git a/tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh b/tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh new file mode 100755 index 00000000000..1dc7edd194a --- /dev/null +++ b/tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +source "${SCRIPT_DIR}/common.env" + +# ===== BEGIN CONFIG ===== +NUM_NODES=3 +GPUS_PER_NODE=8 +STEPS_PER_RUN=4 +MAX_STEPS=4 +NUM_RUNS=1 +NUM_MINUTES=240 +USE_GYM_CONTAINER=true +# ===== END CONFIG ===== + +exit_if_max_steps_reached + +cd "${PROJECT_ROOT}" +uv run examples/nemo_gym/run_grpo_nemo_gym.py \ + --config "${CONFIG_PATH}" \ + grpo.max_num_steps="${MAX_STEPS}" \ + logger.log_dir="${LOG_DIR}" \ + logger.wandb_enabled=true \ + logger.wandb.project=nemo-rl \ + logger.wandb.name="${EXP_NAME}" \ + logger.tensorboard_enabled=true \ + "$@" \ + 2>&1 | tee "${RUN_LOG}" + +uv run tests/json_dump_tb_logs.py "${LOG_DIR}" \ + --output_path "${JSON_METRICS}" \ + --require-tag-prefix generation_metrics/ + +last_step=$(jq 'to_entries | .[] | select(.key == "train/loss") | .value | keys | map(tonumber) | max' "${JSON_METRICS}") +if [[ ${last_step} -lt ${MAX_STEPS} ]]; then + echo "[ERROR] Expected step ${MAX_STEPS}, but the last train/loss step is ${last_step}" + exit 1 +fi + +uv run tests/check_metrics.py "${JSON_METRICS}" \ + 'median(data["train/token_mult_prob_error"]) < 1.1' \ + "data['train/token_mult_prob_error']['${MAX_STEPS}'] < 1.1" \ + 'mean(data["train/gen_kl_error"]) < 0.02' + +refit_count=$(grep -c "✅ Ready for refit" "${RUN_LOG}" || true) +cache_invalidation_count=$(grep -c \ + "✅ Invalidated generation backend KV caches after weight update" \ + "${RUN_LOG}" || true) +cache_invalidation_failure_count=$(grep -cE \ + "Failed to invalidate generation backend KV caches|Dynamo KV cache invalidation failed" \ + "${RUN_LOG}" || true) +if [[ ${cache_invalidation_failure_count} -ne 0 ]]; then + echo "[ERROR] Found ${cache_invalidation_failure_count} cache invalidation failure(s)" + exit 1 +fi +if [[ ${refit_count} -ne ${cache_invalidation_count} ]]; then + echo "[ERROR] Expected one cache invalidation per refit, but found " \ + "${cache_invalidation_count} invalidations for ${refit_count} refits" + exit 1 +fi diff --git a/tests/test_suites/nightly.txt b/tests/test_suites/nightly.txt index 64fb93790e4..cfde74908d1 100644 --- a/tests/test_suites/nightly.txt +++ b/tests/test_suites/nightly.txt @@ -223,6 +223,7 @@ tests/test_suites/llm/grpo-qwen3-30ba3b-2n8g-megatron_fused_linear_logprobs.sh tests/test_suites/llm/sft-gpt-oss-20b-1n8g-fsdp8ep8-automodel.sh # Nemotron 3 Nano 30B A3B Base BF16 tests +tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh tests/test_suites/llm/sft-nanov3-30BA3B-2n8g-fsdp2.sh tests/test_suites/llm/sft-nanov3-30BA3B-2n8g-fsdp2-lora.sh diff --git a/tests/unit/L0_Unit_Tests_Vllm_1.sh b/tests/unit/L0_Unit_Tests_Vllm_1.sh index 6dd5ca10f7a..033218db562 100644 --- a/tests/unit/L0_Unit_Tests_Vllm_1.sh +++ b/tests/unit/L0_Unit_Tests_Vllm_1.sh @@ -18,7 +18,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/run_unit_shard_common.sh" # Base run (tests without extra markers) -uv run --no-sync bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=0 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated +uv run --no-sync bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_dynamo*.py" "unit/models/generation/test_swe1_dynamo_config.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=0 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated # vllm-only run (catch-all across all unit tests) uv run --extra vllm bash -x ./tests/run_unit.sh "unit/" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=0 --num-shards=3 --cov=nemo_rl --cov-append --cov-report=term-missing --cov-report=json --hf-gated --vllm-only diff --git a/tests/unit/L0_Unit_Tests_Vllm_2.sh b/tests/unit/L0_Unit_Tests_Vllm_2.sh index f90f718b12b..6ad24ea6213 100644 --- a/tests/unit/L0_Unit_Tests_Vllm_2.sh +++ b/tests/unit/L0_Unit_Tests_Vllm_2.sh @@ -18,7 +18,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/run_unit_shard_common.sh" # Base run (tests without extra markers) -uv run --no-sync bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=1 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated +uv run --no-sync bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_dynamo*.py" "unit/models/generation/test_swe1_dynamo_config.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=1 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated # vllm-only run (catch-all across all unit tests) uv run --extra vllm bash -x ./tests/run_unit.sh "unit/" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=1 --num-shards=3 --cov=nemo_rl --cov-append --cov-report=term-missing --cov-report=json --hf-gated --vllm-only diff --git a/tests/unit/L0_Unit_Tests_Vllm_3.sh b/tests/unit/L0_Unit_Tests_Vllm_3.sh index de0714c8650..9d136716b38 100644 --- a/tests/unit/L0_Unit_Tests_Vllm_3.sh +++ b/tests/unit/L0_Unit_Tests_Vllm_3.sh @@ -18,7 +18,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/run_unit_shard_common.sh" # Base run (tests without extra markers) -uv run --extra modelopt bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=2 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated +uv run --extra modelopt bash -x ./tests/run_unit.sh "unit/models/generation/test_vllm*.py" "unit/models/generation/test_dynamo*.py" "unit/models/generation/test_swe1_dynamo_config.py" "unit/models/generation/test_openai_server_utils.py" "unit/models/generation/test_fleet_health.py" "unit/models/generation/test_generation_router.py" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=2 --num-shards=3 --cov=nemo_rl --cov-report=term-missing --cov-report=json --hf-gated # vllm-only run (catch-all across all unit tests) uv run --extra vllm bash -x ./tests/run_unit.sh "unit/" "${EXCLUDED_UNIT_TESTS[@]}" --shard-id=2 --num-shards=3 --cov=nemo_rl --cov-append --cov-report=term-missing --cov-report=json --hf-gated --vllm-only diff --git a/tests/unit/algorithms/test_async_utils.py b/tests/unit/algorithms/test_async_utils.py index cacfea384ef..bf1f320f084 100644 --- a/tests/unit/algorithms/test_async_utils.py +++ b/tests/unit/algorithms/test_async_utils.py @@ -1444,6 +1444,34 @@ def test_resume_after_refit_skips_cache_invalidation_when_recompute_disabled(sel collector.policy_generation.invalidate_kv_cache.assert_not_called() + def test_dynamo_cache_invalidation_failure_is_fatal_and_unblocks_waiters(self): + collector = self.create_local_collector() + collector.master_config.policy["generation"] = {"backend": "dynamo"} + collector.master_config.grpo.async_grpo.recompute_kv_cache_after_weight_updates = True + collector.policy_generation.invalidate_kv_cache = mock.Mock( + side_effect=RuntimeError("pause failed") + ) + collector._refit_pause_cleared.clear() + + with pytest.raises(RuntimeError, match="cache invalidation failed"): + collector.resume_after_refit() + + assert collector._refit_pause_cleared.is_set() + + def test_dynamo_prepare_for_refit_drains_pending_generations(self): + """Dynamo layerwise reload never overlaps an active generation.""" + collector = self.create_local_collector() + collector.master_config.policy["generation"] = { + "backend": "dynamo", + "dynamo_cfg": {}, + } + collector.master_config.grpo.async_grpo.in_flight_weight_updates = True + collector.wait_for_pending_generations = mock.MagicMock() + + collector.prepare_for_refit() + + collector.wait_for_pending_generations.assert_called_once_with() + def test_calculate_target_weights(self): """Test target weight calculation logic.""" buffer = ReplayBuffer.remote(max_size=10) diff --git a/tests/unit/algorithms/test_grpo.py b/tests/unit/algorithms/test_grpo.py index 346353fcd45..51c9d4290ed 100644 --- a/tests/unit/algorithms/test_grpo.py +++ b/tests/unit/algorithms/test_grpo.py @@ -13,12 +13,14 @@ # limitations under the License. from contextlib import ExitStack, contextmanager +from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch import pytest import ray import torch +from omegaconf import OmegaConf from torchdata.stateful_dataloader import StatefulDataLoader from nemo_rl.algorithms.advantage_estimator import ( @@ -45,6 +47,7 @@ _resolve_message_level_advantage_penalties, _save_async_replay_buffer_checkpoint, _should_use_async_rollouts, + _should_use_nemo_gym, _validate_multimodal_dedup_capability, _validate_use_kl_in_reward_compat, aggregate_rollout_metrics, @@ -54,6 +57,7 @@ grpo_train, refit_policy_generation, setup, + shutdown_environments, validate, ) from nemo_rl.algorithms.grpo_sync import _train_fields_for_step, grpo_train_sync @@ -72,7 +76,10 @@ ) from nemo_rl.experience.interfaces import NEXT_NEMO_GYM_TASK_INDEX_KEY from nemo_rl.experience.rollouts import calculate_rewards +from nemo_rl.models.generation import configure_generation_config +from nemo_rl.models.generation.dynamo import DynamoConfig from nemo_rl.models.generation.megatron import MegatronGeneration +from nemo_rl.utils.config import load_config, register_omegaconf_resolvers from nemo_rl.utils.timer import Timer from tests.unit.algorithms.utils import ( create_mock_batch, @@ -949,25 +956,43 @@ class StubAsyncTrajectoryCollector: Actor methods expose MagicMocks with a ``remote`` attribute. """ - def __init__(self, health_side_effect=None): + def __init__( + self, + events=None, + health_side_effect=None, + remote_error_event=None, + remote_error_ref=None, + ): + self._events = events + self._remote_error_event = remote_error_event + self._remote_error_ref = remote_error_ref self.check_health = MagicMock() self.check_health.remote = MagicMock( return_value=None, side_effect=health_side_effect ) + def _remote_method(self, event): + mock = MagicMock() + + def remote(*args, **kwargs): + if self._events is not None: + self._events.append(event) + if event == self._remote_error_event: + return self._remote_error_ref + return MagicMock() + + mock.remote = MagicMock(side_effect=remote) + return mock + @property def start_collection(self): """Start collection - returns a remote-callable mock""" - mock = MagicMock() - mock.remote = MagicMock(return_value=MagicMock()) # Returns a fake ObjectRef - return mock + return self._remote_method("start_collection") @property def set_weight_version(self): """Set weight version - returns a remote-callable mock""" - mock = MagicMock() - mock.remote = MagicMock(return_value=MagicMock()) - return mock + return self._remote_method("set_weight_version") @property def pause(self): @@ -993,9 +1018,7 @@ def prepare_for_refit(self): @property def resume_after_refit(self): """Resume after refit - returns a remote-callable mock""" - mock = MagicMock() - mock.remote = MagicMock(return_value=MagicMock()) - return mock + return self._remote_method("resume_after_refit") @property def stop(self): @@ -1046,6 +1069,9 @@ def mock_async_grpo_infrastructure( mock_rollout_metrics, seq_logprob_error_result=None, collector_health_side_effect=None, + collector_events=None, + refit_side_effect=None, + collector_remote_error_event=None, ): """ Context manager that mocks all async GRPO infrastructure (Ray actors, venv, etc). @@ -1062,8 +1088,12 @@ def mock_async_grpo_infrastructure( mock_batch=mock_batch, mock_rollout_metrics=mock_rollout_metrics, ) + collector_remote_error_ref = object() stub_collector = StubAsyncTrajectoryCollector( - health_side_effect=collector_health_side_effect + events=collector_events, + health_side_effect=collector_health_side_effect, + remote_error_event=collector_remote_error_event, + remote_error_ref=collector_remote_error_ref, ) # Patch venv creation @@ -1096,7 +1126,9 @@ def mock_async_grpo_infrastructure( ) # Patch ray.get to return values from our stubs (not remote refs) - def mock_ray_get(ref): + def mock_ray_get(ref, **_kwargs): + if ref is collector_remote_error_ref: + raise RuntimeError(f"{collector_remote_error_event} failed") # If it's already a plain value (from our stubs), return it if isinstance(ref, (int, str, dict, list)): return ref @@ -1127,7 +1159,11 @@ def mock_ray_get(ref): # Patch refit and validate functions stack.enter_context( - patch("nemo_rl.algorithms.grpo.refit_policy_generation", return_value=None) + patch( + "nemo_rl.algorithms.grpo.refit_policy_generation", + side_effect=refit_side_effect, + return_value=None, + ) ) stack.enter_context( patch("nemo_rl.algorithms.grpo.validate", return_value=({}, {})) @@ -1285,6 +1321,7 @@ def test_async_grpo_propagates_main_loop_collector_failure(mock_grpo_components) @pytest.mark.parametrize( ("generation_config", "expected"), [ + ({"backend": "dynamo"}, True), ({"backend": "vllm", "vllm_cfg": {"async_engine": False}}, False), ({"backend": "vllm", "vllm_cfg": {"async_engine": True}}, True), ( @@ -1306,6 +1343,129 @@ def test_should_use_async_rollouts_selects_backend_specific_config( assert _should_use_async_rollouts(master_config) is expected +@pytest.mark.parametrize("backend", ["dynamo", "vllm"]) +def test_initial_refit_completes_before_async_collection_starts( + mock_grpo_components, + backend, +) -> None: + master_config = mock_grpo_components["master_config"] + master_config.policy["generation"]["backend"] = backend + master_config.policy["generation"]["colocated"]["enabled"] = False + master_config.grpo.max_num_steps = 1 + master_config.grpo.val_period = 0 + master_config.grpo.val_at_start = False + master_config.grpo.val_at_end = False + mock_batch = next(iter(mock_grpo_components["train_dataloader"])) + rollout_metrics = {"mean_gen_tokens_per_sample": 2.0} + events = [] + + def record_refit(*args, **kwargs): + events.append("refit") + + with mock_async_grpo_infrastructure( + mock_batch, + rollout_metrics, + collector_events=events, + refit_side_effect=record_refit, + ): + async_grpo_train( + mock_grpo_components["policy"], + _mock_policy_generation(), + mock_grpo_components["train_dataloader"], + mock_grpo_components["val_dataloader"], + mock_grpo_components["tokenizer"], + mock_grpo_components["loss_fn"], + mock_grpo_components["task_to_env"], + mock_grpo_components["val_task_to_env"], + mock_grpo_components["logger"], + mock_grpo_components["checkpointer"], + _initial_grpo_save_state(), + master_config, + ) + + assert events[:3] == ["refit", "set_weight_version", "start_collection"] + + +def test_async_grpo_awaits_resume_after_refit_failure(mock_grpo_components) -> None: + master_config = mock_grpo_components["master_config"] + master_config.policy["generation"]["backend"] = "dynamo" + master_config.policy["generation"]["colocated"]["enabled"] = False + master_config.grpo.max_num_steps = 1 + master_config.grpo.val_period = 0 + master_config.grpo.val_at_start = False + master_config.grpo.val_at_end = False + mock_batch = next(iter(mock_grpo_components["train_dataloader"])) + + with ( + mock_async_grpo_infrastructure( + mock_batch, + {"mean_gen_tokens_per_sample": 2.0}, + collector_remote_error_event="resume_after_refit", + ), + pytest.raises(RuntimeError, match="resume_after_refit failed"), + ): + async_grpo_train( + mock_grpo_components["policy"], + _mock_policy_generation(), + mock_grpo_components["train_dataloader"], + mock_grpo_components["val_dataloader"], + mock_grpo_components["tokenizer"], + mock_grpo_components["loss_fn"], + mock_grpo_components["task_to_env"], + mock_grpo_components["val_task_to_env"], + mock_grpo_components["logger"], + mock_grpo_components["checkpointer"], + _initial_grpo_save_state(), + master_config, + ) + + +def test_shutdown_environments_drains_unique_actors_before_kill() -> None: + shared_environment = MagicMock() + failing_environment = MagicMock() + shared_shutdown_ref = object() + failing_shutdown_ref = object() + shared_environment.shutdown.remote.return_value = shared_shutdown_ref + failing_environment.shutdown.remote.return_value = failing_shutdown_ref + + def get_or_fail(ref, timeout=None): + assert timeout == 10 + if ref is failing_shutdown_ref: + raise RuntimeError("environment shutdown failed") + assert ref is shared_shutdown_ref + return True + + with ( + patch("nemo_rl.algorithms.grpo.ray.get", side_effect=get_or_fail), + patch("nemo_rl.algorithms.grpo.ray.kill") as ray_kill, + ): + shutdown_environments( + {"train": shared_environment, "failing": failing_environment}, + {"validation": shared_environment}, + ) + + shared_environment.shutdown.remote.assert_called_once_with() + failing_environment.shutdown.remote.assert_called_once_with() + ray_kill.assert_called_once_with(failing_environment) + + +def test_should_use_nemo_gym_requires_dynamo_token_wrapper() -> None: + master_config = MagicMock() + master_config.env = {"should_use_nemo_gym": True} + master_config.policy = { + "generation": { + "backend": "dynamo", + "vllm_cfg": {"expose_http_server": False}, + } + } + + with pytest.raises(AssertionError, match="expose_http_server: true"): + _should_use_nemo_gym(master_config) + + master_config.policy["generation"]["vllm_cfg"]["expose_http_server"] = True + assert _should_use_nemo_gym(master_config) is True + + @contextmanager def _patched_logprob_phase(policy): """Provide real tensors for the logprob phase of ``grpo_train``. @@ -2018,6 +2178,241 @@ def test_noncolocated_inference_requires_explicit_gpus_per_node_single_node( setup(master_config, tokenizer, dataset, None) +def test_dynamo_rejects_colocated_inference_before_setup_side_effects( + mock_grpo_components, +): + from nemo_rl.algorithms.grpo import setup + + master_config = mock_grpo_components["master_config"] + master_config.policy["hf_config_overrides"] = {"rope_theta": 1_000_000.0} + master_config.policy["generation"] = { + "backend": "dynamo", + "dynamo_cfg": { + "engine": "vllm", + "startup_timeout_s": 60, + "request_timeout_s": 60, + "control_timeout_s": 30, + "metrics_include_prefixes": None, + "metrics_exclude_prefixes": None, + "worker_args": { + "tool_call_parser": None, + "reasoning_parser": None, + "exclude_tools_when_tool_choice_none": True, + "enable_structural_tag": False, + "structural_tag_scope": "auto", + "structural_tag_schema": "auto", + "custom_jinja_template": None, + "endpoint_types": ["chat", "completions"], + "extra_cli_args": [], + }, + "frontend_args": { + "tokenizer": "default", + "tokenizer_cache": False, + "tokenizer_cache_bytes": 1024, + "router_mode": "kv", + "router_reset_states": True, + "extra_cli_args": [], + }, + }, + "vllm_cfg": { + "async_engine": True, + "tensor_parallel_size": 1, + "pipeline_parallel_size": 1, + "expert_parallel_size": 1, + "gpu_memory_utilization": 0.6, + "max_model_len": 512, + "kv_cache_dtype": "auto", + "load_format": "auto", + "precision": "bfloat16", + "enforce_eager": True, + "expose_http_server": False, + "enable_vllm_metrics_logger": True, + "vllm_metrics_logger_interval": 1.0, + "env_vars": {}, + }, + "vllm_kwargs": {}, + "colocated": { + "enabled": True, + "resources": {"gpus_per_node": None, "num_nodes": None}, + }, + } + + master_config.grpo.async_grpo.in_flight_weight_updates = True + with ( + patch("nemo_rl.algorithms.grpo.Logger") as mock_logger, + pytest.raises(ValueError, match="in_flight_weight_updates must be false"), + ): + setup( + master_config, + tokenizer=MagicMock(), + dataset=MagicMock(), + val_dataset=None, + ) + mock_logger.assert_not_called() + + master_config.grpo.async_grpo.in_flight_weight_updates = False + + with ( + patch("nemo_rl.algorithms.grpo.Logger") as mock_logger, + pytest.raises( + ValueError, + match="must be false", + ), + ): + setup( + master_config, + tokenizer=MagicMock(), + dataset=MagicMock(), + val_dataset=None, + ) + + mock_logger.assert_not_called() + assert ( + master_config.policy["generation"]["vllm_kwargs"]["hf_overrides"] + == (master_config.policy["hf_config_overrides"]) + ) + + del master_config.policy["generation"]["vllm_kwargs"] + del master_config.policy["hf_config_overrides"] + with pytest.raises(ValueError, match="must be false"): + setup( + master_config, + tokenizer=MagicMock(), + dataset=MagicMock(), + val_dataset=None, + ) + assert master_config.policy["generation"]["vllm_kwargs"]["hf_overrides"] == {} + + +def test_setup_initializes_noncolocated_dynamo_with_nemo_gym(monkeypatch) -> None: + from nemo_rl.algorithms import grpo as grpo_mod + + repo_root = Path(__file__).resolve().parents[3] + register_omegaconf_resolvers() + config = OmegaConf.to_container( + load_config(repo_root / "examples/configs/grpo_math_1B_dynamo.yaml"), + resolve=True, + ) + config["cluster"].update({"num_nodes": 2, "gpus_per_node": 4, "segment_size": 1}) + generation = config["policy"]["generation"] + generation["vllm_cfg"].update( + { + "tensor_parallel_size": 2, + "pipeline_parallel_size": 2, + "expert_parallel_size": 2, + "expose_http_server": True, + } + ) + generation["colocated"]["resources"] = { + "gpus_per_node": 4, + "num_nodes": 1, + } + config["env"]["should_use_nemo_gym"] = True + tokenizer = MagicMock() + tokenizer.pad_token_id = 0 + tokenizer.eos_token_id = 1 + config["policy"]["generation"] = configure_generation_config(generation, tokenizer) + master_config = MasterConfig.model_validate(config) + + cluster_instances = [] + + class DummyCluster: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.num_gpus_per_node = kwargs["num_gpus_per_node"] + self.get_placement_groups = MagicMock(return_value=[object()]) + cluster_instances.append(self) + + class DummyLoader: + def __init__(self, *_args, **_kwargs): + pass + + def __len__(self): + return 1 + + class DummyCheckpointer: + def get_latest_checkpoint_path(self): + return None + + def load_training_info(self, _path): + return None + + def get_resume_paths(self, _path): + return None, None + + class DummyPolicy: + def print_node_ip_and_gpu_id(self): + pass + + dynamo_init = MagicMock() + + class DummyDynamoGeneration: + weight_synchronizer = None + dp_openai_server_base_urls = ["http://dynamo-wrapper.example/v1"] + frontend_url = "http://dynamo-frontend.example/v1" + + def __init__(self, *, cluster, config, tokenizer, tokenizer_config): + dynamo_init( + cluster=cluster, + config=config, + tokenizer=tokenizer, + tokenizer_config=tokenizer_config, + ) + + synchronizer = MagicMock() + nemo_gym_actor = object() + spinup_nemo_gym_actor = MagicMock(return_value=nemo_gym_actor) + monkeypatch.setattr(grpo_mod, "Logger", lambda *_args, **_kwargs: MagicMock()) + monkeypatch.setattr( + grpo_mod, "CheckpointManager", lambda *_args, **_kwargs: DummyCheckpointer() + ) + monkeypatch.setattr( + grpo_mod, "ClippedPGLossFn", lambda *_args, **_kwargs: MagicMock() + ) + monkeypatch.setattr(grpo_mod, "StatefulDataLoader", DummyLoader) + monkeypatch.setattr( + grpo_mod, + "get_ray_cluster_topology", + lambda: { + "train-node": ("nvlink_domain_train", 0), + "inference-node": ("nvlink_domain_inference", 1), + }, + ) + monkeypatch.setattr(grpo_mod, "RayVirtualCluster", DummyCluster) + monkeypatch.setattr(grpo_mod, "Policy", lambda *_args, **_kwargs: DummyPolicy()) + monkeypatch.setattr(grpo_mod, "DynamoGeneration", DummyDynamoGeneration) + monkeypatch.setattr( + grpo_mod, "create_weight_synchronizer", lambda **_kwargs: synchronizer + ) + monkeypatch.setattr(grpo_mod, "spinup_nemo_gym_actor", spinup_nemo_gym_actor) + + dataset = MagicMock() + dataset.__len__.return_value = 2 + result = setup(master_config, tokenizer, dataset, None) + + train_cluster, inference_cluster = cluster_instances + assert train_cluster.kwargs["bundle_ct_per_node_list"] == [4] + assert inference_cluster.kwargs["bundle_ct_per_node_list"] == [4] + assert train_cluster.kwargs["node_resource_constraints"] == [ + {"nvlink_domain_train": 0.001} + ] + assert inference_cluster.kwargs["node_resource_constraints"] is None + assert result[1].dp_openai_server_base_urls == ["http://dynamo-wrapper.example/v1"] + assert result[2] is nemo_gym_actor + dynamo_config = dynamo_init.call_args.kwargs["config"] + assert dynamo_init.call_args.kwargs["cluster"] is inference_cluster + assert DynamoConfig.model_validate(dynamo_config).engine_world_size == 4 + synchronizer.init_communicator.assert_called_once_with() + spinup_nemo_gym_actor.assert_called_once_with( + env_configs=master_config.env, + base_urls=["http://dynamo-wrapper.example/v1"], + model_name=master_config.policy["model_name"], + enable_router_replay=False, + routed_experts_dtype="int16", + use_fastokens=False, + ) + + def test_noncolocated_inference_requires_explicit_gpus_per_node_multi_node( mock_grpo_components, ): @@ -2271,6 +2666,123 @@ def init_collective(self, *_args, **_kwargs): assert master_config.grpo.skip_reference_policy_logprobs_calculation is True +def test_setup_starts_nemo_gym_for_trtllm(monkeypatch, mock_grpo_components): + """Guard the TRT-LLM NeMo-Gym startup path in shared GRPO setup.""" + from nemo_rl.algorithms import grpo as grpo_mod + + class DummyLogger: + def log_hyperparams(self, *_args, **_kwargs): + pass + + def log_metrics(self, *_args, **_kwargs): + pass + + class DummyCheckpointer: + def get_latest_checkpoint_path(self): + return None + + def load_training_info(self, _path): + return None + + def get_resume_paths(self, _path): + return None, None + + class DummyLoader: + def __init__(self, *_args, **_kwargs): + pass + + def __len__(self): + return 1 + + class DummyCluster: + def __init__(self, *_args, **_kwargs): + pass + + class DummyPolicy: + def print_node_ip_and_gpu_id(self): + pass + + def prepare_refit_info(self): + return {} + + class DummyTrtllmGeneration: + dp_openai_server_base_urls = ["http://trtllm.example/v1"] + weight_synchronizer = None + + def finish_generation(self): + pass + + def prepare_refit_info(self, _state): + pass + + nemo_gym_actor = object() + spinup_nemo_gym_actor = MagicMock(return_value=nemo_gym_actor) + monkeypatch.setattr(grpo_mod, "Logger", lambda *_args, **_kwargs: DummyLogger()) + monkeypatch.setattr( + grpo_mod, "CheckpointManager", lambda *_args, **_kwargs: DummyCheckpointer() + ) + monkeypatch.setattr( + grpo_mod, "ClippedPGLossFn", lambda *_args, **_kwargs: MagicMock() + ) + monkeypatch.setattr(grpo_mod, "StatefulDataLoader", DummyLoader) + monkeypatch.setattr(grpo_mod, "RayVirtualCluster", DummyCluster) + monkeypatch.setattr(grpo_mod, "Policy", lambda *_args, **_kwargs: DummyPolicy()) + monkeypatch.setattr( + grpo_mod, + "TrtllmGeneration", + lambda *_args, **_kwargs: DummyTrtllmGeneration(), + ) + monkeypatch.setattr(grpo_mod, "spinup_nemo_gym_actor", spinup_nemo_gym_actor) + + master_config = mock_grpo_components["master_config"] + master_config.policy["model_name"] = "test-model" + master_config.policy["tokenizer"] = {"use_fastokens": False} + master_config.policy["dtensor_cfg"] = {"enabled": False} + master_config.policy["megatron_cfg"] = { + "enabled": False, + "pipeline_model_parallel_size": 1, + } + master_config.policy["generation"] = { + "backend": "trtllm", + "temperature": 1.0, + "top_p": 1.0, + "top_k": None, + "val_temperature": 1.0, + "val_top_p": 1.0, + "val_top_k": None, + "colocated": { + "enabled": True, + "resources": {"gpus_per_node": None, "num_nodes": None}, + }, + "trtllm_cfg": { + "tensor_parallel_size": 1, + "async_engine": True, + "expose_http_server": True, + }, + } + master_config.env = {"should_use_nemo_gym": True} + master_config.loss_fn = ClippedPGLossConfig(reference_policy_kl_penalty=0.0) + master_config.grpo.val_period = 0 + master_config.grpo.batch_multiplier = 1 + master_config.cluster["gpus_per_node"] = 1 + master_config.data["shuffle"] = False + master_config.data["num_workers"] = 0 + + dataset = MagicMock() + dataset.__len__ = MagicMock(return_value=1) + result = grpo_mod.setup(master_config, MagicMock(), dataset, None) + + assert result[2] is nemo_gym_actor + spinup_nemo_gym_actor.assert_called_once_with( + env_configs=master_config.env, + base_urls=["http://trtllm.example/v1"], + model_name="test-model", + enable_router_replay=False, + routed_experts_dtype="int16", + use_fastokens=False, + ) + + def test_grpo_train_collects_generation_logger_and_seq_metrics( monkeypatch, mock_grpo_components ): diff --git a/tests/unit/distributed/test_stateless_process_group.py b/tests/unit/distributed/test_stateless_process_group.py new file mode 100644 index 00000000000..94b5b22c2e8 --- /dev/null +++ b/tests/unit/distributed/test_stateless_process_group.py @@ -0,0 +1,182 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ctypes +import pickle +import sys +import types +from contextlib import contextmanager, nullcontext + +import pytest + +from nemo_rl.distributed import stateless_process_group as spg + + +@contextmanager +def _vllm_unique_id_type(): + module_names = [ + "vllm", + "vllm.distributed", + "vllm.distributed.device_communicators", + spg._VLLM_NCCL_MODULE, + ] + previous_modules = {name: sys.modules.get(name) for name in module_names} + modules = {name: types.ModuleType(name) for name in module_names} + for name in module_names[:-1]: + modules[name].__path__ = [] + + modules["vllm"].distributed = modules["vllm.distributed"] + modules["vllm.distributed"].device_communicators = modules[ + "vllm.distributed.device_communicators" + ] + modules["vllm.distributed.device_communicators"].pynccl_wrapper = modules[ + spg._VLLM_NCCL_MODULE + ] + unique_id_type = type( + "ncclUniqueId", + (ctypes.Structure,), + { + "__module__": spg._VLLM_NCCL_MODULE, + "_fields_": [("internal", ctypes.c_byte * 128)], + }, + ) + modules[spg._VLLM_NCCL_MODULE].ncclUniqueId = unique_id_type + + try: + sys.modules.update(modules) + yield unique_id_type + finally: + for name, previous_module in previous_modules.items(): + if previous_module is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous_module + + +def test_vllm_unique_id_pickle_unpickles_as_vllm_ctypes_type(): + unique_id_bytes = bytes(range(128)) + payload = spg._pickle_vllm_unique_id(unique_id_bytes) + + with _vllm_unique_id_type() as unique_id_type: + unique_id = pickle.loads(payload) + + assert isinstance(unique_id, unique_id_type) + assert bytes(unique_id) == unique_id_bytes + + +def test_vllm_unique_id_pickle_requires_nccl_id_size(): + with pytest.raises(ValueError, match="128-byte NCCL unique ID"): + spg._pickle_vllm_unique_id(b"too short") + + +class _Store: + def __init__(self): + self.data = {} + + def set(self, key, value): + self.data[key] = value + + +class _Stream: + cuda_stream = 123 + + def __init__(self): + self.synchronized = False + + def synchronize(self): + self.synchronized = True + + +class _Communicator: + def __init__(self): + self.allreduce_calls = [] + self.broadcast_calls = [] + + def allreduce(self, **kwargs): + self.allreduce_calls.append(kwargs) + + def broadcast(self, **kwargs): + self.broadcast_calls.append(kwargs) + + +def _make_process_group(monkeypatch): + store = _Store() + stream = _Stream() + communicator = _Communicator() + unique_id_bytes = bytes(range(128)) + unique_id = types.SimpleNamespace(as_bytes=unique_id_bytes) + + monkeypatch.setattr(spg.torch.distributed, "TCPStore", lambda **_kwargs: store) + monkeypatch.setattr(spg, "get_unique_id", lambda: unique_id) + monkeypatch.setattr(spg.Communicator, "init", lambda **_kwargs: communicator) + monkeypatch.setattr(spg.torch.cuda, "device", lambda _device: nullcontext()) + monkeypatch.setattr(spg.torch.cuda, "current_stream", lambda: stream) + + group = spg.StatelessProcessGroup( + master_address="127.0.0.1", port=1234, rank=0, world_size=2 + ) + return group, store, stream, communicator, unique_id_bytes + + +def test_vllm_peer_uses_vllm_metadata_and_allreduce_warmup(monkeypatch): + # Coupled to vLLM 0.23.0's stateless process-group wire protocol. Reverify + # this literal whenever the isolated Dynamo vLLM pin changes. + assert spg._VLLM_UNIQUE_ID_KEY == "broadcast_from/0/0" + group, store, stream, communicator, unique_id_bytes = _make_process_group( + monkeypatch + ) + warmup_tensor = object() + monkeypatch.setattr(spg.torch, "zeros", lambda *_args, **_kwargs: warmup_tensor) + + group.init_nccl_communicator(device=0, peer="vllm") + + assert store.data[spg._NEMO_UNIQUE_ID_KEY] == unique_id_bytes + with _vllm_unique_id_type(): + stored_unique_id = pickle.loads(store.data[spg._VLLM_UNIQUE_ID_KEY]) + assert bytes(stored_unique_id) == unique_id_bytes + assert communicator.allreduce_calls == [ + { + "sendbuf": warmup_tensor, + "recvbuf": warmup_tensor, + "op": spg.SUM, + "stream": 123, + } + ] + assert communicator.broadcast_calls == [] + assert stream.synchronized + + +def test_nemo_peer_preserves_broadcast_warmup(monkeypatch): + group, store, stream, communicator, unique_id_bytes = _make_process_group( + monkeypatch + ) + warmup_tensor = object() + expected_tensor = object() + monkeypatch.setattr(spg.torch, "ones", lambda *_args, **_kwargs: warmup_tensor) + monkeypatch.setattr(spg.torch, "allclose", lambda actual, expected: True) + monkeypatch.setattr(spg.torch, "zeros", lambda *_args, **_kwargs: expected_tensor) + + group.init_nccl_communicator(device=0) + + assert store.data == {spg._NEMO_UNIQUE_ID_KEY: unique_id_bytes} + assert communicator.broadcast_calls == [ + { + "sendbuf": warmup_tensor, + "recvbuf": warmup_tensor, + "root": 0, + "stream": 123, + } + ] + assert communicator.allreduce_calls == [] + assert stream.synchronized diff --git a/tests/unit/distributed/test_virtual_cluster.py b/tests/unit/distributed/test_virtual_cluster.py index 2144be7d330..72bb2adc3ee 100644 --- a/tests/unit/distributed/test_virtual_cluster.py +++ b/tests/unit/distributed/test_virtual_cluster.py @@ -298,6 +298,21 @@ def test_single_port_range(self): port = _bind_socket_in_range(s, 12010, 12011) assert port == 12010 + def test_exhaustive_selection_skips_excluded_ports(self, monkeypatch): + mock_sock = MagicMock() + monkeypatch.setattr("random.shuffle", lambda candidates: None) + + port = _bind_socket_in_range( + mock_sock, + 12020, + 12023, + max_retries=None, + excluded_ports={12020, 12021}, + ) + + assert port == 12022 + mock_sock.bind.assert_called_once_with(("", 12022)) + class TestGetFreePortLocal: """Tests for _get_free_port_local().""" diff --git a/tests/unit/environments/test_nemo_gym_health.py b/tests/unit/environments/test_nemo_gym_health.py index f8fd7c6894a..41f5d0c4cc8 100644 --- a/tests/unit/environments/test_nemo_gym_health.py +++ b/tests/unit/environments/test_nemo_gym_health.py @@ -90,6 +90,9 @@ def test_shutdown_is_a_noop_so_teardown_does_not_mask_the_real_error(self): def test_shutdown_still_forwards_when_spun_up(self): env = _unspun() - env.rh = _FakeRunHelper() + run_helper = _FakeRunHelper() + env.rh = run_helper + env.shutdown() env.shutdown() - assert env.rh.shutdowns == 1 + assert run_helper.shutdowns == 1 + assert env.rh is None diff --git a/tests/unit/experience/test_rollouts.py b/tests/unit/experience/test_rollouts.py index db6310c6b0a..01513d1d37e 100644 --- a/tests/unit/experience/test_rollouts.py +++ b/tests/unit/experience/test_rollouts.py @@ -850,6 +850,11 @@ def fake_rewards(batch, task_to_env): assert [call["dedup"] for call in calls] == [deduplicate_multimodal_data] * 2 +class _DummyDynamoGeneration(_DummySGLangGeneration): + def __init__(self): + self.cfg = {"backend": "dynamo"} + + def test_generate_responses_async_requires_sglang_opt_in(): generation_input_data = BatchedDataDict( { @@ -895,6 +900,30 @@ def test_generate_responses_async_allows_sglang_opt_in(): assert gen_metrics["total_generated_tokens"] == 1 +def test_generate_responses_async_allows_dynamo(): + generation_input_data = BatchedDataDict( + { + "input_ids": torch.tensor([[1]]), + "input_lengths": torch.tensor([1], dtype=torch.long), + } + ) + batch = BatchedDataDict({"message_log": [[]]}) + + updated_batch, generated_ids, gen_metrics = asyncio.run( + generate_responses_async( + _DummyDynamoGeneration(), + generation_input_data, + batch, + _DummyTokenizer(), + input_lengths=generation_input_data["input_lengths"], + ) + ) + + assert updated_batch["message_log"][0][-1]["content"] == "ok" + assert generated_ids[0].tolist() == [2] + assert gen_metrics["total_generated_tokens"] == 1 + + @pytest.fixture(scope="function") def rollout_tokenizer(): """Loads the tokenizer for the tests.""" diff --git a/tests/unit/models/generation/test_dynamo_arguments.py b/tests/unit/models/generation/test_dynamo_arguments.py new file mode 100644 index 00000000000..38812282c55 --- /dev/null +++ b/tests/unit/models/generation/test_dynamo_arguments.py @@ -0,0 +1,380 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings + +import pytest +from pydantic import ValidationError + +from nemo_rl.models.generation import configure_generation_config +from nemo_rl.models.generation.dynamo.arguments import ( + build_dynamo_frontend_argv, + build_dynamo_vllm_argv, + build_managed_worker_env, + redact_argv, + redact_environment, +) +from nemo_rl.models.generation.dynamo.config import ( + DynamoCfg, + DynamoConfig, + DynamoWorkerArgs, +) + + +def _config(**overrides) -> dict: + config = { + "backend": "dynamo", + "model_name": "Qwen/Qwen3-0.6B", + "dynamo_cfg": _dynamo_cfg(), + "vllm_cfg": { + "async_engine": True, + "tensor_parallel_size": 2, + "pipeline_parallel_size": 1, + "expert_parallel_size": 2, + "gpu_memory_utilization": 0.8, + "max_model_len": 512, + "precision": "bfloat16", + "kv_cache_dtype": "auto", + "load_format": "auto", + "enforce_eager": False, + "expose_http_server": False, + "enable_vllm_metrics_logger": True, + "vllm_metrics_logger_interval": 1.0, + "env_vars": None, + }, + "vllm_kwargs": {}, + "colocated": {"enabled": False}, + } + config.update(overrides) + return config + + +def _dynamo_cfg() -> dict: + return { + "engine": "vllm", + "startup_timeout_s": 600, + "request_timeout_s": 900, + "control_timeout_s": 600, + "metrics_include_prefixes": None, + "metrics_exclude_prefixes": None, + "worker_args": { + "tool_call_parser": None, + "reasoning_parser": None, + "exclude_tools_when_tool_choice_none": True, + "enable_structural_tag": False, + "structural_tag_scope": "auto", + "structural_tag_schema": "auto", + "custom_jinja_template": None, + "endpoint_types": ["chat", "completions"], + "extra_cli_args": [], + }, + "frontend_args": { + "tokenizer": "default", + "tokenizer_cache": False, + "tokenizer_cache_bytes": 50 * 1024 * 1024, + "router_mode": "kv", + "router_reset_states": True, + "extra_cli_args": [], + }, + } + + +def _flag_value(argv: list[str], flag: str) -> str: + return argv[argv.index(flag) + 1] + + +def test_config_derives_world_size_and_rejects_removed_public_fields() -> None: + assert DynamoConfig.model_validate(_config()).engine_world_size == 2 + for field in ("engine_world_size", "namespace", "dynamo_python", "etcd_port"): + with pytest.raises(ValidationError, match=field): + DynamoConfig.model_validate( + _config(dynamo_cfg={field: 1 if field.endswith("port") else "x"}) + ) + + +@pytest.mark.parametrize( + ("override", "match"), + [ + ({"vllm_cfg": {}}, "nonempty"), + ({"sglang_cfg": {"foo": 1}}, "sglang_cfg"), + ({"trtllm_cfg": {"foo": 1}}, "trtllm_cfg"), + ({"colocated": {"enabled": True}}, "must be false"), + ({"refit_transport": "nccl_reshard"}, "must be null"), + ({"quant_cfg": "nvfp4"}, "quant_cfg"), + ({"vllm_kwargs": {"speculative_config": {"model": "draft"}}}, "draft"), + ], +) +def test_config_rejects_unsupported_modes(override, match) -> None: + if override.get("vllm_cfg") == {}: + config = _config() + config["vllm_cfg"] = {} + else: + config = _config(**override) + with pytest.raises(ValidationError, match=match): + DynamoConfig.model_validate(config) + + +@pytest.mark.parametrize( + ("vllm_cfg", "match"), + [ + ( + {"tensor_parallel_size": 2, "expert_parallel_size": 3}, + "expert_parallel_size", + ), + ({"precision": "fp8"}, "precision"), + ({"kv_cache_dtype": "fp8"}, "kv_cache_dtype"), + ], +) +def test_config_rejects_unsupported_parallelism_and_precision(vllm_cfg, match) -> None: + config = _config() + config["vllm_cfg"].update(vllm_cfg) + with pytest.raises(ValidationError, match=match): + DynamoConfig.model_validate(config) + + +def test_config_classifies_managed_logprobs_and_runtime_fields() -> None: + config = _config() + config["vllm_cfg"].update( + { + "logprobs_mode": "processed_logprobs", + "cap_max_tokens_to_context": False, + "use_deep_gemm": False, + "num_first_layers_in_bf16": 0, + "num_last_layers_in_bf16": 0, + } + ) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + DynamoConfig.model_validate(config) + assert caught == [] + + config["vllm_cfg"]["logprobs_mode"] = "raw_logprobs" + with pytest.raises(ValidationError, match="processed_logprobs"): + DynamoConfig.model_validate(config) + + +@pytest.mark.parametrize("field", ["skip_tokenizer_init", "cap_max_tokens_to_context"]) +def test_config_warns_only_for_active_unsupported_fields(field) -> None: + config = _config() + config["vllm_cfg"][field] = True + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + DynamoConfig.model_validate(config) + assert [str(warning.message) for warning in caught] == [ + f"policy.generation.vllm_cfg.{field} is ignored by backend='dynamo'" + ] + + +@pytest.mark.parametrize( + "field", + [ + "data_parallel_size", + "prefill_context_parallel_size", + "decode_context_parallel_size", + ], +) +@pytest.mark.parametrize("source", ["vllm_cfg", "vllm_kwargs"]) +def test_config_rejects_parallel_dimensions_outside_tp_pp(field, source) -> None: + config = _config() + config[source][field] = 2 + + with pytest.raises(ValidationError, match=f"{source}.{field} must be 1"): + DynamoConfig.model_validate(config) + + +def test_config_rejects_more_than_32_stop_strings() -> None: + config = _config(stop_strings=[str(index) for index in range(33)]) + + with pytest.raises(ValidationError, match="stop_strings supports at most 32"): + DynamoConfig.model_validate(config) + + +def test_config_does_not_limit_stop_token_ids() -> None: + config = _config(stop_token_ids=list(range(33))) + + assert DynamoConfig.model_validate(config).model_extra["stop_token_ids"] == list( + range(33) + ) + + +def test_configure_generation_config_selects_dynamo_load_format() -> None: + class Tokenizer: + pad_token_id = 0 + eos_token_id = 1 + + training = _config(stop_token_ids=None) + evaluation = _config(stop_token_ids=None) + del training["vllm_cfg"]["load_format"] + del evaluation["vllm_cfg"]["load_format"] + + assert ( + configure_generation_config(training, Tokenizer())["vllm_cfg"]["load_format"] + == "dummy" + ) + assert ( + configure_generation_config(evaluation, Tokenizer(), is_eval=True)["vllm_cfg"][ + "load_format" + ] + == "auto" + ) + + +def test_worker_argv_translates_structured_fields_and_warns_unclassified() -> None: + config = _dynamo_cfg() + config["worker_args"].update( + {"tool_call_parser": "qwen3_coder", "reasoning_parser": "nemotron_nano"} + ) + cfg = DynamoCfg.model_validate(config) + generation_config = _config() + generation_config["vllm_cfg"]["unclassified_field"] = 1 + with pytest.warns(UserWarning, match="unclassified_field"): + validated = DynamoConfig.model_validate(generation_config) + vllm_cfg = validated.vllm_cfg.model_dump() + argv = build_dynamo_vllm_argv( + model_name="model", + namespace="nemo-rl-1", + seed=7, + vllm_cfg=vllm_cfg, + vllm_kwargs={"max_num_seqs": 16, "hf_overrides": {"rope_theta": 1e6}}, + dynamo_cfg=cfg, + ) + + assert _flag_value(argv, "--model") == "model" + assert _flag_value(argv, "--weight-transfer-config") == '{"backend":"nccl"}' + assert _flag_value(argv, "--dyn-tool-call-parser") == "qwen3_coder" + assert _flag_value(argv, "--dyn-reasoning-parser") == "nemotron_nano" + assert _flag_value(argv, "--max-num-seqs") == "16" + assert _flag_value(argv, "--hf-overrides") == '{"rope_theta":1000000.0}' + assert "--enable-expert-parallel" in argv + + +def test_worker_argv_rejects_replaced_and_managed_options() -> None: + generation_config = _config() + generation_config["vllm_cfg"]["http_server_serving_chat_kwargs"] = { + "tool_parser": "x" + } + with pytest.raises(ValueError, match="worker_args.custom_jinja_template"): + DynamoConfig.model_validate(generation_config) + config = _dynamo_cfg() + config["worker_args"]["extra_cli_args"] = ["--model", "other"] + with pytest.raises(ValueError, match="--model is set by both"): + build_dynamo_vllm_argv( + model_name="model", + namespace="namespace", + seed=0, + vllm_cfg=_config()["vllm_cfg"], + vllm_kwargs={}, + dynamo_cfg=DynamoCfg.model_validate(config), + ) + + +def test_config_accepts_inherited_unused_sections() -> None: + config = _config( + mcore_generation_config={"some_shared_setting": True}, + refit_cfg={"some_shared_setting": True}, + ) + + validated = DynamoConfig.model_validate(config) + + assert validated.model_extra["mcore_generation_config"] == { + "some_shared_setting": True + } + assert validated.model_extra["refit_cfg"] == {"some_shared_setting": True} + + +def test_frontend_argv_and_environment_are_runtime_owned() -> None: + cfg = DynamoCfg.model_validate(_dynamo_cfg()) + argv = build_dynamo_frontend_argv( + host="0.0.0.0", port=3001, namespace="nemo-rl", dynamo_cfg=cfg + ) + assert _flag_value(argv, "--router-mode") == "kv" + + env = build_managed_worker_env( + base_env={ + "DYN_NAMESPACE": "stale", + "ETCD_ENDPOINTS": "http://stale-etcd:2379", + "ETCD_USERNAME": "stale-user", + "NATS_SERVER": "nats://stale-nats:4222", + "NATS_AUTH_TOKEN": "stale-token", + "NCCL_DEBUG": "INFO", + }, + configured_env={"NCCL_IB_DISABLE": "0"}, + manager_env={ + "DYN_NAMESPACE": "owned", + "DYN_SYSTEM_PORT": "4000", + "ETCD_ENDPOINTS": "http://managed-etcd:2379", + "NATS_SERVER": "nats://managed-nats:4222", + }, + ) + assert env["DYN_NAMESPACE"] == "owned" + assert env["DYN_SYSTEM_PORT"] == "4000" + assert env["ETCD_ENDPOINTS"] == "http://managed-etcd:2379" + assert env["NATS_SERVER"] == "nats://managed-nats:4222" + assert "ETCD_USERNAME" not in env + assert "NATS_AUTH_TOKEN" not in env + with pytest.raises(ValueError, match="VLLM_PORT"): + build_managed_worker_env( + base_env={}, + configured_env={"VLLM_PORT": "9999"}, + manager_env={"VLLM_PORT": "7000"}, + ) + + +def test_every_worker_config_field_reaches_argv(monkeypatch) -> None: + from nemo_rl.models.generation.dynamo import arguments + + sources: set[str] = set() + original_add = arguments._ArgvBuilder.add + + def record_source(self, flag, value=None, *, source): + sources.add(source) + return original_add(self, flag, value, source=source) + + monkeypatch.setattr(arguments._ArgvBuilder, "add", record_source) + config = _dynamo_cfg() + config["worker_args"].update( + { + "tool_call_parser": "qwen3_coder", + "reasoning_parser": "nemotron_nano", + "custom_jinja_template": "template", + } + ) + cfg = DynamoCfg.model_validate(config) + build_dynamo_vllm_argv( + model_name="model", + namespace="namespace", + seed=0, + vllm_cfg=_config()["vllm_cfg"], + vllm_kwargs={}, + dynamo_cfg=cfg, + ) + + configured_fields = { + source.rsplit(".", 1)[-1] + for source in sources + if source.startswith("dynamo_cfg.worker_args.") + } + assert set(DynamoWorkerArgs.model_fields) - {"extra_cli_args"} <= configured_fields + + +def test_redaction_hides_credentials() -> None: + assert redact_argv(["worker", "--api-key", "secret"])[2] == "" + assert redact_argv( + ["worker", "--max-num-batched-tokens", "8192", "--stop-token-ids", "1,2"] + ) == ["worker", "--max-num-batched-tokens", "8192", "--stop-token-ids", "1,2"] + assert redact_environment({"HF_TOKEN": "secret", "NCCL_DEBUG": "INFO"}) == { + "HF_TOKEN": "", + "NCCL_DEBUG": "INFO", + } diff --git a/tests/unit/models/generation/test_dynamo_generation.py b/tests/unit/models/generation/test_dynamo_generation.py new file mode 100644 index 00000000000..7a53e212f89 --- /dev/null +++ b/tests/unit/models/generation/test_dynamo_generation.py @@ -0,0 +1,544 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import pickle +from typing import Any +from unittest.mock import MagicMock + +import pytest +import torch + +from nemo_rl.distributed.batched_data_dict import BatchedDataDict +from nemo_rl.models.generation.dynamo import DynamoGeneration +from nemo_rl.models.generation.dynamo import dynamo_generation as generation_module +from nemo_rl.models.generation.dynamo import metrics as metrics_module +from nemo_rl.models.generation.dynamo import refit as refit_module +from nemo_rl.models.generation.dynamo.config import ( + VLLM_PACKED_BUFFER_SIZE_BYTES, + VLLM_PACKED_NUM_BUFFERS, +) +from nemo_rl.models.generation.dynamo.metrics import ( + DynamoMetricsSampler, + parse_prometheus_metrics, +) +from nemo_rl.models.generation.dynamo.refit import DynamoRefitChannel + + +def _config(*, tp: int = 1, expose_http_server: bool = False) -> dict[str, Any]: + return { + "backend": "dynamo", + "model_name": "Qwen/Qwen3-0.6B", + "max_new_tokens": 16, + "temperature": 1.0, + "top_p": 1.0, + "top_k": None, + "stop_token_ids": None, + "stop_strings": None, + "_pad_token_id": 0, + "colocated": {"enabled": False}, + "dynamo_cfg": { + "engine": "vllm", + "startup_timeout_s": 5, + "request_timeout_s": 30, + "control_timeout_s": 10, + "metrics_include_prefixes": None, + "metrics_exclude_prefixes": None, + "worker_args": { + "tool_call_parser": None, + "reasoning_parser": None, + "exclude_tools_when_tool_choice_none": True, + "enable_structural_tag": False, + "structural_tag_scope": "auto", + "structural_tag_schema": "auto", + "custom_jinja_template": None, + "endpoint_types": ["chat", "completions"], + "extra_cli_args": [], + }, + "frontend_args": { + "tokenizer": "default", + "tokenizer_cache": False, + "tokenizer_cache_bytes": 50 * 1024 * 1024, + "router_mode": "kv", + "router_reset_states": True, + "extra_cli_args": [], + }, + }, + "vllm_cfg": { + "async_engine": True, + "tensor_parallel_size": tp, + "pipeline_parallel_size": 1, + "expert_parallel_size": tp, + "gpu_memory_utilization": 0.8, + "precision": "bfloat16", + "kv_cache_dtype": "auto", + "max_model_len": 5, + "load_format": "auto", + "enforce_eager": False, + "expose_http_server": expose_http_server, + "enable_vllm_metrics_logger": True, + "vllm_metrics_logger_interval": 1.0, + "env_vars": None, + }, + "vllm_kwargs": {}, + } + + +def _patch_runtime( + monkeypatch: pytest.MonkeyPatch, + workers: list[dict[str, Any]] | None = None, + calls: list[str] | None = None, +) -> None: + endpoints = workers or [ + {"instance_id": "worker-0", "system_url": "http://10.0.0.2:4000"} + ] + events = calls if calls is not None else [] + + class FakeRuntime: + def __init__(self, *, cluster, config): + events.append("init") + + def start(self): + events.append("start") + + @property + def frontend_url(self): + return "http://10.0.0.1:3000/v1" + + def refit_workers(self): + return [dict(worker) for worker in endpoints] + + def validate_workers(self, expected): + return expected + + def shutdown(self): + events.append("shutdown") + + monkeypatch.setattr(generation_module, "ManagedDynamoRuntime", FakeRuntime) + + +def _data() -> BatchedDataDict: + return BatchedDataDict( + { + "input_ids": torch.tensor([[1, 2, 3, 0]], dtype=torch.long), + "input_lengths": torch.tensor([3], dtype=torch.long), + "stop_strings": [["stop"]], + } + ) + + +def _completion_response(token_ids: list[int]) -> dict[str, Any]: + return { + "choices": [ + { + "finish_reason": "stop", + "logprobs": {"token_logprobs": [-0.25] * len(token_ids)}, + } + ], + "nvext": {"completion_token_ids": token_ids}, + } + + +def test_runtime_start_world_size_sender_geometry_and_shutdown(monkeypatch) -> None: + calls: list[str] = [] + _patch_runtime( + monkeypatch, + workers=[ + {"instance_id": "a", "system_url": "http://10.0.0.2:4000"}, + {"instance_id": "b", "system_url": "http://10.0.0.3:4000"}, + ], + calls=calls, + ) + generation = DynamoGeneration(cluster=object(), config=_config(tp=2)) + + assert calls[:2] == ["init", "start"] + assert generation.frontend_url == "http://10.0.0.1:3000/v1" + assert generation.dp_openai_server_base_urls == [None] + assert generation.get_inference_world_size() == 4 + sender = generation.get_collective_sender_spec() + assert sender.nccl_peer == "vllm" + assert sender.buffer_size_bytes == VLLM_PACKED_BUFFER_SIZE_BYTES + assert sender.num_buffers == VLLM_PACKED_NUM_BUFFERS + assert generation.shutdown() + assert generation.shutdown() + assert calls.count("shutdown") == 1 + + +def test_blocking_generate_is_rejected_and_async_generation_uses_http( + monkeypatch, +) -> None: + _patch_runtime(monkeypatch) + requests = [] + + async def fake_post(url, payload, timeout_s): + requests.append((url, payload, timeout_s)) + return _completion_response([8, 9]) + + monkeypatch.setattr(generation_module, "async_http_post_json", fake_post) + generation = DynamoGeneration(cluster=object(), config=_config()) + with pytest.raises(NotImplementedError, match="generate_async"): + generation.generate(_data()) + + async def collect(): + return [item async for item in generation.generate_async(_data())] + + outputs = asyncio.run(collect()) + assert outputs[0][0] == 0 + assert outputs[0][1]["output_ids"].tolist() == [[1, 2, 3, 8, 9]] + assert requests[0][0].endswith("/v1/completions") + assert requests[0][1]["max_tokens"] == 2 + assert requests[0][1]["stop"] == ["stop"] + assert "return_tokens_as_token_ids" not in requests[0][1] + + +def test_prompt_at_context_limit_is_rejected(monkeypatch) -> None: + _patch_runtime(monkeypatch) + generation = DynamoGeneration(cluster=object(), config=_config()) + + with pytest.raises(ValueError, match="prompt length 5 must be less than"): + generation._allowed_new_tokens(5) + + +def test_finish_generation_invalidates_sync_rollout_cache(monkeypatch) -> None: + _patch_runtime(monkeypatch) + generation = DynamoGeneration(cluster=object(), config=_config()) + generation.invalidate_kv_cache = MagicMock(return_value=True) + + assert generation.finish_generation() + generation.invalidate_kv_cache.assert_called_once_with() + + +def test_merged_stop_strings_enforce_dynamo_limit(monkeypatch) -> None: + _patch_runtime(monkeypatch) + config = _config() + config["stop_strings"] = [f"configured-{index}" for index in range(16)] + generation = DynamoGeneration(cluster=object(), config=config) + + assert ( + len( + generation._merge_stop_strings( + [[f"request-{index}" for index in range(16)]] + ) + ) + == 32 + ) + with pytest.raises(ValueError, match="at most 32 stop strings"): + generation._merge_stop_strings([[f"request-{index}" for index in range(17)]]) + + +def test_token_wrapper_is_used_for_nemo_gym(monkeypatch) -> None: + _patch_runtime(monkeypatch) + wrappers = [] + + class FakeWrapper: + def __init__(self, **kwargs): + self.kwargs = kwargs + wrappers.append(self) + + def start(self): + return "http://127.0.0.1:3001/v1" + + def shutdown(self): + pass + + monkeypatch.setattr(generation_module, "DynamoTokenWrapperServer", FakeWrapper) + tokenizer = object() + generation = DynamoGeneration( + cluster=object(), + config=_config(expose_http_server=True), + tokenizer=tokenizer, + tokenizer_config={"chat_template_kwargs": {"enable_thinking": False}}, + ) + assert generation.dp_openai_server_base_urls == ["http://127.0.0.1:3001/v1"] + assert wrappers[0].kwargs["tokenizer"] is tokenizer + + +def test_refit_rank_offsets_update_and_pickled_cache_invalidation(monkeypatch) -> None: + workers = [ + {"instance_id": "a", "system_url": "http://10.0.0.2:4000"}, + {"instance_id": "b", "system_url": "http://10.0.0.3:4000"}, + ] + _patch_runtime(monkeypatch, workers=workers) + init_calls = [] + update_calls = [] + cache_calls = [] + monkeypatch.setattr( + refit_module._post_worker_route, + "remote", + lambda **kwargs: init_calls.append(kwargs) or True, + ) + monkeypatch.setattr( + refit_module._update_worker_weights, + "remote", + lambda **kwargs: update_calls.append(kwargs) or True, + ) + monkeypatch.setattr(refit_module.ray, "get", lambda refs: refs) + + generation = DynamoGeneration(cluster=object(), config=_config(tp=2)) + generation.prepare_refit_info({"weight": (torch.Size([4, 8]), torch.bfloat16)}) + assert generation.init_collective("10.1.0.1", 1500, 7, train_world_size=3) == [ + True, + True, + ] + assert [call["payload"]["init_info"]["rank_offset"] for call in init_calls] == [ + 3, + 5, + ] + assert all(call["timeout_s"] == 10 for call in init_calls) + assert generation.update_weights_from_collective() == [True, True] + assert update_calls[0]["update_info"]["packed"] is True + assert all(call["timeout_s"] == 10 for call in update_calls) + + restored = pickle.loads(pickle.dumps(generation)) + assert restored.frontend_url == generation.frontend_url + monkeypatch.setattr( + refit_module._post_worker_route, + "remote", + lambda **kwargs: cache_calls.append(kwargs) or True, + ) + assert restored.invalidate_kv_cache() + assert [call["route"] for call in cache_calls] == [ + "pause_generation", + "pause_generation", + "resume_generation", + "resume_generation", + ] + assert all(call["timeout_s"] == 10 for call in cache_calls) + assert all( + call["payload"] == {"mode": "wait", "clear_cache": True} + for call in cache_calls[:2] + ) + assert restored._managed_runtime is None + + +def test_cache_invalidation_resumes_workers_that_paused_before_peer_failure( + monkeypatch, +) -> None: + calls = [] + + def remote(**kwargs): + calls.append(kwargs) + return (kwargs["route"], kwargs["system_url"]) + + def get(ref): + if ref == ("pause_generation", "http://worker-b:4000"): + raise RuntimeError("pause refused") + return True + + monkeypatch.setattr(refit_module._post_worker_route, "remote", remote) + monkeypatch.setattr(refit_module.ray, "get", get) + channel = DynamoRefitChannel( + [ + {"instance_id": "a", "system_url": "http://worker-a:4000"}, + {"instance_id": "b", "system_url": "http://worker-b:4000"}, + ], + engine_world_size=1, + control_timeout_s=10, + ) + + with pytest.raises(RuntimeError, match="pause/clear failed"): + channel.flush_cache() + + assert [(call["route"], call["system_url"]) for call in calls] == [ + ("pause_generation", "http://worker-a:4000"), + ("pause_generation", "http://worker-b:4000"), + ("resume_generation", "http://worker-a:4000"), + ] + + +def test_native_refit_transaction_keeps_cache_mode_external(monkeypatch) -> None: + calls = [] + monkeypatch.setattr( + refit_module, + "http_post_json", + lambda url, payload, timeout_s: calls.append(payload) or {"status": "ok"}, + ) + assert refit_module._update_worker_weights._function( + system_url="http://worker:4000", + update_info={"names": ["weight"]}, + timeout_s=30, + ) + assert [call["engine_rpc"] for call in calls] == [ + "start_weight_update", + "update_weights", + "finish_weight_update", + ] + assert all(call["reset_prefix_cache"] is False for call in calls) + + +def test_metrics_parser_and_sampler_aliases() -> None: + sampler = DynamoMetricsSampler( + [{"instance_id": "a", "system_url": "http://worker:4000"}], + interval_s=1, + include_prefixes=None, + exclude_prefixes=None, + ) + parsed = parse_prometheus_metrics( + 'vllm:num_requests_running{model_name="model",engine="0"} 3\n' + 'vllm:num_requests_waiting{model_name="model",engine="0"} 2\n' + 'vllm:kv_cache_usage_perc{model_name="model",engine="0"} 0.5\n' + 'vllm:generation_tokens_total{model_name="model",engine="0"} 7\n' + 'vllm:gpu_cache_usage_perc{model_name="model",engine="0"} 0.9\n' + "python_gc_objects_collected_total 10\n", + sampler._include_prefixes, + sampler._exclude_prefixes, + ) + sampler._samples = {name: {0: [value]} for name, value in parsed.items()} + metrics = sampler.snapshot() + assert metrics["inflight_batch_sizes"] == {0: [3.0]} + assert metrics["num_pending_samples"] == {0: [2.0]} + assert metrics["kv_cache_usage_perc"] == {0: [0.5]} + assert metrics["generation_tokens"] == {0: [7.0]} + assert "vllm_gpu_cache_usage_perc" not in parsed + assert "python_gc_objects_collected_total" not in parsed + + +def test_metrics_http_errors_are_ignored(monkeypatch) -> None: + monkeypatch.setattr( + metrics_module.urllib.request, + "urlopen", + lambda *args, **kwargs: (_ for _ in ()).throw( + metrics_module.urllib.error.URLError("refused") + ), + ) + assert metrics_module._http_get_text("http://worker/metrics", 1) is None + + +def test_completion_parser_rejects_misaligned_logprobs() -> None: + response = _completion_response([8, 9]) + response["choices"][0]["logprobs"]["token_logprobs"] = [-0.1] + with pytest.raises(RuntimeError, match="1 token logprobs for 2"): + generation_module._parse_dynamo_completion_response( + response, request_url="http://dynamo/v1/completions" + ) + + +@pytest.mark.parametrize( + ("response", "expected"), + [ + ({"http_status": 408}, True), + ({"http_status": 429}, True), + ({"http_status": 503}, True), + ({"transport_error": "refused"}, True), + ({"json_decode_error": True}, True), + ({"http_status": 400}, False), + ({"http_status": 404}, False), + ], +) +def test_completion_retry_predicate(response, expected) -> None: + assert generation_module._is_retryable_http_response(response) is expected + + +def test_completion_retry_eventually_succeeds(monkeypatch) -> None: + _patch_runtime(monkeypatch) + responses = iter( + [{"status": "error", "http_status": 503}, _completion_response([8])] + ) + calls = [] + + async def fake_post(*args): + calls.append(args) + return next(responses) + + async def no_sleep(_): + return None + + monkeypatch.setattr(generation_module, "async_http_post_json", fake_post) + monkeypatch.setattr(generation_module.asyncio, "sleep", no_sleep) + generation = DynamoGeneration(cluster=object(), config=_config()) + + token_ids, _, _ = asyncio.run( + generation._post_completion_request( + prompt_token_ids=[1], + greedy=False, + stop_strings=None, + max_new_tokens=1, + ) + ) + + assert token_ids == [8] + assert len(calls) == 2 + + +@pytest.mark.parametrize("status", [400, 503]) +def test_completion_retry_stops_on_nonretryable_or_exhaustion( + monkeypatch, status +) -> None: + _patch_runtime(monkeypatch) + calls = [] + + async def fake_post(*args): + calls.append(args) + return {"status": "error", "http_status": status} + + async def no_sleep(_): + return None + + monkeypatch.setattr(generation_module, "async_http_post_json", fake_post) + monkeypatch.setattr(generation_module.asyncio, "sleep", no_sleep) + generation = DynamoGeneration(cluster=object(), config=_config()) + + with pytest.raises(RuntimeError, match=f"HTTP {status}"): + asyncio.run( + generation._post_completion_request( + prompt_token_ids=[1], + greedy=False, + stop_strings=None, + max_new_tokens=1, + ) + ) + + assert len(calls) == (1 if status == 400 else generation_module._HTTP_MAX_ATTEMPTS) + + +def test_direct_completions_are_not_limited_by_default_thread_pool( + monkeypatch, +) -> None: + _patch_runtime(monkeypatch) + request_count = 40 + entered_count = 0 + all_entered = asyncio.Event() + release = asyncio.Event() + + async def fake_post(*args): + nonlocal entered_count + entered_count += 1 + if entered_count == request_count: + all_entered.set() + await release.wait() + return _completion_response([8]) + + monkeypatch.setattr(generation_module, "async_http_post_json", fake_post) + generation = DynamoGeneration(cluster=object(), config=_config()) + + async def run_requests(): + tasks = [ + asyncio.create_task( + generation._post_completion_request( + prompt_token_ids=[1], + greedy=False, + stop_strings=None, + max_new_tokens=1, + ) + ) + for _ in range(request_count) + ] + await asyncio.wait_for(all_entered.wait(), timeout=1) + release.set() + return await asyncio.gather(*tasks) + + responses = asyncio.run(run_requests()) + + assert entered_count == request_count + assert len(responses) == request_count diff --git a/tests/unit/models/generation/test_dynamo_http_client.py b/tests/unit/models/generation/test_dynamo_http_client.py new file mode 100644 index 00000000000..195db9bb55a --- /dev/null +++ b/tests/unit/models/generation/test_dynamo_http_client.py @@ -0,0 +1,216 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import io +import json +import urllib.error + +import aiohttp +import pytest + +from nemo_rl.models.generation.dynamo import dynamo_generation as generation_module +from nemo_rl.models.generation.dynamo import http_client + + +class _SyncResponse: + def __init__(self, body: bytes): + self._body = body + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def read(self) -> bytes: + return self._body + + +class _AsyncResponse: + def __init__(self, body: bytes, status: int = 200): + self._body = body + self.status = status + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + async def read(self) -> bytes: + return self._body + + +class _AsyncSession: + def __init__(self, response=None, error=None): + self._response = response + self._error = error + self.requests = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + def post(self, url, *, json): + self.requests.append((url, json)) + if self._error is not None: + raise self._error + return self._response + + +def _sync_post(monkeypatch, result): + def fake_urlopen(request, timeout): + if isinstance(result, BaseException): + raise result + return _SyncResponse(result) + + monkeypatch.setattr(http_client.urllib.request, "urlopen", fake_urlopen) + return http_client.http_post_json("http://worker/route", {"value": 1}, 3) + + +def test_http_post_json_success_preserves_request_contract(monkeypatch) -> None: + captured = {} + + def fake_urlopen(request, timeout): + captured["request"] = request + captured["timeout"] = timeout + return _SyncResponse(b'{"status":"ok"}') + + monkeypatch.setattr(http_client.urllib.request, "urlopen", fake_urlopen) + + response = http_client.http_post_json( + "http://worker/route", {"value": 1}, timeout_s=3 + ) + + request = captured["request"] + assert response == {"status": "ok"} + assert request.full_url == "http://worker/route" + assert request.get_method() == "POST" + assert json.loads(request.data) == {"value": 1} + assert captured["timeout"] == 3 + + +@pytest.mark.parametrize( + ("body", "expected"), + [ + ( + b"not-json", + {"status": "error", "json_decode_error": True, "raw": "not-json"}, + ), + (b"[1, 2]", {"status": "error", "raw": "[1, 2]"}), + ], +) +def test_http_post_json_rejects_invalid_json_shapes( + monkeypatch, body, expected +) -> None: + assert _sync_post(monkeypatch, body) == expected + + +@pytest.mark.parametrize( + ("error", "retryable", "message"), + [ + ( + urllib.error.HTTPError( + "http://worker/route", + 503, + "unavailable", + hdrs=None, + fp=io.BytesIO(b"try later"), + ), + True, + "HTTP 503: try later", + ), + ( + urllib.error.HTTPError( + "http://worker/route", 400, "bad request", hdrs=None, fp=None + ), + False, + "HTTP 400: ", + ), + (urllib.error.URLError("refused"), True, "URLError"), + (TimeoutError("slow"), True, "TimeoutError"), + ], +) +def test_http_errors_round_trip_through_consumers( + monkeypatch, error, retryable, message +) -> None: + response = _sync_post(monkeypatch, error) + + assert response["status"] == "error" + assert generation_module._is_retryable_http_response(response) is retryable + assert message in http_client.format_dynamo_error(response) + + +@pytest.mark.parametrize( + ("body", "status", "expected", "retryable"), + [ + (b'{"status":"ok"}', 200, {"status": "ok"}, False), + ( + b"not-json", + 200, + {"status": "error", "json_decode_error": True, "raw": "not-json"}, + True, + ), + ( + b"busy", + 503, + {"status": "error", "http_status": 503, "raw": "busy"}, + True, + ), + ], +) +def test_async_http_post_json_uses_same_error_contract( + monkeypatch, body, status, expected, retryable +) -> None: + session = _AsyncSession(_AsyncResponse(body, status)) + monkeypatch.setattr( + http_client.aiohttp, + "ClientSession", + lambda **kwargs: session, + ) + + response = asyncio.run( + http_client.async_http_post_json( + "http://worker/route", {"value": 1}, timeout_s=3 + ) + ) + + assert response == expected + assert generation_module._is_retryable_http_response(response) is retryable + assert session.requests == [("http://worker/route", {"value": 1})] + + +@pytest.mark.parametrize( + "error", [aiohttp.ClientConnectionError("refused"), TimeoutError("slow")] +) +def test_async_http_transport_errors_are_retryable(monkeypatch, error) -> None: + session = _AsyncSession(error=error) + monkeypatch.setattr( + http_client.aiohttp, + "ClientSession", + lambda **kwargs: session, + ) + + response = asyncio.run( + http_client.async_http_post_json( + "http://worker/route", {"value": 1}, timeout_s=3 + ) + ) + + assert response["status"] == "error" + assert generation_module._is_retryable_http_response(response) + assert "transport_error" in response diff --git a/tests/unit/models/generation/test_dynamo_managed_runtime.py b/tests/unit/models/generation/test_dynamo_managed_runtime.py new file mode 100644 index 00000000000..c5533707ab7 --- /dev/null +++ b/tests/unit/models/generation/test_dynamo_managed_runtime.py @@ -0,0 +1,867 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import signal +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + +from nemo_rl.models.generation.dynamo.dynamo_worker import ( + DynamoGpuReservation, + DynamoVllmWorker, +) +from nemo_rl.models.generation.dynamo.managed_runtime import ( + ManagedDynamoRuntime, + _managed_namespace, +) +from nemo_rl.models.generation.dynamo.venv import get_dynamo_venv_dir +from nemo_rl.models.generation.dynamo.worker_pool import ( + FixedDynamoWorkerPool, + _vllm_port_for_node_slot, +) + + +class _Cluster: + num_gpus_per_node = 4 + + +def test_dynamo_package_directory_does_not_shadow_stdlib_http() -> None: + package_dir = ( + Path(__file__).resolve().parents[4] + / "nemo_rl" + / "models" + / "generation" + / "dynamo" + ) + result = subprocess.run( + [sys.executable, "-c", "import http.client"], + cwd=package_dir, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + +def _config(*, tp: int = 1) -> dict: + return { + "backend": "dynamo", + "model_name": "model", + "colocated": {"enabled": False}, + "dynamo_cfg": { + "engine": "vllm", + "startup_timeout_s": 5, + "request_timeout_s": 30, + "control_timeout_s": 10, + "metrics_include_prefixes": None, + "metrics_exclude_prefixes": None, + "worker_args": { + "tool_call_parser": None, + "reasoning_parser": None, + "exclude_tools_when_tool_choice_none": True, + "enable_structural_tag": False, + "structural_tag_scope": "auto", + "structural_tag_schema": "auto", + "custom_jinja_template": None, + "endpoint_types": ["chat", "completions"], + "extra_cli_args": [], + }, + "frontend_args": { + "tokenizer": "default", + "tokenizer_cache": False, + "tokenizer_cache_bytes": 50 * 1024 * 1024, + "router_mode": "kv", + "router_reset_states": True, + "extra_cli_args": [], + }, + }, + "vllm_cfg": { + "async_engine": True, + "tensor_parallel_size": tp, + "pipeline_parallel_size": 1, + "expert_parallel_size": tp, + "gpu_memory_utilization": 0.8, + "max_model_len": 512, + "precision": "bfloat16", + "kv_cache_dtype": "auto", + "load_format": "auto", + "enforce_eager": False, + "expose_http_server": False, + "enable_vllm_metrics_logger": True, + "vllm_metrics_logger_interval": 1.0, + "env_vars": None, + }, + "vllm_kwargs": {}, + } + + +class _RemoteMethod: + def __init__(self, result): + self._result = result + self.calls = [] + + def remote(self, *args, **kwargs): + self.calls.append((args, kwargs)) + return self._result + + +class _FakeWorker: + def __init__(self, alive=True, metadata=None): + self.is_alive = _RemoteMethod(alive) + self.metadata = _RemoteMethod(metadata or {}) + self.shutdown = _RemoteMethod(True) + + +class _FakeReservation: + def __init__(self, metadata=None, system_port=4000): + self.metadata = _RemoteMethod(metadata or {}) + self.select_free_port = _RemoteMethod(system_port) + self.register_process_group = _RemoteMethod(True) + self.cleanup_process_group = _RemoteMethod(True) + + +class _FakeProcess: + returncode = None + + @staticmethod + def poll(): + return None + + +class _FakeHttpResponse: + status = 200 + + def __init__(self, payload): + self._payload = payload + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def read(self): + return json.dumps(self._payload).encode() + + +def test_runtime_construction_is_inert_and_namespace_is_driver_owned( + monkeypatch, +) -> None: + monkeypatch.setenv("SLURM_JOB_ID", "Job/123.4") + assert _managed_namespace() == "nemo-rl-job-123-4" + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=_config()) + assert runtime._started is False + assert runtime._etcd_process is None + with pytest.raises(RuntimeError, match="not been started"): + _ = runtime.frontend_url + + +def test_runtime_rejects_multinode_engine_group_before_spawning() -> None: + with pytest.raises(ValueError, match="fit on one node"): + ManagedDynamoRuntime(cluster=_Cluster(), config=_config(tp=8)) + + +def test_managed_service_and_frontend_environments_are_runtime_owned( + monkeypatch, +) -> None: + config = _config() + config["dynamo_cfg"]["frontend_args"].update( + { + "tokenizer": "fastokens", + "tokenizer_cache": True, + "tokenizer_cache_bytes": 4096, + } + ) + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=config) + runtime._manager_env = { + "DYN_NAMESPACE": "managed", + "DYN_DISCOVERY_BACKEND": "etcd", + "ETCD_ENDPOINTS": "http://managed-etcd:2379", + "NATS_SERVER": "nats://managed-nats:4222", + } + monkeypatch.setenv("DYN_NAMESPACE", "stale") + monkeypatch.setenv("DYN_STALE_SETTING", "remove-me") + monkeypatch.setenv("ETCD_ENDPOINTS", "http://stale-etcd:2379") + monkeypatch.setenv("ETCD_STALE_SETTING", "remove-me") + monkeypatch.setenv("NATS_SERVER", "nats://stale-nats:4222") + monkeypatch.setenv("NATS_STALE_SETTING", "remove-me") + monkeypatch.setenv("NCCL_DEBUG", "INFO") + + service_env = runtime._service_env() + assert service_env["DYN_NAMESPACE"] == "managed" + assert service_env["DYN_DISCOVERY_BACKEND"] == "etcd" + assert "DYN_STALE_SETTING" not in service_env + assert service_env["ETCD_ENDPOINTS"] == "http://managed-etcd:2379" + assert service_env["NATS_SERVER"] == "nats://managed-nats:4222" + assert "ETCD_STALE_SETTING" not in service_env + assert "NATS_STALE_SETTING" not in service_env + assert service_env["NCCL_DEBUG"] == "INFO" + assert "ALLOW_NONE_AUTHENTICATION" not in service_env + + frontend_env = runtime._frontend_env() + assert frontend_env["DYN_TOKENIZER"] == "fastokens" + assert frontend_env["DYN_TOKENIZER_CACHE"] == "1" + assert frontend_env["DYN_TOKENIZER_CACHE_BYTES"] == "4096" + + +def test_dynamo_venv_uses_explicit_env_or_repository_fallback(monkeypatch) -> None: + monkeypatch.setenv("NEMO_RL_DYNAMO_VENV_DIR", "/custom/dynamo") + assert get_dynamo_venv_dir() == Path("/custom/dynamo") + + monkeypatch.delenv("NEMO_RL_DYNAMO_VENV_DIR") + monkeypatch.setenv("NRL_CONTAINER", "1") + assert get_dynamo_venv_dir().parts[-2:] == ("venvs", "dynamo") + + +def test_vllm_node_local_port_bands_are_deterministic() -> None: + assert [_vllm_port_for_node_slot(slot) for slot in range(3)] == [7000, 7100, 7200] + + +def test_startup_failure_cleans_up_partial_worker_pool(monkeypatch, tmp_path) -> None: + calls = [] + exit_hooks = [] + pool_init_kwargs = {} + + class FailingPool: + def __init__(self, **kwargs): + calls.append("pool-init") + pool_init_kwargs.update(kwargs) + + def start(self): + calls.append("pool-start") + raise RuntimeError("worker failed") + + def shutdown(self): + calls.append("pool-shutdown") + + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=_config()) + etcd_dir = tmp_path / "etcd" + nats_dir = tmp_path / "nats" + etcd_dir.mkdir() + nats_dir.mkdir() + ports = iter([1313, 1314, 1315, 3000]) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime._get_node_ip_local", + lambda: "10.0.0.1", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime._get_free_port_local", + lambda low, high, **kwargs: next(ports), + ) + + def start_etcd(): + calls.append("etcd") + runtime._etcd_process = object() + runtime._etcd_data_dir = str(etcd_dir) + + def start_nats(): + calls.append("nats") + runtime._nats_process = object() + runtime._nats_data_dir = str(nats_dir) + + def stop_process(process, label, timeout_s=15): + if process is not None: + calls.append(f"stop-{label}") + + monkeypatch.setattr(runtime, "_start_etcd", start_etcd) + monkeypatch.setattr(runtime, "_start_nats", start_nats) + monkeypatch.setattr(runtime, "_stop_process", stop_process) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.atexit.register", + lambda hook: exit_hooks.append(hook), + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.atexit.unregister", + lambda hook: exit_hooks.remove(hook), + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.FixedDynamoWorkerPool", + FailingPool, + ) + + with pytest.raises(RuntimeError, match="worker failed"): + runtime.start() + assert calls == [ + "etcd", + "nats", + "pool-init", + "pool-start", + "pool-shutdown", + "stop-NATS", + "stop-etcd", + ] + assert runtime._pool is None + assert runtime._started is False + assert exit_hooks == [] + assert pool_init_kwargs["manager_env"]["DYN_ENABLE_EXPERIMENTAL_PARSERS_V2"] == "1" + assert pool_init_kwargs["manager_env"]["DYN_RL_INIT_WEIGHTS_TIMEOUT_S"] == "10.0" + assert not etcd_dir.exists() + assert not nats_dir.exists() + + +def test_stop_process_escalates_its_process_group(monkeypatch) -> None: + class EscalatingProcess: + pid = 1234 + + def __init__(self): + self.wait_count = 0 + + @staticmethod + def poll(): + return None + + def wait(self, timeout): + self.wait_count += 1 + if self.wait_count == 1: + raise subprocess.TimeoutExpired("dynamo", timeout) + return 0 + + signals = [] + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.os.killpg", + lambda pid, sig: signals.append((pid, sig)), + ) + process = EscalatingProcess() + ManagedDynamoRuntime._stop_process(process, "worker", timeout_s=0.01) + assert signals == [(process.pid, signal.SIGTERM), (process.pid, signal.SIGKILL)] + + +def test_reservation_records_and_cleans_registered_process_group(monkeypatch) -> None: + reservation_cls = DynamoGpuReservation.__ray_metadata__.modified_class + reservation = reservation_cls() + signals = [] + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.os.killpg", + lambda pid, sig: signals.append((pid, sig)), + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.time.sleep", lambda _: None + ) + + assert reservation.register_process_group(4321) + assert reservation.cleanup_process_group() + assert reservation.cleanup_process_group() + assert signals == [(4321, signal.SIGTERM), (4321, signal.SIGKILL)] + + +def test_reservation_selects_a_free_nonexcluded_system_port(monkeypatch) -> None: + reservation_cls = DynamoGpuReservation.__ray_metadata__.modified_class + reservation = reservation_cls() + calls = [] + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker._get_free_port_local", + lambda low, high, **kwargs: calls.append((low, high, kwargs)) or 4002, + ) + + assert ( + reservation.select_free_port( + port_range_low=4000, + port_range_high=4003, + excluded_ports=[4001], + ) + == 4002 + ) + assert calls == [ + ( + 4000, + 4003, + {"max_retries": None, "excluded_ports": {4001}}, + ) + ] + + +def test_worker_argument_validation_has_startup_timeout(monkeypatch) -> None: + worker_cls = DynamoVllmWorker.__ray_metadata__.modified_class + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.subprocess.run", + lambda *args, **kwargs: (_ for _ in ()).throw( + subprocess.TimeoutExpired("validator", kwargs["timeout"]) + ), + ) + + with pytest.raises(RuntimeError, match="startup_timeout_s=7"): + worker_cls._validate_argv( + "/opt/dynamo_venv/bin/python", + ["--model", "model"], + {}, + timeout_s=7, + ) + + +def test_worker_registers_process_group_immediately_after_launch(monkeypatch) -> None: + class FakeSocket: + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def bind(self, address): + if address[1] == 7000: + raise AssertionError("VLLM_PORT must not be probed") + return None + + process = SimpleNamespace(pid=4321) + reservation = _FakeReservation() + worker_cls = DynamoVllmWorker.__ray_metadata__.modified_class + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker._get_node_ip_local", + lambda: "10.0.0.1", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.socket.socket", + lambda *args, **kwargs: FakeSocket(), + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.get_dynamo_python", + lambda: "/opt/dynamo_venv/bin/python", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.get_dynamo_venv_dir", + lambda: Path("/opt/dynamo_venv"), + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.subprocess.Popen", + lambda *args, **kwargs: process, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.dynamo_worker.ray.get", lambda ref: ref + ) + monkeypatch.setattr(worker_cls, "_validate_argv", MagicMock()) + monkeypatch.setattr(worker_cls, "_wait_for_system_port", MagicMock()) + + worker_cls( + _config(), + namespace="nemo-rl-test", + group_name="worker-0", + cuda_devices=[0], + system_port=4000, + vllm_port=7000, + manager_env={}, + startup_timeout_s=5, + seed=0, + cleanup_reservation=reservation, + ) + + assert reservation.register_process_group.calls == [((4321,), {})] + + +def test_shutdown_guards_each_owned_resource_independently(monkeypatch) -> None: + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=_config()) + runtime._frontend_process = object() + runtime._nats_process = object() + runtime._etcd_process = object() + calls = [] + + class FailingPool: + def shutdown(self): + calls.append("pool") + raise RuntimeError("pool failure") + + runtime._pool = FailingPool() + + def stop_process(process, label, timeout_s=15): + if process is None: + return + calls.append(label) + if label == "frontend": + raise RuntimeError("frontend failure") + + monkeypatch.setattr(runtime, "_stop_process", stop_process) + runtime.shutdown() + runtime.shutdown() + + assert calls[:4] == ["frontend", "pool", "NATS", "etcd"] + assert runtime._frontend_process is None + assert runtime._pool is None + assert runtime._nats_process is None + assert runtime._etcd_process is None + + +def test_fixed_pool_detects_worker_membership_change(monkeypatch) -> None: + expected = [{"instance_id": "worker-0", "system_url": "http://10.0.0.1:4000"}] + reservation_metadata = {"node_ip": "10.0.0.1", "gpu_id": 0} + reservation = _FakeReservation(metadata=reservation_metadata) + pool = object.__new__(FixedDynamoWorkerPool) + pool._workers = [_FakeWorker(metadata={"instance_id": "changed"})] + pool._reservations = [reservation] + pool._cleanup_reservations = [reservation] + pool._reservation_metadata = [reservation_metadata] + + def fake_get(refs, **kwargs): + if refs == [True]: + return [True] + if refs == [reservation_metadata]: + return [reservation_metadata] + return [{"instance_id": "changed"}] + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", fake_get + ) + with pytest.raises(RuntimeError, match="worker membership changed"): + pool.validate(expected) + + +def test_fixed_pool_detects_reservation_membership_change(monkeypatch) -> None: + expected_worker = { + "instance_id": "worker-0", + "system_url": "http://10.0.0.1:4000", + } + expected_reservation = {"node_ip": "10.0.0.1", "gpu_id": 0} + changed_reservation = {"node_ip": "10.0.0.2", "gpu_id": 0} + reservation = _FakeReservation(metadata=changed_reservation) + pool = object.__new__(FixedDynamoWorkerPool) + pool._workers = [_FakeWorker(metadata=expected_worker)] + pool._reservations = [reservation] + pool._cleanup_reservations = [reservation] + pool._reservation_metadata = [expected_reservation] + + def fake_get(refs, **kwargs): + if refs == [True]: + return [True] + if refs == [changed_reservation]: + return [changed_reservation] + return [expected_worker] + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", fake_get + ) + with pytest.raises(RuntimeError, match="GPU reservation membership changed"): + pool.validate([expected_worker]) + + +def test_fixed_pool_tracks_worker_before_metadata_failure(monkeypatch) -> None: + reservation = _FakeReservation(metadata={"node_ip": "10.0.0.1", "gpu_id": 0}) + worker = _FakeWorker() + + class RemoteFactory: + def __init__(self, actor): + self.actor = actor + + def remote(self, *args, **kwargs): + return self.actor + + class ReservationFactory: + @staticmethod + def options(**kwargs): + return RemoteFactory(reservation) + + class WorkerFactory: + @staticmethod + def options(**kwargs): + return RemoteFactory(worker) + + class Cluster: + @staticmethod + def get_placement_groups(): + return [SimpleNamespace(bundle_count=1)] + + pool = FixedDynamoWorkerPool( + cluster=Cluster(), + config=_config(), + namespace="nemo-rl-test", + engine_world_size=1, + manager_env={}, + startup_timeout_s=5, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.DynamoGpuReservation", + ReservationFactory, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.DynamoVllmWorker", + WorkerFactory, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.get_actor_python_env", + lambda _fqn: "python", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.PlacementGroupSchedulingStrategy", + lambda **kwargs: kwargs, + ) + + def fake_get(refs, **kwargs): + if refs == [reservation.metadata.remote()]: + return [{"node_ip": "10.0.0.1", "gpu_id": 0}] + if refs == 4000: + return refs + raise RuntimeError("metadata failed") + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", fake_get + ) + with pytest.raises(RuntimeError, match="metadata failed"): + pool.start() + + assert pool._workers == [worker] + assert pool._cleanup_reservations == [reservation] + assert pool._metadata == [{}] + + +def test_fixed_pool_launches_all_workers_before_waiting_for_model_metadata( + monkeypatch, +) -> None: + reservation_objects = [ + _FakeReservation( + metadata={"node_ip": "10.0.0.1", "gpu_id": 0}, system_port=4001 + ), + _FakeReservation( + metadata={"node_ip": "10.0.0.1", "gpu_id": 1}, system_port=4002 + ), + ] + reservations = iter(reservation_objects) + workers = [ + _FakeWorker(metadata={"instance_id": "worker-0"}), + _FakeWorker(metadata={"instance_id": "worker-1"}), + ] + workers_to_launch = iter(workers) + launch_kwargs = [] + + class RemoteFactory: + def __init__(self, actor): + self.actor = actor + + def remote(self, *args, **kwargs): + return self.actor + + class ReservationFactory: + @staticmethod + def options(**kwargs): + return RemoteFactory(next(reservations)) + + class WorkerFactory: + @staticmethod + def options(**kwargs): + worker = next(workers_to_launch) + + class CapturingRemoteFactory(RemoteFactory): + def remote(self, *args, **kwargs): + launch_kwargs.append(kwargs) + return super().remote(*args, **kwargs) + + return CapturingRemoteFactory(worker) + + class Cluster: + @staticmethod + def get_placement_groups(): + return [SimpleNamespace(bundle_count=2)] + + pool = FixedDynamoWorkerPool( + cluster=Cluster(), + config=_config(), + namespace="nemo-rl-test", + engine_world_size=1, + manager_env={}, + startup_timeout_s=5, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.DynamoGpuReservation", + ReservationFactory, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.DynamoVllmWorker", + WorkerFactory, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.get_actor_python_env", + lambda _fqn: "python", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.PlacementGroupSchedulingStrategy", + lambda **kwargs: kwargs, + ) + + def fake_get(refs, **kwargs): + if isinstance(refs, int): + return refs + if isinstance(refs, list) and refs and "node_ip" in refs[0]: + return refs + assert pool._workers == workers + return refs + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", fake_get + ) + + pool.start() + + assert pool._metadata == [ + {"instance_id": "worker-0"}, + {"instance_id": "worker-1"}, + ] + assert [kwargs["system_port"] for kwargs in launch_kwargs] == [4001, 4002] + assert [kwargs["vllm_port"] for kwargs in launch_kwargs] == [7000, 7100] + assert [kwargs["cleanup_reservation"] for kwargs in launch_kwargs] == ( + reservation_objects + ) + assert reservation_objects[0].select_free_port.calls == [ + ( + (), + { + "port_range_low": 4000, + "port_range_high": 4100, + "excluded_ports": [], + }, + ) + ] + assert reservation_objects[1].select_free_port.calls == [ + ( + (), + { + "port_range_low": 4000, + "port_range_high": 4100, + "excluded_ports": [4001], + }, + ) + ] + + +def test_fixed_pool_shutdown_releases_workers_and_reservations(monkeypatch) -> None: + pool = object.__new__(FixedDynamoWorkerPool) + worker = _FakeWorker() + reservation = _FakeReservation() + pool._workers = [worker] + pool._reservations = [reservation] + pool._cleanup_reservations = [reservation] + pool._reservation_metadata = [] + pool._metadata = [{"instance_id": "worker-0", "process_pid": 1234}] + killed = [] + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", + lambda refs, **kwargs: [True], + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.kill", + lambda actor, **kwargs: killed.append(actor), + ) + pool.shutdown() + assert killed == [worker, reservation] + assert pool._workers == [] + assert pool._reservations == [] + + +def test_fixed_pool_shutdown_uses_registered_pid_when_worker_dies(monkeypatch) -> None: + worker = _FakeWorker() + reservation = _FakeReservation() + pool = object.__new__(FixedDynamoWorkerPool) + pool._workers = [worker] + pool._reservations = [reservation] + pool._cleanup_reservations = [reservation] + pool._reservation_metadata = [] + pool._metadata = [{}] + shutdown_ref = worker.shutdown.remote() + + def fake_get(ref, **kwargs): + if ref == shutdown_ref: + raise RuntimeError("worker died") + return True + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.get", fake_get + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.worker_pool.ray.kill", + lambda actor, **kwargs: None, + ) + + pool.shutdown() + + assert reservation.cleanup_process_group.calls == [((), {})] + + +def test_frontend_waits_for_model_after_endpoint_registration(monkeypatch) -> None: + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=_config()) + runtime._frontend_port = 3000 + runtime._frontend_process = _FakeProcess() + runtime._namespace = "nemo-rl-test" + runtime._pool = SimpleNamespace(is_alive=lambda: True) + health = { + "instances": [ + { + "namespace": "nemo-rl-test", + "component": "backend", + "endpoint": endpoint, + "instance_id": "worker-0", + } + for endpoint in ("generate", "rl") + ] + } + responses = iter( + [ + health, + {"data": []}, + health, + {"data": [{"id": "model"}]}, + ] + ) + urls: list[str] = [] + + def fake_urlopen(url, timeout): + urls.append(url) + return _FakeHttpResponse(next(responses)) + + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.urllib.request.urlopen", + fake_urlopen, + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.time.sleep", lambda _: None + ) + runtime._wait_for_frontend(expected_workers=1) + assert urls == [ + "http://127.0.0.1:3000/health", + "http://127.0.0.1:3000/v1/models", + "http://127.0.0.1:3000/health", + "http://127.0.0.1:3000/v1/models", + ] + + +def test_frontend_wait_fails_immediately_when_worker_exits(monkeypatch) -> None: + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=_config()) + runtime._frontend_port = 3000 + runtime._frontend_process = _FakeProcess() + runtime._pool = SimpleNamespace(is_alive=lambda: False) + + with pytest.raises(RuntimeError, match="worker exited while the frontend"): + runtime._wait_for_frontend(expected_workers=1) + + +def test_frontend_logs_resolved_tokenizer_environment(monkeypatch, capsys) -> None: + config = _config() + config["dynamo_cfg"]["frontend_args"].update( + {"tokenizer": "fastokens", "tokenizer_cache": True} + ) + runtime = ManagedDynamoRuntime(cluster=_Cluster(), config=config) + runtime._frontend_port = 3000 + runtime._namespace = "nemo-rl-test" + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.get_dynamo_python", + lambda: "/opt/dynamo_venv/bin/python", + ) + monkeypatch.setattr( + "nemo_rl.models.generation.dynamo.managed_runtime.subprocess.Popen", + lambda *args, **kwargs: _FakeProcess(), + ) + + runtime._start_frontend() + + output = capsys.readouterr().out + assert "DYN_TOKENIZER': 'fastokens" in output + assert "" not in output diff --git a/tests/unit/models/generation/test_dynamo_token_wrapper.py b/tests/unit/models/generation/test_dynamo_token_wrapper.py new file mode 100644 index 00000000000..53eb020bc2b --- /dev/null +++ b/tests/unit/models/generation/test_dynamo_token_wrapper.py @@ -0,0 +1,646 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import json + +import pytest +from transformers import AutoTokenizer + +from nemo_rl.models.generation.dynamo.token_wrapper import ( + DynamoTokenWrapperServer, + _inject_gym_token_metadata, + _validate_engine_data, + prepare_dynamo_chat_completion_request, +) + + +class _Tokenizer: + eos_token_id = 2 + eos_token = "" + + def __init__(self) -> None: + self.calls = [] + + def decode(self, token_ids): + return repr(token_ids) + + def encode(self, text, add_special_tokens=False): + assert add_special_tokens is False + token_ids = [] + while text: + if text.startswith(self.eos_token): + token_ids.append(self.eos_token_id) + text = text[len(self.eos_token) :] + elif text.startswith("next"): + token_ids.append(40) + text = text[len("next") :] + elif text.startswith("GEN"): + token_ids.append(99) + text = text[len("GEN") :] + else: + raise AssertionError(f"Unexpected text to encode: {text!r}") + return token_ids + + def apply_chat_template( + self, + conversation, + tools=None, + documents=None, + chat_template=None, + add_generation_prompt=False, + continue_final_message=False, + tokenize=True, + return_tensors=None, + return_dict=False, + **kwargs, + ): + self.calls.append( + { + "tools": tools, + "documents": documents, + "chat_template": chat_template, + "add_generation_prompt": add_generation_prompt, + "continue_final_message": continue_final_message, + "tokenize": tokenize, + "return_tensors": return_tensors, + "return_dict": return_dict, + "kwargs": kwargs, + } + ) + token_ids = [] + rendered = "" + for index, message in enumerate(conversation): + role = message["role"] + content = message.get("content") + for tool_call in message.get("tool_calls", []): + function = tool_call.get("function", tool_call) + arguments = function.get("arguments") + if arguments is not None and not isinstance(arguments, dict): + raise TypeError("Can only get item pairs from a mapping.") + if role == "user" and content == "hello": + token_ids.extend([10]) + rendered += "hello" + elif role == "assistant" and content == "first": + # Model the Nemotron template's context-dependent history + # truncation: the assistant is longer when rendered alone than + # when a later user turn is present. + has_later_user = any( + item["role"] == "user" for item in conversation[index + 1 :] + ) + token_ids.extend( + [3, self.eos_token_id] + if has_later_user + else [300, 301, 302, self.eos_token_id] + ) + rendered += f"first{self.eos_token}" + elif role == "user" and content == "next": + token_ids.extend([40]) + rendered += "next" + elif role == "assistant" and isinstance(content, str): + token_ids.extend([777, self.eos_token_id]) + rendered += f"{content}{self.eos_token}" + else: + token_ids.extend([900]) + rendered += "other" + if add_generation_prompt: + token_ids.extend([99]) + rendered += "GEN" + return token_ids if tokenize else rendered + + +TOOLS = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather for a city.", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + }, + } +] + + +def _tool_conversation() -> list[dict]: + return [ + {"role": "user", "content": "What is the weather in San Francisco?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city":"San Francisco"}', + }, + } + ], + }, + {"role": "tool", "content": "Sunny, 20C"}, + {"role": "user", "content": "What about tomorrow?"}, + ] + + +def test_prepare_dynamo_chat_completion_request_first_turn() -> None: + tokenizer = _Tokenizer() + body = { + "model": "dummy-model", + "messages": [{"role": "user", "content": "hello"}], + "nvext": {"extra_fields": ["timing"], "trace": "keep-me"}, + "chat_template_kwargs": { + "enable_thinking": False, + "force_nonempty_content": True, + }, + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=tokenizer, + tokenizer_chat_template_kwargs={"enable_thinking": True}, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["messages"] == [{"role": "user", "content": "hello"}] + assert "logprobs" not in prepared + assert "return_tokens_as_token_ids" not in prepared + assert prepared["chat_template_kwargs"] == { + "enable_thinking": False, + "force_nonempty_content": True, + } + assert prepared["nvext"] == { + "extra_fields": ["timing", "engine_data"], + "trace": "keep-me", + "token_data": [10, 99], + } + assert tokenizer.calls[0]["kwargs"] == { + "enable_thinking": False, + "force_nonempty_content": True, + } + + +def test_public_qwen3_tokenizer_first_turn_matches_chat_template( + tiny_qwen3_model_path, +) -> None: + tokenizer = AutoTokenizer.from_pretrained(tiny_qwen3_model_path) + messages = [{"role": "user", "content": "Solve 2 + 2."}] + template_kwargs = {"enable_thinking": False} + expected = tokenizer.apply_chat_template( + messages, + tools=None, + documents=None, + chat_template=None, + add_generation_prompt=True, + continue_final_message=False, + tokenize=True, + return_tensors=None, + return_dict=False, + **template_kwargs, + ) + + prepared = prepare_dynamo_chat_completion_request( + {"model": "Qwen/Qwen3-0.6B", "messages": messages}, + tokenizer=tokenizer, + tokenizer_chat_template_kwargs=template_kwargs, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == expected + + +def test_public_qwen3_tool_conversation_matches_plain_chat_template( + tiny_qwen3_model_path, +) -> None: + tokenizer = AutoTokenizer.from_pretrained(tiny_qwen3_model_path) + messages = _tool_conversation() + template_kwargs = {"enable_thinking": False} + expected = tokenizer.apply_chat_template( + messages, + tools=TOOLS, + tokenize=True, + add_generation_prompt=True, + continue_final_message=False, + return_tensors=None, + return_dict=False, + **template_kwargs, + ) + + prepared = prepare_dynamo_chat_completion_request( + {"model": "Qwen/Qwen3-0.6B", "messages": messages, "tools": TOOLS}, + tokenizer=tokenizer, + tokenizer_chat_template_kwargs=template_kwargs, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == expected + + +def test_public_qwen3_multiturn_prefix_splice_parity( + tiny_qwen3_model_path, +) -> None: + tokenizer = AutoTokenizer.from_pretrained(tiny_qwen3_model_path) + messages = _tool_conversation() + template_kwargs = {"enable_thinking": False} + expected = tokenizer.apply_chat_template( + messages, + tools=TOOLS, + tokenize=True, + add_generation_prompt=True, + continue_final_message=False, + return_tensors=None, + return_dict=False, + **template_kwargs, + ) + assistant_prefix = tokenizer.apply_chat_template( + messages[:2], + tools=TOOLS, + tokenize=True, + add_generation_prompt=False, + continue_final_message=False, + return_tensors=None, + return_dict=False, + **template_kwargs, + ) + first_prompt = tokenizer.apply_chat_template( + messages[:1], + tools=TOOLS, + tokenize=True, + add_generation_prompt=True, + continue_final_message=False, + return_tensors=None, + return_dict=False, + **template_kwargs, + ) + generation_token_ids = assistant_prefix[len(first_prompt) : -1] + messages[1]["prompt_token_ids"] = first_prompt + messages[1]["generation_token_ids"] = generation_token_ids + messages[1]["generation_log_probs"] = [-0.1] * len(generation_token_ids) + + assistant_eos_index = [ + index + for index, token_id in enumerate(expected) + if token_id == tokenizer.eos_token_id + ][2] + expected_splice = assistant_prefix[:-2] + expected[assistant_eos_index:] + + prepared = prepare_dynamo_chat_completion_request( + {"model": "Qwen/Qwen3-0.6B", "messages": messages, "tools": TOOLS}, + tokenizer=tokenizer, + tokenizer_chat_template_kwargs=template_kwargs, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == expected_splice + + +def test_prepare_dynamo_chat_completion_request_preserves_logprob_fields() -> None: + tokenizer = _Tokenizer() + body = { + "model": "dummy-model", + "messages": [{"role": "user", "content": "hello"}], + "logprobs": True, + "return_tokens_as_token_ids": True, + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=tokenizer, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["logprobs"] is True + assert prepared["return_tokens_as_token_ids"] is True + assert prepared["nvext"] == { + "extra_fields": ["engine_data"], + "token_data": [10, 99], + } + + +def test_prepare_dynamo_chat_completion_request_preserves_prior_prefix() -> None: + tokenizer = _Tokenizer() + body = { + "model": "dummy-model", + "required_prefix_token_ids": [999], + "messages": [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "first", + "prompt_token_ids": [10], + "generation_token_ids": [31, 32, 2], + "generation_log_probs": [-0.1, -0.2, -0.3], + }, + {"role": "user", "content": "next"}, + ], + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=tokenizer, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == [10, 31, 32, 2, 40, 99] + assert "required_prefix_token_ids" not in prepared + assert "prompt_token_ids" not in prepared["messages"][1] + assert "generation_token_ids" not in prepared["messages"][1] + assert "generation_log_probs" not in prepared["messages"][1] + assert tokenizer.calls[0]["add_generation_prompt"] is True + assert tokenizer.calls[1]["add_generation_prompt"] is False + assert tokenizer.calls[0]["tokenize"] is True + assert tokenizer.calls[1]["tokenize"] is True + + +def test_prepare_dynamo_chat_completion_request_validates_extra_fields() -> None: + body = { + "messages": [{"role": "user", "content": "hello"}], + "nvext": {"extra_fields": ["engine_data", "timing", "engine_data"]}, + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=_Tokenizer(), + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["extra_fields"] == ["engine_data", "timing"] + + body["nvext"]["extra_fields"] = "timing" + with pytest.raises(ValueError, match="extra_fields must be a JSON list"): + prepare_dynamo_chat_completion_request( + body, + tokenizer=_Tokenizer(), + exclude_tools_when_tool_choice_none=True, + ) + + +def test_tool_choice_none_honors_worker_exclusion_setting() -> None: + body = { + "messages": [{"role": "user", "content": "hello"}], + "tools": TOOLS, + "tool_choice": "none", + } + excluded_tokenizer = _Tokenizer() + included_tokenizer = _Tokenizer() + + prepare_dynamo_chat_completion_request( + body, + tokenizer=excluded_tokenizer, + exclude_tools_when_tool_choice_none=True, + ) + prepare_dynamo_chat_completion_request( + body, + tokenizer=included_tokenizer, + exclude_tools_when_tool_choice_none=False, + ) + + assert excluded_tokenizer.calls[0]["tools"] is None + assert included_tokenizer.calls[0]["tools"] == TOOLS + + +def test_prepare_dynamo_chat_completion_request_normalizes_prior_tool_arguments() -> ( + None +): + tokenizer = _Tokenizer() + body = { + "model": "dummy-model", + "messages": [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "older tool call", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "shell", + "arguments": '{"command":"pwd"}', + }, + }, + { + "type": "function", + "function": { + "name": "malformed", + "arguments": "not-json", + }, + }, + ], + }, + {"role": "user", "content": "next"}, + { + "role": "assistant", + "content": "first", + "prompt_token_ids": [10, 777, 2, 40], + "generation_token_ids": [31, 32, 2], + "generation_log_probs": [-0.1, -0.2, -0.3], + }, + {"role": "user", "content": "next"}, + ], + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=tokenizer, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == [10, 777, 2, 40, 31, 32, 2, 40, 99] + assert prepared["messages"][1]["tool_calls"][0]["function"]["arguments"] == ( + '{"command":"pwd"}' + ) + assert prepared["messages"][1]["tool_calls"][1]["function"]["arguments"] == ( + "not-json" + ) + + +def test_prepare_dynamo_chat_completion_request_normalizes_tools_without_prefix() -> ( + None +): + tokenizer = _Tokenizer() + body = { + "messages": [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "older tool call", + "tool_calls": [ + { + "type": "function", + "function": { + "name": "shell", + "arguments": '{"command":"pwd"}', + }, + } + ], + }, + {"role": "user", "content": "next"}, + ] + } + + prepared = prepare_dynamo_chat_completion_request( + body, + tokenizer=tokenizer, + exclude_tools_when_tool_choice_none=True, + ) + + assert prepared["nvext"]["token_data"] == [10, 777, 2, 40, 99] + assert prepared["messages"][1]["tool_calls"][0]["function"]["arguments"] == ( + '{"command":"pwd"}' + ) + assert len(tokenizer.calls) == 2 + + +def test_prepare_dynamo_chat_completion_request_rejects_stream() -> None: + with pytest.raises(ValueError, match="stream=True"): + prepare_dynamo_chat_completion_request( + {"messages": [{"role": "user", "content": "hello"}], "stream": True}, + tokenizer=_Tokenizer(), + exclude_tools_when_tool_choice_none=True, + ) + + +def test_prepare_dynamo_chat_completion_request_rejects_multiple_choices() -> None: + with pytest.raises(ValueError, match="only n=1"): + prepare_dynamo_chat_completion_request( + {"messages": [{"role": "user", "content": "hello"}], "n": 2}, + tokenizer=_Tokenizer(), + exclude_tools_when_tool_choice_none=True, + ) + + +def test_validate_engine_data_requires_prompt_completion_and_logprobs() -> None: + _validate_engine_data( + { + "nvext": { + "engine_data": { + "prompt_token_ids": [1, 2], + "completion_token_ids": [3], + "completion_logprobs": [-0.25], + } + } + } + ) + + with pytest.raises(ValueError, match="engine_data"): + _validate_engine_data({"nvext": {}}) + with pytest.raises(ValueError, match="prompt_token_ids"): + _validate_engine_data( + { + "nvext": { + "engine_data": { + "completion_token_ids": [], + "completion_logprobs": [], + } + } + } + ) + with pytest.raises(ValueError, match="completion_token_ids"): + _validate_engine_data( + { + "nvext": { + "engine_data": { + "prompt_token_ids": [], + "completion_logprobs": [], + } + } + } + ) + with pytest.raises(ValueError, match="completion_logprobs"): + _validate_engine_data( + { + "nvext": { + "engine_data": { + "prompt_token_ids": [], + "completion_token_ids": [], + } + } + } + ) + + +def test_inject_gym_token_metadata_validates_and_populates_message() -> None: + response = { + "choices": [ + { + "message": {"role": "assistant", "content": "answer"}, + "logprobs": None, + } + ], + "nvext": { + "engine_data": { + "prompt_token_ids": [1, 2, 3], + "completion_token_ids": [4, 5], + "completion_logprobs": [-0.25, -0.5], + } + }, + } + + _inject_gym_token_metadata(response) + + assert response["choices"][0]["message"] == { + "role": "assistant", + "content": "answer", + "prompt_token_ids": [1, 2, 3], + "generation_token_ids": [4, 5], + "generation_log_probs": [-0.25, -0.5], + } + + response["nvext"]["engine_data"]["completion_logprobs"] = [-0.25] + with pytest.raises(ValueError, match="1 generation log probabilities for 2"): + _inject_gym_token_metadata(response) + + +def test_forward_chat_completion_reuses_loop_bound_session() -> None: + class FakeResponse: + status = 200 + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + async def text(self): + return json.dumps({"choices": []}) + + class FakeSession: + def __init__(self): + self.calls = [] + + def post(self, url, *, json, headers): + self.calls.append((url, json, headers)) + return FakeResponse() + + server = DynamoTokenWrapperServer( + dynamo_frontend_base_url="http://dynamo/v1", + tokenizer=_Tokenizer(), + tokenizer_chat_template_kwargs=None, + exclude_tools_when_tool_choice_none=True, + request_timeout_s=30, + ) + session = FakeSession() + server._client_session = session + + async def forward_twice(): + await server._forward_chat_completion({}, authorization=None) + await server._forward_chat_completion({}, authorization="Bearer token") + + asyncio.run(forward_twice()) + + assert len(session.calls) == 2 + assert session.calls[1][2]["Authorization"] == "Bearer token" diff --git a/tests/unit/models/generation/test_swe1_dynamo_config.py b/tests/unit/models/generation/test_swe1_dynamo_config.py new file mode 100644 index 00000000000..af3f39c8647 --- /dev/null +++ b/tests/unit/models/generation/test_swe1_dynamo_config.py @@ -0,0 +1,112 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + +from pathlib import Path + +import pytest +from omegaconf import OmegaConf + +from nemo_rl.models.generation import configure_generation_config +from nemo_rl.models.generation.dynamo.config import DynamoConfig +from nemo_rl.utils.config import load_config, register_omegaconf_resolvers + +REPO_ROOT = Path(__file__).resolve().parents[4] +RECIPE = ( + REPO_ROOT / "examples/configs/recipes/llm/" + "grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.yaml" +) +DRIVER = ( + REPO_ROOT / "tests/test_suites/llm/grpo-nanov3-30ba3b-3n8g-megatron-dynamo-swe1.sh" +) + + +def _load_recipe() -> dict: + register_omegaconf_resolvers() + return OmegaConf.to_container(load_config(RECIPE), resolve=True) + + +def test_public_swe_recipe_has_supported_topology_and_telemetry( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("HF_HOME", "/hf-home") + config = _load_recipe() + generation = config["policy"]["generation"] + + class Tokenizer: + pad_token_id = 0 + eos_token_id = 1 + + configured_generation = configure_generation_config(generation, Tokenizer()) + validated = DynamoConfig.model_validate(configured_generation) + + assert config["policy"]["model_name"] == ( + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16" + ) + assert config["cluster"]["gpus_per_node"] == 8 + assert config["cluster"]["num_nodes"] == 3 + assert config["cluster"]["segment_size"] == 1 + assert config["grpo"]["max_num_steps"] == 4 + assert config["grpo"]["async_grpo"]["enabled"] is True + assert config["grpo"]["async_grpo"]["in_flight_weight_updates"] is False + assert ( + config["grpo"]["async_grpo"]["recompute_kv_cache_after_weight_updates"] is True + ) + assert generation["colocated"]["resources"] == { + "gpus_per_node": 8, + "num_nodes": 1, + } + assert validated.engine_world_size == 4 + assert generation["vllm_cfg"]["expert_parallel_size"] == 4 + assert validated.dynamo_cfg.frontend_args.router_mode == "kv" + assert validated.dynamo_cfg.control_timeout_s == 600 + assert validated.vllm_cfg.enable_vllm_metrics_logger is True + assert validated.vllm_cfg.load_format == "dummy" + assert config["env"]["nemo_gym"]["config_paths"][-1].endswith( + "swe_pivot_single_step_tool_use_with_argument_comparison.yaml" + ) + assert ( + config["env"]["nemo_gym"]["policy_model"]["responses_api_models"]["vllm_model"][ + "chat_template_kwargs" + ]["force_nonempty_content"] + is True + ) + assert ( + config["env"]["nemo_gym"]["single_step_tool_use_with_argument_comparison_swe"][ + "responses_api_agents" + ]["tool_simulation_agent"]["resources_server"]["name"] + == "swe_pivot_single_step_tool_use_with_argument_comparison_resources_server" + ) + assert config["logger"]["wandb_enabled"] is True + assert config["logger"]["tensorboard_enabled"] is True + assert config["logger"]["wandb"]["project"] == "nemo-rl" + assert config["data"]["train"]["data_path"].endswith( + "/superv3_data/swe1/train-split.jsonl" + ) + assert config["data"]["validation"]["data_path"].endswith( + "/superv3_data/swe1/val-split.jsonl" + ) + + +def test_recipe_and_driver_have_no_removed_swe_modes() -> None: + recipe_text = RECIPE.read_text(encoding="utf-8") + text = recipe_text + DRIVER.read_text(encoding="utf-8") + for forbidden in ( + "/lustre/", + "/path/to", + "/home/", + "jthomson", + "openhands", + "container_formatter", + "SIF_", + "effort_levels", + "hsg_r2", + "USES_SANDBOX", + ): + assert forbidden.lower() not in text.lower() + assert "load_format:" not in recipe_text + assert "--require-tag-prefix generation_metrics/" in text + assert "project: nemo-rl" in text + assert "Expected step ${MAX_STEPS}" in text + assert 'median(data["train/token_mult_prob_error"]) < 1.1' in text + assert "data['train/token_mult_prob_error']['${MAX_STEPS}'] < 1.1" in text + assert 'mean(data["train/gen_kl_error"]) < 0.02' in text + assert "Expected one cache invalidation per refit" in text diff --git a/tests/unit/models/policy/test_lm_policy_collective.py b/tests/unit/models/policy/test_lm_policy_collective.py new file mode 100644 index 00000000000..0e5814e0dbf --- /dev/null +++ b/tests/unit/models/policy/test_lm_policy_collective.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from nemo_rl.models.policy.lm_policy import Policy +from nemo_rl.models.policy.workers.base_policy_worker import AbstractPolicyWorker + + +def test_policy_forwards_nccl_peer_to_workers(): + calls = [] + + class WorkerGroup: + def run_all_workers_single_data(self, method_name, **kwargs): + calls.append((method_name, kwargs)) + return ["future"] + + def shutdown(self, **_kwargs): + pass + + policy = Policy.__new__(Policy) + policy.worker_group = WorkerGroup() + + futures = policy.init_collective( + "127.0.0.1", + 1234, + 4, + train_world_size=2, + nccl_peer="vllm", + ) + + assert futures == ["future"] + assert calls == [ + ( + "init_collective", + { + "ip": "127.0.0.1", + "port": 1234, + "world_size": 4, + "train_world_size": 2, + "nccl_peer": "vllm", + }, + ) + ] + + +def test_policy_worker_initializes_requested_nccl_peer(monkeypatch): + calls = [] + + class ProcessGroup: + def __init__(self, **kwargs): + calls.append(("create", kwargs)) + + def init_nccl_communicator(self, **kwargs): + calls.append(("init", kwargs)) + + monkeypatch.setattr( + "nemo_rl.distributed.stateless_process_group.StatelessProcessGroup", + ProcessGroup, + ) + monkeypatch.setattr( + "nemo_rl.models.policy.workers.base_policy_worker.torch.cuda.current_device", + lambda: 3, + ) + + worker = AbstractPolicyWorker.__new__(AbstractPolicyWorker) + worker.rank = 1 + worker.init_collective( + "127.0.0.1", + 1234, + 4, + train_world_size=2, + nccl_peer="vllm", + ) + + assert calls == [ + ( + "create", + { + "master_address": "127.0.0.1", + "port": 1234, + "rank": 1, + "world_size": 4, + }, + ), + ("init", {"device": 3, "peer": "vllm"}), + ] + + +def test_policy_forwards_packed_collective_options_to_workers(): + calls = [] + + class WorkerGroup: + def run_all_workers_single_data(self, method_name, **kwargs): + calls.append((method_name, kwargs)) + return ["future"] + + def shutdown(self, **_kwargs): + pass + + policy = Policy.__new__(Policy) + policy.worker_group = WorkerGroup() + + futures = policy.broadcast_weights_for_collective( + kv_scales={"k_scale": 1.25}, + buffer_size_bytes=1024**3, + num_buffers=2, + ) + + assert futures == ["future"] + assert calls == [ + ( + "broadcast_weights_for_collective", + { + "kv_scales": {"k_scale": 1.25}, + "buffer_size_bytes": 1024**3, + "num_buffers": 2, + }, + ) + ] diff --git a/tests/unit/test_recipes_and_test_suites.py b/tests/unit/test_recipes_and_test_suites.py index 26c65391bce..26eb39eb917 100644 --- a/tests/unit/test_recipes_and_test_suites.py +++ b/tests/unit/test_recipes_and_test_suites.py @@ -256,7 +256,7 @@ def test_all_recipe_yamls_accounted_for_in_test_suites( ) -def test_nightly_compute_stays_below_3928_hours(nightly_test_suite, tracker): +def test_nightly_compute_stays_below_4024_hours(nightly_test_suite, tracker): command = f"DRYRUN=1 HF_HOME=... HF_DATASETS_CACHE=... CONTAINER= ACCOUNT= PARTITION= ./tools/launch {' '.join(nightly_test_suite)}" print(f"Running command: {command}") @@ -288,8 +288,10 @@ def test_nightly_compute_stays_below_3928_hours(nightly_test_suite, tracker): f"Last line of output was not as expected: '{last_line}'" ) total_gpu_hours = float(last_line.split(":")[-1].strip()) - assert total_gpu_hours <= 3928, ( - f"Total GPU hours exceeded 3928: {last_line}. We should revisit the test suites to reduce the total GPU hours." + # The managed Dynamo 3x8 H100 SWE1 test adds 96 GPU-hours to the former + # 3928-hour limit. + assert total_gpu_hours <= 4024, ( + f"Total GPU hours exceeded 4024: {last_line}. We should revisit the test suites to reduce the total GPU hours." ) tracker.track("total_nightly_gpu_hours", total_gpu_hours) diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index 5b90f482981..9a92d32a999 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -12,9 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import shutil +import subprocess +import sys import tempfile +from pathlib import Path from unittest.mock import MagicMock, call, patch import pytest @@ -176,6 +180,49 @@ def test_log_metrics(self, mock_summary_writer, temp_dir): mock_writer.add_scalar.assert_any_call("loss", 0.5, 10) mock_writer.add_scalar.assert_any_call("accuracy", 0.8, 10) + def test_json_dump_requires_tag_prefix_across_plugin_categories(self, tmp_path): + """The functional gate accepts image tags and rejects missing prefixes.""" + from torch.utils.tensorboard import SummaryWriter + + log_dir = tmp_path / "tensorboard" + writer = SummaryWriter(log_dir=str(log_dir)) + writer.add_image( + "generation_metrics/per_worker_requests", + torch.zeros((3, 1, 1)), + global_step=1, + ) + writer.add_scalar("train/loss", 1.0, global_step=1) + writer.close() + + script = Path(__file__).parents[2] / "json_dump_tb_logs.py" + metrics_path = tmp_path / "metrics.json" + command = [ + sys.executable, + str(script), + str(log_dir), + "--output_path", + str(metrics_path), + "--require-tag-prefix", + ] + + found = subprocess.run( + [*command, "generation_metrics/"], + capture_output=True, + text=True, + check=False, + ) + assert found.returncode == 0, found.stderr + assert json.loads(metrics_path.read_text()) == {"train/loss": {"1": 1.0}} + + missing = subprocess.run( + [*command, "missing/"], + capture_output=True, + text=True, + check=False, + ) + assert missing.returncode == 1 + assert "No TensorBoard tag starts with 'missing/'" in missing.stderr + @patch("nemo_rl.utils.logger.SummaryWriter") def test_log_metrics_with_prefix(self, mock_summary_writer, temp_dir): """Test logging metrics with a prefix to TensorboardLogger.""" diff --git a/tests/unit/weight_sync/test_weight_synchronizer.py b/tests/unit/weight_sync/test_weight_synchronizer.py index be870962f0b..3335db7a3d4 100644 --- a/tests/unit/weight_sync/test_weight_synchronizer.py +++ b/tests/unit/weight_sync/test_weight_synchronizer.py @@ -19,10 +19,12 @@ import pytest from nemo_rl.models.generation.constants import ( + DYNAMO_BACKEND, MEGATRON_BACKEND, SGLANG_BACKEND, VLLM_BACKEND, ) +from nemo_rl.models.generation.interfaces import CollectiveSenderSpec from nemo_rl.weight_sync.collective_weight_synchronizer import ( CollectiveWeightSynchronizer, ) @@ -72,6 +74,8 @@ def _mock_generation(**overrides): gen.update_weights_from_collective.return_value = [MagicMock()] gen.get_rollout_engine_urls.return_value = ["http://localhost:30000"] gen.init_collective.return_value = [MagicMock()] + gen.get_collective_sender_spec.return_value = CollectiveSenderSpec() + gen.get_inference_world_size.return_value = None for k, v in overrides.items(): setattr(gen, k, v) return gen @@ -348,7 +352,11 @@ def test_sync_weights_calls_broadcast_and_receive(self, mock_ray): sync.sync_weights() assert not sync.is_stale - policy.broadcast_weights_for_collective.assert_called_once() + policy.broadcast_weights_for_collective.assert_called_once_with( + kv_scales=None, + buffer_size_bytes=None, + num_buffers=None, + ) gen.update_weights_from_collective.assert_called_once() @patch("nemo_rl.weight_sync.collective_weight_synchronizer.ray") @@ -396,12 +404,45 @@ def test_init_communicator_sets_up_collective(self, mock_ray): policy.prepare_refit_info.assert_called_once() gen.prepare_refit_info.assert_called_once() policy.init_collective.assert_called_once_with( - "10.0.0.1", 29500, 6, train_world_size=4 + "10.0.0.1", 29500, 6, train_world_size=4, nccl_peer="nemo" ) gen.init_collective.assert_called_once_with( "10.0.0.1", 29500, 6, train_world_size=4 ) + @patch("nemo_rl.weight_sync.collective_weight_synchronizer.ray") + def test_backend_sender_contract_controls_geometry_and_world_size(self, mock_ray): + mock_ray.get.return_value = [True] + policy = _mock_policy() + gen = _mock_generation() + gen.get_collective_sender_spec.return_value = CollectiveSenderSpec( + nccl_peer="vllm", + buffer_size_bytes=1024**3, + num_buffers=2, + ) + gen.get_inference_world_size.return_value = 8 + sync = CollectiveWeightSynchronizer( + policy, + gen, + _mock_cluster(world_size=4, ip="10.0.0.1", port=29500), + _mock_cluster(world_size=2), + ) + + sync.init_communicator() + sync.sync_weights() + + policy.init_collective.assert_called_once_with( + "10.0.0.1", 29500, 12, train_world_size=4, nccl_peer="vllm" + ) + gen.init_collective.assert_called_once_with( + "10.0.0.1", 29500, 12, train_world_size=4 + ) + policy.broadcast_weights_for_collective.assert_called_once_with( + kv_scales=None, + buffer_size_bytes=1024**3, + num_buffers=2, + ) + # --------------------------------------------------------------------------- # NcclReshardWeightSynchronizer @@ -645,6 +686,17 @@ def test_non_colocated_vllm_returns_collective(self): ) assert isinstance(sync, CollectiveWeightSynchronizer) + def test_non_colocated_dynamo_returns_collective(self): + sync = create_weight_synchronizer( + policy=_mock_policy(), + generation=_mock_generation(), + generation_backend=DYNAMO_BACKEND, + colocated=False, + train_cluster=_mock_cluster(), + inference_cluster=_mock_cluster(), + ) + assert isinstance(sync, CollectiveWeightSynchronizer) + def test_non_colocated_sglang_raises(self): policy = _mock_policy() gen = _mock_generation()